diff --git a/web/app/globals.css b/web/app/globals.css index 6ddc4d5..d087814 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -1065,10 +1065,21 @@ button.drawer-plan__time:hover, button.drawer-plan__time:focus-visible { backgro .chart-legend i.target { border-style: dashed; background: var(--yellow); } .weight-history { margin-top: 22px; padding: 20px; } .history-table { margin-top: 14px; border: 2px solid var(--ink); } -.history-table > div { display: grid; grid-template-columns: 1fr 100px 120px; align-items: center; padding: 10px 12px; border-bottom: 1px solid rgba(48,36,71,.25); } +.history-table > div { display: grid; grid-template-columns: 1fr 100px 160px; align-items: center; padding: 10px 12px; border-bottom: 1px solid rgba(48,36,71,.25); } .history-table > div:last-child { border-bottom: 0; } .history-table b { font-family: Consolas, monospace; } .history-table small { color: var(--muted); text-align: right; } +.history-table__status { display: flex; align-items: center; justify-content: flex-end; gap: 8px; } +.history-table__status em { font-style: normal; } +.weight-history-edit { min-height: 30px; padding: 4px 9px; border: 2px solid var(--ink); background: var(--yellow); color: var(--ink); font-size: 10px; font-weight: 900; box-shadow: 2px 2px 0 var(--ink); } +.weight-history-editor { grid-column: 2 / -1; display: flex; align-items: center; justify-content: flex-end; gap: 7px; } +.weight-history-editor > div { display: flex; align-items: center; border: 2px solid var(--ink); background: white; box-shadow: 2px 2px 0 var(--ink); } +.weight-history-editor input { width: 78px; min-height: 32px; padding: 4px 6px; border: 0; font-family: Consolas,monospace; font-weight: 900; } +.weight-history-editor input:focus, .weight-history-editor input:focus-visible { outline: none; box-shadow: none; } +.weight-history-editor span { padding-right: 7px; color: var(--muted); font-size: 10px; } +.weight-history-editor button { min-height: 34px; padding: 5px 9px; border: 2px solid var(--ink); background: white; font-size: 10px; font-weight: 900; box-shadow: 2px 2px 0 var(--ink); } +.weight-history-editor button.is-save { background: var(--pink); color: white; } +.weight-history-editor button:disabled { opacity: .45; box-shadow: none; } .pagination { display: flex; align-items: center; justify-content: center; gap: 16px; margin-top: 14px; } .pagination button { min-height: 34px; padding: 5px 10px; border: 2px solid var(--ink); background: white; font-weight: 900; } .pagination button:disabled { opacity: .4; } @@ -1760,6 +1771,8 @@ button.drawer-plan__time:hover, button.drawer-plan__time:focus-visible { backgro .weight-detail .section-heading { align-items: flex-start; flex-direction: column; } .history-table > div { grid-template-columns: 1fr auto; } .history-table small { grid-column: 1 / -1; text-align: left; } + .history-table__status { justify-content: flex-start; } + .weight-history-editor { grid-column: 1 / -1; justify-content: flex-start; margin-top: 7px; } .completion-stage { align-items: flex-end; padding: 14px 12px 88px; } .settlement-card { width: 100%; max-height: calc(100vh - 112px); padding: 18px 16px 15px; overflow-y: auto; box-shadow: 6px 6px 0 var(--ink); } .completion-clear > span { width: 48px; height: 48px; font-size: 25px; } diff --git a/web/app/page.tsx b/web/app/page.tsx index 4666502..31c0a7c 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -948,10 +948,13 @@ function GrowthView({ attributes, skillbooks, goals }: { attributes: Attribute[] ); } -function WeightView({ entries, target, onTarget }: { entries: WeightEntry[]; target?: number; onTarget: (value: number) => void }) { +function WeightView({ entries, target, onTarget, onCorrectToday }: { entries: WeightEntry[]; target?: number; onTarget: (value: number) => void; onCorrectToday: (value: number) => void }) { const [range, setRange] = useState<7 | 30 | 90 | 365>(30); const [page, setPage] = useState(1); const [targetValue, setTargetValue] = useState(target?.toFixed(1) ?? ""); + const [editingToday, setEditingToday] = useState(false); + const [todayValue, setTodayValue] = useState(""); + const todayKey = currentLocalDate(); const averages = movingAverage(entries); const latest = entries.at(-1); const previous = entries.at(-2); @@ -963,12 +966,23 @@ function WeightView({ entries, target, onTarget }: { entries: WeightEntry[]; tar const history = [...entries].reverse(); const pageCount = Math.max(1, Math.ceil(history.length / pageSize)); const pageEntries = history.slice((page - 1) * pageSize, page * pageSize); + const parsedTodayValue = Number(todayValue); + const todayValueValid = parsedTodayValue >= 20 && parsedTodayValue <= 300; + const submitTodayCorrection = (event: FormEvent) => { + event.preventDefault(); + if (!todayValueValid) return; + onCorrectToday(Math.round(parsedTodayValue * 10) / 10); + setEditingToday(false); + }; return (
BODY TRACK

体重趋势

只记录事实与趋势,不提供健康判断。

最新体重{latest ? latest.value.toFixed(1) : "—"} kg{latest && previous ? `较上次 ${(latest.value - previous.value).toFixed(1)} kg` : "暂无历史记录"}
7 日移动平均{averages.at(-1)?.toFixed(1) ?? "—"} kg{entries.length > 1 ? `基于 ${Math.min(entries.length, 7)} 条有效记录` : "至少需要两条记录"}
目标体重
{ event.preventDefault(); const parsed = Number(targetValue); if (parsed >= 20 && parsed <= 300) onTarget(parsed); }}> setTargetValue(event.target.value)} />kg
{target ? `目标线 ${target.toFixed(1)} kg` : "暂未设置目标"}
{range} DAYS

每日值与移动平均

{([7, 30, 90, 365] as const).map((valueOption) => )}
每日值7日均值目标线{rangeEntries.length > chartEntries.length && {rangeEntries.length} 条记录已压缩为 {chartEntries.length} 个关键点}
-
HISTORY

历史记录

共 {entries.length} 条 · 每页 50 条
{entries.length === 0 ?
暂无体重记录
: <>
{pageEntries.map((entry, index) =>
{entry.date} · {entry.label}{entry.value.toFixed(1)}{page === 1 && index === 0 ? "当前有效值" : "已同步"}
)}
第 {page} / {pageCount} 页
}
+
HISTORY

历史记录

共 {entries.length} 条 · 每页 50 条
{entries.length === 0 ?
暂无体重记录
: <>
{pageEntries.map((entry) => { + const isToday = entry.date === todayKey; + return
{entry.date} · {entry.label}{isToday && editingToday ?
setTodayValue(event.target.value)} autoFocus />kg
: <>{entry.value.toFixed(1)}{isToday ? <>当前有效值 : "已同步"}}
; + })}
第 {page} / {pageCount} 页
}
); } @@ -1431,7 +1445,7 @@ export default function Home() { {tab === "calendar" && settlement.localDate)} initialMode={calendarInitialMode} content={calendarContent} onEditPlan={(id) => setPlanEditor(weekUp.state.plans.find((plan) => plan.id === id) ?? null)} />} {tab === "action-config" && project.source === "week-up" && project.archivedAt === undefined)} courses={weekUp.state.learningMoreCourses} courseProjects={weekUp.state.projects.filter((project) => project.source === "learning-more" && project.archivedAt === undefined)} onNewAttribute={() => setAttributeEditor("new")} onEditAttribute={(attribute) => setAttributeEditor(weekUp.state.attributes.find((item) => item.id === attribute.id) ?? null)} onNewProject={() => setProjectEditor("new")} onEditProject={setProjectEditor} onConfigureCourse={setProjectEditor} onCreateCategory={(name) => { void weekUp.dispatch({ type: "attribute-category.create", name }); }} onRenameCategory={(id, name) => { void weekUp.dispatch({ type: "attribute-category.rename", id, name }); }} onDeleteCategory={(id) => { void weekUp.dispatch({ type: "attribute-category.delete", id }); }} onCreateProjectCategory={(name, color) => { void weekUp.dispatch({ type: "project-category.create", name, color }); }} onRenameProjectCategory={(id, name, color) => { void weekUp.dispatch({ type: "project-category.rename", id, name, color }); }} onDeleteProjectCategory={(id) => { void weekUp.dispatch({ type: "project-category.delete", id }); }} />} {tab === "growth" && } - {tab === "weight" && void weekUp.dispatch({ type: "weight.target", valueKg })} />} + {tab === "weight" && void weekUp.dispatch({ type: "weight.target", valueKg })} onCorrectToday={addWeight} />} diff --git a/web/tests/demo-source.test.mjs b/web/tests/demo-source.test.mjs index 10bf0bd..c894091 100644 --- a/web/tests/demo-source.test.mjs +++ b/web/tests/demo-source.test.mjs @@ -213,6 +213,21 @@ test("offers one visually integrated quick weight entry until today is recorded" assert.match(css, /\.weight-form--quick \.pixel-button \{[^}]*min-height: 38px/); }); +test("allows an inline correction only for today's weight record", async () => { + const [page, css] = await Promise.all([ + readFile(new URL("../app/page.tsx", import.meta.url), "utf8"), + readFile(new URL("../app/globals.css", import.meta.url), "utf8"), + ]); + const weightView = page.slice(page.indexOf("function WeightView"), page.indexOf("function RewardAttributeTiles")); + assert.match(weightView, /const todayKey = currentLocalDate\(\)/); + assert.match(weightView, /const isToday = entry\.date === todayKey/); + assert.match(weightView, /aria-label="修正今日体重"/); + assert.match(weightView, /onCorrectToday\(Math\.round\(parsedTodayValue \* 10\) \/ 10\)/); + assert.match(page, /onCorrectToday=\{addWeight\}/); + assert.match(css, /\.weight-history-editor \{/); + assert.match(css, /\.weight-history-edit \{/); +}); + test("keeps unscheduled plans outside time slots and uses one clickable time ticket", async () => { const [page, css] = await Promise.all([ readFile(new URL("../app/page.tsx", import.meta.url), "utf8"),