From 47069cea4acd2f910d2bb23f2b7ac9c96aad15ae Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Mon, 4 May 2026 16:25:03 +0800 Subject: [PATCH] Harden preset save/apply against partial failures Preset edits were applied to local UI state before persistence finished, so storage failures could show a misleading saved state and then lose changes on reload. Also, batch apply aborted on first add_vocab error and skipped later selected presets. Make preset save await persistence before mutating UI/closing editor, and make batch apply continue across per-term failures while reporting aggregated errors. Constraint: Minimal behavior fix within existing Vocab page flow Rejected: Add transactional backend endpoint for batch apply | larger cross-layer change Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep UI success states aligned with durable persistence completion Tested: npm run build Not-tested: Manual readonly-disk simulation for persist failure --- openless-all/app/src/pages/Vocab.tsx | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/openless-all/app/src/pages/Vocab.tsx b/openless-all/app/src/pages/Vocab.tsx index 3ce04a98..3f0f2630 100644 --- a/openless-all/app/src/pages/Vocab.tsx +++ b/openless-all/app/src/pages/Vocab.tsx @@ -103,7 +103,7 @@ export function Vocab() { setPresetPhrasesDraft(preset.phrases.join(', ')); }; - const savePreset = () => { + const savePreset = async () => { if (!editingPresetId) return; const name = presetNameDraft.trim(); if (!name) return; @@ -119,9 +119,13 @@ export function Vocab() { editingPresetId === NEW_PRESET_DRAFT_ID ? [...presets, { id: `user-${Date.now()}`, name, phrases }] : presets.map(p => (p.id === editingPresetId ? { ...p, name, phrases } : p)); - setPresets(next); - void persistVocabPresets(next).catch(err => setError(err instanceof Error ? err.message : String(err))); - setEditingPresetId(null); + try { + await persistVocabPresets(next); + setPresets(next); + setEditingPresetId(null); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } }; const createPreset = () => { @@ -134,15 +138,23 @@ export function Vocab() { const selected = presets.filter(p => selectedPresetIds.includes(p.id)); if (selected.length === 0) return; const phraseSet = new Set(entries.map(e => e.phrase.trim().toLowerCase())); + let failures = 0; for (const p of selected) { for (const phrase of p.phrases) { if (!phraseSet.has(phrase.trim().toLowerCase())) { - await addVocab(phrase); - phraseSet.add(phrase.trim().toLowerCase()); + try { + await addVocab(phrase); + phraseSet.add(phrase.trim().toLowerCase()); + } catch { + failures += 1; + } } } } await refresh(); + if (failures > 0) { + setError(`部分词条添加失败(${failures})`); + } }; return ( @@ -185,7 +197,7 @@ export function Vocab() { setPresetNameDraft(e.target.value)} placeholder={t('vocab.presets.namePlaceholder')} />