From eb5b85e007963a455efea260e1894c5fc3d9074e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com>
Date: Tue, 5 May 2026 00:08:15 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E5=A4=9A=E6=96=87?=
=?UTF-8?q?=E4=BB=B6=E9=A2=84=E8=A7=88Tab=E7=B3=BB=E7=BB=9F+=E8=84=8F?=
=?UTF-8?q?=E6=A0=87=E8=AE=B0+=E5=85=B3=E9=97=AD=E7=A1=AE=E8=AE=A4+?=
=?UTF-8?q?=E8=B7=AF=E5=BE=84=E9=BB=91=E5=90=8D=E5=8D=95=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- useMultiPreview composable管理多Tab状态、会话持久化
- 面包屑状态dot移除
- 放开Program Files目录访问限制
---
.../components/ConnectionIndicator.vue | 1 -
.../FileSystem/components/FileEditorPanel.vue | 172 +++++++++++++
.../FileSystem/composables/useMultiPreview.ts | 240 ++++++++++++++++++
frontend/src/components/FileSystem/index.vue | 183 +++++++++----
frontend/src/types/file-system.ts | 4 +
frontend/src/utils/constants.js | 1 +
internal/filesystem/path_validator.go | 5 +-
7 files changed, 558 insertions(+), 48 deletions(-)
create mode 100644 frontend/src/components/FileSystem/composables/useMultiPreview.ts
diff --git a/frontend/src/components/FileSystem/components/ConnectionIndicator.vue b/frontend/src/components/FileSystem/components/ConnectionIndicator.vue
index 43a2694..1c15b86 100644
--- a/frontend/src/components/FileSystem/components/ConnectionIndicator.vue
+++ b/frontend/src/components/FileSystem/components/ConnectionIndicator.vue
@@ -6,7 +6,6 @@
-
{{ label }}
+
+
+
+
+
+ {{ getFileTypeIcon(tab.fileItem.name) }}
+
+
+
{{ tab.fileItem.name }}
+
+
+
+
+
@@ -373,6 +401,8 @@ import type { FileEditorPanelConfig } from '@/types/file-system'
import { renderMermaidDiagrams, rerenderMermaidDiagrams } from '@/utils/markedExtensions'
import { useThemeStore } from '@/stores/theme'
import { previewExcel, previewWord, previewCsv } from '@/utils/filePreviewHandlers'
+import { getFileCategory } from '@/utils/fileTypeHelpers'
+import { isDirty } from '../composables/useMultiPreview'
import { connectionManager } from '@/api/connection-manager'
// 异步加载 CodeEditor 组件,减少初始包大小
@@ -435,10 +465,26 @@ interface Emits {
(e: 'imageLoad', dimensions: string): void
(e: 'imageError'): void
(e: 'openLocalFile', link: string): void
+ (e: 'switchTab', tabId: string): void
+ (e: 'closeTab', tabId: string): void
}
const emit = defineEmits()
+// 多 tab 数据
+const previewTabs = computed(() => props.config.previewTabs || [])
+const activeTabId = computed(() => props.config.activeTabId || null)
+
+// 文件类型图标(基于已有分类逻辑)
+const CATEGORY_ICONS: Record = {
+ image: '🖼️', video: '🎬', audio: '🎵', pdf: '📕',
+ html: '🌐', markdown: '📝', text: '⚡', binary: '📦',
+ unknown: '📄'
+}
+const getFileTypeIcon = (filename: string): string => {
+ return CATEGORY_ICONS[getFileCategory(filename)] || '📄'
+}
+
// HTML 预览 URL(实时从 connectionManager 读取,不缓存)
function resolveHtmlPreviewBase(): string {
if (!connectionManager.isRemote()) return 'http://localhost:2652'
@@ -868,6 +914,7 @@ onUnmounted(() => {
flex-direction: column;
height: 100%;
background: var(--color-bg-1);
+ position: relative;
}
.file-editor-panel:fullscreen {
@@ -1430,4 +1477,129 @@ body[arco-theme*='dark'] .markdown-preview-content :deep(.hljs-link) {
color: var(--hljs-link);
text-decoration: underline;
}
+
+/* ========== 多文件预览浮动 Tab 栏 ========== */
+
+.preview-tabs {
+ position: absolute;
+ right: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+ z-index: 20;
+ padding: 6px 4px;
+ pointer-events: auto;
+}
+
+.preview-tab {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2px;
+ width: 56px;
+ padding: 6px 4px 4px;
+ border-radius: 8px 0 0 8px;
+ background: color-mix(in srgb, var(--color-bg-3) 50%, transparent);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ border: 1px solid color-mix(in srgb, var(--color-border-2) 40%, transparent);
+ border-right: none;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ position: relative;
+ opacity: 0.68;
+}
+
+.preview-tabs:hover .preview-tab {
+ opacity: 1;
+}
+
+.preview-tab:hover {
+ background: var(--color-fill-2);
+}
+
+.preview-tab.active {
+ background: var(--color-fill-2);
+ border-left-color: transparent;
+}
+
+.preview-tab.active::before {
+ content: '';
+ position: absolute;
+ left: -1px;
+ top: 0;
+ bottom: 0;
+ width: 3px;
+ background: rgb(var(--primary-6));
+ border-radius: 3px 0 0 3px;
+}
+
+.tab-icon {
+ font-size: 16px;
+ line-height: 1;
+ position: relative;
+}
+
+.tab-dirty {
+ position: absolute;
+ top: -2px;
+ left: -14px;
+ width: 6px;
+ height: 6px;
+ background: rgb(var(--warning-6));
+ border-radius: 50%;
+}
+
+.tab-name {
+ font-size: 10px;
+ line-height: 1.2;
+ color: var(--color-text-2);
+ text-align: center;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ display: block;
+}
+
+.preview-tab.active .tab-name {
+ color: rgb(var(--primary-6));
+ font-weight: 500;
+}
+
+.tab-actions {
+ position: absolute;
+ top: 1px;
+ right: 2px;
+ display: none;
+ align-items: center;
+ gap: 1px;
+}
+
+.preview-tab:hover .tab-actions {
+ display: flex;
+}
+
+.tab-close {
+ width: 14px;
+ height: 14px;
+ border: none;
+ background: transparent;
+ color: var(--color-text-4);
+ font-size: 12px;
+ line-height: 1;
+ cursor: pointer;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+}
+
+.tab-close:hover:not(:disabled) {
+ background: var(--color-fill-3);
+ color: var(--color-text-1);
+}
diff --git a/frontend/src/components/FileSystem/composables/useMultiPreview.ts b/frontend/src/components/FileSystem/composables/useMultiPreview.ts
new file mode 100644
index 0000000..203501c
--- /dev/null
+++ b/frontend/src/components/FileSystem/composables/useMultiPreview.ts
@@ -0,0 +1,240 @@
+/**
+ * 多文件预览 Tab 管理 Composable
+ * 管理预览 Tab 列表、激活状态、内容缓存、会话持久化
+ */
+
+import { ref, computed, watch } from 'vue'
+import { STORAGE_KEYS } from '@/utils/constants'
+import type { FileItem } from '@/types/file-system'
+
+export interface PreviewTab {
+ /** 唯一标识(基于路径) */
+ id: string
+ /** 文件信息 */
+ fileItem: FileItem
+ /** 缓存的预览 URL */
+ previewUrl: string
+ /** 缓存的文件内容 */
+ fileContent: string
+ /** 缓存的原始内容 */
+ originalContent: string
+ /** 缓存的编辑模式状态 */
+ isEditMode: boolean
+ /** 缓存的渲染内容 (HTML/Markdown) */
+ rendered: string
+ /** 是否已加载过内容 */
+ loaded: boolean
+}
+
+/** 持久化存储的精简结构 */
+interface PersistedTab {
+ path: string
+ active: boolean
+ /** 未保存的内容(有修改时才存) */
+ unsavedContent?: string
+ originalContent?: string
+ isEditMode?: boolean
+}
+
+const MAX_TABS = 10
+const STORAGE_KEY = STORAGE_KEYS.FILESYSTEM.MULTI_PREVIEW_TABS
+
+interface UnsavedEntry {
+ content: string
+ original: string
+ editMode: boolean
+}
+
+interface RestoredSession {
+ paths: string[]
+ activePath: string | null
+ unsavedMap: Map
+}
+
+function pathToId(path: string): string {
+ return path.replace(/\\/g, '/').toLowerCase()
+}
+
+export function isDirty(tab: PreviewTab): boolean {
+ return !!(tab.fileContent && tab.originalContent !== undefined && tab.fileContent !== tab.originalContent)
+}
+
+export function useMultiPreview() {
+ const tabs = ref([])
+ const activeTabId = ref(null)
+
+ const activeTab = computed(() => {
+ if (!activeTabId.value) return null
+ return tabs.value.find(t => t.id === activeTabId.value) || null
+ })
+
+ /** 创建一个新 tab */
+ const createTab = (fileItem: FileItem): PreviewTab => ({
+ id: pathToId(fileItem.path),
+ fileItem,
+ previewUrl: '',
+ fileContent: '',
+ originalContent: '',
+ isEditMode: false,
+ rendered: '',
+ loaded: false
+ })
+
+ // ========== 持久化 ==========
+
+ /** 从 localStorage 恢复会话 */
+ const restoreSession = (): RestoredSession => {
+ const unsavedMap = new Map()
+ let activePath: string | null = null
+ const paths: string[] = []
+
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY)
+ if (!raw) return { paths, activePath, unsavedMap }
+
+ const persisted: PersistedTab[] = JSON.parse(raw)
+ if (!Array.isArray(persisted)) return { paths, activePath, unsavedMap }
+
+ for (const p of persisted) {
+ if (!p.path) continue
+ paths.push(p.path)
+ if (p.active) activePath = p.path
+ if (p.unsavedContent !== undefined) {
+ unsavedMap.set(pathToId(p.path), {
+ content: p.unsavedContent,
+ original: p.originalContent || '',
+ editMode: p.isEditMode || false
+ })
+ }
+ }
+ } catch {
+ localStorage.removeItem(STORAGE_KEY)
+ }
+
+ return { paths, activePath, unsavedMap }
+ }
+
+ /** 保存会话到 localStorage */
+ const persistSession = () => {
+ const persisted: PersistedTab[] = tabs.value.map(tab => {
+ const hasUnsaved = tab.fileContent && tab.originalContent !== undefined && tab.fileContent !== tab.originalContent
+ // 限制存储大小,超过 100KB 的内容不存入 localStorage
+ const canSave = hasUnsaved && tab.fileContent.length <= 100_000
+ return {
+ path: tab.fileItem.path,
+ active: tab.id === activeTabId.value,
+ unsavedContent: canSave ? tab.fileContent : undefined,
+ originalContent: canSave ? tab.originalContent : undefined,
+ isEditMode: canSave ? tab.isEditMode : undefined
+ }
+ })
+
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(persisted))
+ } catch {
+ // localStorage 满了,忽略
+ }
+ }
+
+ /** 恢复未保存内容到 tab */
+ const applyUnsavedContent = (tab: PreviewTab, unsavedMap: Map) => {
+ const unsaved = unsavedMap.get(tab.id)
+ if (unsaved) {
+ tab.fileContent = unsaved.content
+ tab.originalContent = unsaved.original
+ tab.isEditMode = unsaved.editMode
+ tab.loaded = true // 标记为已加载,避免重新从磁盘读取覆盖
+ }
+ }
+
+ /** 添加或激活 tab,返回 { tab, isNew } */
+ const addTab = (fileItem: FileItem): { tab: PreviewTab; isNew: boolean } => {
+ const id = pathToId(fileItem.path)
+ const existing = tabs.value.find(t => t.id === id)
+ if (existing) {
+ activeTabId.value = id
+ return { tab: existing, isNew: false }
+ }
+
+ // 超出上限,移除非活跃的最早 tab
+ if (tabs.value.length >= MAX_TABS) {
+ const victimIdx = tabs.value.findIndex(t => t.id !== activeTabId.value && t.id !== id)
+ if (victimIdx !== -1) tabs.value.splice(victimIdx, 1)
+ }
+
+ const tab = createTab(fileItem)
+ tabs.value.push(tab)
+ activeTabId.value = tab.id
+ return { tab, isNew: true }
+ }
+
+ /** 切换 tab */
+ const switchTab = (tabId: string) => {
+ if (tabs.value.find(t => t.id === tabId)) {
+ activeTabId.value = tabId
+ }
+ }
+
+ /** 关闭 tab,返回关闭后应激活的 tabId */
+ const removeTab = (tabId: string): string | null => {
+ const idx = tabs.value.findIndex(t => t.id === tabId)
+ if (idx === -1) return activeTabId.value
+
+ tabs.value.splice(idx, 1)
+
+ if (activeTabId.value === tabId) {
+ const nextIdx = Math.min(idx, tabs.value.length - 1)
+ const nextTab = tabs.value[nextIdx] || null
+ activeTabId.value = nextTab?.id || null
+ }
+
+ return activeTabId.value
+ }
+
+ /** 缓存当前 tab 的状态(切换前调用) */
+ const cacheCurrentTab = (state: {
+ previewUrl: string
+ fileContent: string
+ originalContent: string
+ isEditMode: boolean
+ rendered: string
+ }) => {
+ if (!activeTabId.value) return
+ const tab = tabs.value.find(t => t.id === activeTabId.value)
+ if (tab) {
+ tab.previewUrl = state.previewUrl
+ tab.fileContent = state.fileContent
+ tab.originalContent = state.originalContent
+ tab.isEditMode = state.isEditMode
+ tab.rendered = state.rendered
+ tab.loaded = true
+ }
+ }
+
+ /** 清空所有 tab */
+ const clearAll = () => {
+ tabs.value = []
+ activeTabId.value = null
+ }
+
+ // 监听 tabs/activeTabId 变化,防抖持久化
+ let _persistTimer: ReturnType | null = null
+ watch([tabs, activeTabId], () => {
+ if (_persistTimer) clearTimeout(_persistTimer)
+ _persistTimer = setTimeout(persistSession, 1000)
+ }, { deep: true })
+
+ return {
+ tabs,
+ activeTabId,
+ activeTab,
+ addTab,
+ switchTab,
+ removeTab,
+ cacheCurrentTab,
+ clearAll,
+ persistSession,
+ restoreSession,
+ applyUnsavedContent
+ }
+}
diff --git a/frontend/src/components/FileSystem/index.vue b/frontend/src/components/FileSystem/index.vue
index 0562c09..6e35668 100644
--- a/frontend/src/components/FileSystem/index.vue
+++ b/frontend/src/components/FileSystem/index.vue
@@ -8,7 +8,7 @@
:config="sidebarConfig"
@open-favorite="handleOpenFavorite"
@remove-favorite="handleRemoveFavorite"
- @toggle-pin="handleTogglePin"
+ @toggle-pin="handleToggleFavPin"
@long-press-start="handleLongPressStart"
@long-press-cancel="handleLongPressCancel"
@drag-start="handleDragStart"
@@ -75,6 +75,8 @@
@image-load="handleImageLoad"
@image-error="handleImageError"
@open-local-file="handleOpenLocalFile"
+ @switch-tab="switchToTab"
+ @close-tab="closeTab"
/>
@@ -130,6 +132,7 @@ import { usePathNavigation } from './composables/usePathNavigation'
import { useFilePreview } from './composables/useFilePreview'
import { useFileEdit } from './composables/useFileEdit'
import { useCommonPaths } from './composables/useCommonPaths'
+import { useMultiPreview, type PreviewTab, isDirty } from './composables/useMultiPreview'
// 导入工具函数
import { getFileName, sortFileList } from '@/utils/fileUtils'
@@ -311,6 +314,9 @@ const { fileContent, originalContent, isEditMode, fileContentHeight, contentChan
currentDirectory: filePath
})
+// 多文件预览 Tab
+const multiPreview = useMultiPreview()
+
// ========== 计算属性 ==========
const favoritePaths = computed(() => favorites.value.map(f => f.path))
@@ -413,7 +419,10 @@ const fileEditorPanelConfig = computed(() => {
currentImageDimensions: currentImageDimensions.value,
currentFileExtension,
isBinaryFile: isBinaryFileRef.value,
- fileMtime: selectedFileItem.value?.modified_time || ''
+ fileMtime: selectedFileItem.value?.modified_time || '',
+ // 多 tab
+ previewTabs: multiPreview.tabs.value,
+ activeTabId: multiPreview.activeTabId.value
}
})
@@ -580,7 +589,7 @@ const handleRemoveFavorite = (path: string) => {
removeFav(path)
}
-const handleTogglePin = (path: string) => {
+const handleToggleFavPin = (path: string) => {
togglePin(path)
}
@@ -610,24 +619,14 @@ const handleDragEnd = () => {
// 文件列表事件
const handleFileClick = async (file: FileItem) => {
- // 正常文件系统浏览
if (file.isDir) {
- // 目录:使用 navigate 函数,确保历史记录正确更新
await navigate(file.path)
} else {
- // 文件:选中并读取内容
- selectedFileItem.value = file
- loadFileContent(file.path)
+ openFileAsTab(file)
}
}
-const handleFileDoubleClick = async (file: FileItem) => {
- if (file.isDir) {
- await navigate(file.path)
- } else {
- selectFile(file.path)
- }
-}
+const handleFileDoubleClick = handleFileClick
const handleToggleFavorite = (file: FileItem) => {
toggleFav(file)
@@ -1064,38 +1063,108 @@ const isMediaPreviewable = (filename: string): boolean => {
isPdfFile(filename)
}
+/** 激活 tab:设置选中项 + 加载或恢复内容 */
+const activateTab = async (tab: PreviewTab) => {
+ selectedFileItem.value = tab.fileItem
+ if (tab.loaded) {
+ restoreTabState(tab)
+ } else {
+ await loadFileContent(tab.fileItem.path)
+ tab.loaded = true
+ }
+}
+
+/** 文件 → 添加到 tab 并激活 */
+const openFileAsTab = async (file: FileItem) => {
+ cacheCurrentTabState()
+ const { tab } = multiPreview.addTab(file)
+ await activateTab(tab)
+}
+
+/** 切换 tab */
+const switchToTab = async (tabId: string) => {
+ cacheCurrentTabState()
+ multiPreview.switchTab(tabId)
+ const tab = multiPreview.activeTab.value
+ if (tab) await activateTab(tab)
+}
+
+/** 关闭 tab */
+const closeTab = (tabId: string) => {
+ const tab = multiPreview.tabs.value.find(t => t.id === tabId)
+
+ if (tab && isDirty(tab)) {
+ Modal.confirm({
+ title: '未保存的修改',
+ content: `「${tab!.fileItem.name}」有未保存的修改,关闭后将丢失。`,
+ okText: '关闭',
+ cancelText: '取消',
+ onOk: () => doCloseTab(tabId)
+ })
+ return
+ }
+
+ doCloseTab(tabId)
+}
+
+const doCloseTab = (tabId: string) => {
+ const newActiveId = multiPreview.removeTab(tabId)
+ if (newActiveId) {
+ const tab = multiPreview.activeTab.value
+ if (tab) activateTab(tab)
+ } else {
+ selectedFileItem.value = null
+ clearContent()
+ }
+}
+
+/** 缓存当前 tab 的编辑状态 */
+const cacheCurrentTabState = () => {
+ multiPreview.cacheCurrentTab({
+ previewUrl: previewUrl.value,
+ fileContent: fileContent.value,
+ originalContent: originalContent.value,
+ isEditMode: isEditMode.value,
+ rendered: computeRendered.value
+ })
+}
+
+/** 活跃 tab 内容实时同步(脏标记用,完整缓存由 cacheCurrentTabState 负责) */
+watch([fileContent, originalContent], () => {
+ const tab = multiPreview.activeTab.value
+ if (tab) {
+ tab.fileContent = fileContent.value
+ tab.originalContent = originalContent.value
+ }
+})
+
+/** 从 tab 缓存恢复编辑状态 */
+const restoreTabState = (tab: PreviewTab) => {
+ previewUrl.value = tab.previewUrl
+ fileContent.value = tab.fileContent
+ originalContent.value = tab.originalContent
+ isEditMode.value = tab.isEditMode
+ fileVersion.value++
+}
+
const selectFile = async (path: string) => {
- // 后端已统一返回 / 路径,直接比较
const normalizedPath = path.toLowerCase()
- // 尝试在当前文件列表中查找
- const file = fileList.value.find(f => {
- const normalizedFilePath = f.path.toLowerCase()
- return normalizedFilePath === normalizedPath
- })
+ const file = fileList.value.find(f => f.path.toLowerCase() === normalizedPath)
if (file) {
- // 在当前列表中找到的文件
- selectedFileItem.value = file
+ await openFileAsTab(file)
} else {
- // 不在当前列表中的文件(如收藏夹中的其他目录文件)
- // 构造一个基本的 FileItem 对象
const fileName = getFileName(path)
- selectedFileItem.value = {
+ await openFileAsTab({
path,
name: fileName,
isDir: false,
size: 0,
modified_time: '',
is_favorite: isFavorite(path)
- }
+ })
}
-
- // 加载文件内容
- await loadFileContent(path)
-
- // 记住上次打开的文件
- localStorage.setItem(STORAGE_KEYS.FILESYSTEM.LAST_OPENED_FILE, path)
}
const loadFileContent = async (path: string) => {
@@ -1309,16 +1378,41 @@ onMounted(async () => {
await loadDirectory(startPath)
}
- // 恢复上次打开的文件
- const lastFile = localStorage.getItem(STORAGE_KEYS.FILESYSTEM.LAST_OPENED_FILE)
- if (lastFile) {
- const normalized = lastFile.replace(/\\/g, '/').replace(/\/+$/, '')
- const currentDir = filePath.value.replace(/\\/g, '/').replace(/\/+$/, '')
- const lastFileDir = normalized.substring(0, normalized.lastIndexOf('/')) || '/'
- if (lastFileDir.toLowerCase() === currentDir.toLowerCase()) {
- const found = fileList.value.find(f => f.path.replace(/\\/g, '/').toLowerCase() === normalized.toLowerCase())
- if (found && !found.isDir) {
- await selectFile(found.path)
+ // 恢复多文件预览会话
+ const session = multiPreview.restoreSession()
+ if (session.paths.length > 0 && !connectionManager.isRemote()) {
+ for (const path of session.paths) {
+ const name = path.split(/[/\\]/).pop() || path
+ const { tab } = multiPreview.addTab({ path, name, isDir: false, size: 0, modified_time: '' })
+ if (path === session.activePath || path.replace(/\\/g, '/').toLowerCase() === (session.activePath || '').replace(/\\/g, '/').toLowerCase()) {
+ multiPreview.activeTabId.value = tab.id
+ }
+ multiPreview.applyUnsavedContent(tab, session.unsavedMap)
+ }
+
+ // 加载激活 tab 的内容
+ const active = multiPreview.activeTab.value
+ if (active) {
+ selectedFileItem.value = active.fileItem
+ if (active.loaded) {
+ restoreTabState(active)
+ } else {
+ await loadFileContent(active.fileItem.path)
+ active.loaded = true
+ }
+ }
+ } else {
+ // 无会话记录,回退到旧逻辑:恢复上次打开的单个文件
+ const lastFile = localStorage.getItem(STORAGE_KEYS.FILESYSTEM.LAST_OPENED_FILE)
+ if (lastFile) {
+ const normalized = lastFile.replace(/\\/g, '/').replace(/\/+$/, '')
+ const currentDir = filePath.value.replace(/\\/g, '/').replace(/\/+$/, '')
+ const lastFileDir = normalized.substring(0, normalized.lastIndexOf('/')) || '/'
+ if (lastFileDir.toLowerCase() === currentDir.toLowerCase()) {
+ const found = fileList.value.find(f => f.path.replace(/\\/g, '/').toLowerCase() === normalized.toLowerCase())
+ if (found && !found.isDir) {
+ await selectFile(found.path)
+ }
}
}
}
@@ -1335,6 +1429,9 @@ onUnmounted(() => {
window.removeEventListener('keydown', handleKeyDown)
window.removeEventListener('click', hideContextMenu)
window.removeEventListener('paste', handlePaste)
+ // 应用关闭时立即持久化当前会话
+ cacheCurrentTabState()
+ multiPreview.persistSession()
})
// 键盘快捷键
diff --git a/frontend/src/types/file-system.ts b/frontend/src/types/file-system.ts
index 7d6b219..3d7a2e8 100644
--- a/frontend/src/types/file-system.ts
+++ b/frontend/src/types/file-system.ts
@@ -203,6 +203,10 @@ export interface FileEditorPanelConfig {
isBinaryFile: boolean
/** 文件修改时间(用于检测外部变更) */
fileMtime: string
+ /** 多文件预览 Tab 列表 */
+ previewTabs?: any[]
+ /** 当前激活的 Tab ID */
+ activeTabId?: string | null
}
/**
diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js
index 8d904a6..c4cc980 100644
--- a/frontend/src/utils/constants.js
+++ b/frontend/src/utils/constants.js
@@ -31,6 +31,7 @@ export const STORAGE_KEYS = {
COL_SETTINGS: 'app-filesystem-col-settings', // 列配置(显隐+顺序)
SHOW_HEADER: 'app-filesystem-show-header', // 表头显隐
LAST_OPENED_FILE: 'app-filesystem-last-opened-file', // 上次打开的文件路径
+ MULTI_PREVIEW_TABS: 'app-filesystem-multi-preview-tabs', // 多文件预览 tab 会话
},
// 设备测试模块
diff --git a/internal/filesystem/path_validator.go b/internal/filesystem/path_validator.go
index 9b9f3dc..b9f93d9 100644
--- a/internal/filesystem/path_validator.go
+++ b/internal/filesystem/path_validator.go
@@ -123,12 +123,9 @@ func (v *DefaultPathValidator) checkWindowsSystemPaths(path string) *ValidationE
if len(lowerPath) >= 3 && lowerPath[1] == ':' {
driveLetter := lowerPath[0:1]
- // 检查系统关键目录(仅保留最关键的系统目录)
+ // 检查系统核心目录
forbiddenDirs := []string{
driveLetter + ":\\windows",
- driveLetter + ":\\program files",
- driveLetter + ":\\program files (x86)",
- driveLetter + ":\\program files (arm)",
driveLetter + ":\\system volume information",
driveLetter + ":\\boot",
}