feat(sync): 运动详情改为同步入库,详情页只读本地
按需回源是错的:点一次运动要等七个 Garmin 接口,网络好的时候慢, 网络差的时候直接超时(实测公网下 Network Error)。 - sync_data 顺带补齐缺详情的运动 - POST /api/garmin/sync-details 后台补齐存量,GET 查进度 - 详情页只读本地库;没有就提示去同步,不再回源 - 同步页新增「补齐运动详情」按钮,带进度 身体年龄:加入公开的阻尼系数 - 34 岁 VO₂max 46 原本算出 21 岁。不是算错,是方法本身会饱和: 人与人之间的 VO₂max 标准差约 7,而年龄每年只带来约 0.35 的衰减, 于是稍微能练的人都会撞到参考表最年轻一档。 - 按 50% 向实际年龄收拢,收敛范围 ±20 → ±12 岁,同一算例现在给 27 岁。 - 去掉「高于最年轻一档按 20 岁计」的硬地板,那是一道正好落在用户身上的悬崖。 - 界面同时显示未收拢的原始值,阻尼系数写进评分依据。 路由:为每个路径补无斜杠别名 - F7 写地址栏时去掉尾斜杠,于是 /daily/ 在地址栏是 /daily, 而那个地址匹配不到任何路由,刷新或分享就落到「找不到页面」。 布局:让页面结构上无法被撑宽 - 网格改用 minmax(min(210px,100%),1fr):裸的 minmax(210px,1fr) 允许 两列加起来超过窄屏宽度,第二张卡就被切掉在屏幕外。 - .ring-row 用 minmax(0,1fr),1fr 会以 min-content 兜底,一句长说明就能 把整行顶宽。 - .page-inner 加 overflow-x: clip。 - html/body 用 100dvh:手机浏览器把自己的地址栏盖在布局视口上, 100% 高的应用会把底部 Tab 栏顶到它们下面——对用户来说就是没有 Tab 栏。 测试:新增 122 项(设置 44、身体年龄 44、运动详情 42),全量 446 项通过。 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Link } from 'framework7-react';
|
||||
import { apiClient, ActivityDetail, errorMessage } from '../services/api';
|
||||
import Screen from '../components/Screen';
|
||||
import Chart from '../components/charts/Chart';
|
||||
@@ -195,13 +196,23 @@ function ActivityDetailPage({ id, f7route }: Props) {
|
||||
const [axis, setAxis] = useState<Axis>('time');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [needsSync, setNeedsSync] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
apiClient
|
||||
.getActivityDetail(activityId)
|
||||
.then((d) => { if (!cancelled) setDetail(d); })
|
||||
.catch((err) => { if (!cancelled) setError(errorMessage(err, '加载失败')); })
|
||||
.catch((err) => {
|
||||
if (cancelled) return;
|
||||
// Not yet synced is an ordinary state with an obvious next step, not
|
||||
// a failure to apologise for.
|
||||
if (err?.response?.status === 404 && err.response.data?.needsSync) {
|
||||
setNeedsSync(true);
|
||||
} else {
|
||||
setError(errorMessage(err, '加载失败'));
|
||||
}
|
||||
})
|
||||
.finally(() => { if (!cancelled) setLoading(false); });
|
||||
return () => { cancelled = true; };
|
||||
}, [activityId]);
|
||||
@@ -238,12 +249,24 @@ function ActivityDetailPage({ id, f7route }: Props) {
|
||||
if (loading) {
|
||||
return (
|
||||
<Screen title="运动详情" backLink>
|
||||
<p className="screen-note">正在从 Garmin 获取本次运动的完整数据…</p>
|
||||
<Skeleton count={4} />
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
if (needsSync) {
|
||||
return (
|
||||
<Screen title="运动详情" backLink>
|
||||
<div className="screen-empty">
|
||||
<p>这条运动的详细数据还没同步到本机。</p>
|
||||
<Link href="/sync/" className="button button-fill button-round">
|
||||
去同步
|
||||
</Link>
|
||||
</div>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !detail) {
|
||||
return (
|
||||
<Screen title="运动详情" backLink>
|
||||
|
||||
Reference in New Issue
Block a user