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:
@@ -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>
|
||||
|
||||
@@ -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 天,20–40 分钟'
|
||||
: 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
|
||||
? '已关闭'
|
||||
|
||||
Reference in New Issue
Block a user