Files
qihuaiflow/frontend/src/pages/UserPortal.tsx
T

492 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback } from 'react';
import { request, adminAPI } from '@/api';
// ==================== Types ====================
interface UserOverview {
user: { id: string; username: string; email: string; avatar: string | null; role: string; membership_level: number; membership_expire_at: string | null; created_at: string; };
token_stats: { total_tokens: number; prompt_tokens: number; completion_tokens: number; call_count: number; };
daily_usage: { date: string; calls: number; tokens: number; }[];
tenants: { id: string; name: string; slug: string; status: string; role: string; }[];
recent_transactions: { id: string; type: string; amount: number; tokens: number; description: string; status: string; created_at: string; }[];
api_key_count: number;
}
interface ApiKey { id: string; key_name: string; key_prefix: string; status: string; created_at: string; last_used_at: string | null; }
interface Package { id: string; name: string; description: string; tokens: number; price: number; original_price: number | null; bonus_tokens: number; is_popular: boolean; }
interface Tenant { id: string; name: string; slug: string; description: string; status: string; max_members: number; max_storage_mb: number; max_api_calls_per_day: number; role: string; joined_at: string; member_count: number; }
interface UsageData { daily: { date: string; calls: number; tokens: number; prompt_tokens: number; completion_tokens: number; }[]; model_distribution: { model: string; calls: number; tokens: number; }[]; total: { calls: number; tokens: number; }; days: number; }
const API_BASE = '/app/api/portal';
// ==================== Component ====================
export default function UserPortal() {
const [activeTab, setActiveTab] = useState('overview');
const [loading, setLoading] = useState(false);
const tabs = [
{ key: 'overview', label: '概览', icon: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6' },
{ key: 'apikeys', label: 'API密钥', icon: 'M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z' },
{ key: 'recharge', label: '充值中心', icon: 'M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z' },
{ key: 'tenants', label: '我的租户', icon: 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5' },
{ key: 'analytics', label: '用量分析', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' },
];
return (
<div style={{ minHeight: '100vh', background: '#0d1117', color: '#e6edf3', padding: '24px' }}>
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
<h1 style={{ fontSize: '28px', marginBottom: '24px' }}>用户门户</h1>
{/* Tab Navigation */}
<div style={{ display: 'flex', gap: '4px', borderBottom: '1px solid #30363d', marginBottom: '24px' }}>
{tabs.map(tab => (
<button key={tab.key} onClick={() => setActiveTab(tab.key)}
style={{
padding: '10px 20px', background: 'none', border: 'none', borderBottom: activeTab === tab.key ? '2px solid #58a6ff' : '2px solid transparent',
color: activeTab === tab.key ? '#58a6ff' : '#8b949e', cursor: 'pointer', fontSize: '14px', fontWeight: 600,
}}>
{tab.label}
</button>
))}
</div>
{activeTab === 'overview' && <OverviewTab />}
{activeTab === 'apikeys' && <ApiKeysTab />}
{activeTab === 'recharge' && <RechargeTab />}
{activeTab === 'tenants' && <TenantsTab />}
{activeTab === 'analytics' && <AnalyticsTab />}
</div>
</div>
);
}
// ==================== Overview Tab ====================
function OverviewTab() {
const [data, setData] = useState<UserOverview | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
request<any>(`${API_BASE}/overview`).then(res => {
setData(res.data || res);
setLoading(false);
}).catch(() => setLoading(false));
}, []);
if (loading) return <div style={{ padding: '40px', textAlign: 'center', color: '#8b949e' }}>加载中...</div>;
if (!data) return <div style={{ padding: '40px', textAlign: 'center', color: '#f85149' }}>加载失败</div>;
const cards = [
{ label: '本月Token用量', value: data.token_stats.total_tokens.toLocaleString(), color: '#58a6ff' },
{ label: 'API调用次数', value: data.token_stats.call_count.toLocaleString(), color: '#3fb950' },
{ label: 'API密钥数', value: data.api_key_count.toString(), color: '#bc8cff' },
{ label: '所属租户', value: data.tenants.length.toString(), color: '#f0883e' },
];
return (
<div>
{/* Stats Cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '16px', marginBottom: '24px' }}>
{cards.map((c, i) => (
<div key={i} style={{ background: '#161b22', border: '1px solid #30363d', borderRadius: '12px', padding: '20px' }}>
<div style={{ color: c.color, fontSize: '28px', fontWeight: 700 }}>{c.value}</div>
<div style={{ color: '#8b949e', fontSize: '13px', marginTop: '4px' }}>{c.label}</div>
</div>
))}
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
{/* Daily Usage Chart */}
<div style={{ background: '#161b22', border: '1px solid #30363d', borderRadius: '12px', padding: '20px' }}>
<h3 style={{ fontSize: '16px', marginBottom: '16px' }}>近7天用量</h3>
{data.daily_usage.length > 0 ? (
<div style={{ display: 'flex', alignItems: 'flex-end', gap: '8px', height: '120px' }}>
{data.daily_usage.map((d, i) => {
const maxTokens = Math.max(...data.daily_usage.map(x => x.tokens), 1);
const height = (d.tokens / maxTokens) * 100;
return (
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div style={{ width: '100%', height: `${height}%`, background: '#58a6ff', borderRadius: '4px 4px 0 0', minHeight: '4px' }} />
<div style={{ fontSize: '10px', color: '#8b949e', marginTop: '4px' }}>{d.date.slice(5)}</div>
</div>
);
})}
</div>
) : <div style={{ color: '#8b949e', textAlign: 'center', padding: '40px' }}>暂无数据</div>}
</div>
{/* Recent Transactions */}
<div style={{ background: '#161b22', border: '1px solid #30363d', borderRadius: '12px', padding: '20px' }}>
<h3 style={{ fontSize: '16px', marginBottom: '16px' }}>最近交易</h3>
{data.recent_transactions.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{data.recent_transactions.map(t => (
<div key={t.id} style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 0', borderBottom: '1px solid #21262d' }}>
<div>
<div style={{ fontSize: '13px' }}>{t.description}</div>
<div style={{ fontSize: '11px', color: '#8b949e' }}>{t.created_at}</div>
</div>
<div style={{ color: t.amount >= 0 ? '#3fb950' : '#f85149', fontWeight: 600, fontSize: '14px' }}>
{t.amount >= 0 ? '+' : ''}¥{t.amount.toFixed(2)}
</div>
</div>
))}
</div>
) : <div style={{ color: '#8b949e', textAlign: 'center', padding: '40px' }}>暂无交易</div>}
</div>
</div>
{/* Tenants */}
{data.tenants.length > 0 && (
<div style={{ background: '#161b22', border: '1px solid #30363d', borderRadius: '12px', padding: '20px', marginTop: '16px' }}>
<h3 style={{ fontSize: '16px', marginBottom: '16px' }}>我的租户</h3>
<div style={{ display: 'flex', gap: '12px' }}>
{data.tenants.map(t => (
<div key={t.id} style={{ padding: '12px 16px', background: '#21262d', borderRadius: '8px', flex: 1 }}>
<div style={{ fontWeight: 600 }}>{t.name}</div>
<div style={{ fontSize: '12px', color: '#8b949e' }}>角色: {t.role}</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
// ==================== API Keys Tab ====================
function ApiKeysTab() {
const [keys, setKeys] = useState<ApiKey[]>([]);
const [loading, setLoading] = useState(true);
const [showCreate, setShowCreate] = useState(false);
const [newKeyName, setNewKeyName] = useState('');
const [newKey, setNewKey] = useState<string | null>(null);
const load = useCallback(() => {
setLoading(true);
request<any>(`${API_BASE}/api-keys`).then(res => {
setKeys(res.data || []);
setLoading(false);
}).catch(() => setLoading(false));
}, []);
useEffect(() => { load(); }, [load]);
const createKey = () => {
if (!newKeyName.trim()) return;
request<any>(`${API_BASE}/api-keys/create?key_name=${encodeURIComponent(newKeyName)}`, { method: 'POST' }).then(res => {
setNewKey(res.data?.api_key || null);
setNewKeyName('');
setShowCreate(false);
load();
});
};
const revokeKey = (id: string) => {
if (!confirm('确定撤销此密钥?')) return;
request<any>(`${API_BASE}/api-keys/${id}`, { method: 'DELETE' }).then(() => load());
};
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '16px' }}>
<h2 style={{ fontSize: '20px' }}>API 密钥管理</h2>
<button onClick={() => setShowCreate(!showCreate)} style={{ padding: '8px 16px', background: '#238636', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>
创建密钥
</button>
</div>
{newKey && (
<div style={{ background: '#1a2332', border: '1px solid #58a6ff', borderRadius: '8px', padding: '16px', marginBottom: '16px' }}>
<div style={{ color: '#58a6ff', fontWeight: 600, marginBottom: '8px' }}>密钥已创建 - 请立即保存</div>
<code style={{ display: 'block', padding: '8px', background: '#0d1117', borderRadius: '4px', wordBreak: 'break-all', fontSize: '13px' }}>{newKey}</code>
<button onClick={() => setNewKey(null)} style={{ marginTop: '8px', padding: '4px 12px', background: '#30363d', color: '#e6edf3', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>我已保存</button>
</div>
)}
{showCreate && (
<div style={{ background: '#161b22', border: '1px solid #30363d', borderRadius: '8px', padding: '16px', marginBottom: '16px' }}>
<input value={newKeyName} onChange={e => setNewKeyName(e.target.value)} placeholder="密钥名称(如:生产环境)"
style={{ width: '300px', padding: '8px 12px', background: '#0d1117', border: '1px solid #30363d', borderRadius: '6px', color: '#e6edf3', marginRight: '8px' }} />
<button onClick={createKey} style={{ padding: '8px 16px', background: '#238636', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>确认创建</button>
</div>
)}
{loading ? <div style={{ textAlign: 'center', padding: '40px', color: '#8b949e' }}>加载中...</div> : (
<div style={{ background: '#161b22', border: '1px solid #30363d', borderRadius: '12px', overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid #30363d' }}>
<th style={{ padding: '12px', textAlign: 'left', fontSize: '12px', color: '#8b949e', textTransform: 'uppercase' }}>名称</th>
<th style={{ padding: '12px', textAlign: 'left', fontSize: '12px', color: '#8b949e', textTransform: 'uppercase' }}>密钥前缀</th>
<th style={{ padding: '12px', textAlign: 'left', fontSize: '12px', color: '#8b949e', textTransform: 'uppercase' }}>状态</th>
<th style={{ padding: '12px', textAlign: 'left', fontSize: '12px', color: '#8b949e', textTransform: 'uppercase' }}>创建时间</th>
<th style={{ padding: '12px', textAlign: 'left', fontSize: '12px', color: '#8b949e', textTransform: 'uppercase' }}>操作</th>
</tr>
</thead>
<tbody>
{keys.length === 0 ? (
<tr><td colSpan={5} style={{ padding: '40px', textAlign: 'center', color: '#8b949e' }}>暂无API密钥</td></tr>
) : keys.map(k => (
<tr key={k.id} style={{ borderBottom: '1px solid #21262d' }}>
<td style={{ padding: '12px' }}>{k.key_name}</td>
<td style={{ padding: '12px' }}><code style={{ color: '#f0883e' }}>{k.key_prefix}...</code></td>
<td style={{ padding: '12px' }}>
<span style={{ padding: '2px 8px', borderRadius: '12px', fontSize: '12px', background: k.status === 'active' ? 'rgba(63,185,80,0.15)' : 'rgba(248,81,73,0.15)', color: k.status === 'active' ? '#3fb950' : '#f85149' }}>
{k.status === 'active' ? '活跃' : '已撤销'}
</span>
</td>
<td style={{ padding: '12px', color: '#8b949e' }}>{k.created_at}</td>
<td style={{ padding: '12px' }}>
{k.status === 'active' && <button onClick={() => revokeKey(k.id)} style={{ padding: '4px 10px', background: 'rgba(248,81,73,0.1)', color: '#f85149', border: '1px solid rgba(248,81,73,0.3)', borderRadius: '4px', cursor: 'pointer' }}>撤销</button>}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}
// ==================== Recharge Tab ====================
function RechargeTab() {
const [packages, setPackages] = useState<Package[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
request<any>(`${API_BASE}/billing/packages`).then(res => {
setPackages(res.data || []);
setLoading(false);
}).catch(() => setLoading(false));
}, []);
if (loading) return <div style={{ textAlign: 'center', padding: '40px', color: '#8b949e' }}>加载中...</div>;
return (
<div>
<h2 style={{ fontSize: '20px', marginBottom: '24px' }}>充值中心</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '16px' }}>
{packages.map(pkg => (
<div key={pkg.id} style={{
background: '#161b22', border: pkg.is_popular ? '2px solid #58a6ff' : '1px solid #30363d',
borderRadius: '12px', padding: '24px', position: 'relative',
}}>
{pkg.is_popular && <div style={{ position: 'absolute', top: '-10px', left: '50%', transform: 'translateX(-50%)', background: '#58a6ff', color: '#0d1117', padding: '2px 12px', borderRadius: '10px', fontSize: '12px', fontWeight: 600 }}>推荐</div>}
<h3 style={{ fontSize: '18px', marginBottom: '8px' }}>{pkg.name}</h3>
<p style={{ color: '#8b949e', fontSize: '13px', minHeight: '40px' }}>{pkg.description}</p>
<div style={{ margin: '16px 0' }}>
<span style={{ fontSize: '32px', fontWeight: 700, color: '#58a6ff' }}>¥{pkg.price}</span>
{pkg.original_price && <span style={{ marginLeft: '8px', color: '#8b949e', textDecoration: 'line-through' }}>¥{pkg.original_price}</span>}
</div>
<div style={{ color: '#3fb950', fontSize: '14px', marginBottom: '16px' }}>
{(pkg.tokens / 10000).toFixed(0)} 万 Token{pkg.bonus_tokens > 0 && ` +${(pkg.bonus_tokens / 10000).toFixed(0)}万 赠送`}
</div>
<button style={{
width: '100%', padding: '10px', background: pkg.is_popular ? '#58a6ff' : '#238636',
color: pkg.is_popular ? '#0d1117' : '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 600,
}}>立即购买</button>
</div>
))}
</div>
<div style={{ marginTop: '32px' }}>
<h3 style={{ fontSize: '16px', marginBottom: '16px' }}>交易记录</h3>
<TransactionsList />
</div>
</div>
);
}
function TransactionsList() {
const [txns, setTxns] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
request<any>(`${API_BASE}/billing/transactions`).then(res => {
setTxns(res.data?.items || []);
setLoading(false);
}).catch(() => setLoading(false));
}, []);
if (loading) return <div style={{ color: '#8b949e' }}>加载中...</div>;
return (
<div style={{ background: '#161b22', border: '1px solid #30363d', borderRadius: '12px', overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead><tr style={{ borderBottom: '1px solid #30363d' }}>
{['描述', '类型', '金额', 'Token', '状态', '时间'].map(h => <th key={h} style={{ padding: '12px', textAlign: 'left', fontSize: '12px', color: '#8b949e' }}>{h}</th>)}
</tr></thead>
<tbody>
{txns.length === 0 ? <tr><td colSpan={6} style={{ padding: '40px', textAlign: 'center', color: '#8b949e' }}>暂无交易记录</td></tr> : txns.map(t => (
<tr key={t.id} style={{ borderBottom: '1px solid #21262d' }}>
<td style={{ padding: '12px' }}>{t.description}</td>
<td style={{ padding: '12px' }}>{t.type}</td>
<td style={{ padding: '12px', color: t.amount >= 0 ? '#3fb950' : '#f85149' }}>{t.amount >= 0 ? '+' : ''}¥{t.amount.toFixed(2)}</td>
<td style={{ padding: '12px' }}>{t.tokens?.toLocaleString() || '-'}</td>
<td style={{ padding: '12px' }}><span style={{ color: t.status === 'completed' ? '#3fb950' : '#d29922' }}>{t.status}</span></td>
<td style={{ padding: '12px', color: '#8b949e' }}>{t.created_at}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
// ==================== Tenants Tab ====================
function TenantsTab() {
const [tenants, setTenants] = useState<Tenant[]>([]);
const [loading, setLoading] = useState(true);
const [showCreate, setShowCreate] = useState(false);
const [name, setName] = useState('');
const [slug, setSlug] = useState('');
const [desc, setDesc] = useState('');
const load = useCallback(() => {
setLoading(true);
request<any>(`${API_BASE}/tenants`).then(res => {
setTenants(res.data || []);
setLoading(false);
}).catch(() => setLoading(false));
}, []);
useEffect(() => { load(); }, [load]);
const create = () => {
if (!name.trim() || !slug.trim()) return;
request<any>(`${API_BASE}/tenants/create?name=${encodeURIComponent(name)}&slug=${encodeURIComponent(slug)}&description=${encodeURIComponent(desc)}`, { method: 'POST' }).then(() => {
setShowCreate(false); setName(''); setSlug(''); setDesc('');
load();
});
};
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '16px' }}>
<h2 style={{ fontSize: '20px' }}>我的租户</h2>
<button onClick={() => setShowCreate(!showCreate)} style={{ padding: '8px 16px', background: '#238636', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>创建租户</button>
</div>
{showCreate && (
<div style={{ background: '#161b22', border: '1px solid #30363d', borderRadius: '8px', padding: '20px', marginBottom: '16px' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', marginBottom: '12px' }}>
<input value={name} onChange={e => setName(e.target.value)} placeholder="租户名称" style={{ padding: '8px 12px', background: '#0d1117', border: '1px solid #30363d', borderRadius: '6px', color: '#e6edf3' }} />
<input value={slug} onChange={e => setSlug(e.target.value)} placeholder="标识(如:my-company)" style={{ padding: '8px 12px', background: '#0d1117', border: '1px solid #30363d', borderRadius: '6px', color: '#e6edf3' }} />
</div>
<input value={desc} onChange={e => setDesc(e.target.value)} placeholder="描述(选填)" style={{ width: '100%', padding: '8px 12px', background: '#0d1117', border: '1px solid #30363d', borderRadius: '6px', color: '#e6edf3', marginBottom: '12px' }} />
<button onClick={create} style={{ padding: '8px 16px', background: '#238636', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>确认创建</button>
</div>
)}
{loading ? <div style={{ color: '#8b949e', padding: '40px', textAlign: 'center' }}>加载中...</div> : (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
{tenants.length === 0 ? <div style={{ color: '#8b949e', gridColumn: '1/3', textAlign: 'center', padding: '40px' }}>暂无租户,点击上方创建</div> : tenants.map(t => (
<div key={t.id} style={{ background: '#161b22', border: '1px solid #30363d', borderRadius: '12px', padding: '20px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
<h3 style={{ fontSize: '18px' }}>{t.name}</h3>
<span style={{ padding: '2px 10px', borderRadius: '12px', fontSize: '12px', background: t.role === 'owner' ? 'rgba(188,140,255,0.15)' : 'rgba(88,166,255,0.15)', color: t.role === 'owner' ? '#bc8cff' : '#58a6ff' }}>{t.role}</span>
</div>
<p style={{ color: '#8b949e', fontSize: '13px', marginBottom: '12px' }}>{t.description || '暂无描述'}</p>
<div style={{ display: 'flex', gap: '16px', fontSize: '13px', color: '#8b949e' }}>
<span>成员: {t.member_count}/{t.max_members}</span>
<span>存储: {(t.max_storage_mb / 1024).toFixed(0)}GB</span>
<span>API/日: {t.max_api_calls_per_day.toLocaleString()}</span>
</div>
</div>
))}
</div>
)}
</div>
);
}
// ==================== Analytics Tab ====================
function AnalyticsTab() {
const [data, setData] = useState<UsageData | null>(null);
const [days, setDays] = useState(30);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
request<any>(`${API_BASE}/analytics/usage?days=${days}`).then(res => {
setData(res.data);
setLoading(false);
}).catch(() => setLoading(false));
}, [days]);
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '24px' }}>
<h2 style={{ fontSize: '20px' }}>用量分析</h2>
<select value={days} onChange={e => setDays(Number(e.target.value))} style={{ padding: '8px 12px', background: '#161b22', border: '1px solid #30363d', borderRadius: '6px', color: '#e6edf3' }}>
<option value={7}>最近7天</option>
<option value={14}>最近14天</option>
<option value={30}>最近30天</option>
<option value={90}>最近90天</option>
</select>
</div>
{loading ? <div style={{ color: '#8b949e', textAlign: 'center', padding: '40px' }}>加载中...</div> : data ? (
<div>
{/* Total Stats */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px', marginBottom: '24px' }}>
<div style={{ background: '#161b22', border: '1px solid #30363d', borderRadius: '12px', padding: '20px', textAlign: 'center' }}>
<div style={{ fontSize: '28px', fontWeight: 700, color: '#58a6ff' }}>{data.total.calls.toLocaleString()}</div>
<div style={{ color: '#8b949e', fontSize: '13px' }}>API调用总数</div>
</div>
<div style={{ background: '#161b22', border: '1px solid #30363d', borderRadius: '12px', padding: '20px', textAlign: 'center' }}>
<div style={{ fontSize: '28px', fontWeight: 700, color: '#3fb950' }}>{data.total.tokens.toLocaleString()}</div>
<div style={{ color: '#8b949e', fontSize: '13px' }}>Token消耗总数</div>
</div>
<div style={{ background: '#161b22', border: '1px solid #30363d', borderRadius: '12px', padding: '20px', textAlign: 'center' }}>
<div style={{ fontSize: '28px', fontWeight: 700, color: '#bc8cff' }}>{data.daily.length}</div>
<div style={{ color: '#8b949e', fontSize: '13px' }}>活跃天数</div>
</div>
</div>
{/* Daily Usage Chart */}
<div style={{ background: '#161b22', border: '1px solid #30363d', borderRadius: '12px', padding: '20px', marginBottom: '16px' }}>
<h3 style={{ fontSize: '16px', marginBottom: '16px' }}>每日用量趋势</h3>
{data.daily.length > 0 ? (
<div style={{ display: 'flex', alignItems: 'flex-end', gap: '4px', height: '150px' }}>
{data.daily.map((d, i) => {
const max = Math.max(...data.daily.map(x => x.tokens), 1);
const h = (d.tokens / max) * 100;
return (
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center' }} title={`${d.date}: ${d.tokens} tokens`}>
<div style={{ width: '100%', height: `${h}%`, background: 'linear-gradient(180deg, #58a6ff, #1f6feb)', borderRadius: '4px 4px 0 0', minHeight: '2px' }} />
{data.daily.length <= 14 && <div style={{ fontSize: '9px', color: '#8b949e', marginTop: '2px', transform: 'rotate(-45deg)' }}>{d.date.slice(5)}</div>}
</div>
);
})}
</div>
) : <div style={{ color: '#8b949e', textAlign: 'center', padding: '40px' }}>暂无数据</div>}
</div>
{/* Model Distribution */}
<div style={{ background: '#161b22', border: '1px solid #30363d', borderRadius: '12px', padding: '20px' }}>
<h3 style={{ fontSize: '16px', marginBottom: '16px' }}>模型使用分布</h3>
{data.model_distribution.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{data.model_distribution.map((m, i) => {
const maxTokens = Math.max(...data.model_distribution.map(x => x.tokens), 1);
const pct = (m.tokens / maxTokens) * 100;
const colors = ['#58a6ff', '#3fb950', '#bc8cff', '#f0883e', '#d29922'];
return (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ width: '120px', fontSize: '13px' }}>{m.model}</div>
<div style={{ flex: 1, height: '20px', background: '#21262d', borderRadius: '4px', overflow: 'hidden' }}>
<div style={{ width: `${pct}%`, height: '100%', background: colors[i % colors.length] }} />
</div>
<div style={{ width: '100px', fontSize: '12px', color: '#8b949e', textAlign: 'right' }}>{m.tokens.toLocaleString()} tokens</div>
</div>
);
})}
</div>
) : <div style={{ color: '#8b949e', textAlign: 'center', padding: '40px' }}>暂无数据</div>}
</div>
</div>
) : <div style={{ color: '#f85149', textAlign: 'center', padding: '40px' }}>加载失败</div>}
</div>
);
}