fix(data): 覆盖式导入与快照恢复前自动备份

- importJson / restoreSnapshot 覆盖前先写一份当前数据快照,误操作可回退
- 导入校验增加 id 唯一性检查,宽容读取路径保留首条
- 快照索引对同一时间点的重复写入去重
- 落盘失败时翻转 persistent,恢复后自动撤除告警
- 备份面板提示导入前数据已自动备份
This commit is contained in:
chenshoulin
2026-10-19 17:20:20 +08:00
parent 12c5346f75
commit ede4bfe88f
8 changed files with 214 additions and 14 deletions
+41 -3
View File
@@ -19,7 +19,12 @@ import {
} from '@/data/taskRepository'
import { DEFAULT_PRIORITY, type ColumnId, type Priority, type Task } from '@/types/task'
import { createId } from '@/utils/id'
import { groupByColumn, moveWithinColumn, sortByOrder, sortOrderForPosition } from '@/utils/sortOrder'
import {
groupByColumn,
moveWithinColumn,
sortByOrder,
sortOrderForPosition,
} from '@/utils/sortOrder'
/** 跨标签页同步使用的频道名 */
export const SYNC_CHANNEL_NAME = 'kanban.sync'
@@ -105,7 +110,14 @@ export const useTaskStore = defineStore('tasks', () => {
if (!repository) return
try {
repository.save(tasks.value)
// 之前写入失败过,现在恢复了:把告警撤掉,避免一直吓唬用户。
// 读取失败(数据损坏)时 persistent 仍是 true,不会被这里误清。
if (!persistent.value) {
persistent.value = true
notice.value = null
}
} catch (error) {
persistent.value = false
notice.value = error instanceof Error ? error.message : '保存失败'
}
}
@@ -158,11 +170,35 @@ export const useTaskStore = defineStore('tasks', () => {
refreshSnapshots()
}
/**
* 覆盖式操作(导入 / 恢复快照)执行前,把当前数据留一份快照。
*
* 这两类操作会整份替换内存与落盘数据,一旦选错文件或选错快照,
* 用户原本的数据就没有退路了。先快照一份,配合「备份与恢复」面板,
* 把不可逆操作变成可逆。
*
* @returns 是否真的写入了快照(空看板或存储不可用时跳过)
*/
function snapshotBeforeOverwrite(): boolean {
if (!repository || !persistent.value || tasks.value.length === 0) return false
try {
repository.snapshot(tasks.value)
refreshSnapshots()
return true
} catch {
// 快照只是兜底,失败不应阻断主流程
return false
}
}
/** 从快照恢复;返回实际恢复的任务数 */
function restoreSnapshot(key: string): number {
if (!repository) return 0
const restored = repository.restoreSnapshot(key)
if (restored.length === 0) return 0
// 先读快照再备份当前数据:即使备份把这份快照挤出保留窗口,
// 待恢复的内容也已经拿在手里。
snapshotBeforeOverwrite()
tasks.value = sortByOrder(restored)
commit()
return restored.length
@@ -189,8 +225,7 @@ export const useTaskStore = defineStore('tasks', () => {
} catch (error) {
repository = createMemoryTaskRepository()
persistent.value = false
notice.value =
error instanceof Error ? error.message : '本地存储不可用,本次改动不会被保存'
notice.value = error instanceof Error ? error.message : '本地存储不可用,本次改动不会被保存'
}
}
@@ -338,10 +373,13 @@ export const useTaskStore = defineStore('tasks', () => {
/**
* 覆盖式导入(T14)。校验失败会抛错,此时现有数据保持不变。
* 校验通过后、覆盖之前会自动留一份快照,误导入可以回到导入前的状态。
* @returns 成功导入的任务数量
*/
function importJson(raw: string): number {
const imported = parseImportJson(raw)
// 先解析再备份:非法文件不应该产生任何副作用
snapshotBeforeOverwrite()
tasks.value = sortByOrder(imported)
commit()
return imported.length