feat: refresh accounts automatically every five minutes
This commit is contained in:
+11
-110
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user