refactor(sync): 手动同步与自动同步彻底分开

「历史范围」原本放在设置页,却只对同步页的一个按钮起作用;而同步页最
显眼的主按钮「同步最新数据」写死 2 天,根本不看这个设置。选了「全部
历史」再点主按钮,表现就是应用无视你 —— 这正是反复出现的「只同步下来
两天」。

现在两条链路各管各的:

* 自动同步:只在设置页配置(开关 + 频率),窗口固定 SYNC_DAYS,不再
  读 history_days。措辞也改成「拉取最近几天」,不再暗示会补历史。
* 手动同步:范围就在同步页当场选,紧挨着用它的按钮,并标出每个范围的
  实际代价(自上次同步 / 7 天 / … / 全部历史约 730 天、20-40 分钟)。
  两个按钮合成一个「开始同步」,写死 2 天的那个删掉。

history_days 保留为「上次手动选的范围」,只有同步页读它;默认值改成
-1(自上次同步),对日常使用是正确的起点。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ericwyuan
2026-09-01 09:20:52 +08:00
parent 4d55fb4776
commit 526ece7d14
13 changed files with 156 additions and 116 deletions

View File

@@ -22,12 +22,6 @@ const intervalLabel = (minutes: number) =>
: minutes === 1440 ? '每天一次'
: `${minutes / 60} 小时`;
const historyLabel = (days: number) =>
days === -1 ? '自上次同步'
: days === 0 ? '全部历史'
: days >= 365 ? `${days / 365}`
: `${days}`;
function SettingsPage() {
const [settings, setSettings] = useState<UserSettings | null>(null);
const [options, setOptions] = useState<SettingsOptions | null>(null);
@@ -204,7 +198,9 @@ function SettingsPage() {
<label className="set-row">
<span className="set-label">
<span className="set-sub"></span>
<span className="set-sub">
</span>
</span>
<span className="set-switch">
<input
@@ -228,23 +224,12 @@ function SettingsPage() {
<span className="set-chevron" aria-hidden="true"></span>
</button>
<button
className="set-row"
onClick={() => options && pick('历史范围', options.historyDays,
historyLabel, s.historyDays,
(v) => save({ historyDays: v }))}
>
<span className="set-label">
<span className="set-sub"></span>
</span>
<span className="set-value">{historyLabel(s.historyDays)}</span>
<span className="set-chevron" aria-hidden="true"></span>
</button>
<Link href="/sync/" className="set-row">
<span className="set-label"></span>
<span className="set-value"> · </span>
<span className="set-label">
<span className="set-sub"></span>
</span>
<span className="set-value"></span>
<span className="set-chevron" aria-hidden="true"></span>
</Link>
</div>

View File

@@ -1,7 +1,8 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { f7 } from 'framework7-react';
import {
apiClient, AutoSyncStatus, DetailSyncStatus, errorMessage, GarminLoginStatus,
parseUtc, SyncStatus, UserSettings,
parseUtc, SettingsOptions, SyncStatus,
} from '../services/api';
import Screen from '../components/Screen';
import './DataSync.css';
@@ -19,12 +20,27 @@ const when = (value: string | null | undefined) => {
};
const historyLabel = (days: number) =>
days === 0 ? '全部历史' : days >= 365 ? `${days / 365}` : `${days}`;
days === -1 ? '自上次同步'
: days === 0 ? '全部历史'
: days >= 365 ? `${days / 365}`
: `${days}`;
/** What each range actually costs, so the choice is made with eyes open. */
const rangeHint = (days: number) =>
days === -1 ? '只补上次同步之后缺的那几天,最快'
: days === 0 ? '约 730 天2040 分钟'
: days >= 365 ? `${days} 天,大约 ${Math.ceil((days * 3) / 60)} 分钟`
: `${days} 天,几分钟`;
function SyncPage() {
const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null);
const [auto, setAuto] = useState<AutoSyncStatus | null>(null);
const [settings, setSettings] = useState<UserSettings | null>(null);
const [options, setOptions] = useState<SettingsOptions | null>(null);
// The manual sync range lives here now, not in 设置: the button that uses it
// is on this page, and a setting that only takes effect somewhere else is
// how 全部历史 came to look like it did nothing. It is still persisted, so
// the picker opens where it was left.
const [range, setRange] = useState(-1);
const [details, setDetails] = useState<DetailSyncStatus | null>(null);
const [hasToken, setHasToken] = useState<boolean | null>(null);
@@ -72,7 +88,8 @@ function SyncPage() {
if (s?.status === 'syncing') beginSyncPolling();
});
apiClient.getGarminAuthStatus().then(setHasToken).catch(() => setHasToken(false));
apiClient.getSettings().then(setSettings).catch(() => setSettings(null));
apiClient.getSettings().then((s) => setRange(s.historyDays)).catch(() => {});
apiClient.getSettingsOptions().then(setOptions).catch(() => setOptions(null));
return stopPolling;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -195,30 +212,36 @@ function SyncPage() {
// --- sync ---------------------------------------------------------------
/** The last couple of days, awaited inline — seconds, not minutes. */
const syncLatest = async () => {
setError('');
setMessage('');
setLoading(true);
try {
const result = await apiClient.syncLatest(2);
await refresh();
setMessage(result.message || `已更新最近 ${result.recordsSynced ?? 2}`);
} catch (err: any) {
setError(errorMessage(err, '同步失败'));
} finally {
setLoading(false);
}
/** Choose how far back to pull, and remember it for next time. */
const pickRange = () => {
if (!options) return;
f7.dialog.create({
title: '同步范围',
buttons: [
...options.historyDays.map((v) => ({
text: historyLabel(v) + (v === range ? ' ✓' : ''),
onClick: () => {
setRange(v);
// Persisted, but nothing else reads it — auto-sync has its own
// fixed window and never looks at this.
apiClient.saveSettings({ historyDays: v }).catch(() => {});
},
})),
{ text: '取消', color: 'gray' },
],
verticalButtons: true,
}).open();
};
/** The configured history window, in the background with progress. */
/** The one manual sync: the chosen range, in the background, with progress. */
const syncHistory = async () => {
setError('');
setMessage('');
setLoading(true);
try {
// 0 means "everything"; the backend caps it at what Garmin will serve.
const started = await apiClient.syncGarminData(settings?.historyDays ?? 365);
// 0 means "everything" and -1 "since the last sync"; the backend caps
// the former at what Garmin will actually serve.
const started = await apiClient.syncGarminData(range);
const s = await refresh();
// A refused start (Garmin is throttling us) used to be swallowed: the
// button did nothing, no progress bar appeared, and no reason was shown.
@@ -402,21 +425,32 @@ function SyncPage() {
</section>
) : (
<>
{/* The range is chosen here, right above the button that uses
it. It used to live in 设置 while only taking effect on this
page, so picking 全部历史 there and pressing the (hardcoded
two-day) button here looked like the app ignoring you. */}
<div className="sync-secondary">
<button
className="btn btn-plain"
onClick={pickRange}
disabled={busy || !options}
>
<span className="sync-btn-label"></span>
<span className="sync-btn-sub">
{historyLabel(range)} · {rangeHint(range)}
</span>
</button>
</div>
<button
className="btn btn-primary btn-large sync-primary"
onClick={syncLatest}
onClick={syncHistory}
disabled={busy}
>
{loading ? '同步中…' : '同步最新数据'}
{loading ? '正在启动…' : '开始同步'}
</button>
<div className="sync-secondary">
<button className="btn btn-plain" onClick={syncHistory} disabled={busy}>
<span className="sync-btn-label"></span>
<span className="sync-btn-sub">
{historyLabel(settings?.historyDays ?? 365)}
</span>
</button>
<button
className="btn btn-plain"
onClick={syncDetails}
@@ -456,7 +490,7 @@ function SyncPage() {
</span>
</div>
<div className="sync-fact">
<span className="sync-fact-label"></span>
<span className="sync-fact-label"></span>
<span className="sync-fact-value">
{auto?.account?.autoSync === false
? '已关闭'