From ba12def9f2d3e94d293305d91df9dffb4ee1c527 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 17:03:33 +0800 Subject: [PATCH] Add editor close confirmation, path copy on double-click, and disable overlay close (#1314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * Implement file management optimization features Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * Improve error handling in file save dialog Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * Disable overlay click to close editor window Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * Fix comment clarity for closeOnOverlay prop Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * fix: lint --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> Co-authored-by: 耗子 --- web/src/components/common/DraggableWindow.vue | 21 ++++++- web/src/views/file/EditModal.vue | 59 +++++++++++++++++++ web/src/views/file/PathInput.vue | 19 +++++- web/src/views/ssh/IndexView.vue | 8 +-- 4 files changed, 99 insertions(+), 8 deletions(-) diff --git a/web/src/components/common/DraggableWindow.vue b/web/src/components/common/DraggableWindow.vue index 0dc481ce..86820573 100644 --- a/web/src/components/common/DraggableWindow.vue +++ b/web/src/components/common/DraggableWindow.vue @@ -12,13 +12,16 @@ const props = withDefaults( minHeight?: number defaultWidth?: number defaultHeight?: number + beforeClose?: () => Promise | boolean // 关闭前的确认回调,返回 true 继续关闭,false 取消关闭 + closeOnOverlay?: boolean // 点击遮罩层是否最小化窗口,默认 true }>(), { title: '', minWidth: 400, minHeight: 300, defaultWidth: 800, - defaultHeight: 600 + defaultHeight: 600, + closeOnOverlay: true } ) @@ -200,8 +203,20 @@ function restore() { minimized.value = false } +// 处理遮罩层点击 +function handleOverlayClick() { + if (props.closeOnOverlay) { + minimize() + } +} + // 关闭 -function close() { +async function close() { + // 如果提供了 beforeClose 回调,先执行它 + if (props.beforeClose) { + const result = await props.beforeClose() + if (!result) return // 如果返回 false,取消关闭 + } show.value = false } @@ -240,7 +255,7 @@ onBeforeUnmount(() => { -
+
diff --git a/web/src/views/file/EditModal.vue b/web/src/views/file/EditModal.vue index ae201537..32ac9d36 100644 --- a/web/src/views/file/EditModal.vue +++ b/web/src/views/file/EditModal.vue @@ -25,6 +25,63 @@ const initialPath = computed(() => { return parts.join('/') || '/' }) +// 关闭前确认 +async function handleBeforeClose(): Promise { + // 检查是否有未保存的文件 + if (!editorStore.hasUnsavedFiles) { + return true // 没有未保存的文件,直接关闭 + } + + // 显示确认对话框 + return new Promise((resolve) => { + window.$dialog.warning({ + title: $gettext('Unsaved Changes'), + content: $gettext('You have unsaved changes. Do you want to save them before closing?'), + positiveText: $gettext('Save'), + negativeText: $gettext('Cancel'), + onPositiveClick: async () => { + // 保存所有未保存的文件 + const unsavedTabs = editorStore.unsavedTabs + let allSaved = true + const failedFiles: string[] = [] + + for (const tab of unsavedTabs) { + try { + await new Promise((resolveInner, rejectInner) => { + useRequest(file.save(tab.path, tab.content)) + .onSuccess(() => { + editorStore.markSaved(tab.path) + resolveInner() + }) + .onError(() => { + allSaved = false + failedFiles.push(tab.path) + rejectInner() + }) + }) + } catch { + // 保存失败,已记录到 failedFiles 数组中 + // 继续尝试保存其他文件 + } + } + + if (allSaved) { + window.$message.success($gettext('All files saved successfully')) + resolve(true) // 保存成功,关闭窗口 + } else { + // 显示失败的文件列表 + const fileList = failedFiles.map(f => f.split('/').pop()).join(', ') + window.$message.error($gettext('Failed to save files: %{ files }', { files: fileList })) + resolve(false) // 保存失败,不关闭窗口 + } + }, + onNegativeClick: () => { + resolve(false) // 用户取消,不关闭窗口 + } + }) + }) +} + // 加载文件 function loadFile(path: string) { if (!path) return @@ -97,6 +154,8 @@ watch(minimized, (isMinimized) => { :default-height="defaultHeight" :min-width="600" :min-height="400" + :before-close="handleBeforeClose" + :close-on-overlay="false" > diff --git a/web/src/views/file/PathInput.vue b/web/src/views/file/PathInput.vue index dc8ae29f..e7d1a229 100644 --- a/web/src/views/file/PathInput.vue +++ b/web/src/views/file/PathInput.vue @@ -4,6 +4,7 @@ import { useGettext } from 'vue3-gettext' import { useFileStore } from '@/store' import { checkPath } from '@/utils/file' +import copy2clipboard from '@vavt/copy2clipboard' const { $gettext } = useGettext() const fileStore = useFileStore() @@ -24,6 +25,16 @@ const handleInput = () => { }) } +// 双击地址栏复制路径 +const handlePathDoubleClick = async () => { + try { + await copy2clipboard(path.value) + window.$message.success($gettext('Path copied to clipboard')) + } catch (error) { + window.$message.error($gettext('Failed to copy path')) + } +} + const handleBlur = () => { input.value = input.value.replace(/(^\/)|(\/$)/g, '') if (!checkPath(input.value)) { @@ -140,7 +151,13 @@ onUnmounted(() => { - + {{ $gettext('Root Directory') }} diff --git a/web/src/views/ssh/IndexView.vue b/web/src/views/ssh/IndexView.vue index 078f2fd8..3fe36ebd 100644 --- a/web/src/views/ssh/IndexView.vue +++ b/web/src/views/ssh/IndexView.vue @@ -9,6 +9,7 @@ import CreateModal from '@/views/ssh/CreateModal.vue' import UpdateModal from '@/views/ssh/UpdateModal.vue' import '@fontsource-variable/jetbrains-mono/wght-italic.css' import '@fontsource-variable/jetbrains-mono/wght.css' +import copy2clipboard from '@vavt/copy2clipboard' import { ClipboardAddon } from '@xterm/addon-clipboard' import { FitAddon } from '@xterm/addon-fit' import { Unicode11Addon } from '@xterm/addon-unicode11' @@ -220,8 +221,7 @@ const initTerminal = async (tabId: string) => { try { // 根据ID选择连接方式 - const socket = tab.hostId === LOCAL_SERVER_ID ? await ws.pty('bash') : await ws.ssh(tab.hostId) - tab.ws = socket + tab.ws = tab.hostId === LOCAL_SERVER_ID ? await ws.pty('bash') : await ws.ssh(tab.hostId) tab.ws.binaryType = 'arraybuffer' tab.terminal = new Terminal({ @@ -252,7 +252,7 @@ const initTerminal = async (tabId: string) => { tab.terminal.onSelectionChange(() => { const selection = tab.terminal?.getSelection() if (selection) { - navigator.clipboard.writeText(selection) + copy2clipboard(selection) } }) @@ -400,7 +400,7 @@ const onKeyDown = (event: KeyboardEvent) => { event.preventDefault() const selection = tab.terminal.getSelection() if (selection) { - navigator.clipboard.writeText(selection) + copy2clipboard(selection) } }