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)}`; return `¥${(cents / 100).toFixed(2)}`;
} }
function formatTokenCount(tokens: number) { function formatQuotaAmount(tokens: number) {
if (tokens >= 100_000) { return tokens.toLocaleString('en-US');
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 formatCallsApprox(tokens: number) { function formatTokenQuota(limit: number, used: number) {
const calls = Math.floor(tokens / 3000); const remaining = Math.max(0, limit - used);
return `${calls}`; return { total: limit, remaining, used };
} }
function formatImageQuota(sub: ActiveSubscription) { function formatImageQuota(sub: ActiveSubscription) {
@@ -39,6 +30,59 @@ function formatImageQuota(sub: ActiveSubscription) {
return { unlimited: false, remaining, total, used }; 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 = { type BalanceRingProps = {
balanceCents: number; balanceCents: number;
totalCreditCents?: number; totalCreditCents?: number;
@@ -80,6 +124,8 @@ export function BalanceRing({
const subEmpty = hasSub && !subUnlimited && subRemaining <= 0; const subEmpty = hasSub && !subUnlimited && subRemaining <= 0;
const imageQuota = subscription ? formatImageQuota(subscription) : null; const imageQuota = subscription ? formatImageQuota(subscription) : null;
const tokenQuota = subscription && subLimit > 0 ? formatTokenQuota(subLimit, subUsed) : null;
const showTokenQuota = Boolean(tokenQuota);
const showImageQuota = Boolean( const showImageQuota = Boolean(
subscription && (imageQuota?.unlimited || (imageQuota?.total ?? 0) > 0 || (subscription.periodImagesBonus ?? 0) > 0), 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 className="balance-popover-label balance-popover-label-muted"></span>
<span></span> <span></span>
</div> </div>
) : ( ) : tokenQuota ? (
<> <QuotaUsageRows
<div className="balance-popover-row"> totalLabel="套餐总量"
<span className="balance-popover-label"> totalValue={formatQuotaAmount(tokenQuota.total)}
<span className="balance-dot balance-dot-remain" aria-hidden="true" /> remainingValue={formatQuotaAmount(tokenQuota.remaining)}
usedValue={formatQuotaAmount(tokenQuota.used)}
</span> total={tokenQuota.total}
<strong>{formatCallsApprox(subRemaining)}</strong> remaining={tokenQuota.remaining}
</div> used={tokenQuota.used}
<div className="balance-popover-row"> />
<span className="balance-popover-label"> ) : null}
<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>
</>
)}
{periodEndDate && ( {periodEndDate && (
<p className="balance-popover-hint"> {periodEndDate}</p> <p className="balance-popover-hint"> {periodEndDate}</p>
@@ -302,44 +331,60 @@ export function BalanceRing({
) : ( ) : (
<> <>
<h4></h4> <h4></h4>
<div className="balance-popover-row"> {showTokenQuota && tokenQuota ? (
<span className="balance-popover-label"> <div className="balance-popover-section" style={{ marginTop: 0, paddingTop: 0, borderTop: 'none' }}>
<span className="balance-dot balance-dot-remain" aria-hidden="true" /> <h5>
</span> {subscription?.planType ? ` · ${PLAN_LABELS[subscription.planType] ?? subscription.planType}` : ''}
<strong>{formatYuan(balanceCents)}</strong> </h5>
</div> <QuotaUsageRows
<div className="balance-popover-row"> totalLabel="套餐总量"
<span className="balance-popover-label"> totalValue={formatQuotaAmount(tokenQuota.total)}
<span className="balance-dot balance-dot-spent" aria-hidden="true" /> remainingValue={formatQuotaAmount(tokenQuota.remaining)}
usedValue={formatQuotaAmount(tokenQuota.used)}
</span> total={tokenQuota.total}
<strong>{formatYuan(spent)}</strong> remaining={tokenQuota.remaining}
</div> used={tokenQuota.used}
<div className="balance-popover-row"> />
<span className="balance-popover-label balance-popover-label-muted"></span> <p className="balance-popover-hint"></p>
<span>{formatYuan(total)}</span> </div>
</div> ) : null}
<div className="balance-popover-bar" aria-hidden="true"> <div className="balance-popover-section" style={showTokenQuota ? undefined : { marginTop: 0, paddingTop: 0, borderTop: 'none' }}>
<div className="balance-popover-bar-spent" style={{ width: `${spentPct}%` }} /> <h5></h5>
<div className="balance-popover-bar-remain" style={{ width: `${remainPct}%` }} /> <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> </div>
{(balanceLow || balanceEmpty) && ( {(balanceLow || balanceEmpty) && (
<p className="balance-popover-warning"></p> <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"> <div className="balance-popover-section">
<h5>AI </h5> <h5>AI </h5>
<div className="balance-popover-row"> <div className="balance-popover-row">
<span className="balance-popover-label balance-popover-label-muted"> Token</span> <span className="balance-popover-label balance-popover-label-muted"></span>
<span>{formatTokenCount(tokensUsed)}</span> <span>{formatQuotaAmount(tokensUsed)}</span>
</div> </div>
<p className="balance-popover-hint"></p> <p className="balance-popover-hint"></p>
</div> </div>
@@ -354,39 +399,20 @@ export function BalanceRing({
</div> </div>
) : ( ) : (
<> <>
<div className="balance-popover-row"> <QuotaUsageRows
<span className="balance-popover-label"> totalLabel="套餐总量"
<span className="balance-dot balance-dot-remain" aria-hidden="true" /> totalValue={`${imageQuota.total ?? 0}`}
remainingValue={`${imageQuota.remaining ?? 0}`}
</span> usedValue={`${imageQuota.used}`}
<strong>{imageQuota.remaining ?? 0} </strong> total={imageQuota.total ?? 0}
</div> remaining={imageQuota.remaining ?? 0}
<div className="balance-popover-row"> used={imageQuota.used}
<span className="balance-popover-label"> />
<span className="balance-dot balance-dot-spent" aria-hidden="true" />
使
</span>
<strong>{imageQuota.used} </strong>
</div>
{(subscription?.periodImagesBonus ?? 0) > 0 ? ( {(subscription?.periodImagesBonus ?? 0) > 0 ? (
<p className="balance-popover-hint"> <p className="balance-popover-hint">
{subscription!.periodImagesLimit ?? 0} + {subscription!.periodImagesBonus} {subscription!.periodImagesLimit ?? 0} + {subscription!.periodImagesBonus}
</p> </p>
) : null} ) : 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) && ( {(imageEmpty || imageLow) && (