diff --git a/src/core/backend.ts b/src/core/backend.ts new file mode 100644 index 0000000..da39cfe --- /dev/null +++ b/src/core/backend.ts @@ -0,0 +1,173 @@ +/* ===================================================================== + * backend.ts — 持久化后端抽象层 + * + * store 只管内存状态 + Op Log,持久化通过 Backend 接口抽象。 + * 三个实现: + * - LocalBackend (localStorage,Web 版当前模式) + * - TauriBackend (Rust 文件系统,桌面版,B4 实现) + * - CloudBackend (HTTP API,云端版,C0 实现) + * + * 设计原则: + * - 接口只管读写数据,不管状态逻辑(状态逻辑在 store) + * - 所有方法返回 Promise(TauriBackend/CloudBackend 天然异步) + * - LocalBackend 内部用 localStorage 但包装为 Promise(统一接口) + * ===================================================================== */ +import type { Deck, LibItem, PageTemplate, Theme, ChatMessage, AiCfg } from './types' + +export interface Backend { + // ---- 当前 deck ---- + loadDeck(): Promise + saveDeck(deck: Deck): Promise + loadBackup(): Promise + saveBackup(deck: Deck): Promise + clearBackup(): Promise + + // ---- 文库 ---- + loadLibrary(): Promise + saveLibrary(lib: LibItem[]): Promise + getActiveLibId(): Promise + setActiveLibId(id: string | null): Promise + + // ---- 会话(按 chatId 隔离) ---- + loadChat(chatId: string): Promise + saveChat(chatId: string, msgs: ChatMessage[]): Promise + clearChat(chatId: string): Promise + + // ---- 用户模板 ---- + loadTemplates(): Promise + saveTemplates(tpls: PageTemplate[]): Promise + + // ---- 自定义主题 ---- + loadThemes(): Promise> + saveThemes(themes: Record): Promise + + // ---- AI 配置 ---- + loadCfg(): Promise | null> + saveCfg(cfg: AiCfg): Promise +} + +/* ============================================================ + * LocalBackend — localStorage 实现(Web 版当前模式) + * ============================================================ */ + +const KEYS = { + deck: 'u-ppt.deck.v1', + deckBak: 'u-ppt.deck.v1.bak', + lib: 'u-ppt.library.v1', + activeLib: 'u-ppt.activeLib.v1', + chatPrefix: 'u-ppt.chat.', + templates: 'u-ppt.templates.v1', + themes: 'u-ppt.themes.v1', + cfg: 'u-ppt.ai-cfg.v1', +} + +function lsGet(key: string): string | null { + try { return localStorage.getItem(key) } catch (e) { return null } +} +function lsSet(key: string, val: string): void { + try { localStorage.setItem(key, val) } catch (e) { /* 配额满等忽略 */ } +} +function lsDel(key: string): void { + try { localStorage.removeItem(key) } catch (e) {} +} + +export class LocalBackend implements Backend { + async loadDeck(): Promise { + const raw = lsGet(KEYS.deck) + if (!raw) return null + try { return JSON.parse(raw) } catch (e) { return null } + } + + async saveDeck(deck: Deck): Promise { + lsSet(KEYS.deck, JSON.stringify(deck)) + } + + async loadBackup(): Promise { + const raw = lsGet(KEYS.deckBak) + if (!raw) return null + try { return JSON.parse(raw) } catch (e) { return null } + } + + async saveBackup(deck: Deck): Promise { + lsSet(KEYS.deckBak, JSON.stringify(deck)) + } + + async clearBackup(): Promise { + lsDel(KEYS.deckBak) + } + + async loadLibrary(): Promise { + const raw = lsGet(KEYS.lib) + if (!raw) return [] + try { return JSON.parse(raw) || [] } catch (e) { return [] } + } + + async saveLibrary(lib: LibItem[]): Promise { + lsSet(KEYS.lib, JSON.stringify(lib)) + } + + async getActiveLibId(): Promise { + return lsGet(KEYS.activeLib) + } + + async setActiveLibId(id: string | null): Promise { + if (id) lsSet(KEYS.activeLib, id) + else lsDel(KEYS.activeLib) + } + + async loadChat(chatId: string): Promise { + const raw = lsGet(KEYS.chatPrefix + chatId) + if (!raw) return [] + try { return JSON.parse(raw) || [] } catch (e) { return [] } + } + + async saveChat(chatId: string, msgs: ChatMessage[]): Promise { + lsSet(KEYS.chatPrefix + chatId, JSON.stringify(msgs)) + } + + async clearChat(chatId: string): Promise { + lsDel(KEYS.chatPrefix + chatId) + } + + async loadTemplates(): Promise { + const raw = lsGet(KEYS.templates) + if (!raw) return [] + try { return JSON.parse(raw) || [] } catch (e) { return [] } + } + + async saveTemplates(tpls: PageTemplate[]): Promise { + // 只存用户模板(category === 'user') + lsSet(KEYS.templates, JSON.stringify(tpls.filter(t => t.category === 'user'))) + } + + async loadThemes(): Promise> { + const raw = lsGet(KEYS.themes) + if (!raw) return {} + try { return JSON.parse(raw) || {} } catch (e) { return {} } + } + + async saveThemes(themes: Record): Promise { + lsSet(KEYS.themes, JSON.stringify(themes)) + } + + async loadCfg(): Promise | null> { + const raw = lsGet(KEYS.cfg) + if (!raw) return null + try { return JSON.parse(raw) } catch (e) { return null } + } + + async saveCfg(cfg: AiCfg): Promise { + lsSet(KEYS.cfg, JSON.stringify(cfg)) + } +} + +/* ---------- 当前活动的 Backend 实例 ---------- */ +let activeBackend: Backend = new LocalBackend() + +export function getBackend(): Backend { + return activeBackend +} + +export function setBackend(b: Backend): void { + activeBackend = b +}