feat(settings): add settings page with server, watchdog and notification config
- Add new Settings view with form controls for server, watchdog and notification settings - Implement API endpoints for getting and saving settings - Add settings route and sidebar navigation item - Update button styles to include text color variable - Change default app target to local development address
This commit is contained in:
@@ -34,7 +34,14 @@ api.interceptors.response.use(
|
|||||||
|
|
||||||
export const systemApi = {
|
export const systemApi = {
|
||||||
getStatus: () => api.get('/system/getStatus'),
|
getStatus: () => api.get('/system/getStatus'),
|
||||||
getSoftwareInfo: () => api.get('/system/getSoftwareInfo')
|
getSoftwareInfo: () => api.get('/system/getSoftwareInfo'),
|
||||||
|
getSystemInfo: () => api.get('/system/info'),
|
||||||
|
getSettings: (key) => {
|
||||||
|
const url = key ? `/system/settings/get?key=${key}` : '/system/settings/get';
|
||||||
|
return api.get(url);
|
||||||
|
},
|
||||||
|
setSettings: (settings) =>
|
||||||
|
api.post('/system/settings/set', settings)
|
||||||
};
|
};
|
||||||
|
|
||||||
export const authApi = {
|
export const authApi = {
|
||||||
@@ -109,10 +116,6 @@ export const monitorApi = {
|
|||||||
getSystemStats: () => api.get('/monitor/stats')
|
getSystemStats: () => api.get('/monitor/stats')
|
||||||
};
|
};
|
||||||
|
|
||||||
export const systemInfoApi = {
|
|
||||||
getSystemInfo: () => api.get('/system/info')
|
|
||||||
};
|
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|
||||||
export const TODO_API_NOTES = {
|
export const TODO_API_NOTES = {
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ export default {
|
|||||||
{ path: '/sessions', title: 'Session Management', icon: 'fas fa-key', permission: ['superuser', 'admin'] },
|
{ path: '/sessions', title: 'Session Management', icon: 'fas fa-key', permission: ['superuser', 'admin'] },
|
||||||
{ path: '/logs', title: 'System Logs', icon: 'fas fa-file-alt', permission: ['superuser', 'admin'] },
|
{ path: '/logs', title: 'System Logs', icon: 'fas fa-file-alt', permission: ['superuser', 'admin'] },
|
||||||
{ path: '/monitor', title: 'System Monitoring', icon: 'fas fa-chart-bar', permission: ['superuser', 'admin', 'visitor'] },
|
{ path: '/monitor', title: 'System Monitoring', icon: 'fas fa-chart-bar', permission: ['superuser', 'admin', 'visitor'] },
|
||||||
{ path: '/system-info', title: 'System Information', icon: 'fas fa-info-circle', permission: ['superuser', 'admin', 'visitor'] }
|
{ path: '/system-info', title: 'System Information', icon: 'fas fa-info-circle', permission: ['superuser', 'admin', 'visitor'] },
|
||||||
|
{ path: '/settings', title: 'Settings', icon: 'fas fa-cog', permission: ['superuser', 'admin'] }
|
||||||
];
|
];
|
||||||
|
|
||||||
const menuItems = computed(() => {
|
const menuItems = computed(() => {
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ const routes = [
|
|||||||
name: 'SystemInfo',
|
name: 'SystemInfo',
|
||||||
component: () => import('../views/SystemInfo.vue'),
|
component: () => import('../views/SystemInfo.vue'),
|
||||||
meta: { requiresAuth: true }
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'settings',
|
||||||
|
name: 'Settings',
|
||||||
|
component: () => import('../views/Settings.vue'),
|
||||||
|
meta: { requiresAuth: true, permission: ['superuser', 'admin'] }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -445,6 +445,7 @@
|
|||||||
font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
padding: 8px 15px;
|
padding: 8px 15px;
|
||||||
background: var(--surface-light);
|
background: var(--surface-light);
|
||||||
|
color: var(--text-color);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
331
src/views/Settings.vue
Normal file
331
src/views/Settings.vue
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
<template>
|
||||||
|
<div class="settings-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>Settings</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Server Settings</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Listen Address</label>
|
||||||
|
<input type="text" v-model="settings.listenAddr" placeholder="0.0.0.0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Listen Port</label>
|
||||||
|
<input type="number" v-model="settings.listenPort" placeholder="8080">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Watchdog Settings</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Enable Watchdog</label>
|
||||||
|
<div class="toggle-switch">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="watchdog-enabled"
|
||||||
|
v-model="settings.watchdogEnabled"
|
||||||
|
>
|
||||||
|
<label for="watchdog-enabled" class="toggle-label"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Watchdog Port</label>
|
||||||
|
<input type="number" v-model="settings.watchdogPort" placeholder="9000">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Notification Settings</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Enable Notifications</label>
|
||||||
|
<div class="toggle-switch">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="notification-enabled"
|
||||||
|
v-model="settings.notificationEnabled"
|
||||||
|
>
|
||||||
|
<label for="notification-enabled" class="toggle-label"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Notification Method</label>
|
||||||
|
<div class="select-with-btn">
|
||||||
|
<select v-model="settings.notificationMethod">
|
||||||
|
<option value="webhook">WebHook</option>
|
||||||
|
</select>
|
||||||
|
<button @click="showWebhookDialog = true" class="common-btn settings-btn">
|
||||||
|
<i class="fas fa-cog" aria-hidden="true"></i>
|
||||||
|
<span>Settings</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions save-actions">
|
||||||
|
<button @click="saveSettings" class="submit-btn" :disabled="isSaving">
|
||||||
|
{{ isSaving ? 'Saving...' : 'Save Settings' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showWebhookDialog" class="modal-overlay" @click.self="showWebhookDialog = false">
|
||||||
|
<div class="modal webhook-modal">
|
||||||
|
<h3>WebHook Settings</h3>
|
||||||
|
<div class="form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Method</label>
|
||||||
|
<select v-model="webhookSettings.method">
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
<option value="GET">GET</option>
|
||||||
|
<option value="PUT">PUT</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>URL</label>
|
||||||
|
<input type="text" v-model="webhookSettings.url" placeholder="https://example.com/webhook">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Headers</label>
|
||||||
|
<input type="text" v-model="webhookSettings.headers" placeholder="Content-Type: application/json">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Body Template</label>
|
||||||
|
<textarea v-model="webhookSettings.body" rows="4" placeholder='{"subject":"Alert","text":"Message"}'></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button @click="showWebhookDialog = false" class="cancel-btn">Cancel</button>
|
||||||
|
<button @click="saveWebhookSettings" class="submit-btn">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { systemApi } from '../api/index.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Settings',
|
||||||
|
setup() {
|
||||||
|
const isSaving = ref(false);
|
||||||
|
const showWebhookDialog = ref(false);
|
||||||
|
|
||||||
|
const settings = reactive({
|
||||||
|
listenAddr: '',
|
||||||
|
listenPort: '',
|
||||||
|
notificationEnabled: false,
|
||||||
|
notificationMethod: 'webhook',
|
||||||
|
watchdogEnabled: false,
|
||||||
|
watchdogPort: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const webhookSettings = reactive({
|
||||||
|
method: 'POST',
|
||||||
|
url: '',
|
||||||
|
headers: '',
|
||||||
|
body: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadSettings = async () => {
|
||||||
|
try {
|
||||||
|
const result = await systemApi.getSettings();
|
||||||
|
if (result.success) {
|
||||||
|
const data = result.data;
|
||||||
|
settings.listenAddr = data.ListenAddr || '';
|
||||||
|
settings.listenPort = data.ListenPort || '';
|
||||||
|
settings.notificationEnabled = data['Notification.Enabled'] || false;
|
||||||
|
settings.notificationMethod = data['Notification.Method'] || 'webhook';
|
||||||
|
settings.watchdogEnabled = data['Watchdog.Enabled'] || false;
|
||||||
|
settings.watchdogPort = data['Watchdog.Port'] || '';
|
||||||
|
webhookSettings.method = data['Webhook.Method'] || 'POST';
|
||||||
|
webhookSettings.url = data['Webhook.URL'] || '';
|
||||||
|
webhookSettings.headers = data['Webhook.Headers'] || '';
|
||||||
|
webhookSettings.body = data['Webhook.Body'] || '';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load settings:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveSettings = async () => {
|
||||||
|
isSaving.value = true;
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
ListenAddr: settings.listenAddr,
|
||||||
|
ListenPort: settings.listenPort,
|
||||||
|
'Notification.Enabled': settings.notificationEnabled,
|
||||||
|
'Notification.Method': settings.notificationMethod,
|
||||||
|
'Watchdog.Enabled': settings.watchdogEnabled,
|
||||||
|
'Watchdog.Port': settings.watchdogPort
|
||||||
|
};
|
||||||
|
const result = await systemApi.setSettings(payload);
|
||||||
|
if (result.success) {
|
||||||
|
alert('Settings saved successfully');
|
||||||
|
} else {
|
||||||
|
alert(result.message || 'Failed to save settings');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Failed to save settings');
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveWebhookSettings = async () => {
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
'Webhook.Method': webhookSettings.method,
|
||||||
|
'Webhook.URL': webhookSettings.url,
|
||||||
|
'Webhook.Headers': webhookSettings.headers,
|
||||||
|
'Webhook.Body': webhookSettings.body
|
||||||
|
};
|
||||||
|
const result = await systemApi.setSettings(payload);
|
||||||
|
if (result.success) {
|
||||||
|
showWebhookDialog.value = false;
|
||||||
|
alert('WebHook settings saved successfully');
|
||||||
|
} else {
|
||||||
|
alert(result.message || 'Failed to save WebHook settings');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Failed to save WebHook settings');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
settings,
|
||||||
|
webhookSettings,
|
||||||
|
isSaving,
|
||||||
|
showWebhookDialog,
|
||||||
|
saveSettings,
|
||||||
|
saveWebhookSettings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import '../styles/common.css';
|
||||||
|
|
||||||
|
.settings-page {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-with-btn {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-with-btn select {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-btn i {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-actions {
|
||||||
|
margin-top: 24px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: var(--surface-light);
|
||||||
|
border: 3px solid var(--border-color);
|
||||||
|
transition: 0.3s;
|
||||||
|
border-radius: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
left: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
background-color: var(--text-tertiary);
|
||||||
|
transition: 0.3s;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .toggle-label {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .toggle-label:before {
|
||||||
|
transform: translateX(22px);
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 3px solid var(--border-color);
|
||||||
|
border-radius: 11px;
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 1.47;
|
||||||
|
letter-spacing: -0.374px;
|
||||||
|
background: var(--surface-light);
|
||||||
|
color: var(--text-color);
|
||||||
|
transition: background-color 0.3s, color 0.3s, border-color 0.3s;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px var(--focus-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
.webhook-modal {
|
||||||
|
width: 600px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
const APP_TARGET = '10.0.64.19:8080'
|
const APP_TARGET = '192.168.20.4:19090'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
define: {
|
define: {
|
||||||
|
|||||||
Reference in New Issue
Block a user