feat(h5): show subscription quota totals in balance popover
Memind CI / Test, build, and release guards (push) Successful in 4m50s

Display plan total, remaining, and used amounts with plain comma-separated numbers so H5 balance details match backend token accounting.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-28 17:37:26 +08:00
parent 3df2c62c3c
commit e85b9b986a
+124 -98
View File
@@ -11,22 +11,13 @@ function formatYuan(cents: number) {
return `¥${(cents / 100).toFixed(2)}`;
}
function formatTokenCount(tokens: number) {
if (tokens >= 100_000) {
return `${(tokens / 10_000).toFixed(1).replace(/\.0$/, '')}`;
}
if (tokens >= 10_000) {
return `${(tokens / 10_000).toFixed(2).replace(/\.?0+$/, '')}`;
}
if (tokens >= 1_000) {
return `${(tokens / 1_000).toFixed(1).replace(/\.0$/, '')}k`;
}
return tokens.toLocaleString('zh-CN');
function formatQuotaAmount(tokens: number) {
return tokens.toLocaleString('en-US');
}
function formatCallsApprox(tokens: number) {
const calls = Math.floor(tokens / 3000);
return `${calls}`;
function formatTokenQuota(limit: number, used: number) {
const remaining = Math.max(0, limit - used);
return { total: limit, remaining, used };
}
function formatImageQuota(sub: ActiveSubscription) {
@@ -39,6 +30,59 @@ function formatImageQuota(sub: ActiveSubscription) {
return { unlimited: false, remaining, total, used };
}
type QuotaUsageRowsProps = {
totalLabel: string;
totalValue: string;
remainingValue: string;
usedValue: string;
total: number;
remaining: number;
used: number;
};
function QuotaUsageRows({
totalLabel,
totalValue,
remainingValue,
usedValue,
total,
remaining,
used,
}: QuotaUsageRowsProps) {
return (
<>
<div className="balance-popover-row">
<span className="balance-popover-label balance-popover-label-muted">{totalLabel}</span>
<strong>{totalValue}</strong>
</div>
<div className="balance-popover-row">
<span className="balance-popover-label">
<span className="balance-dot balance-dot-remain" aria-hidden="true" />
</span>
<strong>{remainingValue}</strong>
</div>
<div className="balance-popover-row">
<span className="balance-popover-label">
<span className="balance-dot balance-dot-spent" aria-hidden="true" />
使
</span>
<strong>{usedValue}</strong>
</div>
<div className="balance-popover-bar" aria-hidden="true">
<div
className="balance-popover-bar-spent"
style={{ width: `${total > 0 ? (used / total) * 100 : 0}%` }}
/>
<div
className="balance-popover-bar-remain"
style={{ width: `${total > 0 ? (remaining / total) * 100 : 0}%` }}
/>
</div>
</>
);
}
type BalanceRingProps = {
balanceCents: number;
totalCreditCents?: number;
@@ -80,6 +124,8 @@ export function BalanceRing({
const subEmpty = hasSub && !subUnlimited && subRemaining <= 0;
const imageQuota = subscription ? formatImageQuota(subscription) : null;
const tokenQuota = subscription && subLimit > 0 ? formatTokenQuota(subLimit, subUsed) : null;
const showTokenQuota = Boolean(tokenQuota);
const showImageQuota = Boolean(
subscription && (imageQuota?.unlimited || (imageQuota?.total ?? 0) > 0 || (subscription.periodImagesBonus ?? 0) > 0),
);
@@ -246,34 +292,17 @@ export function BalanceRing({
<span className="balance-popover-label balance-popover-label-muted"></span>
<span></span>
</div>
) : (
<>
<div className="balance-popover-row">
<span className="balance-popover-label">
<span className="balance-dot balance-dot-remain" aria-hidden="true" />
</span>
<strong>{formatCallsApprox(subRemaining)}</strong>
</div>
<div className="balance-popover-row">
<span className="balance-popover-label">
<span className="balance-dot balance-dot-spent" aria-hidden="true" />
使
</span>
<strong>{formatCallsApprox(subUsed)}</strong>
</div>
<div className="balance-popover-bar" aria-hidden="true">
<div
className="balance-popover-bar-spent"
style={{ width: `${subLimit > 0 ? (subUsed / subLimit) * 100 : 0}%` }}
/>
<div
className="balance-popover-bar-remain"
style={{ width: `${subLimit > 0 ? (subRemaining / subLimit) * 100 : 0}%` }}
/>
</div>
</>
)}
) : tokenQuota ? (
<QuotaUsageRows
totalLabel="套餐总量"
totalValue={formatQuotaAmount(tokenQuota.total)}
remainingValue={formatQuotaAmount(tokenQuota.remaining)}
usedValue={formatQuotaAmount(tokenQuota.used)}
total={tokenQuota.total}
remaining={tokenQuota.remaining}
used={tokenQuota.used}
/>
) : null}
{periodEndDate && (
<p className="balance-popover-hint"> {periodEndDate}</p>
@@ -302,44 +331,60 @@ export function BalanceRing({
) : (
<>
<h4></h4>
<div className="balance-popover-row">
<span className="balance-popover-label">
<span className="balance-dot balance-dot-remain" aria-hidden="true" />
</span>
<strong>{formatYuan(balanceCents)}</strong>
</div>
<div className="balance-popover-row">
<span className="balance-popover-label">
<span className="balance-dot balance-dot-spent" aria-hidden="true" />
</span>
<strong>{formatYuan(spent)}</strong>
</div>
<div className="balance-popover-row">
<span className="balance-popover-label balance-popover-label-muted"></span>
<span>{formatYuan(total)}</span>
</div>
<div className="balance-popover-bar" aria-hidden="true">
<div className="balance-popover-bar-spent" style={{ width: `${spentPct}%` }} />
<div className="balance-popover-bar-remain" style={{ width: `${remainPct}%` }} />
{showTokenQuota && tokenQuota ? (
<div className="balance-popover-section" style={{ marginTop: 0, paddingTop: 0, borderTop: 'none' }}>
<h5>
{subscription?.planType ? ` · ${PLAN_LABELS[subscription.planType] ?? subscription.planType}` : ''}
</h5>
<QuotaUsageRows
totalLabel="套餐总量"
totalValue={formatQuotaAmount(tokenQuota.total)}
remainingValue={formatQuotaAmount(tokenQuota.remaining)}
usedValue={formatQuotaAmount(tokenQuota.used)}
total={tokenQuota.total}
remaining={tokenQuota.remaining}
used={tokenQuota.used}
/>
<p className="balance-popover-hint"></p>
</div>
) : null}
<div className="balance-popover-section" style={showTokenQuota ? undefined : { marginTop: 0, paddingTop: 0, borderTop: 'none' }}>
<h5></h5>
<div className="balance-popover-row">
<span className="balance-popover-label">
<span className="balance-dot balance-dot-remain" aria-hidden="true" />
</span>
<strong>{formatYuan(balanceCents)}</strong>
</div>
<div className="balance-popover-row">
<span className="balance-popover-label">
<span className="balance-dot balance-dot-spent" aria-hidden="true" />
</span>
<strong>{formatYuan(spent)}</strong>
</div>
<div className="balance-popover-row">
<span className="balance-popover-label balance-popover-label-muted"></span>
<span>{formatYuan(total)}</span>
</div>
<div className="balance-popover-bar" aria-hidden="true">
<div className="balance-popover-bar-spent" style={{ width: `${spentPct}%` }} />
<div className="balance-popover-bar-remain" style={{ width: `${remainPct}%` }} />
</div>
</div>
{(balanceLow || balanceEmpty) && (
<p className="balance-popover-warning"></p>
)}
{subscription?.planType === 'free' && subscription.periodTokensLimit > 0 && (
<p className="balance-popover-hint">
{formatCallsApprox(Math.max(0, subscription.periodTokensLimit - subscription.periodTokensUsed))}
</p>
)}
</>
)}
<div className="balance-popover-section">
<h5>AI </h5>
<div className="balance-popover-row">
<span className="balance-popover-label balance-popover-label-muted"> Token</span>
<span>{formatTokenCount(tokensUsed)}</span>
<span className="balance-popover-label balance-popover-label-muted"></span>
<span>{formatQuotaAmount(tokensUsed)}</span>
</div>
<p className="balance-popover-hint"></p>
</div>
@@ -354,39 +399,20 @@ export function BalanceRing({
</div>
) : (
<>
<div className="balance-popover-row">
<span className="balance-popover-label">
<span className="balance-dot balance-dot-remain" aria-hidden="true" />
</span>
<strong>{imageQuota.remaining ?? 0} </strong>
</div>
<div className="balance-popover-row">
<span className="balance-popover-label">
<span className="balance-dot balance-dot-spent" aria-hidden="true" />
使
</span>
<strong>{imageQuota.used} </strong>
</div>
<QuotaUsageRows
totalLabel="套餐总量"
totalValue={`${imageQuota.total ?? 0}`}
remainingValue={`${imageQuota.remaining ?? 0}`}
usedValue={`${imageQuota.used}`}
total={imageQuota.total ?? 0}
remaining={imageQuota.remaining ?? 0}
used={imageQuota.used}
/>
{(subscription?.periodImagesBonus ?? 0) > 0 ? (
<p className="balance-popover-hint">
{subscription!.periodImagesLimit ?? 0} + {subscription!.periodImagesBonus}
</p>
) : null}
<div className="balance-popover-bar" aria-hidden="true">
<div
className="balance-popover-bar-spent"
style={{
width: `${imageQuota.total ? (imageQuota.used / imageQuota.total) * 100 : 0}%`,
}}
/>
<div
className="balance-popover-bar-remain"
style={{
width: `${imageQuota.total ? ((imageQuota.remaining ?? 0) / imageQuota.total) * 100 : 0}%`,
}}
/>
</div>
</>
)}
{(imageEmpty || imageLow) && (