feat: show current-cycle token totals
This commit is contained in:
+27
-3
@@ -687,6 +687,11 @@ function AccountQuickSummary({
|
||||
<span>
|
||||
{remaining == null ? "限额暂无" : `最低 ${Math.round(remaining)}% 剩余`}
|
||||
</span>
|
||||
<span>
|
||||
{dashboard.currentCycle == null
|
||||
? "本周期 Token 暂无"
|
||||
: `本周期 ${num(dashboard.currentCycle.totalTokens)} Tokens`}
|
||||
</span>
|
||||
<span>{reset ? `最近重置 ${relative(reset)}` : "暂无重置时间"}</span>
|
||||
</span>
|
||||
);
|
||||
@@ -776,11 +781,22 @@ function AccountDashboardDetails({
|
||||
该连接暂无限额数据,请确认登录后刷新。
|
||||
</div>
|
||||
)}
|
||||
{dashboard.currentCycle && (
|
||||
<div className="cycle-note">
|
||||
<Clock />
|
||||
<span>
|
||||
本周期:{cycleTimestamp(dashboard.currentCycle.startedAt)} 至{" "}
|
||||
{cycleTimestamp(dashboard.currentCycle.resetsAt)}(
|
||||
{cycleDuration(dashboard.currentCycle.windowDurationMinutes)}
|
||||
,按每日数据汇总)
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="stats">
|
||||
<Stat
|
||||
icon={<Coins />}
|
||||
label="累计 Tokens"
|
||||
v={num(dashboard.summary.lifetimeTokens)}
|
||||
label="本周期 Tokens"
|
||||
v={num(dashboard.currentCycle?.totalTokens)}
|
||||
/>
|
||||
<Stat
|
||||
icon={<TrendingUp />}
|
||||
@@ -1664,7 +1680,7 @@ function Header({
|
||||
const num = (v?: number | null) =>
|
||||
v == null
|
||||
? "暂无"
|
||||
: new Intl.NumberFormat("zh-CN", {
|
||||
: new Intl.NumberFormat("en", {
|
||||
notation: "compact",
|
||||
maximumFractionDigits: 1,
|
||||
}).format(v);
|
||||
@@ -1676,6 +1692,14 @@ const duration = (v?: number | null) =>
|
||||
: v < 3600
|
||||
? `${Math.floor(v / 60)} 分 ${v % 60} 秒`
|
||||
: `${(v / 3600).toFixed(1)} 小时`;
|
||||
const cycleTimestamp = (value: number) =>
|
||||
new Date(value * 1000).toLocaleString();
|
||||
const cycleDuration = (minutes: number) =>
|
||||
minutes % (24 * 60) === 0
|
||||
? `${minutes / (24 * 60)} 天窗口`
|
||||
: minutes % 60 === 0
|
||||
? `${minutes / 60} 小时窗口`
|
||||
: `${minutes} 分钟窗口`;
|
||||
const remainingPercent = (limit: Limit) =>
|
||||
100 - Math.min(100, Math.max(0, limit.usedPercent));
|
||||
const limitLabel = (limit: Limit) => {
|
||||
|
||||
@@ -432,6 +432,24 @@ header p {
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--danger) 8%, var(--panel));
|
||||
}
|
||||
.cycle-note {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--border));
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--accent) 7%, var(--panel));
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.cycle-note svg {
|
||||
flex: none;
|
||||
width: 16px;
|
||||
margin-top: 2px;
|
||||
color: var(--accent);
|
||||
}
|
||||
.account-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
|
||||
@@ -24,6 +24,29 @@ describe("API decoders", () => {
|
||||
expect(value.account.email).toBeNull();
|
||||
});
|
||||
|
||||
it("解码当前重置周期 Token 合计", () => {
|
||||
const value = decodeDashboard({
|
||||
accountId: 1,
|
||||
displayName: "个人账号",
|
||||
account: { email: "user@example.com", planType: "plus", connected: true },
|
||||
limits: [],
|
||||
currentCycle: {
|
||||
limitId: "codex",
|
||||
windowType: "secondary",
|
||||
windowDurationMinutes: 10080,
|
||||
startedAt: 1_900_000_000,
|
||||
resetsAt: 1_900_604_800,
|
||||
totalTokens: 123_456,
|
||||
},
|
||||
summary: {},
|
||||
usage: [],
|
||||
fetchedAt: 1_900_000_000,
|
||||
stale: false,
|
||||
});
|
||||
expect(value.currentCycle?.windowType).toBe("secondary");
|
||||
expect(value.currentCycle?.totalTokens).toBe(123_456);
|
||||
});
|
||||
|
||||
it("拒绝未知主题", () => {
|
||||
expect(() =>
|
||||
decodeGeneral({
|
||||
|
||||
@@ -48,6 +48,14 @@ export interface Point {
|
||||
date: string;
|
||||
totalTokens: number;
|
||||
}
|
||||
export interface TokenCycle {
|
||||
limitId: string;
|
||||
windowType: string;
|
||||
windowDurationMinutes: number;
|
||||
startedAt: number;
|
||||
resetsAt: number;
|
||||
totalTokens: number;
|
||||
}
|
||||
export interface Dashboard {
|
||||
accountId: number;
|
||||
displayName: string;
|
||||
@@ -57,6 +65,7 @@ export interface Dashboard {
|
||||
connected: boolean;
|
||||
};
|
||||
limits: Limit[];
|
||||
currentCycle?: TokenCycle;
|
||||
summary: {
|
||||
lifetimeTokens?: number | null;
|
||||
peakDailyTokens?: number | null;
|
||||
@@ -153,6 +162,20 @@ export const decodeAccounts: Decoder<Account[]> = (value) => {
|
||||
if (!Array.isArray(value)) throw new Error("账号列表格式无效");
|
||||
return value.map(decodeAccount);
|
||||
};
|
||||
const decodeTokenCycle: Decoder<TokenCycle> = (value) => {
|
||||
const x = record(value, "currentCycle");
|
||||
return {
|
||||
limitId: string(x.limitId, "limitId"),
|
||||
windowType: string(x.windowType, "windowType"),
|
||||
windowDurationMinutes: number(
|
||||
x.windowDurationMinutes,
|
||||
"windowDurationMinutes",
|
||||
),
|
||||
startedAt: number(x.startedAt, "startedAt"),
|
||||
resetsAt: number(x.resetsAt, "resetsAt"),
|
||||
totalTokens: number(x.totalTokens, "totalTokens"),
|
||||
};
|
||||
};
|
||||
export const decodeDashboard: Decoder<Dashboard> = (value) => {
|
||||
const x = record(value, "Dashboard"),
|
||||
account = record(x.account, "account"),
|
||||
@@ -181,6 +204,9 @@ export const decodeDashboard: Decoder<Dashboard> = (value) => {
|
||||
resetsAt: number(l.resetsAt, "resetsAt"),
|
||||
};
|
||||
}),
|
||||
...(x.currentCycle == null
|
||||
? {}
|
||||
: { currentCycle: decodeTokenCycle(x.currentCycle) }),
|
||||
summary: {
|
||||
lifetimeTokens: optionalNumber(summary.lifetimeTokens, "lifetimeTokens"),
|
||||
peakDailyTokens: optionalNumber(
|
||||
|
||||
@@ -91,6 +91,14 @@ test("shows every account as a collapsible summary on the overview", async ({
|
||||
resetsAt: 1_900_000_000,
|
||||
},
|
||||
],
|
||||
currentCycle: {
|
||||
limitId: "codex",
|
||||
windowType: "secondary",
|
||||
windowDurationMinutes: 10_080,
|
||||
startedAt: 1_899_999_000,
|
||||
resetsAt: 1_900_604_800,
|
||||
totalTokens: 123_456,
|
||||
},
|
||||
summary: {
|
||||
lifetimeTokens: 12_345,
|
||||
peakDailyTokens: 3_000,
|
||||
@@ -151,6 +159,7 @@ test("shows every account as a collapsible summary on the overview", async ({
|
||||
await expect(page.locator(".account-overview")).toHaveCount(2);
|
||||
await expect.poll(() => requestedDashboards.size).toBe(2);
|
||||
await expect(page.getByText("最低 65% 剩余")).toBeVisible();
|
||||
await expect(page.getByText("本周期 123.5K Tokens")).toBeVisible();
|
||||
await expect(page.getByText("限额暂无")).toBeVisible();
|
||||
await expect(page.locator(".account-detail")).toHaveCount(0);
|
||||
await expect(page.locator(".account-select")).toHaveCount(0);
|
||||
@@ -162,6 +171,9 @@ test("shows every account as a collapsible summary on the overview", async ({
|
||||
await expect(
|
||||
first.getByRole("heading", { name: "每日 Token 趋势" }),
|
||||
).toBeVisible();
|
||||
await expect(first.getByText("本周期 Tokens")).toBeVisible();
|
||||
await expect(first.getByText("123.5K", { exact: true })).toBeVisible();
|
||||
await expect(first.getByText("按每日数据汇总")).toBeVisible();
|
||||
await expect(page.locator(".account-detail")).toHaveCount(1);
|
||||
|
||||
await first.getByRole("button", { name: "收起个人账号详情" }).click();
|
||||
|
||||
Reference in New Issue
Block a user