Skip to content

Commit 9702a00

Browse files
committed
数据页显示最近刷新时间;修复 PWA 离线壳缓存清单 (v2.0.2)
- 新增 LastRefreshed 组件:总览/中转站/我的站点/用量统计/经营分析 五个轮询页在 PageContainer extra 显示「最近刷新 HH:mm:ss」, 仅在数据拉取成功时更新(失败/缓存命中语义各页按实际处理) - sw.js 壳清单去掉 v1 遗留的 /app.js、/styles.css(已不存在, addAll 会静默失败导致离线壳缓存失效);缓存名升级 shell-v2
1 parent e989ddd commit 9702a00

8 files changed

Lines changed: 55 additions & 7 deletions

File tree

app/(dashboard)/analytics/page.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Col, Empty, Row, Segmented, Statistic, Typography, theme } from "antd";
99
import { Bar, Column, DualAxes, Heatmap, Line, Pie } from "@ant-design/plots";
1010
import { api, cny } from "../../../lib/client";
1111
import ChartBox from "../chart-box";
12+
import LastRefreshed from "../last-refreshed";
1213
import { useThemeMode } from "../../providers";
1314

1415
const { Text } = Typography;
@@ -50,11 +51,13 @@ export default function AnalyticsPage() {
5051
const [data, setData] = useState<any>(null);
5152
const [own, setOwn] = useState<any>(null); // /api/own/analytics 响应(无自有站/拉取失败为 null)
5253
const [loading, setLoading] = useState(true);
54+
const [refreshedAt, setRefreshedAt] = useState<number | null>(null);
5355

5456
const load = useCallback(async (d: number) => {
5557
try {
5658
const resp = await api(`/api/analytics?days=${d}`);
5759
setData(resp);
60+
setRefreshedAt(Date.now());
5861
} catch {
5962
/* 顶层错误交给下次轮询重试,页面保留旧数据 */
6063
}
@@ -193,17 +196,19 @@ export default function AnalyticsPage() {
193196
<PageContainer
194197
title="经营分析"
195198
subTitle="上游成本 · 下游收入 · 余额跑道"
196-
extra={
199+
extra={[
200+
<LastRefreshed key="last-refreshed" at={refreshedAt} />,
197201
<Segmented
202+
key="days"
198203
value={days}
199204
onChange={(v) => setDays(Number(v))}
200205
options={[
201206
{ label: "7 天", value: 7 },
202207
{ label: "14 天", value: 14 },
203208
{ label: "30 天", value: 30 },
204209
]}
205-
/>
206-
}
210+
/>,
211+
]}
207212
>
208213
{/* KPI 行:总成本 / 日均 / 峰值日 / 预计月化 */}
209214
<Row gutter={[16, 16]}>

app/(dashboard)/last-refreshed.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"use client";
2+
// 页面级「最近刷新时间」:数据页每次轮询成功后更新。
3+
// 显示的是本页最近一次成功拉取数据的时刻;上游余额的实际查询节奏
4+
// 由服务端按「设置 → 自动刷新间隔」执行,两者不是一回事。
5+
import { Typography, theme } from "antd";
6+
import { SyncOutlined } from "@ant-design/icons";
7+
8+
const { Text } = Typography;
9+
10+
function fmt(ts: number) {
11+
const d = new Date(ts);
12+
const p = (n: number) => String(n).padStart(2, "0");
13+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
14+
}
15+
16+
export default function LastRefreshed({ at }: { at: number | null }) {
17+
const { token } = theme.useToken();
18+
if (!at) return null;
19+
return (
20+
<Text type="secondary" style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }}>
21+
<SyncOutlined style={{ marginRight: 4 }} />
22+
最近刷新 {fmt(at)}
23+
</Text>
24+
);
25+
}

app/(dashboard)/my/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import { PlusOutlined, ReloadOutlined } from "@ant-design/icons";
2424
import { Bar, Column, Line } from "@ant-design/plots";
2525
import ChartBox from "../chart-box";
26+
import LastRefreshed from "../last-refreshed";
2627
import { api, cny, cny4, fmtTokens, rateOf } from "../../../lib/client";
2728
import { useThemeMode } from "../../providers";
2829

@@ -149,6 +150,7 @@ export default function MyStationPage() {
149150
const [data, setData] = useState<any>(null);
150151
const [error, setError] = useState<string | null>(null);
151152
const [refreshing, setRefreshing] = useState(false);
153+
const [refreshedAt, setRefreshedAt] = useState<number | null>(null);
152154

153155
// 转售 Key 管理器状态
154156
const [mgrOpen, setMgrOpen] = useState(false);
@@ -168,6 +170,7 @@ export default function MyStationPage() {
168170
if (!force && cached && Date.now() - cached.at < 60000) {
169171
setData(cached.data);
170172
setError(null);
173+
setRefreshedAt(cached.at);
171174
return;
172175
}
173176
try {
@@ -178,6 +181,7 @@ export default function MyStationPage() {
178181
if (rangeRef.current !== r) return;
179182
setData(res);
180183
setError(null);
184+
setRefreshedAt(Date.now());
181185
} catch (e: any) {
182186
if (rangeRef.current !== r) return;
183187
setError(e.message || String(e));
@@ -273,6 +277,7 @@ export default function MyStationPage() {
273277

274278
// ---- 页面头部:范围切换 + 手动刷新(v1 #ownRange / #ownRefresh)----
275279
const headerExtra = [
280+
<LastRefreshed key="refreshed" at={refreshedAt} />,
276281
<Segmented key="range" options={OWN_RANGES} value={range} onChange={(v) => setRange(String(v))} />,
277282
<Button key="refresh" icon={<ReloadOutlined />} loading={refreshing} onClick={onRefresh}>
278283
刷新

app/(dashboard)/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// 功能与口径逐条对照 v1 app.js:renderDashboard / drawTotalChart / drawBurnBars / stationRow
44
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
55
import { PageContainer, ProCard } from "@ant-design/pro-components";
6+
import LastRefreshed from "./last-refreshed";
67
import { App, Button, Col, Empty, Row, Segmented, Tag, Tooltip, Typography, theme } from "antd";
78
import { ReloadOutlined } from "@ant-design/icons";
89
import { Line, Bar } from "@ant-design/plots";
@@ -149,6 +150,7 @@ export default function OverviewPage() {
149150
const [overview, setOverview] = useState<{ hours: number; series: any[] } | null>(null);
150151
const [overviewErr, setOverviewErr] = useState<string | null>(null);
151152
const [refreshingId, setRefreshingId] = useState<string | null>(null);
153+
const [refreshedAt, setRefreshedAt] = useState<number | null>(null);
152154
const hoursRef = useRef(trendHours);
153155
hoursRef.current = trendHours;
154156

@@ -161,6 +163,7 @@ export default function OverviewPage() {
161163
setStations(r.stations);
162164
setSettings(r.settings);
163165
setLoaded(true);
166+
setRefreshedAt(Date.now());
164167
} catch {
165168
/* 401 已由 api() 跳登录,其余错误静默等下轮 */
166169
}
@@ -528,7 +531,7 @@ export default function OverviewPage() {
528531
}
529532

530533
return (
531-
<PageContainer title="总览">
534+
<PageContainer title="总览" extra={<LastRefreshed at={refreshedAt} />}>
532535
{/* 5 张 KPI(renderDashboard stats 区,口径逐项一致) */}
533536
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(190px, 1fr))", gap: 16 }}>
534537
<StatCard

app/(dashboard)/stations/page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
} from "@ant-design/icons";
3333
import { Line } from "@ant-design/plots";
3434
import ChartBox from "../chart-box";
35+
import LastRefreshed from "../last-refreshed";
3536
import dayjs from "dayjs";
3637
import { api, cny, usd, rateOf, fmtTokens, fmtEta, statusOf } from "../../../lib/client";
3738
import { useThemeMode } from "../../providers";
@@ -388,6 +389,7 @@ export default function StationsPage() {
388389
const [loaded, setLoaded] = useState(false);
389390
const [refreshingAll, setRefreshingAll] = useState(false);
390391
const [refreshingIds, setRefreshingIds] = useState<Record<string, boolean>>({});
392+
const [refreshedAt, setRefreshedAt] = useState<number | null>(null);
391393

392394
// 添加/编辑弹窗
393395
const [modalOpen, setModalOpen] = useState(false);
@@ -410,6 +412,7 @@ export default function StationsPage() {
410412
setStations(r.stations);
411413
setSettings(r.settings);
412414
setLoaded(true);
415+
setRefreshedAt(Date.now());
413416
}, []);
414417

415418
useEffect(() => {
@@ -594,6 +597,7 @@ export default function StationsPage() {
594597
title="中转站"
595598
subTitle="管理你的 sub2api / new-api 中转站"
596599
extra={[
600+
<LastRefreshed key="last-refreshed" at={refreshedAt} />,
597601
<Button key="refresh" icon={<ReloadOutlined />} loading={refreshingAll} onClick={onRefreshAll}>刷新</Button>,
598602
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => openModal(null)}>添加中转站</Button>,
599603
]}

app/(dashboard)/usage/page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ReloadOutlined } from "@ant-design/icons";
99
import { Bar, Column } from "@ant-design/plots";
1010
import { api, cny4, fmtTokens, rateOf } from "../../../lib/client";
1111
import ChartBox from "../chart-box";
12+
import LastRefreshed from "../last-refreshed";
1213
import { useThemeMode } from "../../providers";
1314

1415
// 时间档位(照抄 v1 USAGE_RANGES)
@@ -69,6 +70,7 @@ export default function UsagePage() {
6970
const [data, setData] = useState<any>(null); // /api/usage 响应
7071
const [err, setErr] = useState<string>("");
7172
const [refreshing, setRefreshing] = useState(false);
73+
const [refreshedAt, setRefreshedAt] = useState<number | null>(null);
7274
// 与 v1 loadUsage 相同的 30 秒前端缓存(单槽,按 range 记)
7375
const cacheRef = useRef<{ range: string; at: number; data: any } | null>(null);
7476
const rangeRef = useRef(range);
@@ -93,6 +95,7 @@ export default function UsagePage() {
9395
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
9496
const resp = await api(`/api/usage?range=${r}&tz=${encodeURIComponent(tz)}`);
9597
cacheRef.current = { range: r, at: Date.now(), data: resp };
98+
setRefreshedAt(Date.now());
9699
if (rangeRef.current === r) {
97100
setData(resp);
98101
setErr("");
@@ -253,6 +256,7 @@ export default function UsagePage() {
253256
title="用量统计"
254257
subTitle="分站点、分模型、分时段的 Token 消耗"
255258
extra={[
259+
<LastRefreshed key="last-refreshed" at={refreshedAt} />,
256260
<Button key="refresh" icon={<ReloadOutlined />} loading={refreshing} onClick={onRefresh}>
257261
刷新
258262
</Button>,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "relay-monitor",
3-
"version": "2.0.1",
3+
"version": "2.0.2",
44
"private": true,
55
"type": "module",
66
"description": "中转站余额监控面板 v2:Next.js 全栈 + Ant Design Pro (antd 6) + MySQL",

public/sw.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// PWA Service Worker:静态资源网络优先(面板由 Watchtower 自动更新,
22
// 不能让旧版本粘在缓存里),断网时回退缓存壳;API 请求不拦截(数据必须新鲜)。
3-
const CACHE = "relay-monitor-shell-v1";
4-
const SHELL = ["/", "/styles.css", "/app.js", "/manifest.webmanifest", "/icons/icon-192.png"];
3+
const CACHE = "relay-monitor-shell-v2";
4+
// v2(Next.js):JS/CSS 均为带哈希的 /_next/static 资源,运行时按 fetch 缓存即可,
5+
// 壳清单只预缓存入口与图标(v1 的 /app.js /styles.css 已不存在)
6+
const SHELL = ["/", "/manifest.webmanifest", "/icons/icon-192.png"];
57

68
self.addEventListener("install", (e) => {
79
e.waitUntil(caches.open(CACHE).then((c) => c.addAll(SHELL)).catch(() => {}));

0 commit comments

Comments
 (0)