feat: refresh accounts automatically every five minutes

This commit is contained in:
2026-08-14 15:35:27 +08:00
parent c1985852e1
commit 5f50669b98
11 changed files with 66 additions and 145 deletions
+11 -110
View File
@@ -12,7 +12,6 @@ import {
import {
Activity,
Bell,
Check,
ChevronDown,
Clock,
Coins,
@@ -24,7 +23,6 @@ import {
LogOut,
Moon,
MoreHorizontal,
RefreshCw,
Settings,
Sun,
Trash2,
@@ -378,22 +376,19 @@ type DashboardCardState = {
dashboard: Dash | null;
loading: boolean;
error: string;
refresh: "idle" | "loading" | "done";
};
const emptyDashboardCardState = (): DashboardCardState => ({
dashboard: null,
loading: false,
error: "",
refresh: "idle",
});
function Dashboard({ publicView = false }: { publicView?: boolean }) {
const [accounts, setAccounts] = useState<Account[] | null>(null),
[cards, setCards] = useState<Record<number, DashboardCardState>>({}),
[expanded, setExpanded] = useState<Record<number, boolean>>({}),
[error, setError] = useState(""),
[refresh, setRefresh] = useState<"idle" | "loading" | "done">("idle");
[error, setError] = useState("");
const requestRef = useRef(new Map<number, number>()),
activeControllersRef = useRef(new Map<number, AbortController>());
@@ -410,11 +405,7 @@ function Dashboard({ publicView = false }: { publicView?: boolean }) {
}));
};
const loadDashboard = async (
accountId: number,
signal?: AbortSignal,
manual = false,
) => {
const loadDashboard = async (accountId: number, signal?: AbortSignal) => {
const request = (requestRef.current.get(accountId) || 0) + 1;
requestRef.current.set(accountId, request);
let ownController: AbortController | undefined;
@@ -427,7 +418,6 @@ function Dashboard({ publicView = false }: { publicView?: boolean }) {
updateCard(accountId, {
loading: true,
error: "",
...(manual ? { refresh: "loading" as const } : {}),
});
try {
const dashboard = await get(
@@ -444,26 +434,13 @@ function Dashboard({ publicView = false }: { publicView?: boolean }) {
dashboard,
loading: false,
error: "",
...(manual ? { refresh: "done" as const } : {}),
});
if (manual) {
window.setTimeout(
() =>
setCards((current) => {
const card = current[accountId];
if (!card || card.refresh !== "done") return current;
return { ...current, [accountId]: { ...card, refresh: "idle" } };
}),
2000,
);
}
} catch (value) {
if (signal.aborted || requestRef.current.get(accountId) !== request)
return;
updateCard(accountId, {
loading: false,
error: toErrorMessage(value),
...(manual ? { refresh: "idle" as const } : {}),
});
} finally {
if (
@@ -516,42 +493,6 @@ function Dashboard({ publicView = false }: { publicView?: boolean }) {
[],
);
const syncAccount = async (accountId: number) => {
try {
setError("");
await post(`accounts/${accountId}/sync`, decodeOK, {}, undefined, 60_000);
await loadDashboard(accountId, undefined, true);
} catch (value) {
updateCard(accountId, {
loading: false,
refresh: "idle",
error: toErrorMessage(value),
});
}
};
const syncAll = async () => {
if (!accounts?.length) return;
setRefresh("loading");
setError("");
const results = await Promise.allSettled(
accounts.map(async (account) => {
await post(
`accounts/${account.id}/sync`,
decodeOK,
{},
undefined,
60_000,
);
await loadDashboard(account.id, undefined, true);
}),
);
const failed = results.filter((result) => result.status === "rejected");
if (failed.length)
setError(`${failed.length} 个账号刷新失败,请展开对应卡片查看详情。`);
setRefresh(failed.length ? "idle" : "done");
if (!failed.length) window.setTimeout(() => setRefresh("idle"), 2000);
};
if (accounts === null)
return (
<>
@@ -582,21 +523,8 @@ function Dashboard({ publicView = false }: { publicView?: boolean }) {
<>
<Header
title="用量总览"
sub={`共 ${accounts.length} 个账号,点击卡片查看完整详情`}
>
<button
className={"secondary refresh " + refresh}
disabled={refresh === "loading"}
onClick={() => void syncAll()}
>
{refresh === "done" ? <Check /> : <RefreshCw />}
{refresh === "loading"
? "刷新中"
: refresh === "done"
? "刷新完成"
: "刷新全部"}
</button>
</Header>
sub={`${publicView ? "公开只读 · " : ""}共 ${accounts.length} 个账号,服务器每 5 分钟自动同步`}
/>
{error && <div className="banner">{error}</div>}
<div className="account-overview-list">
{accounts.map((account) => (
@@ -612,7 +540,6 @@ function Dashboard({ publicView = false }: { publicView?: boolean }) {
[account.id]: !current[account.id],
}))
}
onRefresh={() => void syncAccount(account.id)}
/>
))}
</div>
@@ -626,14 +553,12 @@ function AccountOverviewCard({
publicView,
expanded,
onToggle,
onRefresh,
}: {
account: Account;
state: DashboardCardState;
publicView: boolean;
expanded: boolean;
onToggle: () => void;
onRefresh: () => void;
}) {
const dashboard = state.dashboard;
const email = dashboard?.account.email ?? account.email;
@@ -675,7 +600,6 @@ function AccountOverviewCard({
<AccountDashboardDetails
dashboard={dashboard}
state={state}
onRefresh={onRefresh}
publicView={publicView}
/>
) : (
@@ -684,11 +608,6 @@ function AccountOverviewCard({
<p className={state.error ? "error" : ""}>
{state.error || "正在读取账号用量…"}
</p>
{state.error && (
<button className="secondary" onClick={onRefresh}>
<RefreshCw /> 重试
</button>
)}
</div>
)}
</div>
@@ -739,12 +658,10 @@ function AccountQuickSummary({
function AccountDashboardDetails({
dashboard,
state,
onRefresh,
publicView,
}: {
dashboard: Dash;
state: DashboardCardState;
onRefresh: () => void;
publicView: boolean;
}) {
return (
@@ -764,18 +681,6 @@ function AccountDashboardDetails({
? new Date(dashboard.fetchedAt * 1000).toLocaleString()
: "尚未同步"}
</span>
<button
className={`secondary refresh ${state.refresh}`}
disabled={state.refresh === "loading"}
onClick={onRefresh}
>
{state.refresh === "done" ? <Check /> : <RefreshCw />}
{state.refresh === "loading"
? "刷新中"
: state.refresh === "done"
? "刷新完成"
: "刷新账号"}
</button>
</div>
</div>
{(state.error || dashboard.lastError) && (
@@ -821,7 +726,9 @@ function AccountDashboardDetails({
</div>
{!dashboard.limits.length && (
<div className="panel empty">
该连接暂无限额数据,请确认登录后刷新。
{publicView
? "该连接暂无限额数据"
: "该连接暂无限额数据,服务器会自动同步。"}
</div>
)}
{dashboard.currentCycle && (
@@ -1111,16 +1018,10 @@ function General() {
<option value="light">浅色</option>
</select>
</label>
<label>
同步间隔(分钟)
<input
type="number"
min="1"
max="60"
value={v.syncMinutes}
onChange={(e) => setV({ ...v, syncMinutes: +e.target.value })}
/>
</label>
<div className="fixed-setting">
<span>自动同步</span>
<b>服务器每 5 分钟自动刷新所有账号</b>
</div>
<label>
历史保留(天)
<select
+16 -17
View File
@@ -328,17 +328,6 @@ header p {
border: 1px solid var(--border);
color: var(--text);
}
.refresh.loading svg {
animation: spin 0.8s linear infinite;
}
.refresh {
width: 116px;
white-space: nowrap;
}
.refresh.done {
color: var(--green);
border-color: var(--green);
}
.account-overview-list {
display: flex;
flex-direction: column;
@@ -721,6 +710,22 @@ header p {
.checks input {
width: auto;
}
.fixed-setting {
display: flex;
flex-direction: column;
justify-content: center;
gap: 8px;
font-size: 13px;
color: var(--muted);
}
.fixed-setting b {
border: 1px solid var(--border);
background: var(--bg);
color: var(--text);
border-radius: 10px;
padding: 12px;
font-weight: 500;
}
.actions {
display: flex;
gap: 10px;
@@ -995,9 +1000,6 @@ a {
.header-actions {
flex-direction: column;
}
.refresh {
width: 100%;
}
.account-summary-toggle {
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 10px 12px;
@@ -1036,9 +1038,6 @@ a {
.account-detail-actions .fresh {
display: block;
}
.account-detail-actions .refresh {
width: 100%;
}
.account-info-grid {
grid-template-columns: 1fr 1fr;
}
+1
View File
@@ -162,6 +162,7 @@ test("shows every account as a collapsible summary on the overview", async ({
await expect(page.getByText("最低 65% 剩余")).toBeVisible();
await expect(page.getByText("本周期 123.5K Tokens")).toBeVisible();
await expect(page.getByText("限额暂无")).toBeVisible();
await expect(page.getByRole("button", { name: "刷新全部" })).toHaveCount(0);
await expect(page.locator(".account-detail")).toHaveCount(0);
await expect(page.locator(".account-select")).toHaveCount(0);