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 (

用户门户

{/* Tab Navigation */}
{tabs.map(tab => ( ))}
{activeTab === 'overview' && } {activeTab === 'apikeys' && } {activeTab === 'recharge' && } {activeTab === 'tenants' && } {activeTab === 'analytics' && }
); } // ==================== Overview Tab ==================== function OverviewTab() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { request(`${API_BASE}/overview`).then(res => { setData(res.data || res); setLoading(false); }).catch(() => setLoading(false)); }, []); if (loading) return
加载中...
; if (!data) return
加载失败
; 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 (
{/* Stats Cards */}
{cards.map((c, i) => (
{c.value}
{c.label}
))}
{/* Daily Usage Chart */}

近7天用量

{data.daily_usage.length > 0 ? (
{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 (
{d.date.slice(5)}
); })}
) :
暂无数据
}
{/* Recent Transactions */}

最近交易

{data.recent_transactions.length > 0 ? (
{data.recent_transactions.map(t => (
{t.description}
{t.created_at}
= 0 ? '#3fb950' : '#f85149', fontWeight: 600, fontSize: '14px' }}> {t.amount >= 0 ? '+' : ''}¥{t.amount.toFixed(2)}
))}
) :
暂无交易
}
{/* Tenants */} {data.tenants.length > 0 && (

我的租户

{data.tenants.map(t => (
{t.name}
角色: {t.role}
))}
)}
); } // ==================== API Keys Tab ==================== function ApiKeysTab() { const [keys, setKeys] = useState([]); const [loading, setLoading] = useState(true); const [showCreate, setShowCreate] = useState(false); const [newKeyName, setNewKeyName] = useState(''); const [newKey, setNewKey] = useState(null); const load = useCallback(() => { setLoading(true); request(`${API_BASE}/api-keys`).then(res => { setKeys(res.data || []); setLoading(false); }).catch(() => setLoading(false)); }, []); useEffect(() => { load(); }, [load]); const createKey = () => { if (!newKeyName.trim()) return; request(`${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(`${API_BASE}/api-keys/${id}`, { method: 'DELETE' }).then(() => load()); }; return (

API 密钥管理

{newKey && (
密钥已创建 - 请立即保存
{newKey}
)} {showCreate && (
setNewKeyName(e.target.value)} placeholder="密钥名称(如:生产环境)" style={{ width: '300px', padding: '8px 12px', background: '#0d1117', border: '1px solid #30363d', borderRadius: '6px', color: '#e6edf3', marginRight: '8px' }} />
)} {loading ?
加载中...
: (
{keys.length === 0 ? ( ) : keys.map(k => ( ))}
名称 密钥前缀 状态 创建时间 操作
暂无API密钥
{k.key_name} {k.key_prefix}... {k.status === 'active' ? '活跃' : '已撤销'} {k.created_at} {k.status === 'active' && }
)}
); } // ==================== Recharge Tab ==================== function RechargeTab() { const [packages, setPackages] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { request(`${API_BASE}/billing/packages`).then(res => { setPackages(res.data || []); setLoading(false); }).catch(() => setLoading(false)); }, []); if (loading) return
加载中...
; return (

充值中心

{packages.map(pkg => (
{pkg.is_popular &&
推荐
}

{pkg.name}

{pkg.description}

¥{pkg.price} {pkg.original_price && ¥{pkg.original_price}}
{(pkg.tokens / 10000).toFixed(0)} 万 Token{pkg.bonus_tokens > 0 && ` +${(pkg.bonus_tokens / 10000).toFixed(0)}万 赠送`}
))}

交易记录

); } function TransactionsList() { const [txns, setTxns] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { request(`${API_BASE}/billing/transactions`).then(res => { setTxns(res.data?.items || []); setLoading(false); }).catch(() => setLoading(false)); }, []); if (loading) return
加载中...
; return (
{['描述', '类型', '金额', 'Token', '状态', '时间'].map(h => )} {txns.length === 0 ? : txns.map(t => ( ))}
{h}
暂无交易记录
{t.description} {t.type} = 0 ? '#3fb950' : '#f85149' }}>{t.amount >= 0 ? '+' : ''}¥{t.amount.toFixed(2)} {t.tokens?.toLocaleString() || '-'} {t.status} {t.created_at}
); } // ==================== Tenants Tab ==================== function TenantsTab() { const [tenants, setTenants] = useState([]); 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(`${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(`${API_BASE}/tenants/create?name=${encodeURIComponent(name)}&slug=${encodeURIComponent(slug)}&description=${encodeURIComponent(desc)}`, { method: 'POST' }).then(() => { setShowCreate(false); setName(''); setSlug(''); setDesc(''); load(); }); }; return (

我的租户

{showCreate && (
setName(e.target.value)} placeholder="租户名称" style={{ padding: '8px 12px', background: '#0d1117', border: '1px solid #30363d', borderRadius: '6px', color: '#e6edf3' }} /> setSlug(e.target.value)} placeholder="标识(如:my-company)" style={{ padding: '8px 12px', background: '#0d1117', border: '1px solid #30363d', borderRadius: '6px', color: '#e6edf3' }} />
setDesc(e.target.value)} placeholder="描述(选填)" style={{ width: '100%', padding: '8px 12px', background: '#0d1117', border: '1px solid #30363d', borderRadius: '6px', color: '#e6edf3', marginBottom: '12px' }} />
)} {loading ?
加载中...
: (
{tenants.length === 0 ?
暂无租户,点击上方创建
: tenants.map(t => (

{t.name}

{t.role}

{t.description || '暂无描述'}

成员: {t.member_count}/{t.max_members} 存储: {(t.max_storage_mb / 1024).toFixed(0)}GB API/日: {t.max_api_calls_per_day.toLocaleString()}
))}
)}
); } // ==================== Analytics Tab ==================== function AnalyticsTab() { const [data, setData] = useState(null); const [days, setDays] = useState(30); const [loading, setLoading] = useState(true); useEffect(() => { setLoading(true); request(`${API_BASE}/analytics/usage?days=${days}`).then(res => { setData(res.data); setLoading(false); }).catch(() => setLoading(false)); }, [days]); return (

用量分析

{loading ?
加载中...
: data ? (
{/* Total Stats */}
{data.total.calls.toLocaleString()}
API调用总数
{data.total.tokens.toLocaleString()}
Token消耗总数
{data.daily.length}
活跃天数
{/* Daily Usage Chart */}

每日用量趋势

{data.daily.length > 0 ? (
{data.daily.map((d, i) => { const max = Math.max(...data.daily.map(x => x.tokens), 1); const h = (d.tokens / max) * 100; return (
{data.daily.length <= 14 &&
{d.date.slice(5)}
}
); })}
) :
暂无数据
}
{/* Model Distribution */}

模型使用分布

{data.model_distribution.length > 0 ? (
{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 (
{m.model}
{m.tokens.toLocaleString()} tokens
); })}
) :
暂无数据
}
) :
加载失败
}
); }