From f226f47545f40c715535a616f5bafc37accd49a9 Mon Sep 17 00:00:00 2001 From: Jeff Gu Date: Mon, 13 Apr 2026 15:09:37 +0800 Subject: [PATCH] =?UTF-8?q?refactor(tokens):=20=E7=AE=80=E5=8C=96=E9=85=8D?= =?UTF-8?q?=E9=A2=9D=E8=BE=93=E5=85=A5=E9=80=BB=E8=BE=91=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/lib/billing.ts | 7 +---- frontend/src/pages/Tokens.tsx | 56 +++++++++++++++++++++++------------ 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/frontend/src/lib/billing.ts b/frontend/src/lib/billing.ts index ea2adf1..83ec268 100644 --- a/frontend/src/lib/billing.ts +++ b/frontend/src/lib/billing.ts @@ -5,10 +5,6 @@ export const DEFAULT_BILLING_DISPLAY_DECIMALS = 6; export const MIN_BILLING_DISPLAY_DECIMALS = 0; export const MAX_BILLING_DISPLAY_DECIMALS = 9; -const trimTrailingZeros = (value: string): string => { - return value.replace(/(\.\d*?[1-9])0+$/u, "$1").replace(/\.0+$/u, "").replace(/\.$/u, ""); -}; - export const normalizeBillingDisplayDecimals = (value: unknown): number => { const parsed = typeof value === "number" && Number.isFinite(value) ? Math.round(value) @@ -38,6 +34,5 @@ export const usdToRawBilling = (value: string | number): number => { export const formatRawBillingInput = (value: number, displayDecimals = DEFAULT_BILLING_DISPLAY_DECIMALS): string => { const decimals = normalizeBillingDisplayDecimals(displayDecimals); - const formatted = rawBillingToUsd(value).toFixed(decimals); - return trimTrailingZeros(formatted) || "0"; + return rawBillingToUsd(value).toFixed(decimals); }; diff --git a/frontend/src/pages/Tokens.tsx b/frontend/src/pages/Tokens.tsx index c3cb50e..8752dd9 100644 --- a/frontend/src/pages/Tokens.tsx +++ b/frontend/src/pages/Tokens.tsx @@ -10,7 +10,7 @@ import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { useToast } from '@/components/ui/use-toast' import { useBillingConfig } from '@/hooks/use-billing-config' -import { formatRawBillingInput, usdToRawBilling } from '@/lib/billing' +import { DEFAULT_BILLING_DISPLAY_DECIMALS, rawBillingToUsd, usdToRawBilling } from '@/lib/billing' import { formatCurrency, copyToClipboard, generateTokenKey, cn } from '@/lib/utils' import { Plus, @@ -33,6 +33,21 @@ import { Checkbox } from '@/components/ui/checkbox' import { PageContainer } from '@/components/ui/page-container' type EditMode = 'form' | 'json' +const INTEGER_QUOTA_PATTERN = /^\d*$/ + +const formatQuotaInputValue = (value: number): string => { + return String(Math.max(0, Math.round(rawBillingToUsd(value)))) +} + +const parseQuotaInputValue = (value: string): number => { + const parsed = Number.parseInt(value || '0', 10) + return usdToRawBilling(Number.isFinite(parsed) ? Math.max(0, parsed) : 0) +} + +const formatAvailableQuota = (value: number): string => { + const usdValue = Math.max(0, Math.floor(rawBillingToUsd(value))) + return `$${usdValue}` +} export function Tokens({ createMode = false, @@ -58,13 +73,12 @@ export function Tokens({ const [selectedChannels, setSelectedChannels] = useState([]) const [searchQuery, setSearchQuery] = useState('') const [openMenu, setOpenMenu] = useState(null) - const [quotaInputValue, setQuotaInputValue] = useState('0') + const [quotaInputValue, setQuotaInputValue] = useState(formatQuotaInputValue(0)) const { addToast } = useToast() const queryClient = useQueryClient() const { data: billingConfig } = useBillingConfig() - const displayDecimals = billingConfig?.displayDecimals ?? 6 - const quotaInputStep = displayDecimals > 0 ? `0.${'0'.repeat(displayDecimals - 1)}1` : '1' + const displayDecimals = billingConfig?.displayDecimals ?? DEFAULT_BILLING_DISPLAY_DECIMALS const { data, isLoading, isFetching, refetch } = useQuery({ queryKey: ['tokens'], @@ -94,9 +108,9 @@ export function Tokens({ setFormData(config) setJsonValue(JSON.stringify(config, null, 2)) setSelectedChannels(config.channel_keys || []) - setQuotaInputValue(formatRawBillingInput(config.total_quota || 0, displayDecimals)) + setQuotaInputValue(formatQuotaInputValue(config.total_quota || 0)) setView('form') - }, [displayDecimals]) + }, []) // Close menu when clicking outside useEffect(() => { @@ -152,7 +166,7 @@ export function Tokens({ setSelectedChannels([]) setEditingKey(null) setEditMode('form') - setQuotaInputValue('0') + setQuotaInputValue(formatQuotaInputValue(0)) }, []) useEffect(() => { @@ -265,7 +279,7 @@ export function Tokens({ } setFormData(normalizedConfig) setSelectedChannels(normalizedConfig.channel_keys || []) - setQuotaInputValue(formatRawBillingInput(normalizedConfig.total_quota || 0, displayDecimals)) + setQuotaInputValue(formatQuotaInputValue(normalizedConfig.total_quota || 0)) setEditMode('form') } catch { addToast('JSON格式错误', 'error') @@ -359,8 +373,10 @@ export function Tokens({ {filteredData?.map((token) => { const config = typeof token.value === 'string' ? JSON.parse(token.value) : token.value const channelKeys = config.channel_keys || [] + const usedQuota = token.usage || 0 + const availableQuota = Math.max((config.total_quota || 0) - usedQuota, 0) const usagePercent = config.total_quota > 0 - ? Math.min(100, ((token.usage || 0) / config.total_quota) * 100) + ? Math.min(100, (usedQuota / config.total_quota) * 100) : 0 const isMenuOpen = openMenu === token.key @@ -426,7 +442,7 @@ export function Tokens({ 渠道: {channelKeys.length === 0 ? '全部' : `${channelKeys.length}个`} - 配额: {formatCurrency(token.usage || 0, displayDecimals)}/{formatCurrency(config.total_quota || 0, displayDecimals)} + 已用/可用: {formatCurrency(usedQuota, displayDecimals)}/{formatAvailableQuota(availableQuota)}
@@ -461,7 +477,7 @@ export function Tokens({
- {formatCurrency(token.usage || 0, displayDecimals)} / {formatCurrency(config.total_quota || 0, displayDecimals)} + 已用 {formatCurrency(usedQuota, displayDecimals)} / 可用 {formatAvailableQuota(availableQuota)} -

使用配额

+

可用额度

- 输入美元金额展示值,系统会按原始整数计费单位保存;当前默认展示 {displayDecimals} 位小数。 + 令牌可用的总额度,单位为美元,当已用额度达到总额度时,令牌将无法调用模型。

{ const nextValue = e.target.value + if (!INTEGER_QUOTA_PATTERN.test(nextValue)) { + return + } setQuotaInputValue(nextValue) - setFormData({ ...formData, total_quota: usdToRawBilling(nextValue) }) + setFormData({ ...formData, total_quota: parseQuotaInputValue(nextValue) }) }} - placeholder={displayDecimals > 0 ? `1.${'0'.repeat(displayDecimals)}` : '1'} - min="0" - step={quotaInputStep} + placeholder="1" />
{quotaPresets.map((preset) => ( @@ -651,7 +669,7 @@ export function Tokens({ type="button" onClick={() => { setFormData({ ...formData, total_quota: preset.value }) - setQuotaInputValue(formatRawBillingInput(preset.value, displayDecimals)) + setQuotaInputValue(formatQuotaInputValue(preset.value)) }} className={cn( "px-4 py-2 rounded-lg text-sm font-medium transition-all",