From 0e396a92fae2345f4d75672552ad558a9eb527eb Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Thu, 15 May 2025 00:26:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=8A=B6=E6=80=81=E7=9B=91=E8=A7=86?= =?UTF-8?q?=E5=99=A8=E6=B7=BB=E5=8A=A0IP=E5=9C=B0=E5=9D=80=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/auth/ipBlacklistCheck.middleware.ts | 2 +- .../src/connections/connections.controller.ts | 4 +- .../src/connections/connections.routes.ts | 10 +- .../backend/src/database/schema.registry.ts | 10 +- packages/backend/src/index.ts | 26 +-- .../quick-commands.controller.ts | 2 +- .../src/repositories/settings.repository.ts | 6 +- .../src/services/connection.service.ts | 6 +- .../src/services/import-export.service.ts | 8 +- .../notification.dispatcher.service.ts | 2 +- .../src/services/quick-commands.service.ts | 6 +- .../backend/src/services/settings.service.ts | 58 +++++-- packages/backend/src/services/sftp.service.ts | 10 +- .../src/settings/settings.controller.ts | 160 ++++++++++-------- .../backend/src/settings/settings.routes.ts | 9 +- .../src/ssh-suspend/ssh-suspend.controller.ts | 12 +- packages/backend/src/types/settings.types.ts | 1 + packages/backend/src/websocket.ts | 4 +- packages/frontend/src/App.vue | 10 +- .../components/AddEditQuickCommandForm.vue | 16 +- .../src/components/CommandHistoryMenu.vue | 8 +- .../src/components/CommandInputBar.vue | 10 +- .../src/components/FileEditorContainer.vue | 18 +- .../src/components/FileEditorOverlay.vue | 4 +- .../frontend/src/components/FileManager.vue | 14 +- .../src/components/FileManagerContextMenu.vue | 2 +- .../src/components/LayoutConfigurator.vue | 10 +- .../src/components/LayoutRenderer.vue | 18 +- .../frontend/src/components/MonacoEditor.vue | 2 +- .../src/components/NotificationSettings.vue | 2 +- .../src/components/RemoteDesktopModal.vue | 2 +- .../src/components/SshKeySelector.vue | 4 +- .../frontend/src/components/StatusCharts.vue | 6 +- .../frontend/src/components/StatusMonitor.vue | 47 +++++ packages/frontend/src/components/TagInput.vue | 6 +- packages/frontend/src/components/Terminal.vue | 2 +- .../src/components/VirtualKeyboard.vue | 2 +- .../components/settings/AppearanceSection.vue | 6 +- .../settings/WorkspaceSettingsSection.vue | 27 ++- .../file-manager/useFileManagerContextMenu.ts | 10 +- .../composables/settings/useSystemSettings.ts | 2 +- .../settings/useWorkspaceSettings.ts | 38 ++++- .../src/composables/useSftpActions.ts | 6 +- .../src/composables/useSidebarResize.ts | 2 +- packages/frontend/src/locales/en-US.json | 8 +- packages/frontend/src/locales/ja-JP.json | 8 +- packages/frontend/src/locales/zh-CN.json | 8 +- packages/frontend/src/main.ts | 16 +- packages/frontend/src/stores/auth.store.ts | 6 +- .../frontend/src/stores/fileEditor.store.ts | 10 +- .../src/stores/quickCommands.store.ts | 12 +- packages/frontend/src/stores/session.store.ts | 18 +- .../stores/session/actions/sessionActions.ts | 10 +- .../session/actions/sshSuspendActions.ts | 28 +-- packages/frontend/src/stores/session/types.ts | 10 +- .../frontend/src/stores/settings.store.ts | 92 +++++----- packages/frontend/src/utils/apiClient.ts | 4 +- packages/frontend/src/views/DashboardView.vue | 24 +-- .../frontend/src/views/QuickCommandsView.vue | 2 +- packages/frontend/src/views/SettingsView.vue | 4 +- 60 files changed, 518 insertions(+), 352 deletions(-) diff --git a/packages/backend/src/auth/ipBlacklistCheck.middleware.ts b/packages/backend/src/auth/ipBlacklistCheck.middleware.ts index f0ce580..b308b6b 100644 --- a/packages/backend/src/auth/ipBlacklistCheck.middleware.ts +++ b/packages/backend/src/auth/ipBlacklistCheck.middleware.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { ipBlacklistService } from '../services/ip-blacklist.service'; -import { settingsService } from '../services/settings.service'; // <-- Import settingsService +import { settingsService } from '../services/settings.service'; /** * IP 黑名单检查中间件 diff --git a/packages/backend/src/connections/connections.controller.ts b/packages/backend/src/connections/connections.controller.ts index a0aaf2d..88ec5e7 100644 --- a/packages/backend/src/connections/connections.controller.ts +++ b/packages/backend/src/connections/connections.controller.ts @@ -1,9 +1,9 @@ import { Request, Response } from 'express'; import * as ConnectionService from '../services/connection.service'; import * as SshService from '../services/ssh.service'; -import * as GuacamoleService from '../services/guacamole.service'; // 导入 GuacamoleService +import * as GuacamoleService from '../services/guacamole.service'; import * as ImportExportService from '../services/import-export.service'; -import * as ConnectionRepository from '../repositories/connection.repository'; // +++ 导入 ConnectionRepository +++ +import * as ConnectionRepository from '../repositories/connection.repository'; diff --git a/packages/backend/src/connections/connections.routes.ts b/packages/backend/src/connections/connections.routes.ts index f1cfd78..801ae0d 100644 --- a/packages/backend/src/connections/connections.routes.ts +++ b/packages/backend/src/connections/connections.routes.ts @@ -11,11 +11,11 @@ import { testUnsavedConnection, exportConnections, importConnections, - getRdpSessionToken, // Import the new controller function - getVncSessionToken, // Import the VNC session token controller function - cloneConnection, // +++ Import the clone controller function +++ - // updateConnectionTags, // No longer directly used by primary flow - addTagToConnections // +++ Import the new controller function for adding tag to multiple connections +++ + getRdpSessionToken, + getVncSessionToken, + cloneConnection, + + addTagToConnections } from './connections.controller'; const router = Router(); diff --git a/packages/backend/src/database/schema.registry.ts b/packages/backend/src/database/schema.registry.ts index 855c4bd..a23f1d5 100644 --- a/packages/backend/src/database/schema.registry.ts +++ b/packages/backend/src/database/schema.registry.ts @@ -2,9 +2,9 @@ import { Database } from 'sqlite3'; import * as schemaSql from './schema'; import * as appearanceRepository from '../repositories/appearance.repository'; import * as terminalThemeRepository from '../repositories/terminal-theme.repository'; -import * as settingsRepository from '../repositories/settings.repository'; // <-- Import settings repository +import * as settingsRepository from '../repositories/settings.repository'; import { presetTerminalThemes } from '../config/preset-themes-definition'; -import { runDb } from './connection'; // Import runDb for init functions +import { runDb } from './connection'; /** * Interface describing a database table definition for initialization. @@ -12,12 +12,12 @@ import { runDb } from './connection'; // Import runDb for init functions export interface TableDefinition { name: string; sql: string; - init?: (db: Database) => Promise; // Optional initialization function + init?: (db: Database) => Promise; } -// --- Initialization Functions --- -// Remove the old initSettingsTable function, as the logic is now in the repository + + /** * Initializes preset terminal themes. diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 96ff7d9..e956dbb 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -26,15 +26,15 @@ if (dataConfigResultGlobal.error && (dataConfigResultGlobal.error as NodeJS.Errn } else if (!dataConfigResultGlobal.error) { console.log(`[ENV Init Early] Loaded environment variables from data .env file: ${dataEnvPathGlobal}`); } -// --- 结束环境变量的早期加载 --- + import express = require('express'); import { Request, Response, NextFunction, RequestHandler } from 'express'; import http from 'http'; -// import fs from 'fs'; // Moved up -// import path from 'path'; // Moved up + + import crypto from 'crypto'; -// import dotenv from 'dotenv'; // Moved up + import session from 'express-session'; import sessionFileStore from 'session-file-store'; import { getDbInstance } from './database/connection'; @@ -50,18 +50,18 @@ import commandHistoryRoutes from './command-history/command-history.routes'; import quickCommandsRoutes from './quick-commands/quick-commands.routes'; import terminalThemeRoutes from './terminal-themes/terminal-theme.routes'; import appearanceRoutes from './appearance/appearance.routes'; -import sshKeysRouter from './ssh_keys/ssh_keys.routes'; // +++ Import SSH Key routes +++ -import quickCommandTagRoutes from './quick-command-tags/quick-command-tag.routes'; // +++ Import Quick Command Tag routes +++ -import sshSuspendRouter from './ssh-suspend/ssh-suspend.routes'; // +++ Import SSH Suspend routes +++ +import sshKeysRouter from './ssh_keys/ssh_keys.routes'; +import quickCommandTagRoutes from './quick-command-tags/quick-command-tag.routes'; +import sshSuspendRouter from './ssh-suspend/ssh-suspend.routes'; import { initializeWebSocket } from './websocket'; import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware'; -// --- 初始化通知系统 (导入即初始化单例) --- -import './services/event.service'; // 确保事件服务被加载 -import './services/notification.processor.service'; // 确保处理器被加载并监听事件 -import './services/notification.dispatcher.service'; // 确保分发器被加载并监听处理器事件 -// --- 结束通知系统初始化 --- -// --- 环境变量和密钥初始化 --- + +import './services/event.service'; +import './services/notification.processor.service'; +import './services/notification.dispatcher.service'; + + // --- 全局错误处理 --- // 捕获未处理的 Promise Rejection diff --git a/packages/backend/src/quick-commands/quick-commands.controller.ts b/packages/backend/src/quick-commands/quick-commands.controller.ts index 978b403..6862063 100644 --- a/packages/backend/src/quick-commands/quick-commands.controller.ts +++ b/packages/backend/src/quick-commands/quick-commands.controller.ts @@ -181,7 +181,7 @@ export const assignTagToCommands = async (req: Request, res: Response): Promise< try { // 调用 Service 函数处理批量分配 - console.log(`[Controller] assignTagToCommands: Received commandIds: ${JSON.stringify(commandIds)}, tagId: ${tagId}`); // +++ 添加日志 +++ + console.log(`[Controller] assignTagToCommands: Received commandIds: ${JSON.stringify(commandIds)}, tagId: ${tagId}`); await QuickCommandsService.assignTagToCommands(commandIds, tagId); res.status(200).json({ success: true, message: `标签 ${tagId} 已成功尝试关联到 ${commandIds.length} 个指令。` }); } catch (error: any) { diff --git a/packages/backend/src/repositories/settings.repository.ts b/packages/backend/src/repositories/settings.repository.ts index 9aee8fe..f3bc860 100644 --- a/packages/backend/src/repositories/settings.repository.ts +++ b/packages/backend/src/repositories/settings.repository.ts @@ -261,9 +261,9 @@ export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise< statusMonitorIntervalSeconds: '3', [SIDEBAR_CONFIG_KEY]: JSON.stringify(defaultSidebarPanesStructure), [CAPTCHA_CONFIG_KEY]: JSON.stringify(defaultCaptchaSettings), - timezone: 'UTC', // 添加时区默认值 - terminalScrollbackLimit: '5000', // 添加终端回滚行数默认值 - terminalEnableRightClickPaste: 'true', // 添加终端右键粘贴默认值 + timezone: 'UTC', // 时区默认值 + terminalScrollbackLimit: '5000', // 终端回滚行数默认值 + terminalEnableRightClickPaste: 'true', // 终端右键粘贴默认值 }; const nowSeconds = Math.floor(Date.now() / 1000); const sqlInsertOrIgnore = `INSERT OR IGNORE INTO settings (key, value, created_at, updated_at) VALUES (?, ?, ?, ?)`; diff --git a/packages/backend/src/services/connection.service.ts b/packages/backend/src/services/connection.service.ts index 618dea7..955f6d5 100644 --- a/packages/backend/src/services/connection.service.ts +++ b/packages/backend/src/services/connection.service.ts @@ -1,19 +1,19 @@ import * as ConnectionRepository from '../repositories/connection.repository'; import { encrypt, decrypt } from '../utils/crypto'; import { AuditLogService } from './audit.service'; -import * as SshKeyService from './ssh_key.service'; // +++ Import SshKeyService +++ +import * as SshKeyService from './ssh_key.service'; import { ConnectionBase, ConnectionWithTags, CreateConnectionInput, UpdateConnectionInput, FullConnectionData -} from '../types/connection.types'; // 从集中类型文件导入 +} from '../types/connection.types'; export type { ConnectionBase, ConnectionWithTags, CreateConnectionInput, UpdateConnectionInput }; -const auditLogService = new AuditLogService(); // 实例化 AuditLogService +const auditLogService = new AuditLogService(); /** * 获取所有连接(包含标签) diff --git a/packages/backend/src/services/import-export.service.ts b/packages/backend/src/services/import-export.service.ts index 798cf20..c860460 100644 --- a/packages/backend/src/services/import-export.service.ts +++ b/packages/backend/src/services/import-export.service.ts @@ -1,10 +1,10 @@ import * as ConnectionRepository from '../repositories/connection.repository'; import * as ProxyRepository from '../repositories/proxy.repository'; -import * as TagService from '../services/tag.service'; // +++ 导入标签服务 +++ +import * as TagService from '../services/tag.service'; import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection'; -import { decrypt, getEncryptionKeyBuffer as getCryptoKeyBuffer } from '../utils/crypto'; // For decrypting connection details -import { getAllDecryptedSshKeys, DecryptedSshKeyDetails } from '../services/ssh_key.service'; // 静态导入, SshKeyData -> DecryptedSshKeyDetails +import { decrypt, getEncryptionKeyBuffer as getCryptoKeyBuffer } from '../utils/crypto'; +import { getAllDecryptedSshKeys, DecryptedSshKeyDetails } from '../services/ssh_key.service'; import archiver from 'archiver'; archiver.registerFormat('zip-encrypted', require("archiver-zip-encrypted")); @@ -13,7 +13,7 @@ archiver.registerFormat('zip-encrypted', require("archiver-zip-encrypted")); interface ImportedConnectionData { name: string; - type: 'SSH' | 'RDP' | 'VNC'; // Add type field + type: 'SSH' | 'RDP' | 'VNC'; host: string; port: number; username: string; diff --git a/packages/backend/src/services/notification.dispatcher.service.ts b/packages/backend/src/services/notification.dispatcher.service.ts index 1daced5..7701ff5 100644 --- a/packages/backend/src/services/notification.dispatcher.service.ts +++ b/packages/backend/src/services/notification.dispatcher.service.ts @@ -1,4 +1,4 @@ -import notificationProcessorService, { ProcessedNotification } from './notification.processor.service'; // 导入导出的接口 +import notificationProcessorService, { ProcessedNotification } from './notification.processor.service'; import { NotificationChannelType, NotificationChannelConfig } from '../types/notification.types'; // 1. 定义通知发送器接口 diff --git a/packages/backend/src/services/quick-commands.service.ts b/packages/backend/src/services/quick-commands.service.ts index 9af819b..4a02cbf 100644 --- a/packages/backend/src/services/quick-commands.service.ts +++ b/packages/backend/src/services/quick-commands.service.ts @@ -1,6 +1,6 @@ import * as QuickCommandsRepository from '../repositories/quick-commands.repository'; -import { QuickCommandWithTags } from '../repositories/quick-commands.repository'; // Import the type with tags -import * as QuickCommandTagRepository from '../repositories/quick-command-tag.repository'; // Import the new tag repository +import { QuickCommandWithTags } from '../repositories/quick-commands.repository'; +import * as QuickCommandTagRepository from '../repositories/quick-command-tag.repository'; // 定义排序类型 export type QuickCommandSortBy = 'name' | 'usage_count'; @@ -119,7 +119,7 @@ export const assignTagToCommands = async (commandIds: number[], tagId: number): // 调用 Repository 函数执行批量关联 // 注意:这里需要导入 QuickCommandTagRepository - console.log(`[Service] assignTagToCommands: Calling repo with commandIds: ${JSON.stringify(commandIds)}, tagId: ${tagId}`); // +++ 添加日志 +++ + console.log(`[Service] assignTagToCommands: Calling repo with commandIds: ${JSON.stringify(commandIds)}, tagId: ${tagId}`); await QuickCommandTagRepository.addTagToCommands(commandIds, tagId); console.log(`[Service] assignTagToCommands: Repo call finished for tag ${tagId}.`); // +++ 修改日志 +++ // 可以在这里添加额外的业务逻辑,例如发送事件通知等 diff --git a/packages/backend/src/services/settings.service.ts b/packages/backend/src/services/settings.service.ts index f059a77..62c99bd 100644 --- a/packages/backend/src/services/settings.service.ts +++ b/packages/backend/src/services/settings.service.ts @@ -33,7 +33,8 @@ const DEFAULT_STATUS_MONITOR_INTERVAL_SECONDS = 3; // 默认状态监控间隔 const IP_BLACKLIST_ENABLED_KEY = 'ipBlacklistEnabled'; // IP 黑名单启用设置键 const SHOW_CONNECTION_TAGS_KEY = 'showConnectionTags'; // 连接标签显示设置键 const SHOW_QUICK_COMMAND_TAGS_KEY = 'showQuickCommandTags'; // 快捷指令标签显示设置键 - +const SHOW_STATUS_MONITOR_IP_ADDRESS_KEY = 'showStatusMonitorIpAddress'; // 状态监视器IP显示设置键 + export const settingsService = { /** * 获取所有设置项 @@ -127,7 +128,7 @@ export const settingsService = { // 出错时返回默认值 true (安全起见,默认启用) return true; } - }, // *** 确保这里有逗号 *** + }, /** * 获取焦点切换顺序 @@ -190,7 +191,7 @@ export const settingsService = { console.error(`[Service] Error calling settingsRepository.setSetting for key ${FOCUS_SEQUENCE_KEY}:`, error); throw new Error('Failed to save focus switcher sequence.'); } - }, // *** 确保这里有逗号 *** + }, /** * 获取导航栏可见性设置 @@ -208,7 +209,7 @@ export const settingsService = { // 出错时返回默认值 true return true; } - }, // *** 确保这里有逗号 *** + }, /** * 设置导航栏可见性 @@ -225,7 +226,7 @@ export const settingsService = { console.error(`[Service] Error calling settingsRepository.setSetting for key ${NAV_BAR_VISIBLE_KEY}:`, error); throw new Error('Failed to save nav bar visibility setting.'); } - }, // *** 确保这里有逗号 *** + }, /** * 获取布局树设置 @@ -241,7 +242,7 @@ export const settingsService = { console.error(`[Service] Error getting layout tree setting (key: ${LAYOUT_TREE_KEY}):`, error); return null; // 出错时返回 null } - }, // *** 确保这里有逗号 *** + }, /** * 设置布局树 @@ -265,7 +266,7 @@ export const settingsService = { console.error(`[Service] Error calling settingsRepository.setSetting for key ${LAYOUT_TREE_KEY}:`, error); throw new Error('Failed to save layout tree setting.'); } - }, // *** 确保这里有逗号 *** + }, /** * 获取终端选中自动复制设置 @@ -283,7 +284,7 @@ export const settingsService = { // 出错时返回默认值 false return false; } - }, // *** 确保这里有逗号 *** + }, /** * 设置终端选中自动复制 @@ -300,7 +301,7 @@ export const settingsService = { 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.'); } - }, // *** 确保这里有逗号 *** + }, /** * 获取状态监控轮询间隔 (秒) @@ -327,7 +328,7 @@ export const settingsService = { } // 返回默认值 return DEFAULT_STATUS_MONITOR_INTERVAL_SECONDS; - }, // *** 确保这里有逗号 *** + }, /** * 设置状态监控轮询间隔 (秒) @@ -349,7 +350,7 @@ export const settingsService = { 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.'); } - }, // *** 确保这里有逗号 *** + }, // --- Sidebar Config Specific Functions --- @@ -493,9 +494,9 @@ export const settingsService = { return valueStr !== 'false'; } catch (error) { console.error(`[Service] Error getting show connection tags setting (key: ${SHOW_CONNECTION_TAGS_KEY}):`, error); - return true; // 默认返回 true + return true; } - }, // *** 确保这里有逗号 *** + }, async setShowConnectionTags(enabled: boolean): Promise { console.log(`[Service] setShowConnectionTags called with: ${enabled}`); @@ -508,7 +509,7 @@ export const settingsService = { console.error(`[Service] Error calling settingsRepository.setSetting for key ${SHOW_CONNECTION_TAGS_KEY}:`, error); throw new Error('Failed to save show connection tags setting.'); } - }, // *** 确保这里有逗号 *** + }, // --- Show Quick Command Tags --- async getShowQuickCommandTags(): Promise { @@ -520,9 +521,9 @@ export const settingsService = { return valueStr !== 'false'; } catch (error) { console.error(`[Service] Error getting show quick command tags setting (key: ${SHOW_QUICK_COMMAND_TAGS_KEY}):`, error); - return true; // 默认返回 true + return true; } - }, // *** 确保这里有逗号 *** + }, async setShowQuickCommandTags(enabled: boolean): Promise { console.log(`[Service] setShowQuickCommandTags called with: ${enabled}`); @@ -535,6 +536,29 @@ export const settingsService = { console.error(`[Service] Error calling settingsRepository.setSetting for key ${SHOW_QUICK_COMMAND_TAGS_KEY}:`, error); throw new Error('Failed to save show quick command tags setting.'); } - } // <-- No comma after the last method + }, + + // --- Show Status Monitor IP Address --- + async getShowStatusMonitorIpAddress(): Promise { + console.log(`[Service] Attempting to get setting for key: ${SHOW_STATUS_MONITOR_IP_ADDRESS_KEY}`); + try { + const valueStr = await settingsRepository.getSetting(SHOW_STATUS_MONITOR_IP_ADDRESS_KEY); + // 默认显示 (true),所以只有当值为 'false' 时才返回 false + return valueStr !== 'false'; + } catch (error) { + console.error(`[Service] Error getting show status monitor IP address setting (key: ${SHOW_STATUS_MONITOR_IP_ADDRESS_KEY}):`, error); + return true; + } + }, + + async setShowStatusMonitorIpAddress(enabled: boolean): Promise { + try { + const valueStr = String(enabled); + await settingsRepository.setSetting(SHOW_STATUS_MONITOR_IP_ADDRESS_KEY, valueStr); + } catch (error) { + console.error(`[Service] Error calling settingsRepository.setSetting for key ${SHOW_STATUS_MONITOR_IP_ADDRESS_KEY}:`, error); + throw new Error('Failed to save show status monitor IP address setting.'); + } + } }; // <-- End of settingsService object definition diff --git a/packages/backend/src/services/sftp.service.ts b/packages/backend/src/services/sftp.service.ts index 308ab8c..40bb1a5 100644 --- a/packages/backend/src/services/sftp.service.ts +++ b/packages/backend/src/services/sftp.service.ts @@ -1,9 +1,9 @@ -import { Client, SFTPWrapper, Stats, WriteStream } from 'ssh2'; // Import WriteStream (Removed Dirent) +import { Client, SFTPWrapper, Stats, WriteStream } from 'ssh2'; import { WebSocket } from 'ws'; -import { ClientState, AuthenticatedWebSocket } from '../websocket/types'; // 导入统一的 ClientState 和 AuthenticatedWebSocket -import * as pathModule from 'path'; // +++ Import path module +++ -import * as jschardet from 'jschardet'; // +++ Import jschardet +++ -import * as iconv from 'iconv-lite'; // +++ Import iconv-lite +++ +import { ClientState, AuthenticatedWebSocket } from '../websocket/types'; +import * as pathModule from 'path'; +import * as jschardet from 'jschardet'; +import * as iconv from 'iconv-lite'; // +++ 导入新类型 +++ import { SftpCompressRequestPayload, diff --git a/packages/backend/src/settings/settings.controller.ts b/packages/backend/src/settings/settings.controller.ts index 658a6c0..5fd11c9 100644 --- a/packages/backend/src/settings/settings.controller.ts +++ b/packages/backend/src/settings/settings.controller.ts @@ -1,14 +1,14 @@ import { Request, Response } from 'express'; import { settingsService } from '../services/settings.service'; import { AuditLogService } from '../services/audit.service'; -import { NotificationService } from '../services/notification.service'; // 添加导入 +import { NotificationService } from '../services/notification.service'; import { ipBlacklistService } from '../services/ip-blacklist.service'; -import { exportConnectionsAsEncryptedZip } from '../services/import-export.service'; // Import the new export service -import { UpdateSidebarConfigDto, UpdateCaptchaSettingsDto, CaptchaSettings } from '../types/settings.types'; // <-- Import CAPTCHA types -import i18next from '../i18n'; // +++ Import i18next +++ +import { exportConnectionsAsEncryptedZip } from '../services/import-export.service'; +import { UpdateSidebarConfigDto, UpdateCaptchaSettingsDto, CaptchaSettings } from '../types/settings.types'; +import i18next from '../i18n'; const auditLogService = new AuditLogService(); -const notificationService = new NotificationService(); // 添加实例 +const notificationService = new NotificationService(); export const settingsController = { /** @@ -39,23 +39,24 @@ export const settingsController = { 'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration', 'showPopupFileEditor', 'shareFileEditorTabs', 'ipWhitelistEnabled', 'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand', - 'statusMonitorIntervalSeconds', // +++ 添加状态监控间隔键 +++ - 'workspaceSidebarPersistent', // +++ 添加侧边栏固定键 +++ - 'showPopupFileManager', // +++ 添加弹窗文件管理器设置键 +++ - 'sidebarPaneWidths', // +++ 添加侧边栏宽度对象键 +++ - 'fileManagerRowSizeMultiplier', // +++ 添加文件管理器行大小键 +++ - 'fileManagerColWidths', // +++ 添加文件管理器列宽键 +++ - 'commandInputSyncTarget', // +++ 添加命令输入同步目标键 +++ - 'timezone', // 添加时区键 - 'rdpModalWidth', // 添加 RDP 模态框宽度键 - 'rdpModalHeight', // 添加 RDP 模态框高度键 - 'vncModalWidth', // 添加 VNC 模态框宽度键 - 'vncModalHeight', // 添加 VNC 模态框高度键 + 'statusMonitorIntervalSeconds', // +++ 状态监控间隔键 +++ + 'workspaceSidebarPersistent', // +++ 侧边栏固定键 +++ + 'showPopupFileManager', // +++ 弹窗文件管理器设置键 +++ + 'sidebarPaneWidths', // +++ 侧边栏宽度对象键 +++ + 'fileManagerRowSizeMultiplier', // +++ 文件管理器行大小键 +++ + 'fileManagerColWidths', // +++ 文件管理器列宽键 +++ + 'commandInputSyncTarget', // +++ 命令输入同步目标键 +++ + 'timezone', // 时区键 + 'rdpModalWidth', // RDP 模态框宽度键 + 'rdpModalHeight', // RDP 模态框高度键 + 'vncModalWidth', // VNC 模态框宽度键 + 'vncModalHeight', // VNC 模态框高度键 'ipBlacklistEnabled', // <-- 添加 IP 黑名单启用键 - 'layoutLocked', // +++ 添加布局锁定键 +++ - 'terminalScrollbackLimit', // 添加终端回滚行数键 - 'fileManagerShowDeleteConfirmation', // 添加文件管理器删除确认键 - 'terminalEnableRightClickPaste' // 添加终端右键粘贴键 + 'layoutLocked', // +++ 布局锁定键 +++ + 'terminalScrollbackLimit', // 终端回滚行数键 + 'fileManagerShowDeleteConfirmation', // 文件管理器删除确认键 + 'terminalEnableRightClickPaste', // 终端右键粘贴键 + 'showStatusMonitorIpAddress' // 添加状态监视器IP显示键 (与服务层和前端统一) ]; const filteredSettings: Record = {}; for (const key in settingsToUpdate) { @@ -89,9 +90,7 @@ export const settingsController = { */ async getFocusSwitcherSequence(req: Request, res: Response): Promise { try { - console.log('[控制器] 收到获取焦点切换顺序的请求。'); const sequence = await settingsService.getFocusSwitcherSequence(); - console.log('[控制器] 向客户端发送焦点切换顺序:', JSON.stringify(sequence)); res.json(sequence); } catch (error: any) { console.error('[控制器] 获取焦点切换顺序时出错:', error); @@ -103,7 +102,6 @@ export const settingsController = { * 设置焦点切换顺序 */ async setFocusSwitcherSequence(req: Request, res: Response): Promise { - console.log('[控制器] 收到设置焦点切换顺序的请求。'); try { // +++ 修改:获取请求体并验证其是否符合 FocusSwitcherFullConfig 结构 +++ const fullConfig = req.body; @@ -121,14 +119,14 @@ export const settingsController = { return; } - console.log('[控制器] 使用验证后的完整配置调用 settingsService.setFocusSwitcherSequence...'); + // +++ 传递验证后的 fullConfig 给服务层 +++ await settingsService.setFocusSwitcherSequence(fullConfig); - console.log('[控制器] settingsService.setFocusSwitcherSequence 成功完成。'); + - console.log('[控制器] 记录审计操作: FOCUS_SWITCHER_SEQUENCE_UPDATED'); + - console.log('[控制器] 发送成功响应。'); + res.status(200).json({ message: '焦点切换顺序已成功更新' }); } catch (error: any) { console.error('[控制器] 设置焦点切换顺序时出错:', error); @@ -145,9 +143,7 @@ export const settingsController = { */ async getNavBarVisibility(req: Request, res: Response): Promise { try { - console.log('[控制器] 收到获取导航栏可见性的请求。'); const isVisible = await settingsService.getNavBarVisibility(); - console.log(`[控制器] 向客户端发送导航栏可见性: ${isVisible}`); res.json({ visible: isVisible }); } catch (error: any) { console.error('[控制器] 获取导航栏可见性时出错:', error); @@ -159,7 +155,6 @@ export const settingsController = { * 设置导航栏可见性 */ async setNavBarVisibility(req: Request, res: Response): Promise { - console.log('[控制器] 收到设置导航栏可见性的请求。'); try { const { visible } = req.body; console.log('[控制器] 请求体 visible:', visible); @@ -170,12 +165,12 @@ export const settingsController = { return; } - console.log('[控制器] 调用 settingsService.setNavBarVisibility...'); + await settingsService.setNavBarVisibility(visible); - console.log('[控制器] settingsService.setNavBarVisibility 成功完成。'); + - console.log('[控制器] 发送成功响应。'); + res.status(200).json({ message: '导航栏可见性已成功更新' }); } catch (error: any) { console.error('[控制器] 设置导航栏可见性时出错:', error); @@ -188,19 +183,18 @@ export const settingsController = { */ async getLayoutTree(req: Request, res: Response): Promise { try { - console.log('[控制器] 收到获取布局树的请求。'); const layoutJson = await settingsService.getLayoutTree(); if (layoutJson) { try { const layout = JSON.parse(layoutJson); - console.log('[控制器] 向客户端发送布局树。'); + res.json(layout); } catch (parseError) { console.error('[控制器] 从数据库解析布局树 JSON 失败:', parseError); res.status(500).json({ message: '获取布局树失败:存储的数据格式无效' }); } } else { - console.log('[控制器] 在设置中未找到布局树,发送 null。'); + res.json(null); } } catch (error: any) { @@ -213,7 +207,6 @@ export const settingsController = { * 设置布局树 */ async setLayoutTree(req: Request, res: Response): Promise { - console.log('[控制器] 收到设置布局树的请求。'); try { const layoutTree = req.body; @@ -225,13 +218,13 @@ export const settingsController = { const layoutJson = JSON.stringify(layoutTree); - console.log('[控制器] 调用 settingsService.setLayoutTree...'); + await settingsService.setLayoutTree(layoutJson); - console.log('[控制器] settingsService.setLayoutTree 成功完成。'); + // auditLogService.logAction('LAYOUT_TREE_UPDATED'); - console.log('[控制器] 发送成功响应。'); + res.status(200).json({ message: '布局树已成功更新' }); } catch (error: any) { console.error('[控制器] 设置布局树时出错:', error); @@ -281,9 +274,7 @@ export const settingsController = { */ async getAutoCopyOnSelect(req: Request, res: Response): Promise { try { - console.log('[控制器] 收到获取“选中时自动复制”设置的请求。'); const isEnabled = await settingsService.getAutoCopyOnSelect(); - console.log(`[控制器] 向客户端发送“选中时自动复制”设置: ${isEnabled}`); res.json({ enabled: isEnabled }); } catch (error: any) { console.error('[控制器] 获取终端选中自动复制设置时出错:', error); @@ -295,7 +286,6 @@ export const settingsController = { * 设置终端选中自动复制 */ async setAutoCopyOnSelect(req: Request, res: Response): Promise { - console.log('[控制器] 收到设置“选中时自动复制”设置的请求。'); try { const { enabled } = req.body; console.log('[控制器] 请求体 enabled:', enabled); @@ -306,12 +296,12 @@ export const settingsController = { return; } - console.log('[控制器] 调用 settingsService.setAutoCopyOnSelect...'); + await settingsService.setAutoCopyOnSelect(enabled); - console.log('[控制器] settingsService.setAutoCopyOnSelect 成功完成。'); + - console.log('[控制器] 发送成功响应。'); + res.status(200).json({ message: '终端选中自动复制设置已成功更新' }); } catch (error: any) { console.error('[控制器] 设置终端选中自动复制时出错:', error); @@ -325,7 +315,6 @@ export const settingsController = { */ async getSidebarConfig(req: Request, res: Response): Promise { try { - console.log('[控制器] 收到获取侧边栏配置的请求。'); const config = await settingsService.getSidebarConfig(); console.log('[控制器] 向客户端发送侧边栏配置:', config); res.json(config); @@ -339,7 +328,6 @@ export const settingsController = { * 设置侧栏配置 */ async setSidebarConfig(req: Request, res: Response): Promise { - console.log('[控制器] 收到设置侧边栏配置的请求。'); try { const configDto: UpdateSidebarConfigDto = req.body; console.log('[控制器] 请求体:', configDto); @@ -352,12 +340,12 @@ export const settingsController = { return; } - console.log('[控制器] 调用 settingsService.setSidebarConfig...'); + await settingsService.setSidebarConfig(configDto); - console.log('[控制器] settingsService.setSidebarConfig 成功完成。'); + - console.log('[控制器] 发送成功响应。'); + res.status(200).json({ message: '侧栏配置已成功更新' }); } catch (error: any) { console.error('[控制器] 设置侧栏配置时出错:', error); @@ -375,7 +363,6 @@ export const settingsController = { */ async getCaptchaConfig(req: Request, res: Response): Promise { try { - console.log('[控制器] 收到获取 CAPTCHA 配置的请求。'); const fullConfig = await settingsService.getCaptchaConfig(); const publicConfig = { @@ -397,7 +384,6 @@ async getCaptchaConfig(req: Request, res: Response): Promise { * 设置 CAPTCHA 配置 */ async setCaptchaConfig(req: Request, res: Response): Promise { - console.log('[控制器] 收到设置 CAPTCHA 配置的请求。'); try { const configDto: UpdateCaptchaSettingsDto = req.body; console.log('[控制器] 请求体 (DTO, 密钥已屏蔽):', { ...configDto, hcaptchaSecretKey: '***', recaptchaSecretKey: '***' }); @@ -409,12 +395,12 @@ async setCaptchaConfig(req: Request, res: Response): Promise { } - console.log('[控制器] 调用 settingsService.setCaptchaConfig...'); + await settingsService.setCaptchaConfig(configDto); - console.log('[控制器] settingsService.setCaptchaConfig 成功完成。'); + - console.log('[控制器] 发送成功响应。'); + res.status(200).json({ message: 'CAPTCHA 配置已成功更新' }); } catch (error: any) { console.error('[控制器] 设置 CAPTCHA 配置时出错:', error); @@ -430,9 +416,7 @@ async setCaptchaConfig(req: Request, res: Response): Promise { // --- Show Connection Tags --- async getShowConnectionTags(req: Request, res: Response): Promise { try { - console.log('[控制器] 收到获取“显示连接标签”设置的请求。'); const isEnabled = await settingsService.getShowConnectionTags(); - console.log(`[控制器] 向客户端发送“显示连接标签”设置: ${isEnabled}`); res.json({ enabled: isEnabled }); } catch (error: any) { console.error('[控制器] 获取“显示连接标签”设置时出错:', error); @@ -441,7 +425,6 @@ async setCaptchaConfig(req: Request, res: Response): Promise { }, // *** 确保这里有逗号 *** async setShowConnectionTags(req: Request, res: Response): Promise { - console.log('[控制器] 收到设置“显示连接标签”设置的请求。'); try { const { enabled } = req.body; console.log('[控制器] 请求体 enabled:', enabled); @@ -452,14 +435,14 @@ async setCaptchaConfig(req: Request, res: Response): Promise { return; } - console.log('[控制器] 调用 settingsService.setShowConnectionTags...'); + await settingsService.setShowConnectionTags(enabled); - console.log('[控制器] settingsService.setShowConnectionTags 成功完成。'); + auditLogService.logAction('SETTINGS_UPDATED', { updatedKeys: ['showConnectionTags'] }); notificationService.sendNotification('SETTINGS_UPDATED', { updatedKeys: ['showConnectionTags'] }); - console.log('[控制器] 发送成功响应。'); + res.status(200).json({ message: '“显示连接标签”设置已成功更新' }); } catch (error: any) { console.error('[控制器] 设置“显示连接标签”时出错:', error); @@ -470,9 +453,7 @@ async setCaptchaConfig(req: Request, res: Response): Promise { // --- Show Quick Command Tags --- async getShowQuickCommandTags(req: Request, res: Response): Promise { try { - console.log('[控制器] 收到获取“显示快捷指令标签”设置的请求。'); const isEnabled = await settingsService.getShowQuickCommandTags(); - console.log(`[控制器] 向客户端发送“显示快捷指令标签”设置: ${isEnabled}`); res.json({ enabled: isEnabled }); } catch (error: any) { console.error('[控制器] 获取“显示快捷指令标签”设置时出错:', error); @@ -481,7 +462,6 @@ async setCaptchaConfig(req: Request, res: Response): Promise { }, // *** 确保这里有逗号 *** async setShowQuickCommandTags(req: Request, res: Response): Promise { - console.log('[控制器] 收到设置“显示快捷指令标签”设置的请求。'); try { const { enabled } = req.body; console.log('[控制器] 请求体 enabled:', enabled); @@ -492,27 +472,63 @@ async setCaptchaConfig(req: Request, res: Response): Promise { return; } - console.log('[控制器] 调用 settingsService.setShowQuickCommandTags...'); + await settingsService.setShowQuickCommandTags(enabled); - console.log('[控制器] settingsService.setShowQuickCommandTags 成功完成。'); + auditLogService.logAction('SETTINGS_UPDATED', { updatedKeys: ['showQuickCommandTags'] }); notificationService.sendNotification('SETTINGS_UPDATED', { updatedKeys: ['showQuickCommandTags'] }); - console.log('[控制器] 发送成功响应。'); + res.status(200).json({ message: '“显示快捷指令标签”设置已成功更新' }); } catch (error: any) { console.error('[控制器] 设置“显示快捷指令标签”时出错:', error); res.status(500).json({ message: '设置“显示快捷指令标签”失败', error: error.message }); } - }, // <-- Add comma here for the new method + }, + + // --- Show Status Monitor IP Address --- + async getShowStatusMonitorIpAddress(req: Request, res: Response): Promise { + try { + const isEnabled = await settingsService.getShowStatusMonitorIpAddress(); + res.json({ enabled: isEnabled }); + } catch (error: any) { + console.error('[控制器] 获取“显示状态监视器IP地址”设置时出错:', error); + res.status(500).json({ message: '获取“显示状态监视器IP地址”设置失败', error: error.message }); + } + }, + + async setShowStatusMonitorIpAddress(req: Request, res: Response): Promise { + try { + const { enabled } = req.body; + console.log('[控制器] 请求体 enabled:', enabled); + + if (typeof enabled !== 'boolean') { + console.warn('[控制器] 收到无效的 enabled 格式:', enabled); + res.status(400).json({ message: '无效的请求体,"enabled" 必须是一个布尔值' }); + return; + } + + + await settingsService.setShowStatusMonitorIpAddress(enabled); + + + auditLogService.logAction('SETTINGS_UPDATED', { updatedKeys: ['showStatusMonitorIpAddress'] }); + notificationService.sendNotification('SETTINGS_UPDATED', { updatedKeys: ['showStatusMonitorIpAddress'] }); + + + res.status(200).json({ message: '“显示状态监视器IP地址”设置已成功更新' }); + } catch (error: any) { + console.error('[控制器] 设置“显示状态监视器IP地址”时出错:', error); + res.status(500).json({ message: '设置“显示状态监视器IP地址”失败', error: error.message }); + } + }, // <-- Add comma here /** * 导出所有连接配置为加密的 ZIP 文件 */ async exportAllConnections(req: Request, res: Response): Promise { try { - console.log('[控制器] 收到导出所有连接的请求。'); const encryptedZipBuffer = await exportConnectionsAsEncryptedZip(true); res.setHeader('Content-Type', 'application/zip'); @@ -520,7 +536,7 @@ async setCaptchaConfig(req: Request, res: Response): Promise { res.send(encryptedZipBuffer); // auditLogService.logAction('CONNECTIONS_EXPORTED', { userId: (req.user as any)?.id || 'unknown' }); // 移除审计日志 - console.log('[控制器] 成功发送加密的连接导出文件。'); + } catch (error: any) { console.error('[控制器] 导出所有连接时出错:', error); diff --git a/packages/backend/src/settings/settings.routes.ts b/packages/backend/src/settings/settings.routes.ts index 08a3f3c..fb9ad0c 100644 --- a/packages/backend/src/settings/settings.routes.ts +++ b/packages/backend/src/settings/settings.routes.ts @@ -68,8 +68,13 @@ router.put('/show-quick-command-tags', settingsController.setShowQuickCommandTag // +++ 导出所有连接路由 +++ // GET /api/v1/settings/export-connections - 导出所有连接为加密的 ZIP 文件 router.get('/export-connections', settingsController.exportAllConnections); - - + +// +++ 显示状态监视器IP地址路由 +++ +// GET /api/v1/settings/show-status-monitor-ip-address - 获取设置 +router.get('/show-status-monitor-ip-address', settingsController.getShowStatusMonitorIpAddress); +// PUT /api/v1/settings/show-status-monitor-ip-address - 更新设置 +router.put('/show-status-monitor-ip-address', settingsController.setShowStatusMonitorIpAddress); + export default router; // +++ CAPTCHA 配置路由 (需要认证更新) +++ diff --git a/packages/backend/src/ssh-suspend/ssh-suspend.controller.ts b/packages/backend/src/ssh-suspend/ssh-suspend.controller.ts index ae3a06e..7a2edeb 100644 --- a/packages/backend/src/ssh-suspend/ssh-suspend.controller.ts +++ b/packages/backend/src/ssh-suspend/ssh-suspend.controller.ts @@ -1,18 +1,16 @@ import { Request, Response } from 'express'; -import { sshSuspendService } from '../services/ssh-suspend.service'; // 导入单例服务 -import { SuspendedSessionInfo } from '../types/ssh-suspend.types'; // 导入类型 +import { sshSuspendService } from '../services/ssh-suspend.service'; +import { SuspendedSessionInfo } from '../types/ssh-suspend.types'; export class SshSuspendController { - // private sshSuspendService: SshSuspendService; // 不再需要,直接使用导入的单例 + constructor() { - // this.sshSuspendService = new SshSuspendService(); // 不再需要实例化 - // 绑定方法到当前实例,以确保 'this' 上下文正确 this.getSuspendedSshSessions = this.getSuspendedSshSessions.bind(this); this.terminateAndRemoveSession = this.terminateAndRemoveSession.bind(this); this.removeSessionEntry = this.removeSessionEntry.bind(this); - this.editSessionNameHttp = this.editSessionNameHttp.bind(this); // 绑定新方法 - this.exportSessionLog = this.exportSessionLog.bind(this); // +++ 绑定导出日志方法 +++ + this.editSessionNameHttp = this.editSessionNameHttp.bind(this); + this.exportSessionLog = this.exportSessionLog.bind(this); } public async getSuspendedSshSessions(req: Request, res: Response): Promise { diff --git a/packages/backend/src/types/settings.types.ts b/packages/backend/src/types/settings.types.ts index 7e36826..70350cf 100644 --- a/packages/backend/src/types/settings.types.ts +++ b/packages/backend/src/types/settings.types.ts @@ -66,6 +66,7 @@ export interface UpdateCaptchaSettingsDto { export interface AppSettings { sidebar?: SidebarConfig; captcha?: CaptchaSettings; + showStatusMonitorIpAddress?: boolean; // 新增:是否在状态监视器中显示IP地址 // 可以添加其他设置模块,例如: // security?: SecuritySettings; // general?: GeneralSettings; diff --git a/packages/backend/src/websocket.ts b/packages/backend/src/websocket.ts index e51690f..e462f31 100644 --- a/packages/backend/src/websocket.ts +++ b/packages/backend/src/websocket.ts @@ -5,8 +5,8 @@ import { initializeHeartbeat } from './websocket/heartbeat'; import { initializeUpgradeHandler } from './websocket/upgrade'; import { initializeConnectionHandler } from './websocket/connection'; import { clientStates } from './websocket/state'; -import { sshSuspendService } from './services/ssh-suspend.service'; // 导入实例 -import { SftpService } from './services/sftp.service'; // +++ 导入 SftpService +++ +import { sshSuspendService } from './services/ssh-suspend.service'; +import { SftpService } from './services/sftp.service'; import { cleanupClientConnection } from './websocket/utils'; diff --git a/packages/frontend/src/App.vue b/packages/frontend/src/App.vue index 03bc57e..cf44947 100644 --- a/packages/frontend/src/App.vue +++ b/packages/frontend/src/App.vue @@ -10,17 +10,11 @@ import { useLayoutStore } from './stores/layout.store'; import { useFocusSwitcherStore } from './stores/focusSwitcher.store'; import { useSessionStore } from './stores/session.store'; import { storeToRefs } from 'pinia'; -// 导入通知显示组件 import UINotificationDisplay from './components/UINotificationDisplay.vue'; -// 导入文件编辑器弹窗组件 import FileEditorOverlay from './components/FileEditorOverlay.vue'; -// 导入样式自定义器组件 import StyleCustomizer from './components/StyleCustomizer.vue'; -// +++ 导入焦点切换配置器组件 +++ import FocusSwitcherConfigurator from './components/FocusSwitcherConfigurator.vue'; -// +++ 导入 RDP 模态框组件 +++ import RemoteDesktopModal from './components/RemoteDesktopModal.vue'; -// +++ 导入 VNC 模态框组件 +++ import VncModal from './components/VncModal.vue'; const { t } = useI18n(); @@ -69,7 +63,7 @@ onMounted(() => { // Use setTimeout to ensure styles are applied and elements have dimensions setTimeout(updateUnderline, 100); - // +++ 添加全局 Alt 键监听器 +++ + // +++ 全局 Alt 键监听器 +++ window.addEventListener('keydown', handleAltKeyDown); // +++ 监听 keydown 设置状态 +++ window.addEventListener('keyup', handleGlobalKeyUp); // +++ 监听 keyup 执行切换 +++ @@ -86,7 +80,7 @@ onMounted(() => { layoutStore.loadHeaderVisibility(); }); -// +++ 添加卸载钩子以移除监听器 +++ +// +++ 卸载钩子以移除监听器 +++ onUnmounted(() => { window.removeEventListener('keydown', handleAltKeyDown); // +++ 移除 keydown 监听 +++ window.removeEventListener('keyup', handleGlobalKeyUp); // +++ 移除 keyup 监听 +++ diff --git a/packages/frontend/src/components/AddEditQuickCommandForm.vue b/packages/frontend/src/components/AddEditQuickCommandForm.vue index 2bf17bb..bb1f3fa 100644 --- a/packages/frontend/src/components/AddEditQuickCommandForm.vue +++ b/packages/frontend/src/components/AddEditQuickCommandForm.vue @@ -59,8 +59,8 @@ import { ref, reactive, computed, watch, onMounted } from 'vue'; import { useI18n } from 'vue-i18n'; import { useQuickCommandsStore, type QuickCommandFE } from '../stores/quickCommands.store'; -import { useQuickCommandTagsStore } from '../stores/quickCommandTags.store'; // +++ Import new tag store +++ -import TagInput from './TagInput.vue'; // +++ Import TagInput component (assuming it exists) +++ +import { useQuickCommandTagsStore } from '../stores/quickCommandTags.store'; +import TagInput from './TagInput.vue'; const props = defineProps<{ commandToEdit?: QuickCommandFE | null; // 接收要编辑的指令对象 (should include tagIds) @@ -109,12 +109,12 @@ onMounted(() => { // --- Tag Creation Handling --- // Assuming TagInput emits 'create-tag' with the tag name const handleCreateTag = async (tagName: string) => { - console.log(`[QuickCmdForm] Received create-tag event for: ${tagName}`); // +++ 添加日志 +++ + console.log(`[QuickCmdForm] Received create-tag event for: ${tagName}`); if (!tagName || tagName.trim().length === 0) return; - console.log(`[QuickCmdForm] Calling quickCommandTagsStore.addTag...`); // +++ 添加日志 +++ + console.log(`[QuickCmdForm] Calling quickCommandTagsStore.addTag...`); const newTag = await quickCommandTagsStore.addTag(tagName.trim()); if (newTag && !formData.tagIds.includes(newTag.id)) { - console.log(`[QuickCmdForm] New tag created (ID: ${newTag.id}), adding to selection.`); // +++ 添加日志 +++ + console.log(`[QuickCmdForm] New tag created (ID: ${newTag.id}), adding to selection.`); // Add the new tag's ID to the selected list formData.tagIds.push(newTag.id); } @@ -122,12 +122,12 @@ const handleCreateTag = async (tagName: string) => { // --- Tag Deletion Handling --- const handleDeleteTag = async (tagId: number) => { - console.log(`[QuickCmdForm] Received delete-tag event for ID: ${tagId}`); // +++ 添加日志 +++ + console.log(`[QuickCmdForm] Received delete-tag event for ID: ${tagId}`); const tagToDelete = quickCommandTagsStore.tags.find(t => t.id === tagId); if (!tagToDelete) return; if (confirm(t('tags.prompts.confirmDelete', { name: tagToDelete.name }))) { - console.log(`[QuickCmdForm] Calling quickCommandTagsStore.deleteTag...`); // +++ 添加日志 +++ + console.log(`[QuickCmdForm] Calling quickCommandTagsStore.deleteTag...`); const success = await quickCommandTagsStore.deleteTag(tagId); if (success) { // If deletion is successful, TagInput's availableTags will update, @@ -135,7 +135,7 @@ const handleDeleteTag = async (tagId: number) => { // We also need to remove it from the local formData.tagIds if it was selected. const index = formData.tagIds.indexOf(tagId); if (index > -1) { - console.log(`[QuickCmdForm] Removing deleted tag ID ${tagId} from selection.`); // +++ 添加日志 +++ + console.log(`[QuickCmdForm] Removing deleted tag ID ${tagId} from selection.`); formData.tagIds.splice(index, 1); } } else { diff --git a/packages/frontend/src/components/CommandHistoryMenu.vue b/packages/frontend/src/components/CommandHistoryMenu.vue index 7b36d04..b09ce37 100644 --- a/packages/frontend/src/components/CommandHistoryMenu.vue +++ b/packages/frontend/src/components/CommandHistoryMenu.vue @@ -46,12 +46,12 @@ diff --git a/packages/frontend/src/components/TagInput.vue b/packages/frontend/src/components/TagInput.vue index 41d51f8..22fc346 100644 --- a/packages/frontend/src/components/TagInput.vue +++ b/packages/frontend/src/components/TagInput.vue @@ -128,7 +128,7 @@ const handleKeyDown = async (event: KeyboardEvent) => { selectTag(existingTag); } else if (!existingTag && allowCreate.value) { // Only create if allowed and not existing // 如果是新标签,则 emit 事件让父组件处理创建 - console.log(`[TagInput] Emitting create-tag for: ${trimmedInput}`); // +++ 添加日志 +++ + console.log(`[TagInput] Emitting create-tag for: ${trimmedInput}`); emit('create-tag', trimmedInput); // 父组件负责创建、更新 availableTags prop,然后 TagInput 会响应式更新 // 父组件也负责将新创建的 tag ID 添加到 modelValue @@ -160,9 +160,9 @@ const removeTagLocally = (tagToRemove: GenericTag) => { // 处理全局删除标签 (点击标签上的 'x' 图标) - Emit event const handleDeleteTagGlobally = (tagToDelete: GenericTag) => { - console.log(`[TagInput] handleDeleteTagGlobally called for tag ID: ${tagToDelete.id}, Name: ${tagToDelete.name}`); // +++ 添加日志 +++ + console.log(`[TagInput] handleDeleteTagGlobally called for tag ID: ${tagToDelete.id}, Name: ${tagToDelete.name}`); // Emit event for parent to handle deletion confirmation and API call - console.log(`[TagInput] Emitting delete-tag with ID: ${tagToDelete.id}`); // +++ 添加日志 +++ + console.log(`[TagInput] Emitting delete-tag with ID: ${tagToDelete.id}`); emit('delete-tag', tagToDelete.id); // Parent should handle confirmation, call store action, and update modelValue/availableTags // We might still want to remove it locally immediately for better UX, diff --git a/packages/frontend/src/components/Terminal.vue b/packages/frontend/src/components/Terminal.vue index 0b31057..57a68bd 100644 --- a/packages/frontend/src/components/Terminal.vue +++ b/packages/frontend/src/components/Terminal.vue @@ -562,7 +562,7 @@ const clearSearch = () => { searchAddon?.clearDecorations(); }; -// +++ 添加 clear 方法 +++ +// +++ clear 方法 +++ const clear = () => { terminal?.clear(); }; diff --git a/packages/frontend/src/components/VirtualKeyboard.vue b/packages/frontend/src/components/VirtualKeyboard.vue index 6b1fdcf..ccf374f 100644 --- a/packages/frontend/src/components/VirtualKeyboard.vue +++ b/packages/frontend/src/components/VirtualKeyboard.vue @@ -1,5 +1,5 @@ diff --git a/packages/frontend/src/composables/file-manager/useFileManagerContextMenu.ts b/packages/frontend/src/composables/file-manager/useFileManagerContextMenu.ts index ab42a8f..0b0977f 100644 --- a/packages/frontend/src/composables/file-manager/useFileManagerContextMenu.ts +++ b/packages/frontend/src/composables/file-manager/useFileManagerContextMenu.ts @@ -1,7 +1,7 @@ -import { ref, nextTick, type Ref, type ComponentPublicInstance } from 'vue'; // 导入 ComponentPublicInstance -import type { FileListItem } from '../../types/sftp.types'; // 修正路径 -import { type useI18n } from 'vue-i18n'; // 导入 useI18n 以获取 t 的类型 -import type FileManagerContextMenu from '../../components/FileManagerContextMenu.vue'; // <-- 导入组件类型 +import { ref, nextTick, type Ref, type ComponentPublicInstance } from 'vue'; +import type { FileListItem } from '../../types/sftp.types'; +import { type useI18n } from 'vue-i18n'; +import type FileManagerContextMenu from '../../components/FileManagerContextMenu.vue'; // 定义菜单项类型 (可以根据需要扩展) export interface ContextMenuItem { @@ -242,7 +242,7 @@ export function useFileManagerContextMenu(options: UseFileManagerContextMenuOpti ]; } else { // Clicked on '..' menu = [ - // +++ 添加粘贴 (可以粘贴到上级目录) +++ + // +++ 粘贴 (可以粘贴到上级目录) +++ { label: t('fileManager.actions.paste'), action: onPaste, disabled: !(isConnected.value && isSftpReady.value) || !hasClipboardContent }, { label: t('fileManager.actions.refresh'), action: onRefresh, disabled: !(isConnected.value && isSftpReady.value) } ]; diff --git a/packages/frontend/src/composables/settings/useSystemSettings.ts b/packages/frontend/src/composables/settings/useSystemSettings.ts index a7db500..6b8def3 100644 --- a/packages/frontend/src/composables/settings/useSystemSettings.ts +++ b/packages/frontend/src/composables/settings/useSystemSettings.ts @@ -2,7 +2,7 @@ import { ref, watch } from 'vue'; import { useSettingsStore } from '../../stores/settings.store'; import { useI18n } from 'vue-i18n'; import { storeToRefs } from 'pinia'; -import { availableLocales } from '../../i18n'; // 导入可用语言列表 +import { availableLocales } from '../../i18n'; export function useSystemSettings() { const settingsStore = useSettingsStore(); diff --git a/packages/frontend/src/composables/settings/useWorkspaceSettings.ts b/packages/frontend/src/composables/settings/useWorkspaceSettings.ts index 5b11fde..5692f41 100644 --- a/packages/frontend/src/composables/settings/useWorkspaceSettings.ts +++ b/packages/frontend/src/composables/settings/useWorkspaceSettings.ts @@ -17,8 +17,9 @@ export function useWorkspaceSettings() { showQuickCommandTagsBoolean, terminalScrollbackLimitNumber, fileManagerShowDeleteConfirmationBoolean, - terminalEnableRightClickPasteBoolean, - showPopupFileManagerBoolean, // +++ Import the new getter +++ + terminalEnableRightClickPasteBoolean, + showPopupFileManagerBoolean, + statusMonitorShowIpBoolean, } = storeToRefs(settingsStore); // --- Popup Editor --- @@ -286,6 +287,30 @@ export function useWorkspaceSettings() { } }; + // --- Status Monitor Show IP --- + const statusMonitorShowIpEnabled = ref(false); + const statusMonitorShowIpLoading = ref(false); + const statusMonitorShowIpMessage = ref(''); + const statusMonitorShowIpSuccess = ref(false); + + const handleUpdateStatusMonitorShowIpSetting = async () => { + statusMonitorShowIpLoading.value = true; + statusMonitorShowIpMessage.value = ''; + statusMonitorShowIpSuccess.value = false; + try { + const valueToSave = statusMonitorShowIpEnabled.value ? 'true' : 'false'; + await settingsStore.updateSetting('showStatusMonitorIpAddress', valueToSave); + statusMonitorShowIpMessage.value = t('common.saved'); + statusMonitorShowIpSuccess.value = true; + } catch (error: any) { + console.error('Failed to update status monitor IP display setting:', error); + statusMonitorShowIpMessage.value = error.message || t('settings.statusMonitorShowIp.error.saveFailed', 'Failed to save status monitor IP display setting.'); // 需要添加相应的i18n键 + statusMonitorShowIpSuccess.value = false; + } finally { + statusMonitorShowIpLoading.value = false; + } + }; + // Watchers to sync local state with store state watch(showPopupFileEditorBoolean, (newValue) => { popupEditorEnabled.value = newValue; }, { immediate: true }); watch(shareFileEditorTabsBoolean, (newValue) => { shareTabsEnabled.value = newValue; }, { immediate: true }); @@ -296,8 +321,9 @@ export function useWorkspaceSettings() { watch(showQuickCommandTagsBoolean, (newValue) => { showQuickCommandTagsLocal.value = newValue; }, { immediate: true }); watch(terminalScrollbackLimitNumber, (newValue) => { terminalScrollbackLimitLocal.value = newValue; }, { immediate: true }); watch(fileManagerShowDeleteConfirmationBoolean, (newValue) => { fileManagerShowDeleteConfirmationLocal.value = newValue; }, { immediate: true }); - watch(terminalEnableRightClickPasteBoolean, (newValue) => { terminalEnableRightClickPasteLocal.value = newValue; }, { immediate: true }); + watch(terminalEnableRightClickPasteBoolean, (newValue) => { terminalEnableRightClickPasteLocal.value = newValue; }, { immediate: true }); watch(showPopupFileManagerBoolean, (newValue) => { showPopupFileManagerLocal.value = newValue; }, { immediate: true }); // +++ Watch for popup file manager +++ + watch(statusMonitorShowIpBoolean, (newValue) => { statusMonitorShowIpEnabled.value = newValue; }, { immediate: true }); return { @@ -367,5 +393,11 @@ export function useWorkspaceSettings() { showPopupFileManagerMessage, showPopupFileManagerSuccess, handleUpdateShowPopupFileManager, + + statusMonitorShowIpEnabled, + statusMonitorShowIpLoading, + statusMonitorShowIpMessage, + statusMonitorShowIpSuccess, + handleUpdateStatusMonitorShowIpSetting, }; } \ No newline at end of file diff --git a/packages/frontend/src/composables/useSftpActions.ts b/packages/frontend/src/composables/useSftpActions.ts index 1bdd3fa..52b3419 100644 --- a/packages/frontend/src/composables/useSftpActions.ts +++ b/packages/frontend/src/composables/useSftpActions.ts @@ -1,8 +1,8 @@ -import { ref, readonly, reactive, computed, type Ref, type ComputedRef } from 'vue'; // 引入 reactive 和 computed +import { ref, readonly, reactive, computed, type Ref, type ComputedRef } from 'vue'; import type { FileListItem, FileAttributes, EditorFileContent, SftpReadFileSuccessPayload, SftpReadFileRequestPayload } from '../types/sftp.types'; import type { WebSocketMessage, MessagePayload, MessageHandler } from '../types/websocket.types'; -// 导入 UI 通知 store -import { useUiNotificationsStore } from '../stores/uiNotifications.store'; // 更正导入 + +import { useUiNotificationsStore } from '../stores/uiNotifications.store'; /** * @interface WebSocketDependencies diff --git a/packages/frontend/src/composables/useSidebarResize.ts b/packages/frontend/src/composables/useSidebarResize.ts index 4a63b9c..91655e1 100644 --- a/packages/frontend/src/composables/useSidebarResize.ts +++ b/packages/frontend/src/composables/useSidebarResize.ts @@ -24,7 +24,7 @@ export function useSidebarResize({ const handleMouseDown = (event: MouseEvent) => { console.log(`[useSidebarResize] handleMouseDown triggered for side: ${side}`, { sidebar: sidebarRef.value, handle: handleRef.value }); // +++ Add Log +++ if (!sidebarRef.value || !handleRef.value) { - console.log('[useSidebarResize] MouseDown ignored: sidebarRef or handleRef is null.'); // +++ Add Log +++ + console.log('[useSidebarResize] MouseDown ignored: sidebarRef or handleRef is null.'); return; } diff --git a/packages/frontend/src/locales/en-US.json b/packages/frontend/src/locales/en-US.json index 8a77f61..65d3501 100644 --- a/packages/frontend/src/locales/en-US.json +++ b/packages/frontend/src/locales/en-US.json @@ -816,6 +816,10 @@ "terminalRightClickPasteSuccess": "Terminal right-click paste setting saved.", "terminalRightClickPasteError": "Failed to save terminal right-click paste setting." }, + "statusMonitorShowIp": { + "title": "Status Monitor: Show IP Address", + "enableLabel": "Show IP address in status monitor" + }, "terminalScrollback": { "title": "Terminal Scrollback Limit", "limitLabel": "Maximum Lines", @@ -940,6 +944,7 @@ "cancel": "Cancel", "save": "Save", "saving": "Saving...", + "saved": "Saved", "testing": "Testing...", "edit": "Edit", "delete": "Delete", @@ -962,7 +967,8 @@ "sortDescending": "Descending", "restore": "Restore", "minimize": "Minimize", - "send":"Send" + "send":"Send", + "copied": "Copied to clipboard" }, "layoutConfigurator": { "title": "Layout Configurator", diff --git a/packages/frontend/src/locales/ja-JP.json b/packages/frontend/src/locales/ja-JP.json index 98398e3..e66455b 100644 --- a/packages/frontend/src/locales/ja-JP.json +++ b/packages/frontend/src/locales/ja-JP.json @@ -96,6 +96,7 @@ "retry": "再試行", "save": "保存", "saving": "保存中...", + "saved": "保存済み", "search": "検索", "settings": "設定", "sortAscending": "昇順", @@ -104,7 +105,8 @@ "width": "幅", "restore": "元に戻す", "minimize": "最小化", - "send":"送信する" + "send":"送信する", + "copied": "クリップボードにコピーしました" }, "connections": { "actions": { @@ -1075,6 +1077,10 @@ "terminalRightClickPasteSuccess": "ターミナルの右クリック貼り付け設定が保存されました。", "terminalRightClickPasteError": "ターミナルの右クリック貼り付け設定の保存に失敗しました。" }, + "statusMonitorShowIp": { + "title": "ステータスモニターでIPアドレスを表示", + "enableLabel": "ステータスモニターにIPアドレスを表示する" + }, "terminalScrollback": { "title": "ターミナルスクロールバック制限", "limitLabel": "最大行数", diff --git a/packages/frontend/src/locales/zh-CN.json b/packages/frontend/src/locales/zh-CN.json index fba238e..8f925e9 100644 --- a/packages/frontend/src/locales/zh-CN.json +++ b/packages/frontend/src/locales/zh-CN.json @@ -810,6 +810,10 @@ "terminalRightClickPasteSuccess": "终端右键粘贴设置已保存。", "terminalRightClickPasteError": "保存终端右键粘贴设置失败。" }, + "statusMonitorShowIp": { + "title": "状态监视器显示IP地址", + "enableLabel": "在状态监视器中显示IP地址" + }, "terminalScrollback": { "title": "终端回滚行数", "limitLabel": "最大行数", @@ -941,6 +945,7 @@ "cancel": "取消", "save": "保存", "saving": "保存中...", + "saved": "已保存", "testing": "测试中...", "edit": "编辑", "delete": "删除", @@ -963,7 +968,8 @@ "sortDescending": "降序", "restore": "还原", "minimize": "最小化", - "send":"发送" + "send":"发送", + "copied": "已复制到剪贴板" }, "layoutConfigurator": { "title": "布局管理器", diff --git a/packages/frontend/src/main.ts b/packages/frontend/src/main.ts index e2dccb8..ff52245 100644 --- a/packages/frontend/src/main.ts +++ b/packages/frontend/src/main.ts @@ -1,16 +1,14 @@ import { createApp } from 'vue'; -import { createPinia } from 'pinia'; // 引入 Pinia -import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; // 引入持久化插件 +import { createPinia } from 'pinia'; +import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; import App from './App.vue'; -import router from './router'; // 引入我们创建的 router -import i18n from './i18n'; // 引入 i18n 实例 -import { useAuthStore } from './stores/auth.store'; // 引入 Auth Store -import { useSettingsStore } from './stores/settings.store'; // 引入 Settings Store -import { useAppearanceStore } from './stores/appearance.store'; // 引入 Appearance Store +import router from './router'; +import i18n from './i18n'; +import { useAuthStore } from './stores/auth.store'; +import { useSettingsStore } from './stores/settings.store'; +import { useAppearanceStore } from './stores/appearance.store'; import './style.css'; -// 导入 Font Awesome CSS import '@fortawesome/fontawesome-free/css/all.min.css'; -// 导入 splitpanes CSS import 'splitpanes/dist/splitpanes.css'; diff --git a/packages/frontend/src/stores/auth.store.ts b/packages/frontend/src/stores/auth.store.ts index 66c134f..42078b9 100644 --- a/packages/frontend/src/stores/auth.store.ts +++ b/packages/frontend/src/stores/auth.store.ts @@ -1,7 +1,7 @@ import { defineStore } from 'pinia'; -import apiClient from '../utils/apiClient'; // 使用统一的 apiClient -import router from '../router'; // 引入 router 用于重定向 -import { setLocale } from '../i18n'; // 导入 setLocale +import apiClient from '../utils/apiClient'; +import router from '../router'; +import { setLocale } from '../i18n'; // 扩展的用户信息接口,包含 2FA 状态和语言偏好 interface UserInfo { diff --git a/packages/frontend/src/stores/fileEditor.store.ts b/packages/frontend/src/stores/fileEditor.store.ts index 8460740..b7d912f 100644 --- a/packages/frontend/src/stores/fileEditor.store.ts +++ b/packages/frontend/src/stores/fileEditor.store.ts @@ -1,10 +1,10 @@ -import { ref, computed, readonly, watch, nextTick } from 'vue'; // Import nextTick +import { ref, computed, readonly, watch, nextTick } from 'vue'; import { defineStore } from 'pinia'; import { useI18n } from 'vue-i18n'; -import { useSessionStore } from './session.store'; // 导入会话 Store -import type { SaveStatus, SftpReadFileSuccessPayload } from '../types/sftp.types'; // 移除 SftpReadFileRequestPayload, 因为 readFile 不再需要它 -import * as iconv from '@vscode/iconv-lite-umd'; // +++ 导入 iconv-lite +++ -import { Buffer } from 'buffer/'; // +++ 导入 Buffer (需要安装 buffer 依赖) +++ +import { useSessionStore } from './session.store'; +import type { SaveStatus, SftpReadFileSuccessPayload } from '../types/sftp.types'; +import * as iconv from '@vscode/iconv-lite-umd'; +import { Buffer } from 'buffer/'; // --- 类型定义 --- // 文件信息,用于打开文件操作 diff --git a/packages/frontend/src/stores/quickCommands.store.ts b/packages/frontend/src/stores/quickCommands.store.ts index e14b6fc..e755888 100644 --- a/packages/frontend/src/stores/quickCommands.store.ts +++ b/packages/frontend/src/stores/quickCommands.store.ts @@ -1,11 +1,11 @@ import { defineStore } from 'pinia'; -import apiClient from '../utils/apiClient'; // 使用统一的 apiClient -import { ref, computed, watch } from 'vue'; // Import watch +import apiClient from '../utils/apiClient'; +import { ref, computed, watch } from 'vue'; import { useUiNotificationsStore } from './uiNotifications.store'; -import { useQuickCommandTagsStore, type QuickCommandTag } from './quickCommandTags.store'; // +++ Import new tag store +++ -import { useI18n } from 'vue-i18n'; // +++ Import i18n for "Untagged" +++ -// Assuming QuickCommand type in types includes tagIds now, or define it here -// import type { QuickCommand } from '../types/quick-commands.types'; +import { useQuickCommandTagsStore, type QuickCommandTag } from './quickCommandTags.store'; +import { useI18n } from 'vue-i18n'; + + // 定义前端使用的快捷指令接口 (包含 tagIds) export interface QuickCommandFE { // Renamed from QuickCommand if necessary diff --git a/packages/frontend/src/stores/session.store.ts b/packages/frontend/src/stores/session.store.ts index 75254eb..d817ba9 100644 --- a/packages/frontend/src/stores/session.store.ts +++ b/packages/frontend/src/stores/session.store.ts @@ -1,11 +1,11 @@ -// packages/frontend/src/stores/session.store.ts + import { defineStore } from 'pinia'; import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; import { useConnectionsStore, type ConnectionInfo } from './connections.store'; -// 从新模块导入状态 + import { sessions, activeSessionId, @@ -13,30 +13,30 @@ import { rdpConnectionInfo, isVncModalOpen, vncConnectionInfo, - // SSH Suspend Mode State + suspendedSshSessions, isLoadingSuspendedSessions, } from './session/state'; -// 从新模块导入 Getters + import { sessionTabs, sessionTabsWithStatus, activeSession, } from './session/getters'; -// 从新模块导入 Actions + import * as sessionActions from './session/actions/sessionActions'; import * as editorActions from './session/actions/editorActions'; import * as sftpManagerActions from './session/actions/sftpManagerActions'; import * as modalActions from './session/actions/modalActions'; import * as commandInputActions from './session/actions/commandInputActions'; -import * as sshSuspendActions from './session/actions/sshSuspendActions'; // 导入 SSH 挂起 Actions +import * as sshSuspendActions from './session/actions/sshSuspendActions'; + -// 导入需要的类型 (例如 FileInfo 可能会在参数中使用) import type { FileInfo } from './fileEditor.store'; -// SftpManagerInstance 类型主要在 action 文件内部使用,但如果 store 直接暴露它,则需要导入 -// import type { SftpManagerInstance } from './session/types'; + + export const useSessionStore = defineStore('session', () => { diff --git a/packages/frontend/src/stores/session/actions/sessionActions.ts b/packages/frontend/src/stores/session/actions/sessionActions.ts index c4b1602..13ef9e9 100644 --- a/packages/frontend/src/stores/session/actions/sessionActions.ts +++ b/packages/frontend/src/stores/session/actions/sessionActions.ts @@ -1,20 +1,20 @@ -// packages/frontend/src/stores/session/actions/sessionActions.ts + import { ref } from 'vue'; import { useRouter } from 'vue-router'; import { useI18n } from 'vue-i18n'; -import { useConnectionsStore, type ConnectionInfo } from '../../connections.store'; // 路径: packages/frontend/src/stores/connections.store.ts +import { useConnectionsStore, type ConnectionInfo } from '../../connections.store'; import { sessions, activeSessionId } from '../state'; import { generateSessionId } from '../utils'; import type { SessionState, SshTerminalInstance, StatusMonitorInstance, DockerManagerInstance, SftpManagerInstance, WsManagerInstance } from '../types'; -// Composables for manager creation - 路径相对于此文件 + import { createWebSocketConnectionManager } from '../../../composables/useWebSocketConnection'; import { createSshTerminalManager, type SshTerminalDependencies } from '../../../composables/useSshTerminal'; import { createStatusMonitorManager, type StatusMonitorDependencies } from '../../../composables/useStatusMonitor'; import { createDockerManager, type DockerManagerDependencies } from '../../../composables/useDockerManager'; -import { registerSshSuspendHandlers } from './sshSuspendActions'; // 导入 SSH 挂起处理器注册函数 -// getOrCreateSftpManager 将在 sftpManagerActions.ts 中定义,并在主 store 中协调 +import { registerSshSuspendHandlers } from './sshSuspendActions'; + // --- 辅助函数 (特定于此模块的 actions) --- const findConnectionInfo = (connectionId: number | string, connectionsStore: ReturnType): ConnectionInfo | undefined => { diff --git a/packages/frontend/src/stores/session/actions/sshSuspendActions.ts b/packages/frontend/src/stores/session/actions/sshSuspendActions.ts index 461cbf1..3259dcd 100644 --- a/packages/frontend/src/stores/session/actions/sshSuspendActions.ts +++ b/packages/frontend/src/stores/session/actions/sshSuspendActions.ts @@ -1,4 +1,4 @@ -// packages/frontend/src/stores/session/actions/sshSuspendActions.ts + import { v4 as uuidv4 } from 'uuid'; import { sessions, suspendedSshSessions, isLoadingSuspendedSessions, activeSessionId } from '../state'; import type { @@ -8,8 +8,8 @@ import type { SshSuspendResumeReqMessage, SshSuspendTerminateReqMessage, SshSuspendRemoveEntryReqMessage, - // SshSuspendEditNameReqMessage, // Removed, using HTTP API - // S2C Payloads + + SshMarkedForSuspendAckPayload, SshUnmarkedForSuspendAckPayload, SshSuspendListResponsePayload, @@ -17,19 +17,19 @@ import type { SshOutputCachedChunkPayload, SshSuspendTerminatedRespPayload, SshSuspendEntryRemovedRespPayload, - // SshSuspendNameEditedRespPayload, // Removed, using HTTP API + SshSuspendAutoTerminatedNotifPayload, -} from '../../../types/websocket.types'; // 路径: packages/frontend/src/types/websocket.types.ts -import type { WsManagerInstance, SessionState } from '../types'; // 路径: packages/frontend/src/stores/session/types.ts // Re-add WsManagerInstance -import { closeSession as closeSessionAction, activateSession as activateSessionAction, openNewSession, closeSession } from './sessionActions'; // 使用 openNewSession 和 closeSession -import { useConnectionsStore } from '../../connections.store'; // 用于获取连接信息 -import { useUiNotificationsStore } from '../../uiNotifications.store'; // 用于显示通知 -import type { SuspendedSshSession } from '../../../types/ssh-suspend.types'; // 路径: packages/frontend/src/types/ssh-suspend.types.ts -import i18n from '../../../i18n'; // 直接导入 i18n 实例 -import type { ComposerTranslation } from 'vue-i18n'; // 导入 ComposerTranslation 类型 -import apiClient from '../../../utils/apiClient'; // +++ 导入 apiClient +++ +} from '../../../types/websocket.types'; +import type { WsManagerInstance, SessionState } from '../types'; +import { closeSession as closeSessionAction, activateSession as activateSessionAction, openNewSession, closeSession } from './sessionActions'; +import { useConnectionsStore } from '../../connections.store'; +import { useUiNotificationsStore } from '../../uiNotifications.store'; +import type { SuspendedSshSession } from '../../../types/ssh-suspend.types'; +import i18n from '../../../i18n'; +import type { ComposerTranslation } from 'vue-i18n'; +import apiClient from '../../../utils/apiClient'; -const t: ComposerTranslation = i18n.global.t; // 从全局 i18n 实例获取 t 函数并显式注解类型 +const t: ComposerTranslation = i18n.global.t; // 辅助函数:获取一个可用的 WebSocket 管理器 // 优先使用当前激活的会话,或者任意一个已连接的 SSH 会话 diff --git a/packages/frontend/src/stores/session/types.ts b/packages/frontend/src/stores/session/types.ts index c6e6501..5aef33e 100644 --- a/packages/frontend/src/stores/session/types.ts +++ b/packages/frontend/src/stores/session/types.ts @@ -1,11 +1,9 @@ -// packages/frontend/src/stores/session/types.ts + import type { Ref } from 'vue'; -import type { FileTab as OriginalFileTab } from '../fileEditor.store'; // 路径: packages/frontend/src/stores/fileEditor.store.ts -import type { WsConnectionStatus } from '../../composables/useWebSocketConnection'; // 路径: packages/frontend/src/composables/useWebSocketConnection.ts - -// 从源模块导入 DockerManagerInstance 类型并使用别名 -import type { DockerManagerInstance as OriginalDockerManagerInstance } from '../../composables/useDockerManager'; // 路径: packages/frontend/src/composables/useDockerManager.ts +import type { FileTab as OriginalFileTab } from '../fileEditor.store'; +import type { WsConnectionStatus } from '../../composables/useWebSocketConnection'; +import type { DockerManagerInstance as OriginalDockerManagerInstance } from '../../composables/useDockerManager'; // 导入工厂函数仅用于通过 ReturnType 推导实例类型 // 这些导入仅用于类型推断,不在运行时使用 diff --git a/packages/frontend/src/stores/settings.store.ts b/packages/frontend/src/stores/settings.store.ts index a2264ab..6e78a2e 100644 --- a/packages/frontend/src/stores/settings.store.ts +++ b/packages/frontend/src/stores/settings.store.ts @@ -1,7 +1,7 @@ import { defineStore } from 'pinia'; -import apiClient from '../utils/apiClient'; // 使用统一的 apiClient -import { ref, computed } from 'vue'; // 移除 watch -import i18n, { setLocale, defaultLng, availableLocales } from '../i18n'; // Import i18n instance, setLocale, defaultLng, and availableLocales +import apiClient from '../utils/apiClient'; +import { ref, computed } from 'vue'; +import i18n, { setLocale, defaultLng, availableLocales } from '../i18n'; import type { PaneName } from './layout.store'; import { useAuthStore } from './auth.store'; import type { ConnectionInfo } from './connections.store'; @@ -62,6 +62,7 @@ interface SettingsState { terminalScrollbackLimit?: string; // 终端回滚行数上限 (e.g., '5000', '0' for unlimited) fileManagerShowDeleteConfirmation?: string; // 'true' or 'false' - 文件管理器删除确认提示 terminalEnableRightClickPaste?: string; // 'true' or 'false' - 终端右键粘贴 + showStatusMonitorIpAddress?: string; // 'true' or 'false' - 状态监视器显示IP地址 [key: string]: string | undefined; } @@ -115,7 +116,7 @@ export const useSettingsStore = defineStore('settings', () => { if (settings.value.showPopupFileEditor === undefined) { settings.value.showPopupFileEditor = 'true'; } - // +++ 添加 showPopupFileManager 默认值 (改为 false) +++ + // +++ showPopupFileManager 默认值 (改为 false) +++ if (settings.value.showPopupFileManager === undefined) { settings.value.showPopupFileManager = 'false'; // 默认禁用弹窗文件管理器 } @@ -304,6 +305,10 @@ export const useSettingsStore = defineStore('settings', () => { settings.value.terminalEnableRightClickPaste = 'true'; // 默认启用右键粘贴 console.log(`[SettingsStore] terminalEnableRightClickPaste not found, set to default: ${settings.value.terminalEnableRightClickPaste}`); } + if (settings.value.showStatusMonitorIpAddress === undefined) { + settings.value.showStatusMonitorIpAddress = 'false'; // 默认禁用状态监视器显示IP + console.log(`[SettingsStore] showStatusMonitorIpAddress not found, set to default: ${settings.value.showStatusMonitorIpAddress}`); + } // --- 语言设置 --- @@ -375,28 +380,29 @@ export const useSettingsStore = defineStore('settings', () => { // 移除外观相关的键检查 const allowedKeys: Array = [ 'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration', - 'showPopupFileEditor', 'showPopupFileManager', 'shareFileEditorTabs', 'ipWhitelistEnabled', // +++ 添加 showPopupFileManager +++ + 'showPopupFileEditor', 'showPopupFileManager', 'shareFileEditorTabs', 'ipWhitelistEnabled', // +++ showPopupFileManager +++ 'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand', - 'statusMonitorIntervalSeconds', // +++ 添加状态监控间隔键 +++ - 'workspaceSidebarPersistent', // +++ 添加侧边栏固定键 +++ - 'sidebarPaneWidths', // +++ 添加侧边栏宽度对象键 +++ - 'fileManagerRowSizeMultiplier', // +++ 添加文件管理器行大小键 +++ - 'fileManagerColWidths', // +++ 添加文件管理器列宽键 +++ - 'commandInputSyncTarget', // +++ 添加命令输入同步目标键 +++ - 'timezone', // 添加时区键 - 'rdpModalWidth', // 添加 RDP 模态框宽度键 - 'rdpModalHeight', // 添加 RDP 模态框高度键 - 'vncModalWidth', // 添加 VNC 模态框宽度键 - 'vncModalHeight', // 添加 VNC 模态框高度键 + 'statusMonitorIntervalSeconds', // +++ 状态监控间隔键 +++ + 'workspaceSidebarPersistent', // +++ 侧边栏固定键 +++ + 'sidebarPaneWidths', // +++ 侧边栏宽度对象键 +++ + 'fileManagerRowSizeMultiplier', // +++ 文件管理器行大小键 +++ + 'fileManagerColWidths', // +++ 文件管理器列宽键 +++ + 'commandInputSyncTarget', // +++ 命令输入同步目标键 +++ + 'timezone', // 时区键 + 'rdpModalWidth', // RDP 模态框宽度键 + 'rdpModalHeight', // RDP 模态框高度键 + 'vncModalWidth', // VNC 模态框宽度键 + 'vncModalHeight', // VNC 模态框高度键 'ipBlacklistEnabled', 'dashboardSortBy', 'dashboardSortOrder', - 'showConnectionTags', - 'showQuickCommandTags', - 'layoutLocked', - 'terminalScrollbackLimit', - 'fileManagerShowDeleteConfirmation', - 'terminalEnableRightClickPaste' + 'showConnectionTags', + 'showQuickCommandTags', + 'layoutLocked', + 'terminalScrollbackLimit', + 'fileManagerShowDeleteConfirmation', + 'terminalEnableRightClickPaste', + 'showStatusMonitorIpAddress' ]; if (!allowedKeys.includes(key)) { console.error(`[SettingsStore] 尝试更新不允许的设置键: ${key}`); @@ -470,28 +476,29 @@ export const useSettingsStore = defineStore('settings', () => { // 移除外观相关的键检查 const allowedKeys: Array = [ 'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration', - 'showPopupFileEditor', 'showPopupFileManager', 'shareFileEditorTabs', 'ipWhitelistEnabled', // +++ 添加 showPopupFileManager +++ + 'showPopupFileEditor', 'showPopupFileManager', 'shareFileEditorTabs', 'ipWhitelistEnabled', // +++ showPopupFileManager +++ 'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand', - 'statusMonitorIntervalSeconds', // +++ 添加状态监控间隔键 +++ - 'workspaceSidebarPersistent', // +++ 添加侧边栏固定键 +++ - 'sidebarPaneWidths', // +++ 添加侧边栏宽度对象键 +++ - 'fileManagerRowSizeMultiplier', // +++ 添加文件管理器行大小键 +++ - 'fileManagerColWidths', // +++ 添加文件管理器列宽键 +++ - 'commandInputSyncTarget', // +++ 添加命令输入同步目标键 +++ - 'timezone', // 添加时区键 - 'rdpModalWidth', // 添加 RDP 模态框宽度键 - 'rdpModalHeight', // 添加 RDP 模态框高度键 - 'vncModalWidth', // 添加 VNC 模态框宽度键 - 'vncModalHeight', // 添加 VNC 模态框高度键 + 'statusMonitorIntervalSeconds', // +++ 状态监控间隔键 +++ + 'workspaceSidebarPersistent', // +++ 侧边栏固定键 +++ + 'sidebarPaneWidths', // +++ 侧边栏宽度对象键 +++ + 'fileManagerRowSizeMultiplier', // +++ 文件管理器行大小键 +++ + 'fileManagerColWidths', // +++ 文件管理器列宽键 +++ + 'commandInputSyncTarget', // +++ 命令输入同步目标键 +++ + 'timezone', // 时区键 + 'rdpModalWidth', // RDP 模态框宽度键 + 'rdpModalHeight', // RDP 模态框高度键 + 'vncModalWidth', // VNC 模态框宽度键 + 'vncModalHeight', // VNC 模态框高度键 'ipBlacklistEnabled', 'dashboardSortBy', 'dashboardSortOrder', - 'showConnectionTags', - 'showQuickCommandTags', - 'layoutLocked', - 'terminalScrollbackLimit', - 'fileManagerShowDeleteConfirmation', - 'terminalEnableRightClickPaste' + 'showConnectionTags', + 'showQuickCommandTags', + 'layoutLocked', + 'terminalScrollbackLimit', + 'fileManagerShowDeleteConfirmation', + 'terminalEnableRightClickPaste', + 'showStatusMonitorIpAddress' ]; const filteredUpdates: Partial = {}; let languageUpdate: string | undefined = undefined; @@ -793,6 +800,10 @@ export const useSettingsStore = defineStore('settings', () => { const terminalEnableRightClickPasteBoolean = computed(() => { return settings.value.terminalEnableRightClickPaste !== 'false'; // Default to true }); + + const statusMonitorShowIpBoolean = computed(() => { + return settings.value.showStatusMonitorIpAddress === 'true'; + }); return { settings, // 只包含通用设置 @@ -838,5 +849,6 @@ export const useSettingsStore = defineStore('settings', () => { terminalScrollbackLimitNumber, // Expose terminal scrollback limit getter fileManagerShowDeleteConfirmationBoolean, // Expose file manager delete confirmation getter terminalEnableRightClickPasteBoolean, // Expose terminal right click paste getter + statusMonitorShowIpBoolean, // 暴露状态监视器显示IP getter }; }); diff --git a/packages/frontend/src/utils/apiClient.ts b/packages/frontend/src/utils/apiClient.ts index e20bc96..3b0ee25 100644 --- a/packages/frontend/src/utils/apiClient.ts +++ b/packages/frontend/src/utils/apiClient.ts @@ -1,6 +1,6 @@ import axios from 'axios'; -import router from '../router'; // 引入 router 用于可能的重定向 -import { useAuthStore } from '../stores/auth.store'; // 引入 auth store 用于检查认证状态和登出 +import router from '../router'; +import { useAuthStore } from '../stores/auth.store'; // 创建 axios 实例 const apiClient = axios.create({ diff --git a/packages/frontend/src/views/DashboardView.vue b/packages/frontend/src/views/DashboardView.vue index 563e1c8..e0df043 100644 --- a/packages/frontend/src/views/DashboardView.vue +++ b/packages/frontend/src/views/DashboardView.vue @@ -1,17 +1,17 @@