fix(desktop): 状态镜像到宿主侧,修复随机端口导致重启丢数据
This commit is contained in:
@@ -12,6 +12,7 @@ import { previewFromEvents, previewFromNodes, readSessionPreview, presetApiOf, m
|
||||
import { photoStore, kindOf } from './photoStore'
|
||||
import { DEFAULT_BG_SVG, b64ToBlob } from './defaultBg'
|
||||
import { WAVE_BG_B64 } from './waveBg'
|
||||
import { loadHostState, queueHostSave, markHydrated, flushHostSave } from './hostState'
|
||||
|
||||
/**
|
||||
* dsh-worktable 客户端(v2):侧边栏底部「工作台」区块。
|
||||
@@ -124,6 +125,8 @@ type ProjectsState = {
|
||||
|
||||
const PERSIST_KEY = 'dsh.worktable.view.v1'
|
||||
const PROJECTS_KEY = 'dsh.worktable.projects.v1'
|
||||
/** 本地最后写入时间(毫秒):与宿主镜像的 updatedAt 比较,决定启动时以哪边为准 */
|
||||
const SAVED_AT_KEY = 'dsh.worktable.savedAt.v1'
|
||||
const MIN_TOP = 56
|
||||
const SNAP_PX = 32
|
||||
/** 插件市场外链(GitHub 仓库,已核实可访问;PRD 提及的 dshfind.com 未验证,不用死链)。 */
|
||||
@@ -308,11 +311,11 @@ function normHsl(v: any): { h: number; s: number; l: number } | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
function loadView(): ViewState {
|
||||
/** 视图状态归一化(本地存档与宿主镜像共用同一套字段校验) */
|
||||
function normalizeView(raw: any): ViewState {
|
||||
try {
|
||||
const raw = localStorage.getItem(PERSIST_KEY)
|
||||
if (!raw) return { ...DEFAULT_VIEW }
|
||||
const p = JSON.parse(raw)
|
||||
if (!raw || typeof raw !== 'object') return { ...DEFAULT_VIEW }
|
||||
const p = raw
|
||||
// 显式挑字段:旧版遗留的 groupBy 等未知字段直接忽略。
|
||||
// 一次性迁移:v2 起默认「手动」排序(最近排序点击即置顶、体验差),旧存「最近」回落为「手动」;
|
||||
// 之后用户手动选择「最近」会写入 sortMigratedV2 标记并被尊重。
|
||||
@@ -356,11 +359,21 @@ function loadView(): ViewState {
|
||||
}
|
||||
}
|
||||
|
||||
function loadProjects(): ProjectsState {
|
||||
function loadView(): ViewState {
|
||||
try {
|
||||
const raw = localStorage.getItem(PROJECTS_KEY)
|
||||
if (!raw) return { ...DEFAULT_PROJECTS }
|
||||
const p = JSON.parse(raw)
|
||||
const raw = localStorage.getItem(PERSIST_KEY)
|
||||
if (!raw) return { ...DEFAULT_VIEW }
|
||||
return normalizeView(JSON.parse(raw))
|
||||
} catch {
|
||||
return { ...DEFAULT_VIEW }
|
||||
}
|
||||
}
|
||||
|
||||
/** 项目状态归一化(本地存档与宿主镜像共用同一套字段校验) */
|
||||
function normalizeProjects(raw: any): ProjectsState {
|
||||
try {
|
||||
if (!raw || typeof raw !== 'object') return { ...DEFAULT_PROJECTS }
|
||||
const p = raw
|
||||
return {
|
||||
order: Array.isArray(p.order) ? p.order.filter((x: unknown): x is string => typeof x === 'string') : [],
|
||||
lastUsed: p.lastUsed && typeof p.lastUsed === 'object' ? p.lastUsed : {},
|
||||
@@ -383,6 +396,21 @@ function loadProjects(): ProjectsState {
|
||||
}
|
||||
}
|
||||
|
||||
function loadProjects(): ProjectsState {
|
||||
try {
|
||||
const raw = localStorage.getItem(PROJECTS_KEY)
|
||||
if (!raw) return { ...DEFAULT_PROJECTS }
|
||||
return normalizeProjects(JSON.parse(raw))
|
||||
} catch {
|
||||
return { ...DEFAULT_PROJECTS }
|
||||
}
|
||||
}
|
||||
|
||||
/** 记下本地写入时间,供启动时与宿主镜像比较新鲜度 */
|
||||
function markSaved(): void {
|
||||
try { localStorage.setItem(SAVED_AT_KEY, String(Date.now())) } catch {}
|
||||
}
|
||||
|
||||
const clamp = (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), hi)
|
||||
|
||||
type FloatRect = { top: number }
|
||||
@@ -1380,6 +1408,8 @@ function WorktableSection(props: any) {
|
||||
setView((prev) => {
|
||||
const next = { ...prev, ...patch }
|
||||
try { localStorage.setItem(PERSIST_KEY, JSON.stringify({ ...next, sortMigratedV2: true })) } catch {}
|
||||
markSaved()
|
||||
queueHostSave({ view: next })
|
||||
return next
|
||||
})
|
||||
}
|
||||
@@ -1388,10 +1418,44 @@ function WorktableSection(props: any) {
|
||||
setProjects((prev) => {
|
||||
const next = typeof patch === 'function' ? patch(prev) : { ...prev, ...patch }
|
||||
try { localStorage.setItem(PROJECTS_KEY, JSON.stringify(next)) } catch {}
|
||||
markSaved()
|
||||
queueHostSave({ projects: next })
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 宿主侧状态恢复(本地补丁):桌面端每次启动端口随机,localStorage 按来源隔离,
|
||||
* 重启后本地必然是空的。这里比对「本地最后写入时间」与「宿主镜像 updatedAt」,
|
||||
* 只在宿主更新时回填,避免用旧镜像盖掉当前页面刚改的内容。
|
||||
* 恢复判定结束前不写宿主(markHydrated),防止首屏默认值先把镜像冲掉。
|
||||
*/
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
void (async () => {
|
||||
const host = await loadHostState()
|
||||
if (cancelled) return
|
||||
let localAt = 0
|
||||
try { localAt = Number(localStorage.getItem(SAVED_AT_KEY) ?? '0') || 0 } catch {}
|
||||
const hostAt = typeof host?.updatedAt === 'number' ? host.updatedAt : 0
|
||||
if (host && hostAt > localAt) {
|
||||
if (host.projects) persistProjects(normalizeProjects(host.projects))
|
||||
if (host.view) persistView(normalizeView(host.view))
|
||||
}
|
||||
markHydrated()
|
||||
if (hostAt <= localAt) flushHostSave()
|
||||
})()
|
||||
const flush = () => flushHostSave()
|
||||
window.addEventListener('pagehide', flush)
|
||||
window.addEventListener('beforeunload', flush)
|
||||
return () => {
|
||||
cancelled = true
|
||||
window.removeEventListener('pagehide', flush)
|
||||
window.removeEventListener('beforeunload', flush)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// 自愈:启动时若控制室存档是坏布局(旧版关掉控制室标签造成窗格退化成选择器),立即重建默认面板
|
||||
useEffect(() => {
|
||||
const saved = projects.views[CONSOLE_ID]
|
||||
|
||||
Reference in New Issue
Block a user