Private
Public Access
1
0
Files
u-desk/frontend/src/components/FileSystem/composables/useMultiPreview.ts

251 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 多文件预览 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<string, string | undefined>
unsavedMap: Map<string, UnsavedEntry>
}
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<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, 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<string, UnsavedEntry>()
const profileMap = new Map<string, string | undefined>()
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<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, 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<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
}
}