fix(ui): 修复紧凑账户余额卡片裁切

This commit is contained in:
jesieleo
2026-08-13 19:32:03 +08:00
parent 186fa619d2
commit 16f0868f40
2 changed files with 99 additions and 43 deletions
@@ -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", () => {