mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 04:22:33 +08:00
Add editor close confirmation, path copy on double-click, and disable overlay close (#1314)
* 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: 耗子 <haozi@loli.email>
This commit is contained in:
@@ -12,13 +12,16 @@ const props = withDefaults(
|
||||
minHeight?: number
|
||||
defaultWidth?: number
|
||||
defaultHeight?: number
|
||||
beforeClose?: () => Promise<boolean> | 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(() => {
|
||||
<Teleport to="body">
|
||||
<!-- 遮罩层 -->
|
||||
<Transition name="fade">
|
||||
<div v-if="show && !minimized" class="draggable-window-overlay" @click="minimize" />
|
||||
<div v-if="show && !minimized" class="draggable-window-overlay" @click="handleOverlayClick" />
|
||||
</Transition>
|
||||
|
||||
<!-- 主窗口 -->
|
||||
|
||||
@@ -25,6 +25,63 @@ const initialPath = computed(() => {
|
||||
return parts.join('/') || '/'
|
||||
})
|
||||
|
||||
// 关闭前确认
|
||||
async function handleBeforeClose(): Promise<boolean> {
|
||||
// 检查是否有未保存的文件
|
||||
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<void>((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"
|
||||
>
|
||||
<FileEditorView ref="editorRef" :initial-path="initialPath" />
|
||||
</DraggableWindow>
|
||||
|
||||
@@ -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(() => {
|
||||
</n-tooltip>
|
||||
</n-button-group>
|
||||
<n-input-group flex-1>
|
||||
<n-tag size="large" v-if="!isInput" flex-1 @click="handleInput">
|
||||
<n-tag
|
||||
size="large"
|
||||
v-if="!isInput"
|
||||
flex-1
|
||||
@click="handleInput"
|
||||
@dblclick="handlePathDoubleClick"
|
||||
>
|
||||
<n-breadcrumb separator=">">
|
||||
<n-breadcrumb-item @click.stop="setPath(-1)">
|
||||
{{ $gettext('Root Directory') }}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user