From 27cb02b825927702e2c22df14e70d43977f85565 Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Wed, 28 May 2025 20:24:30 +0800 Subject: [PATCH] update --- .../components/AddEditQuickCommandForm.vue | 5 +- .../src/components/ConnectionList.vue | 14 +-- .../frontend/src/components/FileManager.vue | 22 ----- .../src/components/LayoutConfigurator.vue | 7 +- .../frontend/src/components/ProxyList.vue | 4 +- .../components/WorkspaceConnectionList.vue | 1 - .../src/components/common/AlertDialog.vue | 88 +++++++++++++++++++ .../src/components/common/ConfirmDialog.vue | 2 +- .../src/composables/useAddConnectionForm.ts | 4 +- .../src/composables/useAlertDialog.ts | 62 +++++++++++++ .../src/composables/useDockerManager.ts | 3 - packages/frontend/src/locales/en-US.json | 4 + packages/frontend/src/locales/ja-JP.json | 4 + packages/frontend/src/locales/zh-CN.json | 4 + .../frontend/src/stores/fileEditor.store.ts | 4 - packages/frontend/src/stores/layout.store.ts | 3 - packages/frontend/src/utils/apiClient.ts | 3 - .../frontend/src/views/ConnectionsView.vue | 8 +- 18 files changed, 186 insertions(+), 56 deletions(-) create mode 100644 packages/frontend/src/components/common/AlertDialog.vue create mode 100644 packages/frontend/src/composables/useAlertDialog.ts diff --git a/packages/frontend/src/components/AddEditQuickCommandForm.vue b/packages/frontend/src/components/AddEditQuickCommandForm.vue index 431a412..aad61b0 100644 --- a/packages/frontend/src/components/AddEditQuickCommandForm.vue +++ b/packages/frontend/src/components/AddEditQuickCommandForm.vue @@ -62,6 +62,7 @@ import { useQuickCommandsStore, type QuickCommandFE } from '../stores/quickComma import { useQuickCommandTagsStore } from '../stores/quickCommandTags.store'; import TagInput from './TagInput.vue'; import { useConfirmDialog } from '../composables/useConfirmDialog'; +import { useAlertDialog } from '../composables/useAlertDialog'; // +++ 导入 useAlertDialog +++ const props = defineProps<{ commandToEdit?: QuickCommandFE | null; // 接收要编辑的指令对象 (should include tagIds) @@ -71,6 +72,7 @@ const emit = defineEmits(['close']); const { t } = useI18n(); const { showConfirmDialog } = useConfirmDialog(); +const { showAlertDialog } = useAlertDialog(); // +++ 获取 showAlertDialog 函数 +++ const quickCommandsStore = useQuickCommandsStore(); const quickCommandTagsStore = useQuickCommandTagsStore(); // +++ Instantiate tag store +++ const isSubmitting = ref(false); @@ -144,8 +146,7 @@ const handleDeleteTag = async (tagId: number) => { formData.tagIds.splice(index, 1); } } else { - // Optional: Show error notification if deletion fails - alert(t('tags.errorDelete', { error: quickCommandTagsStore.error || '未知错误' })); + showAlertDialog({ title: t('common.error', '错误'), message: t('tags.errorDelete', { error: quickCommandTagsStore.error || '未知错误' }) }); } } }; diff --git a/packages/frontend/src/components/ConnectionList.vue b/packages/frontend/src/components/ConnectionList.vue index a78fbba..2bf3d15 100644 --- a/packages/frontend/src/components/ConnectionList.vue +++ b/packages/frontend/src/components/ConnectionList.vue @@ -6,11 +6,13 @@ import { useI18n } from 'vue-i18n'; import { useConnectionsStore, ConnectionInfo } from '../stores/connections.store'; import { useTagsStore } from '../stores/tags.store'; import { useConfirmDialog } from '../composables/useConfirmDialog'; +import { useAlertDialog } from '../composables/useAlertDialog'; -const { t } = useI18n(); +const { t } = useI18n(); const router = useRouter(); const tagsStore = useTagsStore(); const { showConfirmDialog } = useConfirmDialog(); +const { showAlertDialog } = useAlertDialog(); @@ -132,11 +134,9 @@ const handleDelete = async (conn: ConnectionInfo) => { if (confirmed) { const success = await connectionsStore.deleteConnection(conn.id); if (!success) { - // 如果删除失败,显示 store 中的错误信息 (或自定义错误) - // 可以考虑使用更友好的提示方式,例如 toast 通知库 - alert(t('connections.errors.deleteFailed', { error: connectionsStore.error || '未知错误' })); + + showAlertDialog({ title: t('common.error'), message: t('connections.errors.deleteFailed', { error: connectionsStore.error || '未知错误' }) }); } - // 成功时列表会自动更新,无需额外操作 } }; @@ -149,9 +149,9 @@ const handleDelete = async (conn: ConnectionInfo) => { // 显示测试结果 if (result.success) { - alert(t('connections.test.success')); + showAlertDialog({ title: t('common.success'), message: t('connections.test.success') }); } else { - alert(t('connections.test.failed', { error: result.message || '未知错误' })); + showAlertDialog({ title: t('common.error'), message: t('connections.test.failed', { error: result.message || '未知错误' }) }); } }; diff --git a/packages/frontend/src/components/FileManager.vue b/packages/frontend/src/components/FileManager.vue index 0b4b99a..bab9fe2 100644 --- a/packages/frontend/src/components/FileManager.vue +++ b/packages/frontend/src/components/FileManager.vue @@ -455,7 +455,6 @@ const handleItemAction = (item: FileListItem) => { if (!absolutePath) { console.error(`[FileManager ${props.sessionId}-${props.instanceId}] sftp:realpath:success for ${itemPath} missing absolutePath. Payload:`, payload); - alert(`Failed to resolve symbolic link "${item.filename}": Server did not return a valid path.`); return; } if (!targetType) { @@ -475,14 +474,12 @@ const handleItemAction = (item: FileListItem) => { const resolvedPathInfo = payload.absolutePath ? ` (Resolved path: ${payload.absolutePath})` : ''; console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Failed to get realpath or target type for symlink '${itemPath}': ${serverErrorMsg}${resolvedPathInfo}`); - alert(`Failed to resolve symbolic link "${item.filename}": ${serverErrorMsg}.${resolvedPathInfo} Please ensure the target exists and you have permissions.`); } }); timeoutId = setTimeout(() => { cleanupListeners(); console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Timeout getting realpath for symlink '${itemPath}' (ID: ${requestId}).`); - alert(`Timeout resolving symbolic link "${item.filename}".`); }, 10000); // 10 秒超时 wsSend({ type: 'sftp:realpath', requestId: requestId, payload: { path: itemPath } }); return; // Handled by async callbacks @@ -620,13 +617,7 @@ const handleModalConfirm = (value?: string) => { case 'newFile': if (value) { if (manager.fileList.value.some((item: FileListItem) => item.filename === value)) { - // alert(t('fileManager.errors.fileExists', { name: value })); // Consider using modal for this error too console.warn(`[FileManager ${props.sessionId}-${props.instanceId}] File ${value} already exists. Modal should prevent this.`); - // Re-open modal or show error in modal - // For now, we rely on modal's internal logic or a notification system - // To prevent closing, we can avoid calling handleModalClose here if an error occurs. - // However, the current modal design closes on confirm. - // A more robust solution would be for the modal to emit 'error' or handle validation internally. return; // Prevent closing if error } manager.createFile(value); @@ -635,7 +626,6 @@ const handleModalConfirm = (value?: string) => { case 'newFolder': if (value) { if (manager.fileList.value.some((item: FileListItem) => item.filename === value)) { - // alert(t('fileManager.errors.folderExists', { name: value })); console.warn(`[FileManager ${props.sessionId}-${props.instanceId}] Folder ${value} already exists. Modal should prevent this.`); return; // Prevent closing if error } @@ -758,20 +748,17 @@ const triggerFileUpload = () => { fileInputRef.value?.click(); }; const triggerDownload = (items: FileListItem[]) => { // 修改:接受 FileListItem 数组 // 恢复使用 props.wsDeps.isConnected if (!props.wsDeps.isConnected.value) { - alert(t('fileManager.errors.notConnected')); return; } // connectionId 仍然从 props 获取 const currentConnectionId = props.dbConnectionId; if (!currentConnectionId) { console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot download: Missing connection ID.`); - alert(t('fileManager.errors.missingConnectionId')); return; } // 修改:简化检查 if (!currentSftpManager.value) { console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot download: SFTP manager is not available.`); - alert(t('fileManager.errors.sftpManagerNotFound')); return; } @@ -808,18 +795,15 @@ const triggerDownload = (items: FileListItem[]) => { // 修改:接受 FileList // +++ 文件夹下载触发器 +++ const triggerDownloadDirectory = (item: FileListItem) => { if (!props.wsDeps.isConnected.value) { - alert(t('fileManager.errors.notConnected')); return; } const currentConnectionId = props.dbConnectionId; if (!currentConnectionId) { console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot download directory: Missing connection ID.`); - alert(t('fileManager.errors.missingConnectionId')); return; } if (!currentSftpManager.value) { console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot download directory: SFTP manager is not available.`); - alert(t('fileManager.errors.sftpManagerNotFound')); return; } @@ -878,16 +862,10 @@ const triggerDownloadDirectory = (item: FileListItem) => { } catch (e2) { /* ignore */} } - if (response.status === 404) { - alert(t('fileManager.errors.downloadDirectoryNotImplemented', 'Directory download feature is not yet implemented on the server.')); - } else { - alert(`${t('fileManager.errors.downloadDirectoryFailed', 'Failed to download directory')}: ${errorMsg}`); - } } }) .catch(error => { console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Network error during directory download:`, error); - alert(`${t('fileManager.errors.downloadDirectoryFailed', 'Failed to download directory')}: Network error.`); }); }; diff --git a/packages/frontend/src/components/LayoutConfigurator.vue b/packages/frontend/src/components/LayoutConfigurator.vue index c00c981..43976b9 100644 --- a/packages/frontend/src/components/LayoutConfigurator.vue +++ b/packages/frontend/src/components/LayoutConfigurator.vue @@ -7,6 +7,7 @@ import { storeToRefs } from 'pinia'; import draggable from 'vuedraggable'; import LayoutNodeEditor from './LayoutNodeEditor.vue'; import { useConfirmDialog } from '../composables/useConfirmDialog'; +import { useAlertDialog } from '../composables/useAlertDialog'; @@ -27,6 +28,7 @@ const layoutStore = useLayoutStore(); const settingsStore = useSettingsStore(); // +++ Initialize settings store +++ const { layoutLockedBoolean } = storeToRefs(settingsStore); // +++ Get reactive state +++ const { showConfirmDialog } = useConfirmDialog(); +const { showAlertDialog } = useAlertDialog(); // --- State --- const localLayoutTree: Ref = ref(null); @@ -180,9 +182,6 @@ const handleLayoutLockChange = async () => { // Removed event parameter // but the button's appearance relies on layoutLockedBoolean which comes from the store. } catch (error) { console.error('[LayoutConfigurator] Failed to update layout lock setting:', error); - // Optionally show an error message - // No UI element state to revert directly here, the button state depends on layoutLockedBoolean - alert(t('layoutConfigurator.lockUpdateError', '更新布局锁定状态失败。')); } }; @@ -219,8 +218,6 @@ const saveLayout = async () => { // Make async console.log('[LayoutConfigurator] Layout saved successfully, dialog closed.'); } catch (error) { console.error('[LayoutConfigurator] Error saving layout:', error); - // Optionally notify the user about the error - alert(t('layoutConfigurator.saveError', '保存布局时出错,请稍后再试。')); // Keep default text for now } }; diff --git a/packages/frontend/src/components/ProxyList.vue b/packages/frontend/src/components/ProxyList.vue index a914b7d..1d88a6e 100644 --- a/packages/frontend/src/components/ProxyList.vue +++ b/packages/frontend/src/components/ProxyList.vue @@ -3,10 +3,12 @@ import { storeToRefs } from 'pinia'; import { useI18n } from 'vue-i18n'; import { useProxiesStore, ProxyInfo } from '../stores/proxies.store'; import { useConfirmDialog } from '../composables/useConfirmDialog'; +import { useAlertDialog } from '../composables/useAlertDialog'; const { t } = useI18n(); const proxiesStore = useProxiesStore(); const { showConfirmDialog } = useConfirmDialog(); +const { showAlertDialog } = useAlertDialog(); const { proxies, isLoading, error } = storeToRefs(proxiesStore); // 定义组件发出的事件 @@ -21,7 +23,7 @@ const handleDelete = async (proxy: ProxyInfo) => { if (confirmed) { const success = await proxiesStore.deleteProxy(proxy.id); if (!success) { - alert(t('proxies.errors.deleteFailed', { error: proxiesStore.error || '未知错误' })); + showAlertDialog({ title: t('common.error'), message: t('proxies.errors.deleteFailed', { error: proxiesStore.error || t('common.unknownError') }) }); } } }; diff --git a/packages/frontend/src/components/WorkspaceConnectionList.vue b/packages/frontend/src/components/WorkspaceConnectionList.vue index aacf9ff..562abe3 100644 --- a/packages/frontend/src/components/WorkspaceConnectionList.vue +++ b/packages/frontend/src/components/WorkspaceConnectionList.vue @@ -423,7 +423,6 @@ const handleMenuAction = async (action: 'add' | 'edit' | 'delete' | 'clone') => .catch(error => { // 可以在这里处理克隆失败的特定 UI 反馈,如果需要的话 console.error("Cloning failed in component:", error); - // alert(t('connections.errors.cloneFailed', { error: connectionsStore.error || '未知错误' })); // store 中已有错误处理 }); } } diff --git a/packages/frontend/src/components/common/AlertDialog.vue b/packages/frontend/src/components/common/AlertDialog.vue new file mode 100644 index 0000000..2e8636e --- /dev/null +++ b/packages/frontend/src/components/common/AlertDialog.vue @@ -0,0 +1,88 @@ + + + \ No newline at end of file diff --git a/packages/frontend/src/components/common/ConfirmDialog.vue b/packages/frontend/src/components/common/ConfirmDialog.vue index ca538fa..d3a3c59 100644 --- a/packages/frontend/src/components/common/ConfirmDialog.vue +++ b/packages/frontend/src/components/common/ConfirmDialog.vue @@ -64,7 +64,7 @@ onBeforeUnmount(() => {
void | Promise; +} + +export function useAlertDialog() { + const { t } = useI18n(); + + const showAlertDialog = (options: AlertDialogOptions): Promise => { + return new Promise((resolve) => { + const { title, message, okText, onOk } = options; + + const + container = document.createElement('div'); + document.body.appendChild(container); + + const app = createApp({ + render: () => + h(AlertDialog, { + visible: true, + title, + message, + okText: okText || t('common.ok', '确定'), + onOk: async () => { + if (onOk) { + await onOk(); + } + app.unmount(); + container.remove(); + resolve(); + }, + 'onUpdate:visible': (isVisible: boolean) => { + if (!isVisible) { + // This case handles closing via Escape key or clicking outside + app.unmount(); + container.remove(); + resolve(); // Resolve promise when dialog is closed without explicit ok + } + }, + }), + }); + + // Mount an app with i18n instance + const i18n = useI18n(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (app._context.provides as any).i18n = i18n; + + app.mount(container); + }); + }; + + return { + showAlertDialog, + }; +} \ No newline at end of file diff --git a/packages/frontend/src/composables/useDockerManager.ts b/packages/frontend/src/composables/useDockerManager.ts index 29aa9cc..d6c83ab 100644 --- a/packages/frontend/src/composables/useDockerManager.ts +++ b/packages/frontend/src/composables/useDockerManager.ts @@ -177,7 +177,6 @@ export function createDockerManager(sessionId: string, wsDeps: DockerManagerDepe // How to notify UI? Maybe set an error ref? Or rely on status update? // For now, just log. UI component could show a generic error or use a notification system. // Consider adding a transient commandError ref if needed. - alert(`${t('dockerManager.error.commandFailed', { command: payload?.command || '?' })}: ${payload?.message || 'Unknown error'}`); }); const unsubRequestUpdate = onMessage('request_docker_status_update', (payload, message) => { @@ -192,12 +191,10 @@ export function createDockerManager(sessionId: string, wsDeps: DockerManagerDepe const sendDockerCommand = (containerId: string, command: 'start' | 'stop' | 'restart' | 'remove') => { if (!isConnected.value) { console.warn(`[DockerManager ${sessionId}] Cannot send command, WebSocket not connected.`); - alert(t('dockerManager.error.sshNotConnected')); // Use generic disconnected message return; } if (!isDockerAvailable.value) { console.warn(`[DockerManager ${sessionId}] Cannot send command, remote Docker is not available.`); - alert(t('dockerManager.notAvailable')); return; } diff --git a/packages/frontend/src/locales/en-US.json b/packages/frontend/src/locales/en-US.json index 6f707f9..e6be4fa 100644 --- a/packages/frontend/src/locales/en-US.json +++ b/packages/frontend/src/locales/en-US.json @@ -1041,6 +1041,10 @@ "testMessageUnsaved": "Test triggered for unsaved {channelType} configuration" }, "common": { + "ok": "OK", + "success": "Success", + "error": "Error", + "alert": "Alert", "apply": "Apply", "loading": "Loading...", "cancel": "Cancel", diff --git a/packages/frontend/src/locales/ja-JP.json b/packages/frontend/src/locales/ja-JP.json index b53a324..0d90151 100644 --- a/packages/frontend/src/locales/ja-JP.json +++ b/packages/frontend/src/locales/ja-JP.json @@ -78,6 +78,10 @@ "clearTerminal": "ターミナルをクリア" }, "common": { + "ok": "確認", + "success": "成功", + "error": "失敗", + "alert": "お知らせ", "apply": "適用", "all": "すべて", "cancel": "キャンセル", diff --git a/packages/frontend/src/locales/zh-CN.json b/packages/frontend/src/locales/zh-CN.json index d8d4011..032eeca 100644 --- a/packages/frontend/src/locales/zh-CN.json +++ b/packages/frontend/src/locales/zh-CN.json @@ -1073,6 +1073,10 @@ "minimize": "最小化", "send":"发送", "copied": "已复制到剪贴板", + "ok":"确认", + "success":"成功", + "error":"失败", + "alert":"提示", "updateSuccess":"更新成功" }, "layoutConfigurator": { diff --git a/packages/frontend/src/stores/fileEditor.store.ts b/packages/frontend/src/stores/fileEditor.store.ts index 54351df..6c48825 100644 --- a/packages/frontend/src/stores/fileEditor.store.ts +++ b/packages/frontend/src/stores/fileEditor.store.ts @@ -382,10 +382,6 @@ export const useFileEditorStore = defineStore('fileEditor', () => { if (tabToClose.isModified) { // 这里可以集成 UI 通知库来提示 console.warn(`[文件编辑器 Store] 标签页 ${tabId} (${tabToClose.filename}) 已修改但未保存。正在关闭...`); - // alert(`文件 ${tabToClose.filename} 已修改但未保存。确定要关闭吗?`); // 简单的 alert 示例 - // if (!confirm(`文件 ${tabToClose.filename} 已修改但未保存。确定要关闭吗?`)) { - // return; // 用户取消关闭 - // } } console.log(`[文件编辑器 Store] 关闭标签页: ${tabId}`); diff --git a/packages/frontend/src/stores/layout.store.ts b/packages/frontend/src/stores/layout.store.ts index 04ea470..f6d428a 100644 --- a/packages/frontend/src/stores/layout.store.ts +++ b/packages/frontend/src/stores/layout.store.ts @@ -465,9 +465,6 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null { console.log('[Layout Store] Header visibility saved to backend.'); } catch (error) { console.error('[Layout Store] Failed to save header visibility to backend:', error); - // 可选:如果保存失败,回滚状态? - // isHeaderVisible.value = !newValue; - // alert('Failed to save preference.'); // 或者通知用户 } } diff --git a/packages/frontend/src/utils/apiClient.ts b/packages/frontend/src/utils/apiClient.ts index 3b0ee25..4d1ed9b 100644 --- a/packages/frontend/src/utils/apiClient.ts +++ b/packages/frontend/src/utils/apiClient.ts @@ -58,14 +58,12 @@ apiClient.interceptors.response.use( case 403: // 禁止访问 // 可以显示一个权限不足的提示 console.error('Forbidden access.'); - // alert('您没有权限执行此操作。'); // 或者使用更友好的通知组件 break; case 404: // 未找到 console.error('Resource not found.'); break; case 500: // 服务器内部错误 console.error('Internal server error.'); - // alert('服务器发生错误,请稍后重试。'); break; // 可以根据需要添加更多错误状态码的处理 default: @@ -74,7 +72,6 @@ apiClient.interceptors.response.use( } else if (error.request) { // 请求已发出,但没有收到响应 (例如网络问题) console.error('Network error or no response received:', error.request); - // alert('网络错误,请检查您的连接。'); } else { // 发送请求时出了点问题 console.error('Error setting up request:', error.message); diff --git a/packages/frontend/src/views/ConnectionsView.vue b/packages/frontend/src/views/ConnectionsView.vue index 7521b18..566ee4f 100644 --- a/packages/frontend/src/views/ConnectionsView.vue +++ b/packages/frontend/src/views/ConnectionsView.vue @@ -11,6 +11,7 @@ import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; import type { ConnectionInfo } from '../stores/connections.store'; import { useConfirmDialog } from '../composables/useConfirmDialog'; +import { useAlertDialog } from '../composables/useAlertDialog'; import { storeToRefs } from 'pinia'; import { formatDistanceToNow } from 'date-fns'; import { zhCN, enUS, ja } from 'date-fns/locale'; @@ -18,6 +19,7 @@ import type { Locale } from 'date-fns'; const { t, locale } = useI18n(); const { showConfirmDialog } = useConfirmDialog(); +const { showAlertDialog } = useAlertDialog(); const connectionsStore = useConnectionsStore(); const sessionStore = useSessionStore(); const tagsStore = useTagsStore(); @@ -254,7 +256,7 @@ const invertSelection = () => { const openBatchEditModal = () => { if (selectedConnectionIdsForBatch.value.size === 0) { // Optionally, show a notification from uiNotificationsStore using your project's method - alert(t('connections.batchEdit.noSelectionForEdit', '请至少选择一个连接进行编辑。')); // Placeholder + showAlertDialog({ title: t('common.alert', '提示'), message: t('connections.batchEdit.noSelectionForEdit', '请至少选择一个连接进行编辑。') }); // Placeholder return; } showBatchEditForm.value = true; @@ -294,14 +296,14 @@ const handleBatchDeleteConnections = async () => { await connectionsStore.deleteBatchConnections(idsToDelete); - alert(t('connections.batchEdit.successMessage', '选中的连接已成功删除。')); + showAlertDialog({ title: t('common.success', '成功'), message: t('connections.batchEdit.successMessage', '选中的连接已成功删除。') }); selectedConnectionIdsForBatch.value.clear(); await connectionsStore.fetchConnections(); } catch (error: any) { console.error("Batch delete connections error:", error); - alert(t('connections.batchEdit.errorMessage', `批量删除连接失败: ${error.message || '未知错误'}`)); + showAlertDialog({ title: t('common.error'), message: t('connections.batchEdit.errorMessage', `批量删除连接失败: ${error.message || '未知错误'}`) }); } finally { isDeletingSelectedConnections.value = false; }