import { useState, useMemo } from 'react'; import { VChart } from '@visactor/react-vchart'; import { useThemeLabelColor } from '../useThemeLabelColor.js'; interface SiteDistributionData { siteName: string; platform?: string | null; totalBalance?: number; totalSpend?: number; accountCount?: number; spend?: number; calls?: number; tokens?: number; } interface SiteDistributionChartProps { data: SiteDistributionData[]; loading?: boolean; } type ViewMode = 'balance' | 'spend'; function coerceDatumRecord(datum: unknown): Record { return datum && typeof datum === 'object' ? datum as Record : {}; } function safeNumber(value: unknown): number { if (typeof value !== 'number' || Number.isNaN(value) || !Number.isFinite(value)) return 0; return value; } function formatPercentLabel(datum: unknown): string { const item = coerceDatumRecord(datum); const percent = safeNumber(item._percent_); if (percent <= 0) return ''; return `${percent.toFixed(percent >= 10 ? 0 : 1)}%`; } function SkeletonCircle() { return (
{[...Array(4)].map((_, i) => (
))}
); } function EmptyState() { return (
暂无站点数据
添加站点后将自动展示分布图表
); } export default function SiteDistributionChart({ data, loading }: SiteDistributionChartProps) { const [viewMode, setViewMode] = useState('balance'); const labelColor = useThemeLabelColor(); const chartData = useMemo(() => { if (!data || data.length === 0) return []; return data.map((item) => ({ siteName: item.siteName, platform: item.platform, value: safeNumber(viewMode === 'balance' ? item.totalBalance : item.totalSpend ?? item.spend), accountCount: safeNumber(item.accountCount), calls: safeNumber(item.calls), })); }, [data, viewMode]); const hasData = chartData.length > 0 && chartData.some((d) => d.value > 0); const PIE_COLORS = ['#4f46e5', '#06b6d4', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6']; const spec = useMemo(() => { if (!hasData) return null; return { type: 'pie' as const, data: [{ id: 'siteData', values: chartData }], valueField: 'value', categoryField: 'siteName', outerRadius: 0.8, innerRadius: 0.55, pie: { style: { cornerRadius: 4, padAngle: 0.02 } }, label: { visible: true, position: 'outside', formatMethod: (_text: unknown, datum: unknown) => formatPercentLabel(datum), style: { fill: labelColor, fontSize: 12, stroke: 'transparent' }, }, legends: { visible: false }, tooltip: { mark: { content: [ { key: (datum: unknown) => { const item = coerceDatumRecord(datum); return String(item.siteName || '-'); }, value: (datum: unknown) => { const item = coerceDatumRecord(datum); const val = safeNumber(item.value); return `$${val.toFixed(2)}`; }, }, { key: '占比', value: (datum: unknown) => { const item = coerceDatumRecord(datum); const pct = safeNumber(item._percent_); return `${pct.toFixed(1)}%`; }, }, { key: '账户数', value: (datum: unknown) => { const item = coerceDatumRecord(datum); return String(item.accountCount || 0); }, }, { key: '调用', value: (datum: unknown) => { const item = coerceDatumRecord(datum); return String(item.calls || 0); }, }, ] as any, }, }, color: PIE_COLORS, animation: true, background: 'transparent', }; }, [chartData, hasData, labelColor]); const formatValue = (value: number): string => { if (viewMode === 'balance' && value === 0) return '$0.00'; if (value >= 1000) return `$${value.toFixed(2)}`; if (value >= 1) return `$${value.toFixed(3)}`; return `$${value.toFixed(6)}`; }; return (
{/* Header */}
站点分布
{/* Toggle buttons */}
{/* Content */} {loading ? ( ) : !hasData ? ( ) : (
{spec && }
{chartData.map((d, idx) => ( {d.siteName} {formatValue(d.value)} ))}
)}
); }