Add historical check-in card sharing

This commit is contained in:
Codex committed 2026-09-17 13:25:36 +08:00
1 parent a6262126c5
commit 5f4a193888
2 files changed
+2 -1

No files matched your search

+1
View File
@@ -76,6 +76,7 @@ export default function CheckinClient() {
<section className="smart-tip" aria-live="polite"><div><p className="eyebrow">TODAY'S NOTE</p><h2>今天怎么推进</h2></div><p>{smartTip}</p></section>
<section className="stats"><Stat icon={<CheckCircle2 />} value={Math.round(finished / total * 100) + "%"} label="总体完成度" detail={finished + " / " + total + " 项任务"} /><Stat icon={<CalendarDays />} value={complete + " 天"} label="完整完成" detail="目标共 90 天" /><Stat icon={<CheckCircle2 />} value={day.phase} label="当前阶段" detail="12 月 12 日笔试" /><div className="countdown"><span><Timer size={19} /></span><p>距离六级笔试</p><strong>{remaining.days}<em>天</em> {remaining.hours}<em>时</em> {remaining.minutes}<em>分</em></strong><small>2026 年 12 月 12 日 15:00</small></div></section>
<section className="layout"><article className="card"><div className="hero"><button className="round" aria-label="前一天" onClick={() => setSelected(n => Math.max(0, n - 1))} disabled={!selected}><ChevronLeft /></button><div><p>{day.phase} · {day.week}</p><h2>{fmt(day.date)}</h2><small>{isFuture ? "未来日期,暂不可打卡" : isMakeup ? "补打卡 · 已完成项目不可取消" : "今日打卡"} · 已完成 {done.length} / {day.tasks.length} 项</small></div><button className="round" aria-label="后一天" onClick={() => setSelected(n => Math.min(89, n + 1))} disabled={selected === 89}><ChevronRight /></button><Progress value={done.length / day.tasks.length * 100} /></div><div className="body"><div className="task-toolbar"><span>{isToday ? "正在处理今天的任务" : "正在查看 " + fmt(day.date)}</span>{!isToday && <button className="today-button" onClick={() => { setSelected(todayIndex); setMonth(new Date(plan[todayIndex].date + "T00:00:00Z").getUTCMonth()); }}>回到今天</button>}</div><div className="tasks">{day.tasks.map(t => { const locked = isFuture || (isMakeup && done.includes(t.key)); return <label className={(done.includes(t.key) ? "task done " : "task ") + (locked ? "locked" : "")} key={t.key}><Checkbox disabled={locked} checked={done.includes(t.key)} onCheckedChange={v => toggle(t.key, v === true)} /><b>{t.label}</b><span>{t.text}</span>{isMakeup && done.includes(t.key) && <small className="locked-note">已完成,已锁定</small>}</label>; })}</div>{isToday && done.length === day.tasks.length && <section className="completion-card"><div><p className="eyebrow">TODAY COMPLETE</p><h3>今天的任务已全部完成</h3><p>生成一张不含复盘内容的打卡卡片,记录日期、任务清单、阶段与倒计时。</p></div><button className="share-card-button" onClick={() => void shareCheckinCard()}><Share2 size={18} />生成并分享打卡卡片</button></section>}<div className="review"><p>今日状态</p><div>{["专注", "平稳", "有点累", "需要调整"].map(m => <button disabled={isFuture} className={record.mood === m ? "pill active" : "pill"} key={m} onClick={() => void save({ ...record, mood: record.mood === m ? "" : m })}>{m}</button>)}</div><label>一句复盘(可选)<textarea disabled={isFuture} value={record.note} onChange={e => setRecords(x => ({ ...x, [day.date]: { ...record, note: e.target.value } }))} onBlur={() => void save(record)} placeholder={isFuture ? "未到此日期,暂不能修改。" : "今天的收获、卡住的地方,或明天要调整的一点。"} /></label></div></div></article><aside><section className="calendar side-calendar"><div className="calendar-head"><div><p className="eyebrow">CHECK-IN</p><h2>打卡日历</h2></div><div className="month-tabs">{[8, 9, 10, 11].map(m => <button aria-label={`查看${m + 1}月`} className={month === m ? "active" : ""} onClick={() => setMonth(m)} key={m}>{m + 1}月</button>)}</div></div><div className="weekdays">{["日", "一", "二", "三", "四", "五", "六"].map(x => <span key={x}>{x}</span>)}</div><div className="calendar-grid">{blanks.map((_, i) => <span key={"e" + i} />)}{dates.map(date => { const idx = plan.findIndex(p => p.date === date), p = plan[idx], n = parseTasks(records[date]?.doneTasks).length, full = !!p && n === p.tasks.length, partial = !!p && n > 0 && !full, makeup = records[date]?.checkinType === "makeup"; return <button disabled={!p} key={date} onClick={() => { if (p) { setSelected(idx); setMonth(new Date(date + "T00:00:00Z").getUTCMonth()); } }} className={"calendar-day " + (idx === selected ? "chosen " : "") + (full ? "full " : partial ? "partial " : "") + (makeup ? "makeup " : "") + (p ? "" : "outside")}><b>{Number(date.slice(-2))}</b>{p && <small>{full ? makeup ? "补打卡" : "完成" : partial ? n + "/" + p.tasks.length : date > today ? "未到" : "待打卡"}</small>}</button>; })}</div><p className="calendar-note"><i className="full-dot" />完成 <i className="partial-dot" />进行中 <i className="makeup-dot" />补打卡</p></section><div className="dark"><p>{isFuture ? "日期限制" : isMakeup ? "补打卡" : "当前阶段"}</p><h2>{isFuture ? "暂未开放" : isMakeup ? "补做未完成项目" : day.phase}</h2><p>{isFuture ? "当天到来后即可正常打卡。" : isMakeup ? "已有完成项已锁定,只能勾选此前未完成的项目。" : "每天稳步推进,答案会慢慢变得清楚。"}</p></div><div className="side"><b>90 天阶段</b>{["Ⅰ 恢复期 · 9/13–10/5", "Ⅱ 专项期 · 10/6–11/2", "Ⅲ 真题期 · 11/3–11/29", "Ⅳ 冲刺期 · 11/30–12/11"].map(x => <p key={x}>{x}</p>)}</div></aside></section>
{!isToday && !isFuture && done.length === day.tasks.length && <section className="history-share-float"><div><p className="eyebrow">CHECK-IN ARCHIVE</p><b>{fmt(day.date)} 已完整完成</b><span>随时生成分享卡片</span></div><button className="share-card-button" onClick={() => void shareCheckinCard()}><Share2 size={18} />分享这次打卡</button></section>}
</div></main>;
}
+1 -1
View File
@@ -174,7 +174,7 @@ h1{letter-spacing:-.04em;font-size:clamp(2rem,4vw,2.85rem)}
.task-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:13px;color:#64748b;font-size:.84rem}.today-button{border:1px solid #bfd3fb;background:#f7faff;color:#1d4ed8;border-radius:999px;padding:7px 11px;font:inherit;font-weight:700;cursor:pointer;transition:.16s background,.16s transform}.today-button:hover{background:#e7f0ff;transform:translateY(-1px)}
.task:hover{transform:translateX(2px);border-color:#94bafc}.task.done{background:#f2f7ff}
.task b{background:#e7efff;color:#315fd5}.task.done b{background:#cfe0ff}
.completion-card{display:flex;align-items:center;justify-content:space-between;gap:22px;margin-top:18px;padding:19px 20px;border:1px solid #bad3ff;border-radius:18px;background:linear-gradient(125deg,#edf5ff,#fff)}.completion-card h3{margin:4px 0 7px;font-size:1.12rem;letter-spacing:-.02em}.completion-card>div>p:last-child{margin:0;color:#64748b;font-size:.88rem;line-height:1.55}.share-card-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;flex:none;border:0;border-radius:12px;background:#2563eb;color:#fff;padding:12px 15px;font:inherit;font-weight:700;cursor:pointer;box-shadow:0 9px 18px #bfdbfe;transition:.16s transform,.16s background}.share-card-button:hover{background:#1d4ed8;transform:translateY(-1px)}
.completion-card{display:flex;align-items:center;justify-content:space-between;gap:22px;margin-top:18px;padding:21px 22px;border:1px solid #315fd5;border-radius:18px;background:linear-gradient(120deg,#102b63,#2563d8 68%,#4d8cf0);box-shadow:0 14px 28px #bdd6ff}.completion-card .eyebrow{color:#c9ddff}.completion-card h3{margin:4px 0 7px;color:#fff;font-size:1.18rem;letter-spacing:-.02em}.completion-card>div>p:last-child{margin:0;color:#dbeafe;font-size:.9rem;line-height:1.55}.share-card-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;flex:none;border:0;border-radius:12px;background:#fff;color:#1d4ed8;padding:13px 16px;font:inherit;font-weight:800;cursor:pointer;box-shadow:0 9px 18px #102b6355;transition:.16s transform,.16s background}.share-card-button:hover{background:#edf5ff;transform:translateY(-1px)}.history-share-float{position:fixed;right:26px;bottom:24px;z-index:20;display:flex;align-items:center;gap:16px;max-width:calc(100vw - 32px);padding:14px 16px 14px 18px;border:1px solid #315fd5;border-radius:17px;background:linear-gradient(120deg,#102b63,#2563d8);color:#fff;box-shadow:0 18px 38px #102b6377}.history-share-float .eyebrow{margin:0 0 4px;color:#c9ddff}.history-share-float b,.history-share-float span{display:block}.history-share-float b{font-size:.94rem}.history-share-float span{margin-top:3px;color:#dbeafe;font-size:.78rem}
.review{margin-top:26px}.review textarea{background:#fbfdff;border-color:#d5e0f0}.pill{transition:.15s transform}.pill:hover:not(:disabled){transform:translateY(-1px)}
aside{display:grid;gap:14px}.side-calendar{margin:0;padding:18px;border-radius:22px;border-color:#d9e6ff;box-shadow:0 16px 36px #bfd5fb77}.calendar-head{gap:10px}.calendar-head h2{font-size:1.22rem;letter-spacing:-.02em}.month-tabs{gap:3px}.month-tabs button{padding:6px 8px;font-size:.76rem;min-width:36px}.weekdays{margin:16px 0 7px;font-size:.7rem;gap:4px}.calendar-grid{gap:4px}.calendar-day{min-height:47px;padding:5px 4px;border-radius:10px;font-size:.8rem}.calendar-day small{font-size:.56rem;line-height:1.1}.calendar-day.chosen{outline-width:2px}.calendar-note{font-size:.7rem;line-height:1.9}.calendar-note i{width:7px;height:7px;margin:0 3px 0 8px}.dark,.side{margin:0;border-radius:20px}.dark{padding:22px;background:linear-gradient(145deg,#102555,#1d4ed8)}.dark h2{font-size:1.45rem;letter-spacing:-.025em}.side{padding:18px;border-color:#d9e6ff;box-shadow:0 10px 25px #dbeafe66}.side p{margin:12px 0;font-size:.8rem}
@media(max-width:980px){.layout{grid-template-columns:minmax(0,1fr) 300px}.calendar-day{min-height:43px}.calendar-day small{font-size:.51rem}.month-tabs button{padding:5px 6px;min-width:31px}}