mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-08-30 17:11:12 +08:00
fix(ui): 修复紧凑账户余额卡片裁切
This commit is contained in:
@@ -2453,7 +2453,7 @@ function ProviderAccountsOverview({
|
||||
}) {
|
||||
const t = useAppText();
|
||||
const selectedAccountProviders = new Set((accountProviders ?? []).map((provider) => provider.trim()).filter(Boolean));
|
||||
const sortedAccounts = [...accounts].sort(compareProviderAccountSnapshots);
|
||||
const sortedAccounts = accounts.map(providerAccountSnapshotForOverview).sort(compareProviderAccountSnapshots);
|
||||
const filteredAccounts = selectedAccountProviders.size > 0
|
||||
? sortedAccounts.filter((account) => providerAccountSelectionMatches(account, selectedAccountProviders))
|
||||
: sortedAccounts
|
||||
@@ -2789,14 +2789,20 @@ function ProviderAccountSummaryCard({
|
||||
if (variant === "cards") {
|
||||
if (compactBento) {
|
||||
return (
|
||||
<div className={cn("overview-account-bento-tile overview-nested-surface group/account-card relative flex h-full min-h-0 min-w-0 flex-col overflow-hidden border", providerAccountBentoSpanClass(cardBentoSpan), providerAccountCardPaddingClass(dimensions))} data-account-status={account.status}>
|
||||
<div className="flex min-w-0 items-start justify-between gap-2">
|
||||
<div className="flex min-w-0 items-start gap-2">
|
||||
<div className={cn("overview-account-bento-tile overview-nested-surface group/account-card relative flex h-full min-h-0 min-w-0 flex-col overflow-hidden border p-2", providerAccountBentoSpanClass(cardBentoSpan))} data-account-status={account.status} data-provider-account-card-layout="compact">
|
||||
<div className="flex min-h-0 min-w-0 flex-1 items-center gap-2">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<ProviderAccountLogo account={account} className="h-7 w-7 rounded-md" providers={providers} />
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-[12px] font-semibold">{providerAccountSnapshotLabel(account)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 max-w-[45%] shrink-0 text-right" data-provider-account-compact-meter="true">
|
||||
<div className="truncate text-[10px] font-medium leading-none text-muted-foreground">
|
||||
{primaryMeter ? formatProviderAccountMeterTitle(primaryMeter, t) : account.message || account.errors?.[0]?.message || t("Unavailable")}
|
||||
</div>
|
||||
{primaryMeter ? <div className="mt-0.5 truncate text-[18px] font-semibold leading-none tracking-tight">{formatProviderAccountMeterValue(primaryMeter, t)}</div> : null}
|
||||
</div>
|
||||
{dragHandle || providerAccountShowRefresh(dimensions) ? (
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{dragHandle}
|
||||
@@ -2804,14 +2810,8 @@ function ProviderAccountSummaryCard({
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 min-w-0">
|
||||
<div className="truncate text-[10px] font-medium text-muted-foreground">
|
||||
{primaryMeter ? formatProviderAccountMeterTitle(primaryMeter, t) : account.message || account.errors?.[0]?.message || t("Unavailable")}
|
||||
</div>
|
||||
{primaryMeter ? <div className="mt-0.5 truncate text-[18px] font-semibold leading-tight tracking-tight">{formatProviderAccountMeterValue(primaryMeter, t)}</div> : null}
|
||||
</div>
|
||||
{primaryProgress !== undefined && providerAccountShowProgress(dimensions) ? (
|
||||
<div className="overview-account-bento-track mt-auto h-1.5 overflow-hidden rounded-full">
|
||||
<div className="overview-account-bento-track mt-1.5 h-1.5 shrink-0 overflow-hidden rounded-full">
|
||||
<div className="overview-account-bento-fill h-full rounded-full" style={{ width: `${primaryProgress}%` }} />
|
||||
</div>
|
||||
) : null}
|
||||
@@ -2821,7 +2821,7 @@ function ProviderAccountSummaryCard({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("overview-account-bento-tile overview-nested-surface group/account-card relative flex h-full min-h-0 min-w-0 flex-col overflow-hidden border", providerAccountBentoSpanClass(cardBentoSpan), providerAccountCardPaddingClass(dimensions))} data-account-status={account.status}>
|
||||
<div className={cn("overview-account-bento-tile overview-nested-surface group/account-card relative flex h-full min-h-0 min-w-0 flex-col overflow-hidden border", providerAccountBentoSpanClass(cardBentoSpan), providerAccountCardPaddingClass(dimensions))} data-account-status={account.status} data-provider-account-card-layout="expanded">
|
||||
<div className="flex min-w-0 shrink-0 items-start justify-between gap-3">
|
||||
<div className="flex min-w-0 items-start gap-2">
|
||||
<ProviderAccountLogo account={account} className="h-8 w-8 rounded-md" providers={providers} />
|
||||
@@ -3921,6 +3921,21 @@ function primaryProviderAccountDisplayMeter(account: ProviderAccountSnapshot): P
|
||||
return providerAccountQuotaMeters(account)[0] ?? primaryProviderAccountBalanceMeter(account) ?? primaryProviderAccountMeter(account);
|
||||
}
|
||||
|
||||
const providerAccountBalanceBreakdownMeterIds = new Set(["granted_balance", "topped_up_balance"]);
|
||||
|
||||
function providerAccountSnapshotForOverview(account: ProviderAccountSnapshot): ProviderAccountSnapshot {
|
||||
const hasTotalBalance = account.meters.some(
|
||||
(meter) => meter.kind === "balance" && meter.id.trim().toLowerCase() === "balance"
|
||||
);
|
||||
if (!hasTotalBalance) {
|
||||
return account;
|
||||
}
|
||||
const meters = account.meters.filter(
|
||||
(meter) => meter.kind !== "balance" || !providerAccountBalanceBreakdownMeterIds.has(meter.id.trim().toLowerCase())
|
||||
);
|
||||
return meters.length === account.meters.length ? account : { ...account, meters };
|
||||
}
|
||||
|
||||
function providerAccountSelectionMatches(account: ProviderAccountSnapshot, values: ReadonlySet<string>): boolean {
|
||||
return values.has(providerAccountSnapshotKey(account)) || values.has(account.provider);
|
||||
}
|
||||
|
||||
@@ -242,38 +242,79 @@ test("OverviewView compacts large account bento cards before showing a summary t
|
||||
assert.doesNotMatch(html, /overview-account-bento-more/);
|
||||
});
|
||||
|
||||
test("OverviewView places compact bento account values below the meter label", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<OverviewView
|
||||
overviewWidgets={[{ enabled: true, id: "account", size: "4:2", type: "account-balance", variant: "cards" }]}
|
||||
providerAccounts={[
|
||||
{
|
||||
credentialId: "deepseek",
|
||||
meters: [
|
||||
{
|
||||
id: "balance",
|
||||
kind: "balance",
|
||||
label: "Balance",
|
||||
remaining: 21.59,
|
||||
unit: "CNY"
|
||||
}
|
||||
],
|
||||
provider: "DeepSeek",
|
||||
source: "standard",
|
||||
status: "ok",
|
||||
updatedAt: "2026-06-30T00:00:00.000Z"
|
||||
},
|
||||
...accountSnapshots()
|
||||
]}
|
||||
refreshProviderAccounts={() => undefined}
|
||||
setUsageRange={() => undefined}
|
||||
usageRange="30d"
|
||||
usageStats={usageStats("30d")}
|
||||
onWidgetsChange={() => undefined}
|
||||
/>
|
||||
);
|
||||
test("OverviewView uses height-aware balance layouts at every account card size", () => {
|
||||
const deepSeekAccount: ProviderAccountSnapshot = {
|
||||
credentialId: "deepseek",
|
||||
meters: [
|
||||
{
|
||||
id: "balance",
|
||||
kind: "balance",
|
||||
label: "Balance",
|
||||
remaining: 21.59,
|
||||
unit: "CNY"
|
||||
},
|
||||
{
|
||||
id: "granted_balance",
|
||||
kind: "balance",
|
||||
label: "Granted balance",
|
||||
remaining: 20,
|
||||
unit: "CNY"
|
||||
},
|
||||
{
|
||||
id: "topped_up_balance",
|
||||
kind: "balance",
|
||||
label: "Topped-up balance",
|
||||
remaining: 1.59,
|
||||
unit: "CNY"
|
||||
}
|
||||
],
|
||||
provider: "DeepSeek",
|
||||
source: "standard",
|
||||
status: "ok",
|
||||
updatedAt: "2026-06-30T00:00:00.000Z"
|
||||
};
|
||||
const cardSizes = [
|
||||
{ columnClass: "col-span-1", layout: "compact", rowClass: "row-span-1", size: "1:1" },
|
||||
{ columnClass: "col-span-2", layout: "compact", rowClass: "row-span-1", size: "2:1" },
|
||||
{ columnClass: "col-span-1", layout: "expanded", rowClass: "row-span-2", size: "1:2" },
|
||||
{ columnClass: "col-span-2", layout: "expanded", rowClass: "row-span-2", size: "2:2" }
|
||||
] as const;
|
||||
|
||||
assert.match(html, /Balance<\/div><div class="mt-0\.5 truncate text-\[18px\][^"]*">¥21\.59<\/div>/);
|
||||
for (const cardSize of cardSizes) {
|
||||
const html = renderToStaticMarkup(
|
||||
<OverviewView
|
||||
overviewWidgets={[{
|
||||
accountCardSizes: { "DeepSeek::deepseek": cardSize.size },
|
||||
enabled: true,
|
||||
id: "account",
|
||||
size: "4:2",
|
||||
type: "account-balance",
|
||||
variant: "cards"
|
||||
}]}
|
||||
providerAccounts={[deepSeekAccount, accountSnapshots()[1]]}
|
||||
refreshProviderAccounts={() => undefined}
|
||||
setUsageRange={() => undefined}
|
||||
usageRange="30d"
|
||||
usageStats={usageStats("30d")}
|
||||
onWidgetsChange={() => undefined}
|
||||
/>
|
||||
);
|
||||
const cardStart = html.indexOf('data-provider-account-sortable-id="DeepSeek::deepseek"');
|
||||
assert.ok(cardStart >= 0, `DeepSeek card should render at size ${cardSize.size}`);
|
||||
const nextCardStart = html.indexOf('data-provider-account-sortable-id=', cardStart + 1);
|
||||
const cardHtml = html.slice(cardStart, nextCardStart >= 0 ? nextCardStart : undefined);
|
||||
|
||||
assert.match(cardHtml, new RegExp(`overview-account-bento-tile[^"]*${cardSize.columnClass}[^"]*${cardSize.rowClass}[^"]*" data-account-status="ok" data-provider-account-card-layout="${cardSize.layout}"`));
|
||||
assert.match(cardHtml, /Balance/);
|
||||
assert.match(cardHtml, /¥21\.59/);
|
||||
assert.doesNotMatch(html, /Granted balance|Topped-up balance/);
|
||||
assert.doesNotMatch(cardHtml, /overflow-y-auto|176px/);
|
||||
if (cardSize.layout === "compact") {
|
||||
assert.match(cardHtml, /data-provider-account-compact-meter="true"/);
|
||||
} else {
|
||||
assert.doesNotMatch(cardHtml, /data-provider-account-compact-meter="true"/);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("OverviewView applies manual bento account card sizes", () => {
|
||||
|
||||
Reference in New Issue
Block a user