/** * 多文件预览 Tab 管理 Composable * 管理预览 Tab 列表、激活状态、内容缓存、会话持久化 */ import { ref, computed, watch } from 'vue' import { STORAGE_KEYS } from '@/utils/constants' import { connectionManager } from '@/api/connection-manager' import type { FileItem } from '@/types/file-system' export interface PreviewTab { /** 唯一标识(基于路径) */ id: string /** 文件信息 */ fileItem: FileItem /** 所属连接 profileId */ profileId?: string /** 缓存的预览 URL */ previewUrl: string /** 缓存的文件内容 */ fileContent: string /** 缓存的原始内容 */ originalContent: string /** 缓存的编辑模式状态 */ isEditMode: boolean /** 缓存的渲染内容 (HTML/Markdown) */ rendered: string /** 是否已加载过内容 */ loaded: boolean } /** 持久化存储的精简结构 */ interface PersistedTab { path: string active: boolean profileId?: string /** 未保存的内容(有修改时才存) */ 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 profileMap: Map unsavedMap: Map } function pathToId(path: string): string { const normalized = path.replace(/\\/g, '/') return connectionManager.isRemote() ? normalized : normalized.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, profileId?: string): PreviewTab => ({ id: pathToId(fileItem.path), fileItem, profileId, previewUrl: '', fileContent: '', originalContent: '', isEditMode: false, rendered: '', loaded: false }) // ========== 持久化 ========== /** 从 localStorage 恢复会话 */ const restoreSession = (): RestoredSession => { const unsavedMap = new Map() const profileMap = new Map() let activePath: string | null = null const paths: string[] = [] try { const raw = localStorage.getItem(STORAGE_KEY) if (!raw) return { paths, activePath, profileMap, unsavedMap } const persisted: PersistedTab[] = JSON.parse(raw) if (!Array.isArray(persisted)) return { paths, activePath, profileMap, unsavedMap } for (const p of persisted) { if (!p.path) continue paths.push(p.path) profileMap.set(pathToId(p.path), p.profileId) 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, profileMap, unsavedMap } } /** 保存会话到 localStorage */ const persistSession = () => { const persisted: PersistedTab[] = tabs.value.map(tab => { const hasUnsaved = isDirty(tab) // 限制存储大小,超过 100KB 的内容不存入 localStorage const canSave = hasUnsaved && tab.fileContent.length <= 100_000 return { path: tab.fileItem.path, active: tab.id === activeTabId.value, profileId: tab.profileId, 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, profileId?: string): { 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, profileId) 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 } }