From 598df938bf75d0197f3b6a17eff43bc96894206a Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Sun, 11 May 2025 11:20:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=A8=A1=E6=80=81=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/auth/auth.routes.ts | 2 +- .../src/repositories/tag.repository.ts | 34 +++ packages/backend/src/services/sftp.service.ts | 18 +- packages/backend/src/services/tag.service.ts | 20 ++ .../backend/src/settings/settings.routes.ts | 18 +- packages/backend/src/sftp/sftp.routes.ts | 2 +- packages/backend/src/tags/tags.controller.ts | 37 +++ packages/backend/src/tags/tags.routes.ts | 4 +- packages/backend/src/websocket/connection.ts | 8 +- .../src/websocket/handlers/ssh.handler.ts | 2 +- packages/backend/src/websocket/types.ts | 8 +- packages/backend/src/websocket/utils.ts | 4 +- packages/frontend/src/App.vue | 2 +- .../src/components/AddConnectionForm.vue | 2 +- .../src/components/CommandInputBar.vue | 2 +- .../src/components/FileEditorContainer.vue | 12 +- .../src/components/FileEditorOverlay.vue | 12 +- .../frontend/src/components/FileManager.vue | 44 +-- .../components/ManageTagConnectionsModal.vue | 251 ++++++++++++++++++ packages/frontend/src/components/Terminal.vue | 8 +- .../src/components/TerminalTabBar.vue | 12 +- .../components/WorkspaceConnectionList.vue | 71 ++++- .../file-manager/useFileManagerContextMenu.ts | 12 +- .../src/composables/useSftpActions.ts | 30 +-- packages/frontend/src/locales/en-US.json | 20 +- packages/frontend/src/locales/ja-JP.json | 18 +- packages/frontend/src/locales/zh-CN.json | 18 +- .../frontend/src/stores/connections.store.ts | 16 +- .../frontend/src/stores/fileEditor.store.ts | 14 +- .../src/stores/focusSwitcher.store.ts | 10 +- packages/frontend/src/stores/layout.store.ts | 8 +- .../src/stores/quickCommands.store.ts | 2 +- .../session/actions/sshSuspendActions.ts | 8 +- packages/frontend/src/stores/session/types.ts | 14 +- packages/frontend/src/stores/tags.store.ts | 31 +++ .../frontend/src/types/websocket.types.ts | 12 +- .../frontend/src/views/CommandHistoryView.vue | 4 +- packages/frontend/src/views/DashboardView.vue | 2 +- .../frontend/src/views/QuickCommandsView.vue | 4 +- packages/frontend/src/views/WorkspaceView.vue | 8 +- 40 files changed, 634 insertions(+), 170 deletions(-) create mode 100644 packages/frontend/src/components/ManageTagConnectionsModal.vue diff --git a/packages/backend/src/auth/auth.routes.ts b/packages/backend/src/auth/auth.routes.ts index eb0d9b3..85548cd 100644 --- a/packages/backend/src/auth/auth.routes.ts +++ b/packages/backend/src/auth/auth.routes.ts @@ -20,7 +20,7 @@ import { listUserPasskeysHandler, deleteUserPasskeyHandler, updateUserPasskeyNameHandler, // 新增:更新 Passkey 名称的处理器 - checkHasPasskeys // +++ 新增:检查是否有 Passkey 配置的处理器 + checkHasPasskeys } from './auth.controller'; import { isAuthenticated } from './auth.middleware'; import { ipBlacklistCheckMiddleware } from './ipBlacklistCheck.middleware'; diff --git a/packages/backend/src/repositories/tag.repository.ts b/packages/backend/src/repositories/tag.repository.ts index 0635485..c41c79a 100644 --- a/packages/backend/src/repositories/tag.repository.ts +++ b/packages/backend/src/repositories/tag.repository.ts @@ -94,3 +94,37 @@ export const deleteTag = async (id: number): Promise => { throw new Error('删除标签失败'); } }; + +/** + * 更新标签与连接的关联关系 + */ +export const updateTagConnections = async (tagId: number, connectionIds: number[]): Promise => { + const db = await getDbInstance(); + try { + // 开始事务 + await runDb(db, 'BEGIN TRANSACTION'); + + // 1. 删除该标签旧的连接关联 + const deleteSql = `DELETE FROM connection_tags WHERE tag_id = ?`; + await runDb(db, deleteSql, [tagId]); + + // 2. 如果有新的连接 ID,则插入新的关联 + if (connectionIds && connectionIds.length > 0) { + const insertSql = `INSERT INTO connection_tags (tag_id, connection_id) VALUES (?, ?)`; + // 使用 Promise.all 来并行执行插入操作,或者逐个执行 + // 为简单起见,这里逐个执行,但对于大量数据,并行或批量插入更优 + for (const connectionId of connectionIds) { + // 检查 connectionId 是否有效(例如,是否存在于 connections 表中)可以增加健壮性,但此处省略 + await runDb(db, insertSql, [tagId, connectionId]); + } + } + + // 提交事务 + await runDb(db, 'COMMIT'); + } catch (err: any) { + // 如果发生错误,回滚事务 + await runDb(db, 'ROLLBACK'); + console.error(`[仓库] 更新标签 ${tagId} 的连接关联时出错:`, err.message); + throw new Error(`更新标签连接关联失败: ${err.message}`); + } +}; diff --git a/packages/backend/src/services/sftp.service.ts b/packages/backend/src/services/sftp.service.ts index 2d059e3..f926dde 100644 --- a/packages/backend/src/services/sftp.service.ts +++ b/packages/backend/src/services/sftp.service.ts @@ -54,7 +54,7 @@ interface ActiveUpload { bytesWritten: number; stream: WriteStream; sessionId: string; // Link back to the session for cleanup - relativePath?: string; // +++ 新增:存储相对路径 +++ + relativePath?: string; } export class SftpService { @@ -646,7 +646,7 @@ export class SftpService { } } - // +++ 新增:复制文件或目录 +++ + // +++ 复制文件或目录 +++ async copy(sessionId: string, sources: string[], destinationDir: string, requestId: string): Promise { const state = this.clientStates.get(sessionId); if (!state || !state.sftp) { @@ -720,7 +720,7 @@ export class SftpService { } } - // +++ 新增:移动文件或目录 +++ + // +++ 移动文件或目录 +++ async move(sessionId: string, sources: string[], destinationDir: string, requestId: string): Promise { const state = this.clientStates.get(sessionId); if (!state || !state.sftp) { @@ -803,7 +803,7 @@ export class SftpService { } } - // +++ 新增:辅助方法 - 复制文件 +++ + // +++ 辅助方法 - 复制文件 +++ private copyFile(sftp: SFTPWrapper, sourcePath: string, destPath: string): Promise { return new Promise((resolve, reject) => { const readStream = sftp.createReadStream(sourcePath); @@ -833,7 +833,7 @@ export class SftpService { }); } - // +++ 新增:辅助方法 - 递归复制目录 +++ + // +++ 辅助方法 - 递归复制目录 +++ private async copyDirectoryRecursive(sftp: SFTPWrapper, sourcePath: string, destPath: string): Promise { try { // Create destination directory @@ -861,7 +861,7 @@ export class SftpService { } } - // +++ 新增:辅助方法 - 获取 Stats (Promise wrapper) +++ + // +++ 辅助方法 - 获取 Stats (Promise wrapper) +++ private getStats(sftp: SFTPWrapper, path: string): Promise { return new Promise((resolve, reject) => { sftp.lstat(path, (err, stats) => { @@ -951,7 +951,7 @@ export class SftpService { } } - // +++ 新增:辅助方法 - 列出目录内容 (Promise wrapper) +++ + // +++ 辅助方法 - 列出目录内容 (Promise wrapper) +++ private listDirectory(sftp: SFTPWrapper, path: string): Promise { // 使用本地接口 SftpDirEntry return new Promise((resolve, reject) => { sftp.readdir(path, (err, list) => { // list 的类型现在是 SftpDirEntry[] @@ -964,7 +964,7 @@ export class SftpService { }); } - // +++ 新增:辅助方法 - 执行重命名 (Promise wrapper) +++ + // +++ 辅助方法 - 执行重命名 (Promise wrapper) +++ private performRename(sftp: SFTPWrapper, oldPath: string, newPath: string): Promise { return new Promise((resolve, reject) => { sftp.rename(oldPath, newPath, (err) => { @@ -977,7 +977,7 @@ export class SftpService { }); } - // +++ 新增:辅助方法 - 格式化 Stats 为 FileListItem +++ + // +++ 辅助方法 - 格式化 Stats 为 FileListItem +++ private formatStatsToFileListItem(itemPath: string, stats: Stats): any { return { filename: pathModule.basename(itemPath), diff --git a/packages/backend/src/services/tag.service.ts b/packages/backend/src/services/tag.service.ts index 93b41d0..f35bbb5 100644 --- a/packages/backend/src/services/tag.service.ts +++ b/packages/backend/src/services/tag.service.ts @@ -75,3 +75,23 @@ export const updateTag = async (id: number, name: string): Promise => { return TagRepository.deleteTag(id); }; + +/** + * 更新标签与连接的关联关系 + */ +export const updateTagConnections = async (tagId: number, connectionIds: number[]): Promise => { + // 在服务层可以添加额外的业务逻辑,例如验证 tagId 和 connectionIds 的有效性 + // 例如,检查标签是否存在,连接 ID 是否都存在于数据库中等。 + // 此处为简化,直接调用 repository 方法。 + + // 确保 connectionIds 是一个数组,即使是空数组 + const idsToUpdate = Array.isArray(connectionIds) ? connectionIds : []; + + try { + await TagRepository.updateTagConnections(tagId, idsToUpdate); + } catch (error: any) { + // 服务层可以进一步处理或包装错误 + console.error(`Service: 更新标签 ${tagId} 的连接关联时发生错误:`, error.message); + throw new Error(`服务层更新标签连接关联失败: ${error.message}`); + } +}; diff --git a/packages/backend/src/settings/settings.routes.ts b/packages/backend/src/settings/settings.routes.ts index 50ad1f3..08a3f3c 100644 --- a/packages/backend/src/settings/settings.routes.ts +++ b/packages/backend/src/settings/settings.routes.ts @@ -15,19 +15,19 @@ router.use(isAuthenticated); router.get('/', settingsController.getAllSettings); // GET /api/v1/settings router.put('/', settingsController.updateSettings); // PUT /api/v1/settings -// +++ 新增:焦点切换顺序路由 +++ +// +++ 焦点切换顺序路由 +++ // GET /api/v1/settings/focus-switcher-sequence - 获取焦点切换顺序 router.get('/focus-switcher-sequence', settingsController.getFocusSwitcherSequence); // PUT /api/v1/settings/focus-switcher-sequence - 更新焦点切换顺序 router.put('/focus-switcher-sequence', settingsController.setFocusSwitcherSequence); -// +++ 新增:导航栏可见性路由 +++ +// +++ 导航栏可见性路由 +++ // GET /api/v1/settings/nav-bar-visibility - 获取导航栏可见性 router.get('/nav-bar-visibility', settingsController.getNavBarVisibility); // PUT /api/v1/settings/nav-bar-visibility - 更新导航栏可见性 router.put('/nav-bar-visibility', settingsController.setNavBarVisibility); -// +++ 新增:布局树路由 +++ +// +++ 布局树路由 +++ // GET /api/v1/settings/layout - 获取布局树 router.get('/layout', settingsController.getLayoutTree); // PUT /api/v1/settings/layout - 更新布局树 @@ -41,38 +41,38 @@ router.get('/ip-blacklist', settingsController.getIpBlacklist); router.delete('/ip-blacklist/:ip', settingsController.deleteIpFromBlacklist); -// +++ 新增:终端选中自动复制路由 +++ +// +++ 终端选中自动复制路由 +++ // GET /api/v1/settings/auto-copy-on-select - 获取设置 router.get('/auto-copy-on-select', settingsController.getAutoCopyOnSelect); // PUT /api/v1/settings/auto-copy-on-select - 更新设置 router.put('/auto-copy-on-select', settingsController.setAutoCopyOnSelect); -// +++ 新增:侧栏配置路由 +++ +// +++ 侧栏配置路由 +++ // GET /api/v1/settings/sidebar - 获取侧栏配置 router.get('/sidebar', settingsController.getSidebarConfig); // PUT /api/v1/settings/sidebar - 更新侧栏配置 router.put('/sidebar', settingsController.setSidebarConfig); -// +++ 新增:显示连接标签路由 +++ +// +++ 显示连接标签路由 +++ // GET /api/v1/settings/show-connection-tags - 获取设置 router.get('/show-connection-tags', settingsController.getShowConnectionTags); // PUT /api/v1/settings/show-connection-tags - 更新设置 router.put('/show-connection-tags', settingsController.setShowConnectionTags); -// +++ 新增:显示快捷指令标签路由 +++ +// +++ 显示快捷指令标签路由 +++ // GET /api/v1/settings/show-quick-command-tags - 获取设置 router.get('/show-quick-command-tags', settingsController.getShowQuickCommandTags); // PUT /api/v1/settings/show-quick-command-tags - 更新设置 router.put('/show-quick-command-tags', settingsController.setShowQuickCommandTags); -// +++ 新增:导出所有连接路由 +++ +// +++ 导出所有连接路由 +++ // GET /api/v1/settings/export-connections - 导出所有连接为加密的 ZIP 文件 router.get('/export-connections', settingsController.exportAllConnections); export default router; -// +++ 新增:CAPTCHA 配置路由 (需要认证更新) +++ +// +++ CAPTCHA 配置路由 (需要认证更新) +++ // PUT /api/v1/settings/captcha - 更新 CAPTCHA 配置 // 注意:这个路由定义在 `export default router` 之后,这是不正确的。 // 我会将它移到 `export default router` 之前,并确保它也在 `isAuthenticated` 中间件的作用域内。 diff --git a/packages/backend/src/sftp/sftp.routes.ts b/packages/backend/src/sftp/sftp.routes.ts index 13ba6e6..43a819e 100644 --- a/packages/backend/src/sftp/sftp.routes.ts +++ b/packages/backend/src/sftp/sftp.routes.ts @@ -10,7 +10,7 @@ router.use(isAuthenticated); // GET /api/v1/sftp/download?connectionId=...&remotePath=... router.get('/download', downloadFile); -// +++ 新增:GET /api/v1/sftp/download-directory?connectionId=...&remotePath=... +++ +// +++ GET /api/v1/sftp/download-directory?connectionId=...&remotePath=... +++ router.get('/download-directory', downloadDirectory); diff --git a/packages/backend/src/tags/tags.controller.ts b/packages/backend/src/tags/tags.controller.ts index aa094c9..ccdd1de 100644 --- a/packages/backend/src/tags/tags.controller.ts +++ b/packages/backend/src/tags/tags.controller.ts @@ -130,3 +130,40 @@ export const deleteTag = async (req: Request, res: Response): Promise => { res.status(500).json({ message: error.message || '删除标签时发生内部服务器错误。' }); } }; + +/** + * 更新标签与连接的关联关系 (PUT /api/v1/tags/:id/connections) + */ +export const updateTagConnections = async (req: Request, res: Response): Promise => { + const tagId = parseInt(req.params.id, 10); + const { connection_ids } = req.body; // 前端发送的是 connection_ids + + if (isNaN(tagId)) { + res.status(400).json({ message: '无效的标签 ID。' }); + return; + } + + if (!Array.isArray(connection_ids)) { + res.status(400).json({ message: 'connection_ids 必须是一个数组。' }); + return; + } + + // 可选:验证 connection_ids 中的每个 ID 是否为数字 + if (!connection_ids.every(id => typeof id === 'number')) { + res.status(400).json({ message: 'connection_ids 数组中的所有元素必须是数字。' }); + return; + } + + try { + await TagService.updateTagConnections(tagId, connection_ids); + res.status(200).json({ message: '标签的连接关联更新成功。' }); + } catch (error: any) { + console.error(`Controller: 更新标签 ${tagId} 的连接关联时发生错误:`, error); + // 可以根据 TagService 抛出的错误类型来返回更具体的错误码和消息 + if (error.message.includes('标签未找到')) { // 假设服务层或仓库层会检查标签是否存在 + res.status(404).json({ message: '标签未找到。' }); + } else { + res.status(500).json({ message: error.message || '更新标签连接关联时发生内部服务器错误。' }); + } + } +}; diff --git a/packages/backend/src/tags/tags.routes.ts b/packages/backend/src/tags/tags.routes.ts index 1dfd50e..c5839fc 100644 --- a/packages/backend/src/tags/tags.routes.ts +++ b/packages/backend/src/tags/tags.routes.ts @@ -5,7 +5,8 @@ import { getTags, getTagById, updateTag, - deleteTag + deleteTag, + updateTagConnections // +++ 导入新的控制器方法 +++ } from './tags.controller'; const router = Router(); @@ -19,5 +20,6 @@ router.get('/', getTags); // GET /api/v1/tags - 获取标签列表 router.get('/:id', getTagById); // GET /api/v1/tags/:id - 获取单个标签 router.put('/:id', updateTag); // PUT /api/v1/tags/:id - 更新标签 router.delete('/:id', deleteTag); // DELETE /api/v1/tags/:id - 删除标签 +router.put('/:id/connections', updateTagConnections); // PUT /api/v1/tags/:id/connections - 更新标签的连接关联 export default router; diff --git a/packages/backend/src/websocket/connection.ts b/packages/backend/src/websocket/connection.ts index d785f8d..3b9b63f 100644 --- a/packages/backend/src/websocket/connection.ts +++ b/packages/backend/src/websocket/connection.ts @@ -18,15 +18,15 @@ import { SshSuspendAutoTerminatedNotification, SshMarkForSuspendRequest, SshMarkedForSuspendAck, - SshUnmarkForSuspendRequest, // +++ 新增导入 +++ - SshUnmarkedForSuspendAck, // +++ 新增导入 +++ + SshUnmarkForSuspendRequest, + SshUnmarkedForSuspendAck, ClientState } from './types'; import { SshSuspendService } from '../services/ssh-suspend.service'; import { SftpService } from '../services/sftp.service'; import { cleanupClientConnection } from './utils'; import { clientStates } from './state'; -import { temporaryLogStorageService } from '../services/temporary-log-storage.service'; // +++ 新增导入 +import { temporaryLogStorageService } from '../services/temporary-log-storage.service'; // Handlers import { handleRdpProxyConnection } from './handlers/rdp.handler'; @@ -233,7 +233,7 @@ export function initializeConnectionHandler(wss: WebSocketServer, sshSuspendServ // console.warn(`[WebSocket Handler][${type}] WebSocket 在发送 SSH_OUTPUT_CACHED_CHUNK 前已关闭 (会话 ${newFrontendSessionId})。`); } - // +++ 新增:发送 ssh:connected 消息 +++ + // +++ 发送 ssh:connected 消息 +++ if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'ssh:connected', diff --git a/packages/backend/src/websocket/handlers/ssh.handler.ts b/packages/backend/src/websocket/handlers/ssh.handler.ts index 044e924..55b4a1b 100644 --- a/packages/backend/src/websocket/handlers/ssh.handler.ts +++ b/packages/backend/src/websocket/handlers/ssh.handler.ts @@ -4,7 +4,7 @@ import { AuthenticatedWebSocket, ClientState } from '../types'; import { clientStates, sftpService, statusMonitorService, auditLogService, notificationService } from '../state'; import * as SshService from '../../services/ssh.service'; import { cleanupClientConnection } from '../utils'; -import { temporaryLogStorageService } from '../../services/temporary-log-storage.service'; // +++ 新增导入 +import { temporaryLogStorageService } from '../../services/temporary-log-storage.service'; import { startDockerStatusPolling } from './docker.handler'; import WebSocket from 'ws'; diff --git a/packages/backend/src/websocket/types.ts b/packages/backend/src/websocket/types.ts index 4526e62..8561af4 100644 --- a/packages/backend/src/websocket/types.ts +++ b/packages/backend/src/websocket/types.ts @@ -110,7 +110,7 @@ export interface SshMarkForSuspendRequest { type: "SSH_MARK_FOR_SUSPEND"; payload: { sessionId: string; // The ID of the active SSH session to be marked - initialBuffer?: string; // +++ 新增:可选的初始屏幕缓冲区内容 +++ + initialBuffer?: string; // +++ 可选的初始屏幕缓冲区内容 +++ }; } @@ -205,7 +205,7 @@ export interface SshMarkedForSuspendAck { }; } -export interface SshUnmarkedForSuspendAck { // +++ 新增 S2C 类型 +++ +export interface SshUnmarkedForSuspendAck { // +++ S2C 类型 +++ type: "SSH_UNMARKED_FOR_SUSPEND_ACK"; payload: { sessionId: string; // The ID of the session that was unmarked @@ -231,7 +231,7 @@ export type SshSuspendClientToServerMessages = | SshSuspendRemoveEntryRequest | SshSuspendEditNameRequest | SshMarkForSuspendRequest - | SshUnmarkForSuspendRequest; // +++ 新增到联合类型 +++ + | SshUnmarkForSuspendRequest; // Union type for all server-to-client messages for SSH Suspend export type SshSuspendServerToClientMessages = @@ -244,7 +244,7 @@ export type SshSuspendServerToClientMessages = | SshSuspendNameEditedResponse | SshSuspendAutoTerminatedNotification | SshMarkedForSuspendAck - | SshUnmarkedForSuspendAck; // +++ 新增到联合类型 +++ + | SshUnmarkedForSuspendAck; // It might be useful to have a general type for incoming messages if not already present // For example, if you have a main message handler: diff --git a/packages/backend/src/websocket/utils.ts b/packages/backend/src/websocket/utils.ts index c8a8701..19d10f9 100644 --- a/packages/backend/src/websocket/utils.ts +++ b/packages/backend/src/websocket/utils.ts @@ -2,9 +2,9 @@ import { PortInfo, ClientState } from './types'; import { SftpService } from '../services/sftp.service'; import { StatusMonitorService } from '../services/status-monitor.service'; import { clientStates, sftpService, statusMonitorService } from './state'; -import { sshSuspendService } from '../services/ssh-suspend.service'; // +++ 新增导入 +++ +import { sshSuspendService } from '../services/ssh-suspend.service'; -// --- 新增:解析 Ports 字符串的辅助函数 --- +// --- 解析 Ports 字符串的辅助函数 --- export function parsePortsString(portsString: string | undefined | null): PortInfo[] { if (!portsString) { return []; diff --git a/packages/frontend/src/App.vue b/packages/frontend/src/App.vue index c25139a..0dfeec6 100644 --- a/packages/frontend/src/App.vue +++ b/packages/frontend/src/App.vue @@ -43,7 +43,7 @@ const route = useRoute(); const navRef = ref(null); const underlineRef = ref(null); -// +++ 新增:存储上一次由切换器聚焦的 ID +++ +// +++ 存储上一次由切换器聚焦的 ID +++ const lastFocusedIdBySwitcher = ref(null); const isAltPressed = ref(false); // 跟踪 Alt 键是否按下 const altShortcutKey = ref(null); diff --git a/packages/frontend/src/components/AddConnectionForm.vue b/packages/frontend/src/components/AddConnectionForm.vue index 8e23d8c..f5b4f09 100644 --- a/packages/frontend/src/components/AddConnectionForm.vue +++ b/packages/frontend/src/components/AddConnectionForm.vue @@ -679,7 +679,7 @@ const testButtonText = computed(() => {
+
+
  • { {{ t('workspaceConnectionList.noSshConnectionsToConnectMenu') }}
  • +
  • +
  • + + {{ t('workspaceConnectionList.manageTags.menuItem') }} +
@@ -819,7 +863,12 @@ const cancelEditingTag = () => { :connection="selectedRdpConnection" @close="closeRdpModal" /> --> - + + diff --git a/packages/frontend/src/composables/file-manager/useFileManagerContextMenu.ts b/packages/frontend/src/composables/file-manager/useFileManagerContextMenu.ts index 441e729..0628e16 100644 --- a/packages/frontend/src/composables/file-manager/useFileManagerContextMenu.ts +++ b/packages/frontend/src/composables/file-manager/useFileManagerContextMenu.ts @@ -25,21 +25,21 @@ export interface UseFileManagerContextMenuOptions { currentPath: Ref; isConnected: Ref; isSftpReady: Ref; - clipboardState: Ref>; // +++ 新增:剪贴板状态 +++ + clipboardState: Ref>; // +++ 剪贴板状态 +++ t: ReturnType['t']; // 使用 useI18n 获取 t 的类型 // --- 回调函数 --- onRefresh: () => void; onUpload: () => void; onDownload: (items: FileListItem[]) => void; // 文件下载回调 - onDownloadDirectory: (item: FileListItem) => void; // +++ 新增:文件夹下载回调 +++ + onDownloadDirectory: (item: FileListItem) => void; // +++ 文件夹下载回调 +++ onDelete: () => void; // 删除操作现在由外部处理 onRename: (item: FileListItem) => void; onChangePermissions: (item: FileListItem) => void; onNewFolder: () => void; onNewFile: () => void; - onCopy: () => void; // +++ 新增:复制回调 +++ - onCut: () => void; // +++ 新增:剪切回调 +++ - onPaste: () => void; // +++ 新增:粘贴回调 +++ + onCopy: () => void; // +++ 复制回调 +++ + onCut: () => void; // +++ 剪切回调 +++ + onPaste: () => void; // +++ 粘贴回调 +++ } export function useFileManagerContextMenu(options: UseFileManagerContextMenuOptions) { @@ -111,7 +111,7 @@ export function useFileManagerContextMenu(options: UseFileManagerContextMenuOpti { label: t('fileManager.actions.copy'), action: onCopy, disabled: !canPerformActions }, ]; - // --- 新增:多选下载 --- + // --- 多选下载 --- // 多选时暂时禁用文件夹下载,只允许下载文件 // 如果需要支持多选文件夹下载或混合下载,需要更复杂的逻辑和后端支持(例如打包成 zip) // 目前仅在 allFilesSelected 为 true 时启用多文件下载 diff --git a/packages/frontend/src/composables/useSftpActions.ts b/packages/frontend/src/composables/useSftpActions.ts index ff2f67a..142545e 100644 --- a/packages/frontend/src/composables/useSftpActions.ts +++ b/packages/frontend/src/composables/useSftpActions.ts @@ -31,7 +31,7 @@ const sortFiles = (a: FileListItem, b: FileListItem): number => { return a.filename.localeCompare(b.filename); }; -// *** 新增:文件树节点接口 *** +// *** 文件树节点接口 *** export interface FileTreeNode { filename: string; longname: string; // 保留 longname 以便显示 @@ -61,16 +61,16 @@ export function createSftpActionsManager( // const fileList = ref([]); // 不再直接使用 fileList ref const isLoading = ref(false); - const loadingRequestId = ref(null); // 新增:跟踪当前加载请求 ID + const loadingRequestId = ref(null); // 跟踪当前加载请求 ID // const error = ref(null); // 不再使用本地 error ref const instanceSessionId = sessionId; // 保存会话 ID 用于日志 const uiNotificationsStore = useUiNotificationsStore(); // 初始化 UI 通知 store - const initialLoadDone = ref(false); // +++ 新增:跟踪此实例是否已完成初始加载 +++ + const initialLoadDone = ref(false); // +++ 跟踪此实例是否已完成初始加载 +++ // 用于存储注销函数的数组 const unregisterCallbacks: (() => void)[] = []; - // *** 新增:响应式文件树 *** + // *** 响应式文件树 *** const fileTree = reactive({ filename: '/', // 根节点代表根目录 longname: '/', @@ -223,7 +223,7 @@ export function createSftpActionsManager( console.warn(`[SFTP ${instanceSessionId}] 尝试加载目录 ${path} 但 SFTP 未就绪。`); // 日志改为中文 return; } - // *** 新增:如果已经在加载,则阻止新的加载请求 *** + // *** 如果已经在加载,则阻止新的加载请求 *** if (isLoading.value) { console.warn(`[SFTP ${instanceSessionId}] 尝试加载目录 ${path} 但已在加载中。`); return; @@ -415,7 +415,7 @@ export function createSftpActionsManager( }); }; - // +++ 新增:复制项目 +++ + // +++ 复制项目 +++ const copyItems = (sourcePaths: string[], destinationDir: string) => { if (!isSftpReady.value) { uiNotificationsStore.showError(t('fileManager.errors.sftpNotReady')); @@ -433,7 +433,7 @@ export function createSftpActionsManager( // 可选:显示一个“正在复制...”的通知 }; - // +++ 新增:移动项目 +++ + // +++ 移动项目 +++ const moveItems = (sourcePaths: string[], destinationDir: string) => { if (!isSftpReady.value) { uiNotificationsStore.showError(t('fileManager.errors.sftpNotReady')); @@ -579,12 +579,12 @@ export function createSftpActionsManager( // 移除通用的 onActionSuccessRefresh - // *** 新增:具体操作成功后的处理函数 *** + // *** 具体操作成功后的处理函数 *** // *** 移除旧的 invalidateCache *** // const invalidateCache = (path: string) => { ... }; - // *** 新增:辅助函数 - 从文件树中移除节点 *** + // *** 辅助函数 - 从文件树中移除节点 *** const removeNodeFromTree = (parentPath: string, filename: string): boolean => { const parentNode = findNodeByPath(fileTree, parentPath); if (parentNode && parentNode.children) { @@ -787,7 +787,7 @@ export function createSftpActionsManager( } }; - // +++ 新增:处理复制成功 +++ + // +++ 处理复制成功 +++ const onCopySuccess = (payload: MessagePayload, message: WebSocketMessage) => { // 后端应发送 { destination: string, items: FileListItem[] | null } const copyPayload = payload as { destination: string, items: FileListItem[] | null }; @@ -825,7 +825,7 @@ export function createSftpActionsManager( } }; - // +++ 新增:处理移动成功 +++ + // +++ 处理移动成功 +++ const onMoveSuccess = (payload: MessagePayload, message: WebSocketMessage) => { // 后端应发送 { sources: string[], destination: string, items: FileListItem[] | null } const movePayload = payload as { sources: string[], destination: string, items: FileListItem[] | null }; @@ -867,7 +867,7 @@ export function createSftpActionsManager( }; - // *** 新增:处理上传成功 *** + // *** 处理上传成功 *** const onUploadSuccess = (payload: MessagePayload, message: WebSocketMessage) => { const newItem = payload as FileListItem | null; // 后端应发送 FileListItem 或 null const fullPath = message.path; // 后端现在应该在 message 中包含完整的上传路径 @@ -944,14 +944,14 @@ export function createSftpActionsManager( unregisterCallbacks.push(onMessage('sftp:rename:success', onRenameSuccess)); unregisterCallbacks.push(onMessage('sftp:chmod:success', onChmodSuccess)); unregisterCallbacks.push(onMessage('sftp:writefile:success', onWriteFileSuccess)); // 使用 onWriteFileSuccess - unregisterCallbacks.push(onMessage('sftp:upload:success', onUploadSuccess)); // *** 新增:监听上传成功 *** + unregisterCallbacks.push(onMessage('sftp:upload:success', onUploadSuccess)); // *** 监听上传成功 *** unregisterCallbacks.push(onMessage('sftp:mkdir:error', onActionError)); unregisterCallbacks.push(onMessage('sftp:rmdir:error', onActionError)); unregisterCallbacks.push(onMessage('sftp:unlink:error', onActionError)); unregisterCallbacks.push(onMessage('sftp:rename:error', onActionError)); unregisterCallbacks.push(onMessage('sftp:chmod:error', onActionError)); unregisterCallbacks.push(onMessage('sftp:writefile:error', onActionError)); - // +++ 新增:监听复制/移动错误 +++ + // +++ 监听复制/移动错误 +++ unregisterCallbacks.push(onMessage('sftp:copy:success', onCopySuccess)); unregisterCallbacks.push(onMessage('sftp:copy:error', onActionError)); unregisterCallbacks.push(onMessage('sftp:move:success', onMoveSuccess)); @@ -959,7 +959,7 @@ export function createSftpActionsManager( // 移除 onUnmounted 块 - // *** 新增:计算属性 fileList *** + // *** 计算属性 fileList *** const fileList = computed(() => { const node = findNodeByPath(fileTree, currentPathRef.value); if (node && node.childrenLoaded && node.children) { diff --git a/packages/frontend/src/locales/en-US.json b/packages/frontend/src/locales/en-US.json index c1e9e71..fb3d1a0 100644 --- a/packages/frontend/src/locales/en-US.json +++ b/packages/frontend/src/locales/en-US.json @@ -418,7 +418,9 @@ "removeSelection": "Remove this tag selection", "deleteTagGlobally": "Delete this tag globally", "createSuccess": "Tag created successfully.", - "updateSuccess": "Tag updated successfully." + "updateSuccess": "Tag updated successfully.", + "deleteSuccess": "Tag \"{name}\" deleted successfully.", + "deleteFailed": "Failed to delete tag \"{name}\": {error}" }, "settings": { "title": "Settings", @@ -928,9 +930,21 @@ "connectingAllSshInGroup": "Connecting {count} SSH connections in group '{groupName}'...", "noSshConnectionsInGroup": "No SSH connections to connect in group '{groupName}'.", "connectAllSshInGroupMenu": "Connect All", - "noSshConnectionsToConnectMenu": "No SSH Connections" + "noSshConnectionsToConnectMenu": "No SSH Connections", + "manageTags": { + "title": "Manage Tag Connections", + "searchPlaceholder": "Search connections...", + "selectAll": "Select All", + "deselectAll": "Deselect All", + "noConnectionsFound": "No connections found.", + "saveSuccess": "Tag connections updated successfully.", + "saveFailed": "Failed to update tag connections.", + "menuItem": "Manage Tag", + "cannotManageUntagged": "Cannot manage connections for 'Untagged' group.", + "invertSelection": "Invert Selection" + } }, -"remoteDesktopModal": { + "remoteDesktopModal": { "title": "Remote Desktop", "titlePlaceholder": "Remote Desktop Connection", "status": { diff --git a/packages/frontend/src/locales/ja-JP.json b/packages/frontend/src/locales/ja-JP.json index e34c2f9..d589682 100644 --- a/packages/frontend/src/locales/ja-JP.json +++ b/packages/frontend/src/locales/ja-JP.json @@ -1123,7 +1123,9 @@ "removeSelection": "このタグの選択を解除", "title": "タグ管理", "createSuccess": "タグが正常に作成されました。", - "updateSuccess": "タグが正常に更新されました。" + "updateSuccess": "タグが正常に更新されました。", + "deleteSuccess": "タグ「{name}」が正常に削除されました。", + "deleteFailed": "タグ「{name}」の削除に失敗しました: {error}" }, "terminalTabBar": { "selectServerTitle": "接続するサーバーを選択" @@ -1147,7 +1149,19 @@ "connectingAllSshInGroup": "グループ '{groupName}' 内の {count} 個の SSH 接続に接続しています...", "noSshConnectionsInGroup": "グループ '{groupName}' 内に接続可能な SSH 接続がありません。", "connectAllSshInGroupMenu": "すべて接続", - "noSshConnectionsToConnectMenu": "SSH 接続なし" + "noSshConnectionsToConnectMenu": "SSH 接続なし", + "manageTags": { + "title": "タグ接続の管理", + "searchPlaceholder": "接続を検索...", + "selectAll": "すべて選択", + "deselectAll": "すべて選択解除", + "noConnectionsFound": "接続が見つかりません。", + "saveSuccess": "タグ接続が正常に更新されました。", + "saveFailed": "タグ接続の更新に失敗しました。", + "menuItem": "タグを管理", + "cannotManageUntagged": "「タグなし」グループの接続は管理できません。", + "invertSelection": "選択を反転" + } }, "sshKeys": { "selector": { diff --git a/packages/frontend/src/locales/zh-CN.json b/packages/frontend/src/locales/zh-CN.json index 366ff94..f3eec76 100644 --- a/packages/frontend/src/locales/zh-CN.json +++ b/packages/frontend/src/locales/zh-CN.json @@ -417,7 +417,9 @@ "removeSelection": "移除此标签选择", "deleteTagGlobally": "全局删除此标签", "createSuccess": "标签创建成功。", - "updateSuccess": "标签更新成功。" + "updateSuccess": "标签更新成功。", + "deleteSuccess": "标签 \"{name}\" 删除成功。", + "deleteFailed": "标签 \"{name}\" 删除失败: {error}" }, "settings": { "title": "设置", @@ -930,7 +932,19 @@ "connectingAllSshInGroup": "正在连接组 '{groupName}' 中的 {count} 个 SSH 连接...", "noSshConnectionsInGroup": "组 '{groupName}' 中没有 SSH 类型的连接可供连接。", "connectAllSshInGroupMenu": "连接全部", - "noSshConnectionsToConnectMenu": "无 SSH 连接" + "noSshConnectionsToConnectMenu": "无 SSH 连接", + "manageTags": { + "title": "管理标签连接", + "searchPlaceholder": "搜索连接...", + "selectAll": "全选", + "deselectAll": "取消全选", + "noConnectionsFound": "未找到连接。", + "saveSuccess": "标签连接更新成功。", + "saveFailed": "更新标签连接失败。", + "menuItem": "管理标签", + "cannotManageUntagged": "无法管理“未标记”分组的连接。", + "invertSelection": "反选" + } }, "remoteDesktopModal": { "title": "远程桌面", diff --git a/packages/frontend/src/stores/connections.store.ts b/packages/frontend/src/stores/connections.store.ts index 78beecd..0cde114 100644 --- a/packages/frontend/src/stores/connections.store.ts +++ b/packages/frontend/src/stores/connections.store.ts @@ -10,9 +10,9 @@ export interface ConnectionInfo { port: number; username: string; auth_method: 'password' | 'key'; - proxy_id?: number | null; // 新增:关联的代理 ID (可选) - tag_ids?: number[]; // 新增:关联的标签 ID 数组 (可选) - ssh_key_id?: number | null; // +++ 新增:关联的 SSH 密钥 ID (可选) +++ + proxy_id?: number | null; // 关联的代理 ID (可选) + tag_ids?: number[]; // 关联的标签 ID 数组 (可选) + ssh_key_id?: number | null; // +++ 关联的 SSH 密钥 ID (可选) +++ created_at: number; updated_at: number; last_connected_at: number | null; @@ -98,7 +98,7 @@ export const useConnectionsStore = defineStore('connections', { passphrase?: string; // SSH specific vncPassword?: string; // VNC specific password proxy_id?: number | null; - tag_ids?: number[]; // 新增:允许传入 tag_ids + tag_ids?: number[]; // 允许传入 tag_ids }) { this.isLoading = true; this.error = null; @@ -190,7 +190,7 @@ export const useConnectionsStore = defineStore('connections', { } }, - // 新增:测试连接 Action + // 测试连接 Action async testConnection(connectionId: number): Promise<{ success: boolean; message?: string }> { // 注意:这里不改变 isLoading 状态,或者可以引入单独的 testing 状态 // this.isLoading = true; @@ -211,7 +211,7 @@ export const useConnectionsStore = defineStore('connections', { } }, - // 新增:克隆连接 Action (调用后端克隆接口) + // 克隆连接 Action (调用后端克隆接口) async cloneConnection(originalId: number, newName: string): Promise { this.isLoading = true; // 可以考虑为克隆操作设置单独的加载状态 this.error = null; @@ -237,7 +237,7 @@ export const useConnectionsStore = defineStore('connections', { } }, - // +++ 新增:为多个连接添加一个标签 (调用新的后端 API) +++ + // +++ 为多个连接添加一个标签 (调用新的后端 API) +++ async addTagToConnectionsAction(connectionIds: number[], tagId: number): Promise { if (connectionIds.length === 0) return true; // 没有连接需要更新,直接返回成功 @@ -285,7 +285,7 @@ export const useConnectionsStore = defineStore('connections', { } }, - // +++ 新增:获取 VNC 会话令牌 +++ + // +++ 获取 VNC 会话令牌 +++ async getVncSessionToken(connectionId: number, width?: number, height?: number): Promise { // this.isLoading = true; // 考虑是否需要独立的加载状态,或者由调用方处理 // this.error = null; diff --git a/packages/frontend/src/stores/fileEditor.store.ts b/packages/frontend/src/stores/fileEditor.store.ts index 10565b2..8460740 100644 --- a/packages/frontend/src/stores/fileEditor.store.ts +++ b/packages/frontend/src/stores/fileEditor.store.ts @@ -22,7 +22,7 @@ export interface FileTab { filename: string; content: string; // 当前解码后的内容 (前端解码) originalContent: string; // 初始加载或上次保存时解码后的内容 (前端解码) - rawContentBase64: string | null; // +++ 新增:存储原始 Base64 数据 +++ + rawContentBase64: string | null; // +++ 存储原始 Base64 数据 +++ language: string; selectedEncoding: string; // 当前选择或自动检测到的编码 isLoading: boolean; @@ -70,7 +70,7 @@ export const getFilenameFromPath = (filePath: string): string => { return filePath.split('/').pop() || filePath; }; -// +++ 新增:前端解码辅助函数 +++ +// +++ 前端解码辅助函数 +++ const decodeRawContent = (rawContentBase64: string, encoding: string): string => { try { const buffer = Buffer.from(rawContentBase64, 'base64'); @@ -106,8 +106,8 @@ export const useFileEditorStore = defineStore('fileEditor', () => { const tabs = ref(new Map()); // 存储所有打开的标签页 (使用 FileTab) const activeTabId = ref(null); // 当前激活的标签页 ID // const editorVisibleState = ref<'visible' | 'minimized' | 'closed'>('closed'); // 移除,面板可见性由布局控制 - const popupTrigger = ref(0); // 新增:用于触发弹窗显示的信号 - const popupFileInfo = ref<{ filePath: string; sessionId: string } | null>(null); // 新增:存储弹窗文件信息 + const popupTrigger = ref(0); // 用于触发弹窗显示的信号 + const popupFileInfo = ref<{ filePath: string; sessionId: string } | null>(null); // 存储弹窗文件信息 // --- 计算属性 --- const orderedTabs = computed(() => Array.from(tabs.value.values())); // 获取标签页数组,用于渲染 @@ -413,7 +413,7 @@ export const useFileEditorStore = defineStore('fileEditor', () => { // setEditorVisibility('closed'); // 移除:容器可见性由外部控制 }; - // +++ 新增:关闭其他标签页 +++ + // +++ 关闭其他标签页 +++ const closeOtherTabs = (targetTabId: string) => { console.log(`[文件编辑器 Store] closeOtherTabs: Action called. Current keys in tabs map:`, Array.from(tabs.value.keys())); // ++ Log current keys at start if (!tabs.value.has(targetTabId)) { @@ -429,7 +429,7 @@ export const useFileEditorStore = defineStore('fileEditor', () => { }); }; - // +++ 新增:关闭右侧标签页 +++ + // +++ 关闭右侧标签页 +++ const closeTabsToTheRight = (targetTabId: string) => { const tabsArray = Array.from(tabs.value.values()); const targetIndex = tabsArray.findIndex(tab => tab.id === targetTabId); @@ -447,7 +447,7 @@ export const useFileEditorStore = defineStore('fileEditor', () => { }); }; - // +++ 新增:关闭左侧标签页 +++ + // +++ 关闭左侧标签页 +++ const closeTabsToTheLeft = (targetTabId: string) => { const tabsArray = Array.from(tabs.value.values()); const targetIndex = tabsArray.findIndex(tab => tab.id === targetTabId); diff --git a/packages/frontend/src/stores/focusSwitcher.store.ts b/packages/frontend/src/stores/focusSwitcher.store.ts index 82f107b..cc8f3ef 100644 --- a/packages/frontend/src/stores/focusSwitcher.store.ts +++ b/packages/frontend/src/stores/focusSwitcher.store.ts @@ -34,7 +34,7 @@ interface FocusSwitcherState { isConfiguratorVisible: boolean; activateFileManagerSearchTrigger: number; activateTerminalSearchTrigger: number; - // 新增:存储注册的聚焦动作 + // 存储注册的聚焦动作 registeredActions: Map boolean | Promise>>; } @@ -56,13 +56,13 @@ export const useFocusSwitcherStore = defineStore('focusSwitcher', () => { { id: 'fileEditorActive', label: t('focusSwitcher.input.fileEditorActive', '文件编辑器') }, { id: 'fileManagerPathInput', label: t('focusSwitcher.input.fileManagerPathInput', '文件管理器路径编辑') }, ]); - const sequenceOrder = ref([]); // +++ 新增:存储顺序 +++ - const itemConfigs = ref>({}); // +++ 新增:存储所有配置 +++ + const sequenceOrder = ref([]); // +++ 存储顺序 +++ + const itemConfigs = ref>({}); // +++ 存储所有配置 +++ const isConfiguratorVisible = ref(false); const activateFileManagerSearchTrigger = ref(0); const activateTerminalSearchTrigger = ref(0); - // 新增:存储注册的聚焦动作 (Map: id -> Array of actions) + // 存储注册的聚焦动作 (Map: id -> Array of actions) const registeredActions = ref boolean | Promise>>>(new Map()); // --- Actions --- @@ -378,7 +378,7 @@ export const useFocusSwitcherStore = defineStore('focusSwitcher', () => { return order[nextIndex]; // 返回序列中的下一个 ID } - // +++ 新增:根据快捷键获取目标 ID +++ + // +++ 根据快捷键获取目标 ID +++ // +++ 修改:根据 itemConfigs 获取快捷键对应的目标 ID +++ function getFocusTargetIdByShortcut(shortcut: string): string | null { for (const id in itemConfigs.value) { diff --git a/packages/frontend/src/stores/layout.store.ts b/packages/frontend/src/stores/layout.store.ts index dace426..1be2a1a 100644 --- a/packages/frontend/src/stores/layout.store.ts +++ b/packages/frontend/src/stores/layout.store.ts @@ -168,9 +168,9 @@ export const useLayoutStore = defineStore('layout', () => { 'editor', 'statusMonitor', 'commandHistory', 'quickCommands', 'dockerManager', 'suspendedSshSessions' // <-- 添加新的挂起 SSH 会话视图 ]); - // 新增:控制布局(Header/Footer)可见性的状态 + // 控制布局(Header/Footer)可见性的状态 const isLayoutVisible: Ref = ref(true); // 控制整体布局(Header/Footer)可见性 - // 新增:控制主导航栏(Header)可见性的状态 + // 控制主导航栏(Header)可见性的状态 const isHeaderVisible: Ref = ref(true); // 默认可见 // --- 计算属性 --- @@ -183,7 +183,7 @@ export const useLayoutStore = defineStore('layout', () => { return allPossiblePanes.value.filter(pane => !used.has(pane)); }); -// +++ 新增:递归确保节点及其子节点都有 ID +++ +// +++ 递归确保节点及其子节点都有 ID +++ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null { if (!node) return null; @@ -366,7 +366,7 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null { } } - // 新增:更新侧栏配置 + // 更新侧栏配置 async function updateSidebarPanes(newPanes: { left: PaneName[], right: PaneName[] }) { // Make async // --- Add Validation --- if (newPanes && diff --git a/packages/frontend/src/stores/quickCommands.store.ts b/packages/frontend/src/stores/quickCommands.store.ts index 216a5c7..64a697b 100644 --- a/packages/frontend/src/stores/quickCommands.store.ts +++ b/packages/frontend/src/stores/quickCommands.store.ts @@ -155,7 +155,7 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => { return result; }); - // +++ 新增 Getter: 获取当前可见的扁平指令列表 (用于键盘导航) +++ + // +++ Getter: 获取当前可见的扁平指令列表 (用于键盘导航) +++ const flatVisibleCommands = computed((): QuickCommandFE[] => { const flatList: QuickCommandFE[] = []; filteredAndGroupedCommands.value.forEach(group => { diff --git a/packages/frontend/src/stores/session/actions/sshSuspendActions.ts b/packages/frontend/src/stores/session/actions/sshSuspendActions.ts index a797212..5a491ca 100644 --- a/packages/frontend/src/stores/session/actions/sshSuspendActions.ts +++ b/packages/frontend/src/stores/session/actions/sshSuspendActions.ts @@ -4,14 +4,14 @@ import { sessions, suspendedSshSessions, isLoadingSuspendedSessions, activeSessi import type { MessagePayload, SshMarkForSuspendReqMessage, - SshUnmarkForSuspendReqMessage, // +++ 新增导入 +++ + SshUnmarkForSuspendReqMessage, SshSuspendResumeReqMessage, SshSuspendTerminateReqMessage, SshSuspendRemoveEntryReqMessage, // SshSuspendEditNameReqMessage, // Removed, using HTTP API // S2C Payloads SshMarkedForSuspendAckPayload, - SshUnmarkedForSuspendAckPayload, // +++ 新增导入 +++ + SshUnmarkedForSuspendAckPayload, SshSuspendListResponsePayload, SshSuspendResumedNotifPayload, SshOutputCachedChunkPayload, @@ -27,7 +27,7 @@ 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 +++ +import apiClient from '../../../utils/apiClient'; // +++ 导入 apiClient +++ const t: ComposerTranslation = i18n.global.t; // 从全局 i18n 实例获取 t 函数并显式注解类型 @@ -721,7 +721,7 @@ export const registerSshSuspendHandlers = (wsManager: WsManagerInstance): void = // 但通常这些处理器会随 wsManager 实例的生命周期一起存在。 // wsManager.onMessage('SSH_SUSPEND_STARTED_RESP', (p: MessagePayload) => handleSshSuspendStartedResp(p as SshSuspendStartedRespPayload)); wsManager.onMessage('SSH_MARKED_FOR_SUSPEND_ACK', (p: MessagePayload) => handleSshMarkedForSuspendAck(p as SshMarkedForSuspendAckPayload)); - wsManager.onMessage('SSH_UNMARKED_FOR_SUSPEND_ACK', (p: MessagePayload) => handleSshUnmarkedForSuspendAck(p as SshUnmarkedForSuspendAckPayload)); // +++ 新增处理器 +++ + wsManager.onMessage('SSH_UNMARKED_FOR_SUSPEND_ACK', (p: MessagePayload) => handleSshUnmarkedForSuspendAck(p as SshUnmarkedForSuspendAckPayload)); wsManager.onMessage('SSH_SUSPEND_LIST_RESPONSE', (p: MessagePayload) => handleSshSuspendListResponse(p as SshSuspendListResponsePayload)); wsManager.onMessage('SSH_SUSPEND_RESUMED_NOTIF', (p: MessagePayload) => handleSshSuspendResumedNotif(p as SshSuspendResumedNotifPayload)); wsManager.onMessage('SSH_OUTPUT_CACHED_CHUNK', (p: MessagePayload) => handleSshOutputCachedChunk(p as SshOutputCachedChunkPayload)); diff --git a/packages/frontend/src/stores/session/types.ts b/packages/frontend/src/stores/session/types.ts index eb84dae..dcc6162 100644 --- a/packages/frontend/src/stores/session/types.ts +++ b/packages/frontend/src/stores/session/types.ts @@ -36,15 +36,15 @@ export interface SessionState { terminalManager: SshTerminalInstance; statusMonitorManager: StatusMonitorInstance; dockerManager: DockerManagerInstance; // 现在应该可以找到 DockerManagerInstance - // --- 新增:独立编辑器状态 --- + // --- 独立编辑器状态 --- editorTabs: Ref; // 编辑器标签页列表 activeEditorTabId: Ref; // 当前活动的编辑器标签页 ID - // --- 新增:命令输入框内容 --- + // --- 命令输入框内容 --- commandInputContent: Ref; // 当前会话的命令输入框内容 - isResuming?: boolean; // 新增:标记会话是否正在从挂起状态恢复 - isMarkedForSuspend?: boolean; // +++ 新增:标记会话是否已被用户请求标记为待挂起 +++ - disposables?: (() => void)[]; // 新增:用于存储清理函数,例如取消注册消息处理器 - pendingOutput?: string[]; // 新增:用于暂存恢复会话时,在终端实例准备好之前收到的输出 + isResuming?: boolean; // 标记会话是否正在从挂起状态恢复 + isMarkedForSuspend?: boolean; // +++ 标记会话是否已被用户请求标记为待挂起 +++ + disposables?: (() => void)[]; // 用于存储清理函数,例如取消注册消息处理器 + pendingOutput?: string[]; // 用于暂存恢复会话时,在终端实例准备好之前收到的输出 } // 为标签栏定义包含状态的类型 @@ -52,5 +52,5 @@ export interface SessionTabInfoWithStatus { sessionId: string; connectionName: string; status: WsConnectionStatus; // 添加状态字段 - isMarkedForSuspend?: boolean; // +++ 新增:用于UI指示会话是否已标记待挂起 +++ + isMarkedForSuspend?: boolean; // +++ 用于UI指示会话是否已标记待挂起 +++ } \ No newline at end of file diff --git a/packages/frontend/src/stores/tags.store.ts b/packages/frontend/src/stores/tags.store.ts index 2a181e4..c8fbc23 100644 --- a/packages/frontend/src/stores/tags.store.ts +++ b/packages/frontend/src/stores/tags.store.ts @@ -123,6 +123,36 @@ export const useTagsStore = defineStore('tags', () => { } } + // 更新标签关联的连接 + async function updateTagConnections(tagId: number, connectionIds: number[]): Promise { + isLoading.value = true; + error.value = null; + try { + // 假设后端 API 端点是 PUT /api/tags/:tagId/connections + await apiClient.put(`/tags/${tagId}/connections`, { connection_ids: connectionIds }); + // 更新成功后,清除相关缓存并重新获取数据以确保一致性 + localStorage.removeItem('tagsCache'); // 清除标签缓存 + localStorage.removeItem('connectionsCache'); // 清除连接缓存,因为连接的 tag_ids 可能已更改 + + await fetchTags(); // 重新获取标签 + // 可能还需要通知 connectionsStore 重新获取连接,或者在这里直接调用 + // (这取决于您希望如何管理 store 间的依赖和数据同步) + // 例如: const connectionsStore = useConnectionsStore(); await connectionsStore.fetchConnections(); + // 为简单起见,这里假设调用者会处理连接列表的刷新,或者依赖于后续的自动刷新机制。 + // 或者,更健壮的做法是在此 action 成功后,让 connectionsStore 也刷新。 + // 但为了减少此处的直接依赖,暂时只刷新 tagsStore。 + // WorkspaceConnectionList 在模态框保存成功后会重新 fetchConnections。 + + return true; + } catch (err: any) { + console.error(`Failed to update connections for tag ${tagId}:`, err); + error.value = err.response?.data?.message || err.message || '更新标签连接失败'; + return false; + } finally { + isLoading.value = false; + } + } + return { tags, isLoading, @@ -131,5 +161,6 @@ export const useTagsStore = defineStore('tags', () => { addTag, updateTag, deleteTag, + updateTagConnections, // 暴露新的 action }; }); diff --git a/packages/frontend/src/types/websocket.types.ts b/packages/frontend/src/types/websocket.types.ts index 555fe5a..6d953fd 100644 --- a/packages/frontend/src/types/websocket.types.ts +++ b/packages/frontend/src/types/websocket.types.ts @@ -47,7 +47,7 @@ export interface SshSuspendEditNameReqPayload { export interface SshMarkForSuspendReqPayload { sessionId: string; - initialBuffer?: string; // +++ 新增:可选的初始屏幕缓冲区内容 +++ + initialBuffer?: string; // +++ 可选的初始屏幕缓冲区内容 +++ } export interface SshUnmarkForSuspendReqPayload { @@ -61,7 +61,7 @@ export interface SshMarkedForSuspendAckPayload { error?: string; } -export interface SshUnmarkedForSuspendAckPayload { // +++ 新增 +++ +export interface SshUnmarkedForSuspendAckPayload { sessionId: string; success: boolean; error?: string; @@ -151,7 +151,7 @@ export interface SshMarkForSuspendReqMessage extends WebSocketMessage { payload: SshMarkForSuspendReqPayload; } -export interface SshUnmarkForSuspendReqMessage extends WebSocketMessage { // +++ 新增 +++ +export interface SshUnmarkForSuspendReqMessage extends WebSocketMessage { type: 'SSH_UNMARK_FOR_SUSPEND'; payload: SshUnmarkForSuspendReqPayload; } @@ -162,7 +162,7 @@ export interface SshMarkedForSuspendAckMessage extends WebSocketMessage { payload: SshMarkedForSuspendAckPayload; } -export interface SshUnmarkedForSuspendAckMessage extends WebSocketMessage { // +++ 新增 +++ +export interface SshUnmarkedForSuspendAckMessage extends WebSocketMessage { type: 'SSH_UNMARKED_FOR_SUSPEND_ACK'; payload: SshUnmarkedForSuspendAckPayload; } @@ -216,11 +216,11 @@ export type SshSuspendC2SMessage = | SshSuspendRemoveEntryReqMessage | SshSuspendEditNameReqMessage | SshMarkForSuspendReqMessage - | SshUnmarkForSuspendReqMessage; // +++ 新增 +++ + | SshUnmarkForSuspendReqMessage; export type SshSuspendS2CMessage = | SshMarkedForSuspendAckMessage - | SshUnmarkedForSuspendAckMessage // +++ 新增 +++ + | SshUnmarkedForSuspendAckMessage | SshSuspendStartedRespMessage | SshSuspendListResponseMessage | SshSuspendResumedNotifMessage diff --git a/packages/frontend/src/views/CommandHistoryView.vue b/packages/frontend/src/views/CommandHistoryView.vue index 74005b0..d05b566 100644 --- a/packages/frontend/src/views/CommandHistoryView.vue +++ b/packages/frontend/src/views/CommandHistoryView.vue @@ -230,14 +230,14 @@ const deleteSingleCommand = (id: number) => { commandHistoryStore.deleteCommand(id); }; -// 新增:执行命令 (发出事件) +// 执行命令 (发出事件) const executeCommand = (command: string) => { emitWorkspaceEvent('terminal:sendCommand', { command }); // Optionally reset selection after execution // selectedIndex.value = -1; // REMOVED: Store handles index }; -// +++ 新增:聚焦搜索框的方法 +++ +// +++ 聚焦搜索框的方法 +++ const focusSearchInput = (): boolean => { if (searchInputRef.value) { searchInputRef.value.focus(); diff --git a/packages/frontend/src/views/DashboardView.vue b/packages/frontend/src/views/DashboardView.vue index eb17477..563e1c8 100644 --- a/packages/frontend/src/views/DashboardView.vue +++ b/packages/frontend/src/views/DashboardView.vue @@ -44,7 +44,7 @@ const getInitialSelectedTagId = (): number | null => { return storedValue && storedValue !== 'null' ? parseInt(storedValue, 10) : null; }; const selectedTagId = ref(getInitialSelectedTagId()); -const searchQuery = ref(''); // +++ 新增搜索查询状态 +++ +const searchQuery = ref(''); // +++ 控制添加/编辑表单的显示状态 +++ const showAddEditConnectionForm = ref(false); diff --git a/packages/frontend/src/views/QuickCommandsView.vue b/packages/frontend/src/views/QuickCommandsView.vue index 372eca1..b05f53c 100644 --- a/packages/frontend/src/views/QuickCommandsView.vue +++ b/packages/frontend/src/views/QuickCommandsView.vue @@ -280,7 +280,7 @@ watch(editingTagId, async (newId) => { } }); -// 新增:监听显示模式变化,重置选择 +// 监听显示模式变化,重置选择 watch(showQuickCommandTagsBoolean, () => { quickCommandsStore.resetSelection(); }); @@ -432,7 +432,7 @@ const executeCommand = (command: QuickCommandFE) => { // selectedIndex.value = -1; // REMOVED: Store handles index }; -// +++ 新增:聚焦搜索框的方法 +++ +// +++ 聚焦搜索框的方法 +++ const focusSearchInput = (): boolean => { if (searchInputRef.value) { searchInputRef.value.focus(); diff --git a/packages/frontend/src/views/WorkspaceView.vue b/packages/frontend/src/views/WorkspaceView.vue index c090162..27b67cf 100644 --- a/packages/frontend/src/views/WorkspaceView.vue +++ b/packages/frontend/src/views/WorkspaceView.vue @@ -75,7 +75,7 @@ const currentSearchTerm = ref(''); // 当前搜索的关键词 const mobileTerminalRef = ref | null>(null); // +++ 添加 mobileTerminalRef +++ const isVirtualKeyboardVisible = ref(true); // +++ State for virtual keyboard visibility +++ -// --- 新增:处理全局键盘事件 --- +// --- 处理全局键盘事件 --- const handleGlobalKeyDown = (event: KeyboardEvent) => { // 检查是否按下了 Alt 键以及上/下箭头键 if (event.altKey && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) { @@ -422,7 +422,7 @@ const handleCloseSearch = () => { // +++ 修改 +++ } }; -// +++ 新增:处理清空终端事件 +++ +// +++ 处理清空终端事件 +++ const handleClearTerminal = () => { // +++ 修改 +++ const currentSession = activeSession.value; if (!currentSession) { @@ -511,7 +511,7 @@ const handleCloseEditorTab = (tabId: string) => { } }; - // +++ 新增:处理编辑器编码更改事件 +++ + // +++ 处理编辑器编码更改事件 +++ const handleChangeEncoding = (payload: { tabId: string; encoding: string }) => { const isShared = shareFileEditorTabsBoolean.value; console.log(`[WorkspaceView] handleChangeEncoding for tab ${payload.tabId} to ${payload.encoding}, Shared mode: ${isShared}`); @@ -543,7 +543,7 @@ const handleCloseEditorTab = (tabId: string) => { sessionStore.handleOpenNewSession(id); }; -// +++ 新增:处理虚拟键盘按键事件 +++ +// +++ 处理虚拟键盘按键事件 +++ const handleVirtualKeyPress = (keySequence: string) => { const currentSession = activeSession.value; if (!currentSession) {