fix(sync): 限流拦截不再把'上次同步'刷成现在 + 设置页新增立即同步按钮

- _set_sync_status 支持保留旧 last_sync_time;两处限流拦截(start_sync /
  sync_data)改为传旧值——被拒的同步零请求发生,不能把 UI 的'上次同步'
  拨快成'刚刚',掩盖数据早已停更的事实
- SettingsPage 同步区块新增'立即同步'行:走 /garmin/sync-latest(最近2天,
  秒级阻塞返回),成功/限流/未绑定三种状态都有明确反馈;限流中后端零请求
  拦截并回传恢复倒计时文案
This commit is contained in:
ericwyuan
2026-09-02 20:14:11 +08:00
parent 2d9a2be185
commit 224144915c
2 changed files with 71 additions and 3 deletions

View File

@@ -29,6 +29,10 @@ function SettingsPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [saved, setSaved] = useState('');
/** A sync is running right now (blocks the button). */
const [syncing, setSyncing] = useState(false);
/** A Garmin account is bound; sync-latest refuses to run without one. */
const [hasToken, setHasToken] = useState(false);
useEffect(() => {
Promise.all([apiClient.getSettings(), apiClient.getSettingsOptions()])
@@ -36,6 +40,8 @@ function SettingsPage() {
.catch((err) => setError(errorMessage(err, '加载设置失败')))
.finally(() => setLoading(false));
apiClient.getGarminAuthStatus().then(setHasToken).catch(() => setHasToken(false));
if (FEATURES.ai) {
apiClient.getModels().then(setModels).catch(() => setModels([]));
}
@@ -64,6 +70,30 @@ function SettingsPage() {
}
};
/** 立即同步最近几天(秒级阻塞返回,不用进同步页)。
/garmin/sync-latest 只做最近 1-7 天的增量拉取,速度足够直接等待;
正在限流冷却时后端零请求直接拦截并返回恢复倒计时。 */
const syncNow = async () => {
setError('');
setSaved('');
setSyncing(true);
try {
const res = await apiClient.syncLatest(2);
if (res.status === 'rate_limited') {
setError(res.message || 'Garmin 正在限流,请耐心等待自动恢复。');
} else if (res.status === 'success') {
setSaved(res.message || '已同步最近数据');
window.setTimeout(() => setSaved(''), 3200);
} else {
setError(res.message || '同步没有完成,去同步页看具体原因。');
}
} catch (err: any) {
setError(errorMessage(err, '同步失败'));
} finally {
setSyncing(false);
}
};
const pick = (
title: string,
values: Array<string | number>,
@@ -224,6 +254,24 @@ function SettingsPage() {
<span className="set-chevron" aria-hidden="true"></span>
</button>
<button
className="set-row"
onClick={syncNow}
disabled={syncing || !hasToken}
>
<span className="set-label">
<span className="set-sub">
{!hasToken
? '还没绑定 Garmin 账号,先在下面的「数据同步」里绑定'
: '不等自动同步,马上拉取最近 2 天的数据'}
</span>
</span>
<span className="set-value">
{syncing ? '正在同步…' : ''}
</span>
</button>
<Link href="/sync/" className="set-row">
<span className="set-label">