新增:多文件预览Tab系统+脏标记+关闭确认+路径黑名单优化
- useMultiPreview composable管理多Tab状态、会话持久化 - 面包屑状态dot移除 - 放开Program Files目录访问限制
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user