feat: redesign quota summary for v0.3.0
This commit is contained in:
+67
-17
@@ -632,24 +632,44 @@ function AccountQuickSummary({
|
||||
: "等待数据"}
|
||||
</span>
|
||||
);
|
||||
const remaining = dashboard.limits.length
|
||||
? Math.min(...dashboard.limits.map((limit) => remainingPercent(limit)))
|
||||
: null;
|
||||
const reset = dashboard.limits
|
||||
.map((limit) => limit.resetsAt)
|
||||
.filter((value) => value > 0)
|
||||
.sort((a, b) => a - b)[0];
|
||||
const limits = quickSummaryLimits(dashboard.limits);
|
||||
if (!limits.length)
|
||||
return <span className="account-summary-metrics">限额暂无</span>;
|
||||
return (
|
||||
<span className="account-summary-metrics">
|
||||
<span>
|
||||
{remaining == null ? "限额暂无" : `最低 ${Math.round(remaining)}% 剩余`}
|
||||
</span>
|
||||
<span>
|
||||
{dashboard.currentCycle == null
|
||||
? "本周期 Token 暂无"
|
||||
: `本周期 ${num(dashboard.currentCycle.totalTokens)} Tokens`}
|
||||
</span>
|
||||
<span>{reset ? `最近重置 ${relative(reset)}` : "暂无重置时间"}</span>
|
||||
<span
|
||||
className={`account-summary-metrics account-summary-limits${limits.length === 1 ? " single" : ""}`}
|
||||
>
|
||||
{limits.map(({ kind, limit }) => {
|
||||
const remaining = Math.round(remainingPercent(limit));
|
||||
return (
|
||||
<span
|
||||
className="account-summary-limit"
|
||||
data-window-kind={kind}
|
||||
key={`${kind}-${limit.limitId}-${limit.windowType}`}
|
||||
aria-label={`${quickLimitLabel(kind, limit)},剩余 ${remaining}%,${limit.resetsAt ? `${relative(limit.resetsAt)}重置` : "重置时间暂无"}`}
|
||||
>
|
||||
<span className="account-summary-limit-name">
|
||||
{quickLimitLabel(kind, limit)}
|
||||
</span>
|
||||
<strong style={{ color: `hsl(${remaining * 1.2} 70% 45%)` }}>
|
||||
{remaining}%
|
||||
</strong>
|
||||
<span className="account-summary-limit-remaining">剩余</span>
|
||||
<span
|
||||
className="account-summary-limit-reset"
|
||||
title={
|
||||
limit.resetsAt
|
||||
? new Date(limit.resetsAt * 1000).toLocaleString()
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{limit.resetsAt
|
||||
? `${relative(limit.resetsAt)}重置`
|
||||
: "重置时间暂无"}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1670,6 +1690,36 @@ const cycleDuration = (minutes: number) =>
|
||||
: `${minutes} 分钟窗口`;
|
||||
const remainingPercent = (limit: Limit) =>
|
||||
100 - Math.min(100, Math.max(0, limit.usedPercent));
|
||||
type QuickLimitKind = "hour" | "week" | "other";
|
||||
const quickSummaryLimits = (
|
||||
limits: Limit[],
|
||||
): Array<{ kind: QuickLimitKind; limit: Limit }> => {
|
||||
const sorted = [...limits].sort(
|
||||
(a, b) => a.windowDurationMinutes - b.windowDurationMinutes,
|
||||
);
|
||||
const hour = sorted.find(
|
||||
(limit) =>
|
||||
limit.windowDurationMinutes > 0 && limit.windowDurationMinutes < 24 * 60,
|
||||
);
|
||||
const week = sorted.find(
|
||||
(limit) =>
|
||||
limit.windowDurationMinutes >= 6 * 24 * 60 &&
|
||||
limit.windowDurationMinutes <= 8 * 24 * 60,
|
||||
);
|
||||
const selected: Array<{ kind: QuickLimitKind; limit: Limit }> = [];
|
||||
if (hour) selected.push({ kind: "hour", limit: hour });
|
||||
if (week && week !== hour) selected.push({ kind: "week", limit: week });
|
||||
if (selected.length) return selected;
|
||||
return sorted.slice(0, 2).map((limit) => ({ kind: "other", limit }));
|
||||
};
|
||||
const quickLimitLabel = (kind: QuickLimitKind, limit: Limit) => {
|
||||
if (kind === "week") return "周额度";
|
||||
if (kind === "hour") {
|
||||
const hours = limit.windowDurationMinutes / 60;
|
||||
return `${Number.isInteger(hours) ? hours : hours.toFixed(1)} 小时额度`;
|
||||
}
|
||||
return limitLabel(limit).replace(/限额$/, "额度");
|
||||
};
|
||||
const limitLabel = (limit: Limit) => {
|
||||
if (limit.limitName) return limit.limitName;
|
||||
if (limit.windowDurationMinutes >= 60) {
|
||||
|
||||
@@ -391,6 +391,37 @@ header p {
|
||||
.account-summary-metrics span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.account-summary-limits {
|
||||
display: grid;
|
||||
justify-content: end;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
.account-summary-limits.single {
|
||||
justify-content: center;
|
||||
}
|
||||
.account-summary-limit {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(76px, auto) 42px 28px minmax(92px, auto);
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
}
|
||||
.account-summary-limit-name {
|
||||
color: var(--text);
|
||||
font-weight: 650;
|
||||
}
|
||||
.account-summary-limit strong {
|
||||
font-size: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
.account-summary-limit-remaining {
|
||||
font-size: 12px;
|
||||
}
|
||||
.account-summary-limit-reset {
|
||||
padding-left: 10px;
|
||||
border-left: 1px solid var(--border);
|
||||
text-align: left;
|
||||
}
|
||||
.account-chevron {
|
||||
flex: none;
|
||||
width: 20px;
|
||||
@@ -1057,6 +1088,21 @@ a {
|
||||
justify-content: flex-start;
|
||||
gap: 6px 12px;
|
||||
}
|
||||
.account-summary-limits {
|
||||
justify-content: stretch;
|
||||
}
|
||||
.account-summary-limits.single {
|
||||
justify-content: center;
|
||||
}
|
||||
.account-summary-limit {
|
||||
grid-template-columns: auto auto auto minmax(0, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
.account-summary-limit-reset {
|
||||
overflow: hidden;
|
||||
padding-left: 6px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.account-chevron {
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
|
||||
@@ -91,6 +91,14 @@ test("shows every account as a collapsible summary on the overview", async ({
|
||||
windowDurationMinutes: 300,
|
||||
resetsAt: 1_900_000_000,
|
||||
},
|
||||
{
|
||||
limitId: "secondary",
|
||||
limitName: "周限额",
|
||||
windowType: "weekly",
|
||||
usedPercent: 20,
|
||||
windowDurationMinutes: 10_080,
|
||||
resetsAt: 1_900_604_800,
|
||||
},
|
||||
],
|
||||
currentCycle: {
|
||||
limitId: "codex",
|
||||
@@ -122,7 +130,16 @@ test("shows every account as a collapsible summary on the overview", async ({
|
||||
planType: "team",
|
||||
connected: true,
|
||||
},
|
||||
limits: [],
|
||||
limits: [
|
||||
{
|
||||
limitId: "secondary",
|
||||
limitName: "周限额",
|
||||
windowType: "weekly",
|
||||
usedPercent: 40,
|
||||
windowDurationMinutes: 10_080,
|
||||
resetsAt: 1_900_604_800,
|
||||
},
|
||||
],
|
||||
summary: {},
|
||||
usage: [],
|
||||
fetchedAt: 0,
|
||||
@@ -163,9 +180,29 @@ test("shows every account as a collapsible summary on the overview", async ({
|
||||
await page.goto("/");
|
||||
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();
|
||||
const firstSummary = page
|
||||
.locator('[data-account-id="1"]')
|
||||
.locator(".account-summary-limits");
|
||||
await expect(firstSummary.locator('[data-window-kind="hour"]')).toContainText(
|
||||
"5 小时额度65%剩余",
|
||||
);
|
||||
await expect(firstSummary.locator('[data-window-kind="week"]')).toContainText(
|
||||
"周额度80%剩余",
|
||||
);
|
||||
await expect(
|
||||
firstSummary.locator(".account-summary-limit-reset"),
|
||||
).toHaveCount(2);
|
||||
expect(
|
||||
await firstSummary.evaluate(
|
||||
(element) => element.scrollWidth <= element.clientWidth,
|
||||
),
|
||||
).toBe(true);
|
||||
const weeklyOnlySummary = page
|
||||
.locator('[data-account-id="2"]')
|
||||
.locator(".account-summary-limits.single");
|
||||
await expect(weeklyOnlySummary).toContainText("周额度60%剩余");
|
||||
await expect(weeklyOnlySummary).toHaveCSS("justify-content", "center");
|
||||
await expect(page.getByText("本周期 123.5K Tokens")).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "刷新全部" })).toHaveCount(0);
|
||||
await expect(page.locator(".account-detail")).toHaveCount(0);
|
||||
await expect(page.locator(".account-select")).toHaveCount(0);
|
||||
@@ -286,14 +323,14 @@ test("shows the build version in the authenticated brand", async ({ page }) => {
|
||||
test("keeps the authenticated brand on one line with a release version", async ({
|
||||
page,
|
||||
}) => {
|
||||
await openSettings(page, "0.2.0");
|
||||
await openSettings(page, "0.3.0");
|
||||
const brand = page.locator(
|
||||
(page.viewportSize()?.width ?? 0) <= 800
|
||||
? ".mobile-topbar .logo"
|
||||
: "aside .logo",
|
||||
);
|
||||
await expect(brand).toContainText("Codex Helper");
|
||||
await expect(brand.locator(".version-badge")).toHaveText("v0.2.0");
|
||||
await expect(brand.locator(".version-badge")).toHaveText("v0.3.0");
|
||||
const centers = await brand
|
||||
.locator("svg, .brand-name, .version-badge")
|
||||
.evaluateAll((elements) =>
|
||||
|
||||
Reference in New Issue
Block a user