This commit is contained in:
Baobhan Sith
2025-04-20 21:53:17 +08:00
parent 7b00a111dd
commit b862e85ea5
7 changed files with 174 additions and 27 deletions
@@ -6,6 +6,8 @@ const FOCUS_SEQUENCE_KEY = 'focusSwitcherSequence'; // 焦点切换顺序设置
const NAV_BAR_VISIBLE_KEY = 'navBarVisible'; // 导航栏可见性设置键 const NAV_BAR_VISIBLE_KEY = 'navBarVisible'; // 导航栏可见性设置键
const LAYOUT_TREE_KEY = 'layoutTree'; // 布局树设置键 const LAYOUT_TREE_KEY = 'layoutTree'; // 布局树设置键
const AUTO_COPY_ON_SELECT_KEY = 'autoCopyOnSelect'; // 终端选中自动复制设置键 const AUTO_COPY_ON_SELECT_KEY = 'autoCopyOnSelect'; // 终端选中自动复制设置键
const STATUS_MONITOR_INTERVAL_SECONDS_KEY = 'statusMonitorIntervalSeconds'; // 状态监控间隔设置键
const DEFAULT_STATUS_MONITOR_INTERVAL_SECONDS = 3; // 默认状态监控间隔
export const settingsService = { export const settingsService = {
/** /**
@@ -240,5 +242,54 @@ export const settingsService = {
console.error(`[Service] Error calling settingsRepository.setSetting for key ${AUTO_COPY_ON_SELECT_KEY}:`, error); console.error(`[Service] Error calling settingsRepository.setSetting for key ${AUTO_COPY_ON_SELECT_KEY}:`, error);
throw new Error('Failed to save auto copy on select setting.'); throw new Error('Failed to save auto copy on select setting.');
} }
}, // *** 确保这里有逗号 ***
/**
* 获取状态监控轮询间隔 (秒)
* @returns 返回间隔秒数 (number),如果未设置或无效则返回默认值
*/
async getStatusMonitorIntervalSeconds(): Promise<number> {
console.log(`[Service] Attempting to get setting for key: ${STATUS_MONITOR_INTERVAL_SECONDS_KEY}`);
try {
const intervalStr = await settingsRepository.getSetting(STATUS_MONITOR_INTERVAL_SECONDS_KEY);
console.log(`[Service] Raw value from repository for ${STATUS_MONITOR_INTERVAL_SECONDS_KEY}:`, intervalStr);
if (intervalStr) {
const intervalNum = parseInt(intervalStr, 10);
// 验证是否为正整数
if (!isNaN(intervalNum) && intervalNum > 0) {
return intervalNum;
} else {
console.warn(`[Service] Invalid status monitor interval value found ('${intervalStr}'). Returning default.`);
}
} else {
console.log(`[Service] No status monitor interval found in settings. Returning default.`);
}
} catch (error) {
console.error(`[Service] Error getting status monitor interval setting (key: ${STATUS_MONITOR_INTERVAL_SECONDS_KEY}):`, error);
}
// 返回默认值
return DEFAULT_STATUS_MONITOR_INTERVAL_SECONDS;
}, // *** 确保这里有逗号 ***
/**
* 设置状态监控轮询间隔 (秒)
* @param interval 间隔秒数 (number)
*/
async setStatusMonitorIntervalSeconds(interval: number): Promise<void> {
console.log(`[Service] setStatusMonitorIntervalSeconds called with: ${interval}`);
// 验证输入是否为正整数
if (!Number.isInteger(interval) || interval <= 0) {
console.error(`[Service] Attempted to save invalid status monitor interval: ${interval}`);
throw new Error('Invalid interval value provided. Must be a positive integer.');
}
try {
const intervalStr = String(interval);
console.log(`[Service] Attempting to save setting. Key: ${STATUS_MONITOR_INTERVAL_SECONDS_KEY}, Value: ${intervalStr}`);
await settingsRepository.setSetting(STATUS_MONITOR_INTERVAL_SECONDS_KEY, intervalStr);
console.log(`[Service] Successfully saved setting for key: ${STATUS_MONITOR_INTERVAL_SECONDS_KEY}`);
} catch (error) {
console.error(`[Service] Error calling settingsRepository.setSetting for key ${STATUS_MONITOR_INTERVAL_SECONDS_KEY}:`, error);
throw new Error('Failed to save status monitor interval setting.');
}
} // *** 最后的方法后面不需要逗号 *** } // *** 最后的方法后面不需要逗号 ***
}; };
@@ -1,6 +1,7 @@
import { Client } from 'ssh2'; import { Client } from 'ssh2';
import { WebSocket } from 'ws'; import { WebSocket } from 'ws';
import { ClientState } from '../websocket'; // 导入统一的 ClientState import { ClientState } from '../websocket'; // 导入统一的 ClientState
import { settingsService } from './settings.service'; // +++ 导入 settingsService +++
// 定义服务器状态的数据结构 (与前端 StatusMonitor.vue 匹配) // 定义服务器状态的数据结构 (与前端 StatusMonitor.vue 匹配)
interface ServerStatus { interface ServerStatus {
@@ -31,7 +32,7 @@ interface NetworkStats {
} }
} }
const DEFAULT_POLLING_INTERVAL = 1000; // 修改为 1 秒轮询间隔 (毫秒) // const DEFAULT_POLLING_INTERVAL = 3000; // --- 移除常量,将从 settingsService 获取 ---
// 用于存储上一次的网络统计信息以计算速率 // 用于存储上一次的网络统计信息以计算速率
const previousNetStats = new Map<string, { rx: number, tx: number, timestamp: number }>(); const previousNetStats = new Map<string, { rx: number, tx: number, timestamp: number }>();
@@ -45,9 +46,9 @@ export class StatusMonitorService {
/** /**
* 启动指定会话的状态轮询 * 启动指定会话的状态轮询
* @param sessionId 会话 ID * @param sessionId 会话 ID
* @param interval 轮询间隔 (毫秒),可选,默认为 DEFAULT_POLLING_INTERVAL * @param interval 轮询间隔 (毫秒),可选,默认为 DEFAULT_POLLING_INTERVAL // --- 参数移除 ---
*/ */
startStatusPolling(sessionId: string, interval: number = DEFAULT_POLLING_INTERVAL): void { async startStatusPolling(sessionId: string): Promise<void> { // --- 改为 async, 移除 interval 参数 ---
const state = this.clientStates.get(sessionId); const state = this.clientStates.get(sessionId);
if (!state || !state.sshClient) { if (!state || !state.sshClient) {
//console.warn(`[StatusMonitor] 无法为会话 ${sessionId} 启动状态轮询:状态无效或 SSH 客户端不存在。`); //console.warn(`[StatusMonitor] 无法为会话 ${sessionId} 启动状态轮询:状态无效或 SSH 客户端不存在。`);
@@ -57,11 +58,23 @@ export class StatusMonitorService {
//console.warn(`[StatusMonitor] 会话 ${sessionId} 的状态轮询已在运行中。`); //console.warn(`[StatusMonitor] 会话 ${sessionId} 的状态轮询已在运行中。`);
return; return;
} }
//console.warn(`[StatusMonitor] 为会话 ${sessionId} 启动状态轮询,间隔 ${interval}ms`);
// +++ 从 settingsService 获取轮询间隔 +++
let intervalMs: number;
try {
const intervalSeconds = await settingsService.getStatusMonitorIntervalSeconds();
intervalMs = intervalSeconds * 1000;
console.log(`[StatusMonitor ${sessionId}] 使用配置的轮询间隔: ${intervalSeconds} 秒 (${intervalMs}ms)`);
} catch (error) {
console.error(`[StatusMonitor ${sessionId}] 获取轮询间隔设置失败,将使用默认值 3000ms:`, error);
intervalMs = 3000; // 出错时回退到 3 秒
}
//console.warn(`[StatusMonitor] 为会话 ${sessionId} 启动状态轮询,间隔 ${intervalMs}ms`);
// 移除立即执行,让 setInterval 负责第一次调用,给连接更多准备时间 // 移除立即执行,让 setInterval 负责第一次调用,给连接更多准备时间
state.statusIntervalId = setInterval(() => { state.statusIntervalId = setInterval(() => {
this.fetchAndSendServerStatus(sessionId); this.fetchAndSendServerStatus(sessionId);
}, interval); }, intervalMs); // --- 使用获取到的间隔 ---
} }
/** /**
@@ -94,6 +107,7 @@ export class StatusMonitorService {
const status = await this.fetchServerStatus(state.sshClient, sessionId); const status = await this.fetchServerStatus(state.sshClient, sessionId);
state.ws.send(JSON.stringify({ type: 'status_update', payload: { connectionId: state.dbConnectionId, status } })); state.ws.send(JSON.stringify({ type: 'status_update', payload: { connectionId: state.dbConnectionId, status } }));
} catch (error: any) { } catch (error: any) {
// --- 移除 console.warn ---
// console.warn(`[StatusMonitor] 获取会话 ${sessionId} 服务器状态失败:`, error); // console.warn(`[StatusMonitor] 获取会话 ${sessionId} 服务器状态失败:`, error);
state.ws.send(JSON.stringify({ type: 'status_error', payload: { connectionId: state.dbConnectionId, message: `获取状态失败: ${error.message}` } })); state.ws.send(JSON.stringify({ type: 'status_error', payload: { connectionId: state.dbConnectionId, message: `获取状态失败: ${error.message}` } }));
} }
@@ -116,7 +130,7 @@ export class StatusMonitorService {
const osReleaseOutput = await this.executeSshCommand(sshClient, 'cat /etc/os-release'); const osReleaseOutput = await this.executeSshCommand(sshClient, 'cat /etc/os-release');
const nameMatch = osReleaseOutput.match(/^PRETTY_NAME="?([^"]+)"?/m); const nameMatch = osReleaseOutput.match(/^PRETTY_NAME="?([^"]+)"?/m);
status.osName = nameMatch ? nameMatch[1] : (osReleaseOutput.match(/^NAME="?([^"]+)"?/m)?.[1] ?? 'Unknown'); status.osName = nameMatch ? nameMatch[1] : (osReleaseOutput.match(/^NAME="?([^"]+)"?/m)?.[1] ?? 'Unknown');
} catch (err) { console.warn(`[StatusMonitor ${sessionId}] Failed to get OS name:`, err); } } catch (err) { /* 静默处理 */ } // --- 移除 console.warn ---
// --- CPU Model (Try /proc/cpuinfo first, fallback to lscpu) --- // --- CPU Model (Try /proc/cpuinfo first, fallback to lscpu) ---
try { try {
@@ -126,13 +140,13 @@ export class StatusMonitorService {
cpuModelOutput = await this.executeSshCommand(sshClient, "cat /proc/cpuinfo | grep 'model name' | head -n 1"); cpuModelOutput = await this.executeSshCommand(sshClient, "cat /proc/cpuinfo | grep 'model name' | head -n 1");
status.cpuModel = cpuModelOutput.match(/model name\s*:\s*(.*)/i)?.[1].trim(); status.cpuModel = cpuModelOutput.match(/model name\s*:\s*(.*)/i)?.[1].trim();
} catch (procErr) { } catch (procErr) {
console.warn(`[StatusMonitor ${sessionId}] Failed to get CPU model from /proc/cpuinfo, trying lscpu...`, procErr); // console.warn(`[StatusMonitor ${sessionId}] Failed to get CPU model from /proc/cpuinfo, trying lscpu...`, procErr); // --- 移除 console.warn ---
// Fallback to lscpu if /proc/cpuinfo fails // Fallback to lscpu if /proc/cpuinfo fails
try { try {
cpuModelOutput = await this.executeSshCommand(sshClient, "lscpu | grep 'Model name:'"); cpuModelOutput = await this.executeSshCommand(sshClient, "lscpu | grep 'Model name:'");
status.cpuModel = cpuModelOutput.match(/Model name:\s+(.*)/)?.[1].trim(); status.cpuModel = cpuModelOutput.match(/Model name:\s+(.*)/)?.[1].trim();
} catch (lscpuErr) { } catch (lscpuErr) {
console.warn(`[StatusMonitor ${sessionId}] Failed to get CPU model from lscpu as well:`, lscpuErr); // console.warn(`[StatusMonitor ${sessionId}] Failed to get CPU model from lscpu as well:`, lscpuErr); // --- 移除 console.warn ---
} }
} }
// If still no model found after both attempts // If still no model found after both attempts
@@ -140,7 +154,7 @@ export class StatusMonitorService {
status.cpuModel = 'Unknown'; status.cpuModel = 'Unknown';
} }
} catch (err) { // Catch any unexpected error during the process } catch (err) { // Catch any unexpected error during the process
console.warn(`[StatusMonitor ${sessionId}] Error getting CPU model:`, err); // console.warn(`[StatusMonitor ${sessionId}] Error getting CPU model:`, err); // --- 移除 console.warn ---
status.cpuModel = 'Unknown'; status.cpuModel = 'Unknown';
} }
@@ -172,7 +186,7 @@ export class StatusMonitorService {
} }
} }
} else { status.swapTotal = 0; status.swapUsed = 0; status.swapPercent = 0; } } else { status.swapTotal = 0; status.swapUsed = 0; status.swapPercent = 0; }
} catch (err) { console.warn(`[StatusMonitor ${sessionId}] Failed to get memory/swap usage:`, err); } } catch (err) { /* 静默处理 */ } // --- 移除 console.warn ---
// --- Disk Usage (Root Partition, POSIX format for compatibility) --- // --- Disk Usage (Root Partition, POSIX format for compatibility) ---
try { try {
@@ -193,7 +207,7 @@ export class StatusMonitorService {
} }
} }
} }
} catch (err) { console.warn(`[StatusMonitor ${sessionId}] Failed to get disk usage:`, err); } } catch (err) { /* 静默处理 */ } // --- 移除 console.warn ---
// --- CPU Usage (Simplified from top) --- // --- CPU Usage (Simplified from top) ---
try { try {
@@ -203,14 +217,14 @@ export class StatusMonitorService {
const idlePercent = parseFloat(idleMatch[1]); const idlePercent = parseFloat(idleMatch[1]);
status.cpuPercent = parseFloat((100 - idlePercent).toFixed(1)); status.cpuPercent = parseFloat((100 - idlePercent).toFixed(1));
} }
} catch (err) { console.warn(`[StatusMonitor ${sessionId}] Failed to get CPU usage from top:`, err); } } catch (err) { /* 静默处理 */ } // --- 移除 console.warn ---
// --- Load Average --- // --- Load Average ---
try { try {
const uptimeOutput = await this.executeSshCommand(sshClient, 'uptime'); const uptimeOutput = await this.executeSshCommand(sshClient, 'uptime');
const match = uptimeOutput.match(/load average(?:s)?:\s*([\d.]+)[, ]?\s*([\d.]+)[, ]?\s*([\d.]+)/); const match = uptimeOutput.match(/load average(?:s)?:\s*([\d.]+)[, ]?\s*([\d.]+)[, ]?\s*([\d.]+)/);
if (match) status.loadAvg = [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3])]; if (match) status.loadAvg = [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3])];
} catch (err) { console.warn(`[StatusMonitor ${sessionId}] Failed to get uptime/load average:`, err); } } catch (err) { /* 静默处理 */ } // --- 移除 console.warn ---
// --- Network Rates --- // --- Network Rates ---
try { try {
@@ -233,9 +247,9 @@ export class StatusMonitorService {
} else { status.netRxRate = 0; status.netTxRate = 0; } // First run or no time diff } else { status.netRxRate = 0; status.netTxRate = 0; } // First run or no time diff
previousNetStats.set(sessionId, { rx: currentRx, tx: currentTx, timestamp }); previousNetStats.set(sessionId, { rx: currentRx, tx: currentTx, timestamp });
} else { console.warn(`[StatusMonitor ${sessionId}] Could not find stats for default interface ${defaultInterface}`); } } else { /* 静默处理 */ } // --- 移除 console.warn ---
} }
} catch (err) { console.warn(`[StatusMonitor ${sessionId}] Failed to get network stats:`, err); } } catch (err) { /* 静默处理 */ } // --- 移除 console.warn ---
} catch (error) { } catch (error) {
console.error(`[StatusMonitor ${sessionId}] General error fetching server status:`, error); console.error(`[StatusMonitor ${sessionId}] General error fetching server status:`, error);
@@ -256,7 +270,7 @@ export class StatusMonitorService {
output = await this.executeSshCommand(sshClient, 'cat /proc/net/dev'); output = await this.executeSshCommand(sshClient, 'cat /proc/net/dev');
} catch (error) { } catch (error) {
// 如果命令失败,记录警告并返回 null // 如果命令失败,记录警告并返回 null
console.warn("[StatusMonitor] Failed to execute 'cat /proc/net/dev':", error); // console.warn("[StatusMonitor] Failed to execute 'cat /proc/net/dev':", error); // --- 移除 console.warn ---
return null; return null;
} }
// 如果命令成功,继续解析 // 如果命令成功,继续解析
@@ -276,7 +290,7 @@ export class StatusMonitorService {
return Object.keys(stats).length > 0 ? stats : null; return Object.keys(stats).length > 0 ? stats : null;
} catch (parseError) { } catch (parseError) {
// 如果解析失败,记录错误并返回 null // 如果解析失败,记录错误并返回 null
console.error("[StatusMonitor] Error parsing /proc/net/dev output:", parseError); // console.error("[StatusMonitor] Error parsing /proc/net/dev output:", parseError); // --- 移除 console.error ---
return null; return null;
} }
} }
@@ -293,10 +307,10 @@ export class StatusMonitorService {
const interfaceName = output.trim(); const interfaceName = output.trim();
if (interfaceName) return interfaceName; if (interfaceName) return interfaceName;
// 如果 ip route 没返回有效接口名,也尝试 fallback // 如果 ip route 没返回有效接口名,也尝试 fallback
console.warn("[StatusMonitor] 'ip route' did not return a valid interface name. Falling back..."); // console.warn("[StatusMonitor] 'ip route' did not return a valid interface name. Falling back..."); // --- 移除 console.warn ---
} catch (error) { } catch (error) {
console.warn("[StatusMonitor] Failed to get default interface using 'ip route', falling back:", error); // console.warn("[StatusMonitor] Failed to get default interface using 'ip route', falling back:", error); // --- 移除 console.warn ---
// Fallback: 尝试查找第一个非 lo 接口 // Fallback: 尝试查找第一个非 lo 接口
try { try {
const netDevOutput = await this.executeSshCommand(sshClient, 'cat /proc/net/dev'); const netDevOutput = await this.executeSshCommand(sshClient, 'cat /proc/net/dev');
@@ -308,7 +322,7 @@ export class StatusMonitorService {
} }
} }
} catch (fallbackError) { } catch (fallbackError) {
console.error("[StatusMonitor] Failed to fallback to /proc/net/dev for interface:", fallbackError); // console.error("[StatusMonitor] Failed to fallback to /proc/net/dev for interface:", fallbackError); // --- 移除 console.error ---
} }
// Ensure null is returned if both primary and fallback fail within the outer catch // Ensure null is returned if both primary and fallback fail within the outer catch
return null; return null;
@@ -341,6 +355,7 @@ export class StatusMonitorService {
}).on('data', (data: Buffer) => { }).on('data', (data: Buffer) => {
output += data.toString('utf8'); output += data.toString('utf8');
}).stderr.on('data', (data: Buffer) => { }).stderr.on('data', (data: Buffer) => {
// --- 移除 console.warn ---
// console.warn(`[StatusMonitor] Command '${command}' stderr: ${data.toString('utf8').trim()}`); // console.warn(`[StatusMonitor] Command '${command}' stderr: ${data.toString('utf8').trim()}`);
}); });
}); });
@@ -33,7 +33,8 @@ export const settingsController = {
const allowedSettingsKeys = [ const allowedSettingsKeys = [
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration', 'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
'showPopupFileEditor', 'shareFileEditorTabs', 'ipWhitelistEnabled', 'showPopupFileEditor', 'shareFileEditorTabs', 'ipWhitelistEnabled',
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand' // +++ 添加 Docker 设置键 +++ 'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
'statusMonitorIntervalSeconds' // +++ 添加状态监控间隔键 +++
]; ];
const filteredSettings: Record<string, string> = {}; const filteredSettings: Record<string, string> = {};
for (const key in settingsToUpdate) { for (const key in settingsToUpdate) {
+13
View File
@@ -643,6 +643,19 @@
"saveFailed": "Failed to save Docker settings.", "saveFailed": "Failed to save Docker settings.",
"invalidInterval": "Refresh interval must be a positive integer." "invalidInterval": "Refresh interval must be a positive integer."
} }
},
"statusMonitor": {
"title": "Status Monitor Settings",
"refreshIntervalLabel": "Status Refresh Interval (seconds):",
"refreshIntervalHint": "How often to fetch server CPU, memory, disk, etc. status (minimum 1).",
"saveButton": "Save Status Monitor Settings",
"success": {
"saved": "Status monitor settings saved successfully."
},
"error": {
"saveFailed": "Failed to save status monitor settings.",
"invalidInterval": "Refresh interval must be a positive integer."
}
} }
}, },
"common": { "common": {
+13
View File
@@ -643,6 +643,19 @@
"saveFailed": "保存 Docker 设置失败。", "saveFailed": "保存 Docker 设置失败。",
"invalidInterval": "刷新间隔必须是正整数。" "invalidInterval": "刷新间隔必须是正整数。"
} }
},
"statusMonitor": {
"title": "状态监控设置",
"refreshIntervalLabel": "状态刷新间隔 (秒):",
"refreshIntervalHint": "获取服务器 CPU、内存、磁盘等状态的频率(最小为 1)。",
"saveButton": "保存状态监控设置",
"success": {
"saved": "状态监控设置已成功保存。"
},
"error": {
"saveFailed": "保存状态监控设置失败。",
"invalidInterval": "刷新间隔必须是正整数。"
}
} }
}, },
"common": { "common": {
+16 -3
View File
@@ -16,6 +16,7 @@ interface SettingsState {
autoCopyOnSelect?: string; // 'true' or 'false' - 终端选中自动复制 autoCopyOnSelect?: string; // 'true' or 'false' - 终端选中自动复制
dockerStatusIntervalSeconds?: string; // NEW: Docker 状态刷新间隔 (秒) dockerStatusIntervalSeconds?: string; // NEW: Docker 状态刷新间隔 (秒)
dockerDefaultExpand?: string; // NEW: Docker 默认展开详情 'true' or 'false' dockerDefaultExpand?: string; // NEW: Docker 默认展开详情 'true' or 'false'
statusMonitorIntervalSeconds?: string; // NEW: 状态监控轮询间隔 (秒)
// Add other general settings keys here as needed // Add other general settings keys here as needed
[key: string]: string | undefined; // Allow other string settings [key: string]: string | undefined; // Allow other string settings
} }
@@ -74,7 +75,10 @@ export const useSettingsStore = defineStore('settings', () => {
if (settings.value.dockerDefaultExpand === undefined) { if (settings.value.dockerDefaultExpand === undefined) {
settings.value.dockerDefaultExpand = 'false'; // 默认不展开 settings.value.dockerDefaultExpand = 'false'; // 默认不展开
} }
// NEW: Status Monitor interval default
if (settings.value.statusMonitorIntervalSeconds === undefined) {
settings.value.statusMonitorIntervalSeconds = '3'; // 默认 3 秒
}
// --- 语言设置 --- // --- 语言设置 ---
const langFromSettings = settings.value.language; const langFromSettings = settings.value.language;
@@ -124,7 +128,8 @@ export const useSettingsStore = defineStore('settings', () => {
const allowedKeys: Array<keyof SettingsState> = [ const allowedKeys: Array<keyof SettingsState> = [
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration', 'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
'showPopupFileEditor', 'shareFileEditorTabs', 'ipWhitelistEnabled', 'showPopupFileEditor', 'shareFileEditorTabs', 'ipWhitelistEnabled',
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand' // +++ 添加 Docker 设置键 +++ 'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
'statusMonitorIntervalSeconds' // +++ 添加状态监控间隔键 +++
]; ];
if (!allowedKeys.includes(key)) { if (!allowedKeys.includes(key)) {
console.error(`[SettingsStore] 尝试更新不允许的设置键: ${key}`); console.error(`[SettingsStore] 尝试更新不允许的设置键: ${key}`);
@@ -157,7 +162,8 @@ export const useSettingsStore = defineStore('settings', () => {
const allowedKeys: Array<keyof SettingsState> = [ const allowedKeys: Array<keyof SettingsState> = [
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration', 'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
'showPopupFileEditor', 'shareFileEditorTabs', 'ipWhitelistEnabled', 'showPopupFileEditor', 'shareFileEditorTabs', 'ipWhitelistEnabled',
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand' // +++ 添加 Docker 设置键 +++ 'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
'statusMonitorIntervalSeconds' // +++ 添加状态监控间隔键 +++
]; ];
const filteredUpdates: Partial<SettingsState> = {}; const filteredUpdates: Partial<SettingsState> = {};
let languageUpdate: 'en' | 'zh' | undefined = undefined; let languageUpdate: 'en' | 'zh' | undefined = undefined;
@@ -224,6 +230,12 @@ export const useSettingsStore = defineStore('settings', () => {
return settings.value.dockerDefaultExpand === 'true'; return settings.value.dockerDefaultExpand === 'true';
}); });
// NEW: Getter for Status Monitor interval, returning number
const statusMonitorIntervalSecondsNumber = computed(() => {
const val = parseInt(settings.value.statusMonitorIntervalSeconds || '3', 10);
return isNaN(val) || val <= 0 ? 3 : val; // Fallback to 3 if invalid
});
return { return {
settings, // 只包含通用设置 settings, // 只包含通用设置
isLoading, isLoading,
@@ -234,6 +246,7 @@ export const useSettingsStore = defineStore('settings', () => {
ipWhitelistEnabled, // 暴露 IP 白名单启用状态 ipWhitelistEnabled, // 暴露 IP 白名单启用状态
autoCopyOnSelectBoolean, autoCopyOnSelectBoolean,
dockerDefaultExpandBoolean, // +++ 暴露 Docker 默认展开 getter +++ dockerDefaultExpandBoolean, // +++ 暴露 Docker 默认展开 getter +++
statusMonitorIntervalSecondsNumber, // +++ 暴露状态监控间隔 getter +++
// 移除外观相关的 getters 和 actions // 移除外观相关的 getters 和 actions
loadInitialSettings, loadInitialSettings,
updateSetting, updateSetting,
+42 -1
View File
@@ -156,6 +156,21 @@
</div> </div>
<!-- END: Docker Settings Section --> <!-- END: Docker Settings Section -->
<!-- NEW: Status Monitor Settings Section -->
<div class="settings-section">
<h2>{{ t('settings.statusMonitor.title') }}</h2>
<form @submit.prevent="handleUpdateStatusMonitorInterval">
<div class="form-group">
<label for="statusMonitorInterval">{{ t('settings.statusMonitor.refreshIntervalLabel') }}</label>
<input type="number" id="statusMonitorInterval" v-model.number="statusMonitorIntervalLocal" min="1" step="1" required>
<small>{{ t('settings.statusMonitor.refreshIntervalHint') }}</small>
</div>
<button type="submit" :disabled="statusMonitorLoading">{{ statusMonitorLoading ? $t('common.saving') : t('settings.statusMonitor.saveButton') }}</button>
<p v-if="statusMonitorMessage" :class="{ 'success-message': statusMonitorSuccess, 'error-message': !statusMonitorSuccess }">{{ statusMonitorMessage }}</p>
</form>
</div>
<!-- END: Status Monitor Settings Section -->
<div class="settings-section"> <div class="settings-section">
<h2>{{ $t('settings.ipWhitelist.title') }}</h2> <h2>{{ $t('settings.ipWhitelist.title') }}</h2>
<p>{{ $t('settings.ipWhitelist.description') }}</p> <p>{{ $t('settings.ipWhitelist.description') }}</p>
@@ -252,7 +267,7 @@ const { t } = useI18n();
// --- Reactive state from store --- // --- Reactive state from store ---
// 使用 storeToRefs 获取响应式 getter,包括 language // 使用 storeToRefs 获取响应式 getter,包括 language
const { settings, isLoading: settingsLoading, error: settingsError, showPopupFileEditorBoolean, shareFileEditorTabsBoolean, autoCopyOnSelectBoolean, dockerDefaultExpandBoolean, language: storeLanguage } = storeToRefs(settingsStore); // +++ 添加 dockerDefaultExpandBoolean 和 language getter +++ const { settings, isLoading: settingsLoading, error: settingsError, showPopupFileEditorBoolean, shareFileEditorTabsBoolean, autoCopyOnSelectBoolean, dockerDefaultExpandBoolean, statusMonitorIntervalSecondsNumber, language: storeLanguage } = storeToRefs(settingsStore); // +++ 添加 statusMonitorIntervalSecondsNumber getter +++
// --- Local state for forms --- // --- Local state for forms ---
const ipWhitelistInput = ref(''); const ipWhitelistInput = ref('');
@@ -290,6 +305,10 @@ const dockerExpandDefault = ref(false); // 本地状态,用于 Docker 默认
const dockerSettingsLoading = ref(false); const dockerSettingsLoading = ref(false);
const dockerSettingsMessage = ref(''); const dockerSettingsMessage = ref('');
const dockerSettingsSuccess = ref(false); const dockerSettingsSuccess = ref(false);
const statusMonitorIntervalLocal = ref(3); // 本地状态,用于状态监控间隔 v-model
const statusMonitorLoading = ref(false);
const statusMonitorMessage = ref('');
const statusMonitorSuccess = ref(false);
// --- Watcher to sync local form state with store state --- // --- Watcher to sync local form state with store state ---
@@ -308,6 +327,7 @@ watch(settings, (newSettings, oldSettings) => {
autoCopyEnabled.value = autoCopyOnSelectBoolean.value; // 同步选中即复制状态 autoCopyEnabled.value = autoCopyOnSelectBoolean.value; // 同步选中即复制状态
dockerInterval.value = parseInt(newSettings.dockerStatusIntervalSeconds || '2', 10); // 同步 Docker 间隔 dockerInterval.value = parseInt(newSettings.dockerStatusIntervalSeconds || '2', 10); // 同步 Docker 间隔
dockerExpandDefault.value = dockerDefaultExpandBoolean.value; // 同步 Docker 默认展开状态 dockerExpandDefault.value = dockerDefaultExpandBoolean.value; // 同步 Docker 默认展开状态
statusMonitorIntervalLocal.value = statusMonitorIntervalSecondsNumber.value; // 同步状态监控间隔
}, { deep: true, immediate: true }); // immediate: true to run on initial load }, { deep: true, immediate: true }); // immediate: true to run on initial load
@@ -398,6 +418,27 @@ const handleUpdateDockerSettings = async () => {
} }
}; };
// --- Status Monitor interval setting method ---
const handleUpdateStatusMonitorInterval = async () => {
statusMonitorLoading.value = true;
statusMonitorMessage.value = '';
statusMonitorSuccess.value = false;
try {
const intervalValue = statusMonitorIntervalLocal.value;
if (isNaN(intervalValue) || intervalValue < 1 || !Number.isInteger(intervalValue)) {
throw new Error(t('settings.statusMonitor.error.invalidInterval')); // 需要添加翻译
}
await settingsStore.updateSetting('statusMonitorIntervalSeconds', String(intervalValue)); // 保存为字符串
statusMonitorMessage.value = t('settings.statusMonitor.success.saved'); // 需要添加翻译
statusMonitorSuccess.value = true;
} catch (error: any) {
console.error('更新状态监控间隔失败:', error);
statusMonitorMessage.value = error.message || t('settings.statusMonitor.error.saveFailed'); // 需要添加翻译
statusMonitorSuccess.value = false;
} finally {
statusMonitorLoading.value = false;
}
};
// --- 外观设置 --- // --- 外观设置 ---
const openStyleCustomizer = () => { const openStyleCustomizer = () => {