From aad7e6f76f30245e57dd0ec7256584ec5bd7fe07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Mon, 17 Aug 2026 10:11:47 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E5=86=99=E6=BB=A1=E9=9D=99=E9=BB=98=E4=B8=A2?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E2=80=94=E2=80=94=E5=A4=B1=E8=B4=A5=E5=91=8A?= =?UTF-8?q?=E8=AD=A6=20+=20=E5=AF=BC=E5=85=A5=E5=9B=BE=E7=89=87=E4=BD=93?= =?UTF-8?q?=E7=A7=AF=E9=97=B8=E9=97=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 5 +++++ src/components/modals/ImportModal.vue | 12 ++++++++++++ src/core/store.ts | 24 ++++++++++++++++++++---- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/App.vue b/src/App.vue index 5f77c9e..1d3c799 100644 --- a/src/App.vue +++ b/src/App.vue @@ -168,6 +168,11 @@ onMounted(() => { document.addEventListener('keydown', onKey) }) +/* 持久化失败告警 → toast(防止静默丢数据) */ +watch(() => store.saveWarning.text, (txt) => { + if (txt) toast(txt) +}) + /* 文库关闭时若在首页且从文库加载了 deck 则切入编辑 */ watch(libraryVisible, (now, prev) => { if (prev && !now && deckLoaded.value) { diff --git a/src/components/modals/ImportModal.vue b/src/components/modals/ImportModal.vue index 0b1a705..0423870 100644 --- a/src/components/modals/ImportModal.vue +++ b/src/components/modals/ImportModal.vue @@ -149,6 +149,18 @@ function doImport() { let insertedImages = 0 let insertedSlides = 0 + // 图片体积闸门:localStorage 约 5MB 字符上限,超限导入后刷新会丢图 + if (images.length > 0) { + const imgChars = images.reduce((s, e) => s + (e.data?.length || 0), 0) + let deckChars = 0 + try { deckChars = JSON.stringify(store.getDeck()).length } catch (e) { /* ignore */ } + const QUOTA_CHARS = 4_500_000 + if (deckChars + imgChars > QUOTA_CHARS) { + emit('toast', '图片过大:导入后约 ' + ((deckChars + imgChars) / 1048576).toFixed(1) + 'MB,超本地存储上限(约 5MB),刷新可能丢失。请压缩图片、减少数量,或先清理文库/旧图') + return + } + } + // 1. 图片 → 当前幻灯片 if (images.length > 0) { if (images.length === 1) { diff --git a/src/core/store.ts b/src/core/store.ts index c992713..79bf303 100644 --- a/src/core/store.ts +++ b/src/core/store.ts @@ -100,10 +100,25 @@ let saveTimer: ReturnType | null = null /* ---------- 工具 ---------- */ function clone(o: T): T { return JSON.parse(JSON.stringify(o)) } +/* ---------- 持久化告警(写入失败置位,供 UI 提示,避免静默丢数据) ---------- */ +const saveWarning = reactive({ text: '' }) + +/** 安全写入 localStorage;失败时置告警(非静默),返回是否成功 */ +function safeSet(key: string, val: string): boolean { + try { localStorage.setItem(key, val); saveWarning.text = ''; return true } + catch (e) { + if (!saveWarning.text) { + saveWarning.text = '本地存储空间不足,最新改动可能未保存。请导出 JSON 备份,或删除部分图片/文库释放空间' + } + console.error('[u-ppt] 保存失败:', key, e) + return false + } +} + function scheduleSave() { if (saveTimer) clearTimeout(saveTimer) saveTimer = setTimeout(() => { - try { localStorage.setItem(LS_KEY, JSON.stringify(state.deck)) } catch (e) { /* 配额满等忽略 */ } + safeSet(LS_KEY, JSON.stringify(state.deck)) }, SAVE_DELAY) } @@ -475,7 +490,7 @@ function getCfg(): AiCfg { } function setCfg(cfg: AiCfg) { - localStorage.setItem(LS_CFG, JSON.stringify({ ...DEFAULT_CFG, ...cfg })) + safeSet(LS_CFG, JSON.stringify({ ...DEFAULT_CFG, ...cfg })) } /* ---------- 自定义主题:AI 配色建议应用后存储 ---------- */ @@ -578,7 +593,7 @@ function getLibrary(): LibItem[] { try { return JSON.parse(localStorage.getItem(LIB_KEY) || '[]') || [] } catch (e) { return [] } } function setLibrary(arr: LibItem[]) { - try { localStorage.setItem(LIB_KEY, JSON.stringify(arr)) } catch (e) {} + safeSet(LIB_KEY, JSON.stringify(arr)) } function getActiveLibId() { return state.activeLibId } function setActive(id: string | null) { @@ -666,7 +681,7 @@ function getChat(chatId: string): ChatMessage[] { /** 写入某个 chatId 的会话 */ function setChat(chatId: string, msgs: ChatMessage[]) { - try { localStorage.setItem(CHAT_PREFIX + chatId, JSON.stringify(msgs)) } catch (e) {} + safeSet(CHAT_PREFIX + chatId, JSON.stringify(msgs)) } /** 清除某个 chatId 的会话 */ @@ -691,6 +706,7 @@ function recoverBackup(): boolean { export const store = { // 状态(响应式) state, + saveWarning, // computed deck, slides, count, currentIndex, theme, selectedId, activeLibId, currentSlide, canUndo, canRedo,