Skip to content

Commit 5969f55

Browse files
committed
feat(backup): 清偿TD-G——备份/恢复UI面板(REQ-107可达化)
1 parent 1a88581 commit 5969f55

2 files changed

Lines changed: 104 additions & 0 deletions

File tree

‎app/src/components/BackupPanel.tsx‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* BackupPanel — 数据备份/恢复面板(TD-2026-08-20-G 清偿:REQ-107 TRUST-1
3+
* backup_create/backup_restore 后端能力此前全应用无 UI,数据备份不可达)。
4+
*
5+
* @ai-context: 创建备份(data_dir → backups/backup-<时间>.zip,含 SQLite+图+音频)
6+
* + 从备份 zip 恢复(tauri-plugin-dialog 文件选择 + 显式确认——
7+
* 恢复覆盖当前数据,entropy.db 改名 .pre-restore 兜底,完成后提示重启)。
8+
* @ai-context: 备份目录即应用数据目录内,提示用户可另行拷贝到外部存储(TRUST-1
9+
* 本地优先:数据不出本机,备份也仅在本机卷)。
10+
*/
11+
import { useState } from "react";
12+
import { invoke } from "@tauri-apps/api/core";
13+
import { open } from "@tauri-apps/plugin-dialog";
14+
15+
interface BackupSummary {
16+
archivePath: string;
17+
fileCount: number;
18+
totalBytes: number;
19+
}
20+
21+
/** 字节 → 可读大小(纯函数)。 */
22+
function fmtBytes(b: number): string {
23+
if (b < 1024) return `${b} B`;
24+
if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`;
25+
if (b < 1024 * 1024 * 1024) return `${(b / 1024 / 1024).toFixed(1)} MB`;
26+
return `${(b / 1024 / 1024 / 1024).toFixed(2)} GB`;
27+
}
28+
29+
export default function BackupPanel() {
30+
const [busy, setBusy] = useState(false);
31+
const [error, setError] = useState("");
32+
const [info, setInfo] = useState("");
33+
34+
const createBackup = async () => {
35+
setBusy(true);
36+
setError("");
37+
setInfo("");
38+
try {
39+
const s = await invoke<BackupSummary>("backup_create");
40+
setInfo(`备份完成:${s.fileCount} 个文件 · ${fmtBytes(s.totalBytes)} · ${s.archivePath}`);
41+
} catch (e) {
42+
setError(`备份失败: ${e}`);
43+
} finally {
44+
setBusy(false);
45+
}
46+
};
47+
48+
const restore = async () => {
49+
// 文件选择(zip)
50+
const selected = await open({ multiple: false, filters: [{ name: "备份文件", extensions: ["zip"] }] });
51+
if (typeof selected !== "string" || !selected) return;
52+
// 显式确认(恢复覆盖当前数据——TRUST-1 高影响操作)
53+
if (!window.confirm("恢复将覆盖当前全部数据(现有数据库改名 .pre-restore 兜底)。\n继续?")) {
54+
return;
55+
}
56+
setBusy(true);
57+
setError("");
58+
setInfo("");
59+
try {
60+
const n = await invoke<number>("backup_restore", { archivePath: selected });
61+
setInfo(`恢复完成(${n} 个文件)。请重启应用使数据生效。`);
62+
} catch (e) {
63+
setError(`恢复失败: ${e}`);
64+
} finally {
65+
setBusy(false);
66+
}
67+
};
68+
69+
return (
70+
<div style={{ marginTop: 10 }}>
71+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }}>
72+
<span style={{ fontSize: 12, fontWeight: 600 }}>🗄 数据备份</span>
73+
<button
74+
onClick={() => void createBackup()}
75+
disabled={busy}
76+
style={{ padding: "3px 10px", fontSize: 11, border: "1px solid #0d9488", borderRadius: 6, background: "#f0fdfa", color: "#0f766e", cursor: busy ? "wait" : "pointer" }}
77+
title="备份 SQLite + 会话图片 + 会话音频为 zip"
78+
>
79+
{busy ? "处理中…" : "创建备份"}
80+
</button>
81+
<button
82+
onClick={() => void restore()}
83+
disabled={busy}
84+
style={{ padding: "3px 10px", fontSize: 11, border: "1px solid #fca5a5", borderRadius: 6, background: "#fff", color: "#dc2626", cursor: busy ? "wait" : "pointer" }}
85+
title="从备份 zip 恢复(覆盖当前数据,需重启生效)"
86+
>
87+
从备份恢复…
88+
</button>
89+
</div>
90+
{error && <div style={{ fontSize: 11, color: "#dc2626" }}>{error}</div>}
91+
{!error && info && <div style={{ fontSize: 11, color: "#0f766e" }}>{info}</div>}
92+
<div style={{ fontSize: 10, color: "#9ca3af" }}>
93+
备份保存在应用数据目录 backups/(本地优先:数据不出本机);如需异地保存请自行拷贝备份文件
94+
</div>
95+
</div>
96+
);
97+
}

‎app/src/pages/ClassroomPage.tsx‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { OcrDeviceSetting } from "../components/OcrDeviceSetting";
1919
import ModelDiskPanel from "../components/ModelDiskPanel";
2020
// TD-2026-08-20-H 清偿:音频落盘状态与清理入口
2121
import AudioStoragePanel from "../components/AudioStoragePanel";
22+
// TD-2026-08-20-G 清偿:数据备份/恢复(REQ-107 TRUST-1)
23+
import BackupPanel from "../components/BackupPanel";
2224
// v0.7.0 M1(REQ-101):音频预处理链开关(CER 微基准定默认后的用户通道)
2325
import { AudioPreprocSetting } from "../components/AudioPreprocSetting";
2426
// 2026-08 A2:实时音频电平条(VU 表——试听自检实时化)
@@ -534,6 +536,11 @@ export default function ClassroomPage({ onOpenSessions }: { onOpenSessions?: (se
534536
<AudioStoragePanel />
535537
</div>
536538

539+
{/* TD-2026-08-20-G 清偿:数据备份/恢复(REQ-107 TRUST-1 能力可达化) */}
540+
<div style={panel}>
541+
<BackupPanel />
542+
</div>
543+
537544
{/* 词表管理(v0.4.0 M5:REQ-040:热词/替换词闭环 + 课件预热) */}
538545
<div style={panel}>
539546
<VocabManager />

0 commit comments

Comments
 (0)