fix(data): 覆盖式导入与快照恢复前自动备份
- importJson / restoreSnapshot 覆盖前先写一份当前数据快照,误操作可回退 - 导入校验增加 id 唯一性检查,宽容读取路径保留首条 - 快照索引对同一时间点的重复写入去重 - 落盘失败时翻转 persistent,恢复后自动撤除告警 - 备份面板提示导入前数据已自动备份
This commit is contained in:
+41
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user