Files
admin-web/src/core/workspace-tabs/workspaceTabs.ts
T
2026-08-05 15:54:59 +08:00

148 lines
4.8 KiB
TypeScript

import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import type { WorkspaceTab, WorkspaceTabCandidate, WorkspaceTabsSnapshot } from './types'
const STORAGE_PREFIX = 'cc-admin-workspace-tabs:v1'
export const WORKSPACE_TAB_LIMIT = 12
export const MAX_CACHED_WORKSPACE_VIEWS = 24
function storageKey(userId: string) {
return `${STORAGE_PREFIX}:${userId}`
}
function validTab(value: unknown): value is WorkspaceTab {
if (!value || typeof value !== 'object') return false
const tab = value as Partial<WorkspaceTab>
return typeof tab.id === 'string'
&& Boolean(tab.id)
&& typeof tab.title === 'string'
&& typeof tab.path === 'string'
&& tab.path.startsWith('/')
&& typeof tab.lastVisitedAt === 'number'
&& Number.isFinite(tab.lastVisitedAt)
}
export function readWorkspaceTabsSnapshot(userId: string): WorkspaceTabsSnapshot {
if (!userId) return { tabs: [], activeId: null }
try {
const parsed = JSON.parse(sessionStorage.getItem(storageKey(userId)) || 'null') as Partial<WorkspaceTabsSnapshot> | null
if (!parsed || !Array.isArray(parsed.tabs)) return { tabs: [], activeId: null }
const tabs = parsed.tabs.filter(validTab)
const activeId = typeof parsed.activeId === 'string' && tabs.some((tab) => tab.id === parsed.activeId)
? parsed.activeId
: null
return { tabs, activeId }
} catch {
return { tabs: [], activeId: null }
}
}
export function readStoredActiveWorkspacePath(userId: string) {
const snapshot = readWorkspaceTabsSnapshot(userId)
return snapshot.tabs.find((tab) => tab.id === snapshot.activeId)?.path ?? null
}
export const useWorkspaceTabsStore = defineStore('workspace-tabs', () => {
const tabs = ref<WorkspaceTab[]>([])
const activeId = ref<string | null>(null)
const initializedUserId = ref('')
const cacheEpochs = ref<Record<string, number>>({})
const activeTab = computed(() => tabs.value.find((tab) => tab.id === activeId.value) ?? null)
function persist() {
if (!initializedUserId.value) return
sessionStorage.setItem(storageKey(initializedUserId.value), JSON.stringify({
tabs: tabs.value,
activeId: activeId.value,
} satisfies WorkspaceTabsSnapshot))
}
function initialize(userId: string) {
if (!userId || initializedUserId.value === userId) return
initializedUserId.value = userId
const snapshot = readWorkspaceTabsSnapshot(userId)
tabs.value = snapshot.tabs
activeId.value = snapshot.activeId
}
function bumpCacheEpoch(tabId: string) {
cacheEpochs.value = {
...cacheEpochs.value,
[tabId]: (cacheEpochs.value[tabId] ?? 0) + 1,
}
}
function removeByIds(ids: readonly string[]) {
const idSet = new Set(ids)
if (idSet.size === 0) return activeTab.value
const activeWasRemoved = activeId.value !== null && idSet.has(activeId.value)
for (const id of idSet) bumpCacheEpoch(id)
tabs.value = tabs.value.filter((tab) => !idSet.has(tab.id))
if (activeWasRemoved) {
activeId.value = [...tabs.value].sort((left, right) => right.lastVisitedAt - left.lastVisitedAt)[0]?.id ?? null
} else if (activeId.value && !tabs.value.some((tab) => tab.id === activeId.value)) {
activeId.value = null
}
persist()
return activeTab.value
}
function enforceLimit(dirtyTabIds: ReadonlySet<string>) {
while (tabs.value.length > WORKSPACE_TAB_LIMIT) {
const candidate = tabs.value
.filter((tab) => tab.id !== activeId.value && !dirtyTabIds.has(tab.id))
.sort((left, right) => left.lastVisitedAt - right.lastVisitedAt)[0]
if (!candidate) break
removeByIds([candidate.id])
}
}
function open(candidate: WorkspaceTabCandidate, dirtyTabIds: ReadonlySet<string> = new Set()) {
const now = Date.now()
const existing = tabs.value.find((tab) => tab.id === candidate.id)
if (existing) {
existing.title = candidate.title
existing.path = candidate.path
existing.lastVisitedAt = now
} else {
tabs.value.push({ ...candidate, lastVisitedAt: now })
}
activeId.value = candidate.id
enforceLimit(dirtyTabIds)
persist()
}
function prune(keep: (tab: WorkspaceTab) => boolean) {
const removedIds = tabs.value.filter((tab) => !keep(tab)).map((tab) => tab.id)
return { removedIds, fallback: removeByIds(removedIds) }
}
function cacheKey(tabId: string | null) {
if (!tabId) return 'workspace-route'
return `${tabId}:${cacheEpochs.value[tabId] ?? 0}`
}
function reset(removePersisted = false) {
if (removePersisted && initializedUserId.value) sessionStorage.removeItem(storageKey(initializedUserId.value))
tabs.value = []
activeId.value = null
cacheEpochs.value = {}
initializedUserId.value = ''
}
return {
tabs,
activeId,
activeTab,
initializedUserId,
initialize,
open,
removeByIds,
prune,
cacheKey,
bumpCacheEpoch,
reset,
}
})