Files
calculator2026/src/lib/projectRegistry.ts
T
2026-04-07 16:27:49 +08:00

159 lines
5.2 KiB
TypeScript

import { DEFAULT_PROJECT_ID, QUICK_PROJECT_ID, normalizeProjectId } from '@/lib/workspace'
import { i18n } from '@/i18n'
const PROJECT_REGISTRY_KEY = 'jgjs-project-registry-v1'
export interface ProjectMeta {
id: string
name: string
createdAt: string
updatedAt: string
lastOpenedAt: string
}
type ProjectRegistryPayload = {
projects: ProjectMeta[]
}
const nowIso = () => new Date().toISOString()
const lastEditedTouchAt = new Map<string, number>()
const defaultProjects = (): ProjectMeta[] => {
return []
}
const sanitizeProject = (item: Partial<ProjectMeta> | null | undefined): ProjectMeta | null => {
if (!item || typeof item !== 'object') return null
const id = normalizeProjectId(item.id)
const name = String(item.name || '').trim() || (
id === QUICK_PROJECT_ID
? i18n.global.t('quickCalc.projectName')
: i18n.global.t('tab.messages.projectNamePrefix', { id })
)
const createdAt = typeof item.createdAt === 'string' && item.createdAt ? item.createdAt : nowIso()
const updatedAt = typeof item.updatedAt === 'string' && item.updatedAt ? item.updatedAt : createdAt
const lastOpenedAt =
typeof item.lastOpenedAt === 'string' && item.lastOpenedAt ? item.lastOpenedAt : updatedAt
return { id, name, createdAt, updatedAt, lastOpenedAt }
}
const readPayload = (): ProjectRegistryPayload => {
try {
const raw = localStorage.getItem(PROJECT_REGISTRY_KEY)
if (!raw) return { projects: defaultProjects() }
const parsed = JSON.parse(raw) as Partial<ProjectRegistryPayload>
const projects = Array.isArray(parsed?.projects)
? parsed.projects.map(item => sanitizeProject(item)).filter((item): item is ProjectMeta => Boolean(item))
: []
if (projects.length === 0) return { projects: defaultProjects() }
return { projects }
} catch {
return { projects: defaultProjects() }
}
}
const writePayload = (payload: ProjectRegistryPayload) => {
localStorage.setItem(PROJECT_REGISTRY_KEY, JSON.stringify(payload))
}
export const listProjects = () => {
const payload = readPayload()
return payload.projects
.filter(item => item.id !== QUICK_PROJECT_ID && item.id !== DEFAULT_PROJECT_ID)
.slice()
.sort((a, b) => new Date(b.lastOpenedAt).getTime() - new Date(a.lastOpenedAt).getTime())
}
export const upsertProject = (
projectIdRaw: string,
nameRaw?: string,
options?: {
touchUpdatedAt?: boolean
touchLastOpenedAt?: boolean
}
) => {
const id = normalizeProjectId(projectIdRaw)
if (id === QUICK_PROJECT_ID || id === DEFAULT_PROJECT_ID) return
const name = String(nameRaw || '').trim()
const payload = readPayload()
const now = nowIso()
const touchUpdatedAt = options?.touchUpdatedAt === true
const touchLastOpenedAt = options?.touchLastOpenedAt !== false
const index = payload.projects.findIndex(item => item.id === id)
if (index < 0) {
payload.projects.push({
id,
name: name || (
id === QUICK_PROJECT_ID
? i18n.global.t('quickCalc.projectName')
: i18n.global.t('tab.messages.projectNamePrefix', { id })
),
createdAt: now,
updatedAt: now,
lastOpenedAt: now
})
} else {
const current = payload.projects[index]
payload.projects[index] = {
...current,
name: name || current.name,
updatedAt: touchUpdatedAt ? now : current.updatedAt,
lastOpenedAt: touchLastOpenedAt ? now : current.lastOpenedAt
}
}
writePayload(payload)
}
export const touchProjectEdited = (
projectIdRaw: string,
options?: {
throttleMs?: number
}
) => {
const id = normalizeProjectId(projectIdRaw)
if (id === QUICK_PROJECT_ID || id === DEFAULT_PROJECT_ID) return false
const throttleMs = Math.max(0, Number(options?.throttleMs ?? 5000))
const nowMs = Date.now()
const lastTouch = lastEditedTouchAt.get(id) ?? 0
if (throttleMs > 0 && nowMs - lastTouch < throttleMs) return false
const payload = readPayload()
const index = payload.projects.findIndex(item => item.id === id)
if (index < 0) return false
const now = nowIso()
const current = payload.projects[index]
payload.projects[index] = {
...current,
updatedAt: now,
lastOpenedAt: now
}
writePayload(payload)
lastEditedTouchAt.set(id, nowMs)
return true
}
export const createProject = (nameRaw?: string) => {
const payload = readPayload()
const now = nowIso()
let id = ''
do {
id = normalizeProjectId(`p-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`)
} while (payload.projects.some(item => item.id === id))
const name = String(nameRaw || '').trim() || i18n.global.t('tab.messages.projectNamePrefix', { id: payload.projects.length + 1 })
const project: ProjectMeta = { id, name, createdAt: now, updatedAt: now, lastOpenedAt: now }
payload.projects.push(project)
writePayload(payload)
return project
}
export const deleteProject = (projectIdRaw: string) => {
const id = normalizeProjectId(projectIdRaw)
if (id === QUICK_PROJECT_ID || id === DEFAULT_PROJECT_ID) return false
const payload = readPayload()
const nextProjects = payload.projects.filter(item => item.id !== id)
if (nextProjects.length === payload.projects.length) return false
payload.projects = nextProjects
writePayload(payload)
return true
}