feat(ui): 设置页重做 + 同步页简化 + 评分依据页

需求 2.5 / 2.6 / 2.7 / 2.8 / 5.1 / 5.2 / 5.3

设置页
- 个人资料:身高/体重/出生日期/性别,另显示算出的 BMI 与年龄
- 单位:公制 / 英制
- 同步:自动同步开关、频率、历史范围
- 改动即存,不放「保存」按钮——每项都是单值且效果直观,
  而能保持脏状态的表单就是会悄悄丢编辑的表单
- 开关用原生 checkbox 只换绘制,键盘与读屏行为保持不变

/rating-basis/ 评分依据
- 11 项参考区间逐条列出边界与出处,包括「一般性参考,无权威标准」这种
  诚实的答案。判断从哪来必须能查到。

同步页
- 首要按钮「同步最新数据」,等几秒直接返回结果
- 次要按钮「同步历史」,按设置里的历史范围后台跑
- 状态压缩成三格:上次同步 / 自动同步 / 已同步天数
- 删掉原来四段说明文字,它们把两个动作埋在了下面

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
ericwyuan
2026-08-24 00:44:07 +08:00
parent b0e0799a97
commit 0c7fecc124
7 changed files with 714 additions and 266 deletions

View File

@@ -0,0 +1,73 @@
import { useEffect, useState } from 'react';
import { apiClient, errorMessage, RatingBasis } from '../services/api';
import Screen from '../components/Screen';
import Skeleton from '../components/Skeleton';
import './BodyAge.css';
/**
* Where every rating in the app comes from.
*
* A band that paints a number 偏低 is making a claim about the user's health.
* This screen is the receipt for that claim — one row per metric, with the
* source named, including the ones whose honest source is "general guidance,
* no authoritative standard".
*/
function RatingBasisPage() {
const [basis, setBasis] = useState<RatingBasis | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
apiClient
.getRatingBasis()
.then(setBasis)
.catch((err) => setError(errorMessage(err, '加载失败')))
.finally(() => setLoading(false));
}, []);
if (loading) return <Screen title="评分依据" backLink><Skeleton count={5} /></Screen>;
if (error || !basis) {
return (
<Screen title="评分依据" backLink>
<div className="screen-error">{error || '加载失败'}</div>
</Screen>
);
}
return (
<Screen title="评分依据" backLink>
<p className="ba-summary">{basis.note}</p>
<section className="sec">
<h3 className="sec-title"></h3>
<div className="ba-basis">
{basis.bands.map((b) => (
<div className="ba-basis-item" key={b.metric}>
<div className="ba-basis-name">{b.metric}</div>
<div className="ba-basis-detail rb-bands">{b.bands}</div>
<div className="ba-basis-source">{b.source}</div>
</div>
))}
</div>
</section>
<section className="sec">
<h3 className="sec-title">{basis.fitnessAge.title}</h3>
<p className="ba-summary">{basis.fitnessAge.summary}</p>
<div className="ba-basis">
{basis.fitnessAge.steps.map((s) => (
<div className="ba-basis-item" key={s.name}>
<div className="ba-basis-name">{s.name}</div>
<div className="ba-basis-detail">{s.detail}</div>
<div className="ba-basis-source">{s.source}</div>
</div>
))}
</div>
</section>
<p className="screen-disclaimer">{basis.fitnessAge.caveat}</p>
</Screen>
);
}
export default RatingBasisPage;