feat: 数据删除机制(purge 物理清空 + 报告页隐私区块 + 删后回同意门)+ 测试桩补 purge

This commit is contained in:
Jayway Liu
2026-10-04 07:42:47 +08:00
parent ade1f719e1
commit e022faf975
14 changed files with 330 additions and 14 deletions
+59 -2
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import type { EventStore } from '../../engine/eventStore';
import type { JevClient } from '../../services/jev';
import { BADGE_MAP } from './badges';
@@ -11,14 +11,33 @@ interface Props {
store: EventStore;
jev: JevClient;
onReplay: () => void;
/** 删除全部个人数据(ADR-0004 数据删除权):purge + 清空进度 + 回到同意门,由 App 编排 */
onPurge: () => Promise<void>;
}
/** Act 1 结束后的画像报告页:Jev 一次调用并行问 7 个维度 score */
export function ReportPage({ store, jev, onReplay }: Props) {
export function ReportPage({ store, jev, onReplay, onPurge }: Props) {
const [scores, setScores] = useState<DimensionScore[] | null>(null);
const [error, setError] = useState<string | null>(null);
const [retry, setRetry] = useState(0);
const [playerName, setPlayerName] = useState('');
const purgeDialogRef = useRef<HTMLDialogElement>(null);
const [purging, setPurging] = useState(false);
const [purgeError, setPurgeError] = useState<string | null>(null);
async function confirmPurge() {
setPurging(true);
setPurgeError(null);
try {
// 成功后 App 把应用切回同意门,本页随之卸载
await onPurge();
} catch (e) {
setPurgeError(e instanceof Error ? e.message : String(e));
purgeDialogRef.current?.close();
} finally {
setPurging(false);
}
}
useEffect(() => {
let cancelled = false;
@@ -107,6 +126,44 @@ export function ReportPage({ store, jev, onReplay }: Props) {
<button className="secondary" onClick={onReplay}>
重玩 Act 1(新轨迹,历史保留)
</button>
<section style={{ marginTop: 48, borderTop: '1px solid rgba(201,219,203,.2)', paddingTop: 24 }}>
<h2 style={{ fontSize: 18, color: '#f1eee3' }}>数据与隐私</h2>
<p className="hint" style={{ maxWidth: 560 }}>
本应用只在本机记录你的剧情选择、语音讲述转写与人格画像事件,用于人格评估与伴侣匹配推荐;数据不出设备,删除即彻底消失。
</p>
<button className="secondary" onClick={() => purgeDialogRef.current?.showModal()}>
删除全部数据
</button>
{purgeError && <p className="scene-error">删除失败:{purgeError}</p>}
<dialog
ref={purgeDialogRef}
aria-labelledby="purge-title"
style={{
background: 'rgba(13,25,23,.98)',
color: '#e6eae0',
border: '1px solid rgba(201,219,203,.34)',
borderRadius: 3,
padding: '24px 28px',
maxWidth: 480,
}}
>
<h3 id="purge-title" style={{ marginTop: 0 }}>
确定删除全部数据?
</h3>
<p style={{ lineHeight: 1.8 }}>
此操作不可恢复,所有剧情进度、人格画像、讲述内容将被永久删除,且不提供导出留档。
删除后应用将回到初始的告知与同意页面,如同首次安装。
</p>
<div style={{ display: 'flex', gap: 12, marginTop: 20, flexWrap: 'wrap' }}>
<button className="secondary" disabled={purging} onClick={confirmPurge}>
{purging ? '正在删除……' : '确认永久删除'}
</button>
<button className="secondary" disabled={purging} onClick={() => purgeDialogRef.current?.close()}>
取消
</button>
</div>
</dialog>
</section>
</div>
);
}