Remove recent billing list from balance popover.
Keep the popover focused on balance summary and token usage without fetching usage records on open. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,11 +1,8 @@
|
|||||||
import { useEffect, useRef, useState, type CSSProperties } from 'react';
|
import { useEffect, useRef, useState, type CSSProperties } from 'react';
|
||||||
import { getMyUsage } from '../api/client';
|
|
||||||
import type { UsageRecord } from '../types';
|
|
||||||
|
|
||||||
const RING_R = 16;
|
const RING_R = 16;
|
||||||
const CIRC = 2 * Math.PI * RING_R;
|
const CIRC = 2 * Math.PI * RING_R;
|
||||||
const POPOVER_WIDTH = 260;
|
const POPOVER_WIDTH = 260;
|
||||||
const RECENT_USAGE_LIMIT = 5;
|
|
||||||
|
|
||||||
function formatYuan(cents: number) {
|
function formatYuan(cents: number) {
|
||||||
return `¥${(cents / 100).toFixed(2)}`;
|
return `¥${(cents / 100).toFixed(2)}`;
|
||||||
@@ -24,30 +21,6 @@ function formatTokenCount(tokens: number) {
|
|||||||
return tokens.toLocaleString('zh-CN');
|
return tokens.toLocaleString('zh-CN');
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTokenCompact(tokens: number) {
|
|
||||||
if (tokens >= 1_000) {
|
|
||||||
return `${(tokens / 1_000).toFixed(1).replace(/\.0$/, '')}k`;
|
|
||||||
}
|
|
||||||
return String(tokens);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatUsageTime(ts: number) {
|
|
||||||
const date = new Date(ts);
|
|
||||||
const now = new Date();
|
|
||||||
const isToday =
|
|
||||||
date.getFullYear() === now.getFullYear() &&
|
|
||||||
date.getMonth() === now.getMonth() &&
|
|
||||||
date.getDate() === now.getDate();
|
|
||||||
const time = date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
|
||||||
if (isToday) return `今天 ${time}`;
|
|
||||||
return date.toLocaleString('zh-CN', {
|
|
||||||
month: 'numeric',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
type BalanceRingProps = {
|
type BalanceRingProps = {
|
||||||
balanceCents: number;
|
balanceCents: number;
|
||||||
totalCreditCents?: number;
|
totalCreditCents?: number;
|
||||||
@@ -63,9 +36,6 @@ export function BalanceRing({
|
|||||||
}: BalanceRingProps) {
|
}: BalanceRingProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [popoverStyle, setPopoverStyle] = useState<CSSProperties>({});
|
const [popoverStyle, setPopoverStyle] = useState<CSSProperties>({});
|
||||||
const [usageRecords, setUsageRecords] = useState<UsageRecord[] | null>(null);
|
|
||||||
const [usageLoading, setUsageLoading] = useState(false);
|
|
||||||
const [usageError, setUsageError] = useState<string | null>(null);
|
|
||||||
const wrapRef = useRef<HTMLDivElement>(null);
|
const wrapRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const total = Math.max(totalCreditCents ?? balanceCents, balanceCents, 0);
|
const total = Math.max(totalCreditCents ?? balanceCents, balanceCents, 0);
|
||||||
@@ -78,7 +48,6 @@ export function BalanceRing({
|
|||||||
|
|
||||||
const low = balanceCents > 0 && balanceCents <= 100;
|
const low = balanceCents > 0 && balanceCents <= 100;
|
||||||
const empty = balanceCents <= 0;
|
const empty = balanceCents <= 0;
|
||||||
const showRecentUsage = low || empty;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
@@ -118,36 +87,6 @@ export function BalanceRing({
|
|||||||
};
|
};
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
let cancelled = false;
|
|
||||||
setUsageLoading(true);
|
|
||||||
setUsageError(null);
|
|
||||||
void getMyUsage()
|
|
||||||
.then((records) => {
|
|
||||||
if (!cancelled) setUsageRecords(records);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (!cancelled) {
|
|
||||||
setUsageError(err instanceof Error ? err.message : '加载失败');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (!cancelled) setUsageLoading(false);
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [open, tokensUsed, balanceCents]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) return;
|
|
||||||
setUsageRecords(null);
|
|
||||||
setUsageError(null);
|
|
||||||
setUsageLoading(false);
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const recentUsage = (usageRecords ?? []).slice(0, RECENT_USAGE_LIMIT);
|
|
||||||
const centerLabel = total <= 0 ? '—' : empty ? '0%' : `${pct}%`;
|
const centerLabel = total <= 0 ? '—' : empty ? '0%' : `${pct}%`;
|
||||||
const ariaLabel = total > 0 ? `账户额度,剩余 ${pct}%` : '账户额度';
|
const ariaLabel = total > 0 ? `账户额度,剩余 ${pct}%` : '账户额度';
|
||||||
|
|
||||||
@@ -219,32 +158,6 @@ export function BalanceRing({
|
|||||||
<p className="balance-popover-hint">按人民币结算,每次对话按实际用量扣费</p>
|
<p className="balance-popover-hint">按人民币结算,每次对话按实际用量扣费</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
className={`balance-popover-section balance-popover-usage${showRecentUsage ? ' expanded' : ''}`}
|
|
||||||
>
|
|
||||||
<h5>近期扣费</h5>
|
|
||||||
{usageLoading ? (
|
|
||||||
<p className="balance-popover-muted">加载中…</p>
|
|
||||||
) : usageError ? (
|
|
||||||
<p className="balance-popover-muted">{usageError}</p>
|
|
||||||
) : recentUsage.length === 0 ? (
|
|
||||||
<p className="balance-popover-muted">暂无扣费记录</p>
|
|
||||||
) : (
|
|
||||||
<ul className="balance-usage-list">
|
|
||||||
{recentUsage.map((row) => (
|
|
||||||
<li key={row.id} className="balance-usage-item">
|
|
||||||
<span className="balance-usage-time">{formatUsageTime(row.createdAt)}</span>
|
|
||||||
<span className="balance-usage-tokens">
|
|
||||||
输入 {formatTokenCompact(row.inputTokens)} / 输出{' '}
|
|
||||||
{formatTokenCompact(row.outputTokens)}
|
|
||||||
</span>
|
|
||||||
<strong className="balance-usage-cost">{formatYuan(row.costCents)}</strong>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="balance-popover-cta"
|
className="balance-popover-cta"
|
||||||
|
|||||||
@@ -2770,54 +2770,6 @@ body,
|
|||||||
color: var(--color-text-faint);
|
color: var(--color-text-faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
.balance-popover-muted {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--color-text-faint);
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-usage-list {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-usage-item {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
gap: 2px 8px;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-usage-time {
|
|
||||||
color: var(--color-text-faint);
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-usage-tokens {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-usage-cost {
|
|
||||||
color: #e8a44a;
|
|
||||||
font-size: 11px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-usage-credit {
|
|
||||||
color: #4caf7d;
|
|
||||||
font-size: 11px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-popover-usage:not(.expanded) .balance-usage-list {
|
|
||||||
max-height: 96px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-popover-cta {
|
.balance-popover-cta {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|||||||
Reference in New Issue
Block a user