feat: 添加标签管理模态框

This commit is contained in:
Baobhan Sith
2025-05-11 11:20:26 +08:00
parent 1eb1efde72
commit 598df938bf
40 changed files with 634 additions and 170 deletions
@@ -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<boolean> {
this.isLoading = true; // 可以考虑为克隆操作设置单独的加载状态
this.error = null;
@@ -237,7 +237,7 @@ export const useConnectionsStore = defineStore('connections', {
}
},
// +++ 新增:为多个连接添加一个标签 (调用新的后端 API) +++
// +++ 为多个连接添加一个标签 (调用新的后端 API) +++
async addTagToConnectionsAction(connectionIds: number[], tagId: number): Promise<boolean> {
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<string | null> {
// this.isLoading = true; // 考虑是否需要独立的加载状态,或者由调用方处理
// this.error = null;
@@ -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<string, FileTab>()); // 存储所有打开的标签页 (使用 FileTab)
const activeTabId = ref<string | null>(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);
@@ -34,7 +34,7 @@ interface FocusSwitcherState {
isConfiguratorVisible: boolean;
activateFileManagerSearchTrigger: number;
activateTerminalSearchTrigger: number;
// 新增:存储注册的聚焦动作
// 存储注册的聚焦动作
registeredActions: Map<string, Array<() => boolean | Promise<boolean | undefined>>>;
}
@@ -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<string[]>([]); // +++ 新增:存储顺序 +++
const itemConfigs = ref<Record<string, FocusItemConfig>>({}); // +++ 新增:存储所有配置 +++
const sequenceOrder = ref<string[]>([]); // +++ 存储顺序 +++
const itemConfigs = ref<Record<string, FocusItemConfig>>({}); // +++ 存储所有配置 +++
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<Map<string, Array<() => boolean | Promise<boolean | undefined>>>>(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) {
+4 -4
View File
@@ -168,9 +168,9 @@ export const useLayoutStore = defineStore('layout', () => {
'editor', 'statusMonitor', 'commandHistory', 'quickCommands',
'dockerManager', 'suspendedSshSessions' // <-- 添加新的挂起 SSH 会话视图
]);
// 新增:控制布局(Header/Footer)可见性的状态
// 控制布局(Header/Footer)可见性的状态
const isLayoutVisible: Ref<boolean> = ref(true); // 控制整体布局(Header/Footer)可见性
// 新增:控制主导航栏(Header)可见性的状态
// 控制主导航栏(Header)可见性的状态
const isHeaderVisible: Ref<boolean> = 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 &&
@@ -155,7 +155,7 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
return result;
});
// +++ 新增 Getter: 获取当前可见的扁平指令列表 (用于键盘导航) +++
// +++ Getter: 获取当前可见的扁平指令列表 (用于键盘导航) +++
const flatVisibleCommands = computed((): QuickCommandFE[] => {
const flatList: QuickCommandFE[] = [];
filteredAndGroupedCommands.value.forEach(group => {
@@ -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));
@@ -36,15 +36,15 @@ export interface SessionState {
terminalManager: SshTerminalInstance;
statusMonitorManager: StatusMonitorInstance;
dockerManager: DockerManagerInstance; // 现在应该可以找到 DockerManagerInstance
// --- 新增:独立编辑器状态 ---
// --- 独立编辑器状态 ---
editorTabs: Ref<FileTab[]>; // 编辑器标签页列表
activeEditorTabId: Ref<string | null>; // 当前活动的编辑器标签页 ID
// --- 新增:命令输入框内容 ---
// --- 命令输入框内容 ---
commandInputContent: Ref<string>; // 当前会话的命令输入框内容
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指示会话是否已标记待挂起 +++
}
@@ -123,6 +123,36 @@ export const useTagsStore = defineStore('tags', () => {
}
}
// 更新标签关联的连接
async function updateTagConnections(tagId: number, connectionIds: number[]): Promise<boolean> {
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
};
});