Private
Public Access
1
0

新增:多文件预览Tab系统+脏标记+关闭确认+路径黑名单优化

- useMultiPreview composable管理多Tab状态、会话持久化
- 面包屑状态dot移除
- 放开Program Files目录访问限制
This commit is contained in:
2026-05-05 00:08:15 +08:00
parent ee4b1f5ac1
commit eb5b85e007
7 changed files with 558 additions and 48 deletions

View File

@@ -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<string, UnsavedEntry>
}
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<PreviewTab[]>([])
const activeTabId = ref<string | null>(null)
const activeTab = computed<PreviewTab | null>(() => {
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<string, UnsavedEntry>()
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<string, UnsavedEntry>) => {
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<typeof setTimeout> | 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
}
}