Add unified JSON backup and restore

This commit is contained in:
Codex committed 2026-09-17 13:39:42 +08:00
1 parent 5f4a193888
commit bbdfe97c4e
6 files changed
+79 -2

No files matched your search

+1 -1
View File
@@ -21,7 +21,7 @@ export default function ReviewClient() {
const saveDay = async () => { setStatus("正在保存…"); const r = await fetch("/api/study", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type: "day", date, durations, weeklyReview: review, nextFocus: focus }) }); const x = await r.json(); if (!r.ok) return setStatus(x.error); setStudies(v => ({ ...v, [date]: x.studyDay })); setStatus("学习记录已保存"); };
const saveMock = async () => { setStatus("正在保存模考…"); const r = await fetch("/api/study", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type: "mock", date, ...mock }) }); const x = await r.json(); if (!r.ok) return setStatus(x.error); setMocks(v => [...v.filter(m => m.examDate !== date), x.mockExam].sort((a, b) => a.examDate.localeCompare(b.examDate))); setStatus("模考记录已保存"); };
const exportData = () => { const rows = ["日期,词汇分钟,听力分钟,阅读分钟,输出分钟,复盘分钟,周复盘,下周重点", ...Object.values(studies).sort((a,b)=>a.planDate.localeCompare(b.planDate)).map(x => { const d = parse(x.durations); return [x.planDate, ...keys.map(k => d[k.key] || 0), x.weeklyReview, x.nextFocus].map(v => `"${String(v).replaceAll('"','""')}"`).join(","); }), "", "模考日期,听力,阅读,写作,翻译,总分,备注", ...mocks.map(x => [x.examDate,x.listening,x.reading,x.writing,x.translation,[x.listening,x.reading,x.writing,x.translation].reduce((n,v)=>n+(Number(v)||0),0),x.note].map(v => `"${String(v).replaceAll('"','""')}"`).join(","))]; const url = URL.createObjectURL(new Blob([rows.join("\n")], { type: "text/csv;charset=utf-8" })); const a = document.createElement("a"); a.href = url; a.download = "cet6-study-record.csv"; a.click(); URL.revokeObjectURL(url); };
return <main><div className="shell study-hub"><header><div><p className="eyebrow">STUDY HUB</p><h1>复盘与冲刺</h1><p className="sub">记录投入、复盘阶段成果,再把计划推进到下一天。</p></div><div className="header-actions"><button className="overview-link" onClick={exportData}><Download size={17} />导出 CSV</button><a className="overview-link" href="/"><ArrowLeft size={17} />返回打卡</a></div></header><p className="overview-status">{status}</p>
return <main><div className="shell study-hub"><header><div><p className="eyebrow">STUDY HUB</p><h1>复盘与冲刺</h1><p className="sub">记录投入、复盘阶段成果,再把计划推进到下一天。</p></div><div className="header-actions"><a className="overview-link" href="/backup">数据备份</a><a className="overview-link" href="/"><ArrowLeft size={17} />返回打卡</a></div></header><p className="overview-status">{status}</p>
<section className="hub-grid"><article className="hub-card plan-card"><p className="eyebrow">PLAN PULSE</p><h2>计划调整建议</h2><strong>{insight.missing} 项</strong><p>历史日期仍有未完成任务。若要在原计划结束前补齐,建议未来每天额外完成 <b>{insight.extra} 项</b>;优先补“复盘”和当天同类型任务。</p><small>这是顺延建议,不会自动改动原计划。</small></article><article className="hub-card"><p className="eyebrow">DAILY TIME</p><h2>今日用时</h2><label className="date-field">记录日期<input type="date" min="2026-09-13" max="2026-12-11" value={date} onChange={e => { setDate(e.target.value); const x = studies[e.target.value]; setDurations(parse(x?.durations)); setReview(x?.weeklyReview || ""); setFocus(x?.nextFocus || ""); }} /></label><div className="duration-grid">{keys.map(x => <label key={x.key}>{x.label}<input inputMode="numeric" value={durations[x.key] || ""} onChange={e => setDurations(v => ({ ...v, [x.key]: Number(e.target.value) || 0 }))} placeholder="分钟" /></label>)}</div><button className="primary-action" onClick={saveDay}><Timer size={17} />保存用时</button></article></section>
<section className="hub-grid lower"><article className="hub-card"><p className="eyebrow">WEEKLY REVIEW · {insight.week}</p><h2>本周复盘</h2><label>本周哪里最有效?哪里卡住?<textarea value={review} onChange={e => setReview(e.target.value)} placeholder="例如:长篇匹配定位更稳,但听力仍容易漏掉转折词。" /></label><label>下周只抓的一件事<textarea value={focus} onChange={e => setFocus(e.target.value)} placeholder="写一条具体、可执行的重点。" /></label><button className="primary-action" onClick={saveDay}><Save size={17} />保存复盘</button></article><article className="hub-card"><p className="eyebrow">MOCK EXAM</p><h2>模考记录 <span className="score-total">{total || "—"}</span></h2><div className="duration-grid score-grid">{(["listening","reading","writing","translation"] as const).map(k => <label key={k}>{({listening:"听力",reading:"阅读",writing:"写作",translation:"翻译"}[k])}<input inputMode="decimal" value={mock[k]} onChange={e => setMock(v => ({ ...v, [k]: e.target.value }))} placeholder="分数" /></label>)}</div><label>本次模考观察<textarea value={mock.note} onChange={e => setMock(v => ({ ...v, note: e.target.value }))} placeholder="写下失分原因、时间分配或下一次要验证的策略。" /></label><button className="primary-action" onClick={saveMock}><Trophy size={17} />保存本次模考</button><div className="mock-history">{mocks.length ? mocks.slice(-4).reverse().map(x => <p key={x.examDate}><b>{x.examDate}</b><span>{[x.listening,x.reading,x.writing,x.translation].reduce((n,v)=>n+(Number(v)||0),0)} 分</span></p>) : <p>还没有模考记录。</p>}</div></article></section>
</div></main>;