feat(h5): show subscription quota totals in balance popover
Memind CI / Test, build, and release guards (push) Successful in 4m50s
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:
+124
-98
@@ -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) && (
|
||||
|
||||
Reference in New Issue
Block a user