import { useState, useEffect, useCallback } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Plus, Trash2, Pencil, RefreshCw, Loader2, TrendingUp, TrendingDown, Wallet, CalendarDays, AlertTriangle, Bell, } from 'lucide-react'; import { toast } from 'sonner'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { billingApi } from '@/services/api'; const providerLabels: Record = { aliyun: '阿里云', tencent: '腾讯云', aws: 'AWS', huawei: '华为云', azure: 'Azure', }; const providerColors: Record = { aliyun: 'bg-orange-500', tencent: 'bg-blue-500', aws: 'bg-amber-500', huawei: 'bg-red-500', azure: 'bg-sky-500', }; function formatCurrency(n: number) { return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; } export function CloudBilling() { const confirm = useConfirm(); const [activeTab, setActiveTab] = useState<'overview' | 'records' | 'alerts'>('overview'); const [stats, setStats] = useState(null); const [statsLoading, setStatsLoading] = useState(true); const [records, setRecords] = useState([]); const [recordsLoading, setRecordsLoading] = useState(true); const [filterProvider, setFilterProvider] = useState(''); const [filterStart, setFilterStart] = useState(''); const [filterEnd, setFilterEnd] = useState(''); const [rules, setRules] = useState([]); const [rulesLoading, setRulesLoading] = useState(true); const [recordDialogOpen, setRecordDialogOpen] = useState(false); const [editingRecord, setEditingRecord] = useState(null); const [ruleDialogOpen, setRuleDialogOpen] = useState(false); const [editingRule, setEditingRule] = useState(null); const [submitting, setSubmitting] = useState(false); const emptyRecord = { provider: 'aliyun', billing_date: '', cost: '', service: '', region: '', account_name: '', description: '' }; const [recordForm, setRecordForm] = useState(emptyRecord); const emptyRule = { name: '', provider: '', metric: 'daily', threshold: '', operator: '>', enabled: true }; const [ruleForm, setRuleForm] = useState(emptyRule); const loadStats = useCallback(async () => { setStatsLoading(true); try { const res = await billingApi.getStats(); setStats(res); } catch (e: any) { toast.error('加载统计失败', { description: e.message }); } finally { setStatsLoading(false); } }, []); const loadRecords = useCallback(async () => { setRecordsLoading(true); try { const params: any = {}; if (filterProvider) params.provider = filterProvider; if (filterStart) params.start_date = filterStart; if (filterEnd) params.end_date = filterEnd; const res = await billingApi.list(params); setRecords(res.list || []); } catch (e: any) { toast.error('加载账单失败', { description: e.message }); } finally { setRecordsLoading(false); } }, [filterProvider, filterStart, filterEnd]); const loadRules = useCallback(async () => { setRulesLoading(true); try { const res = await billingApi.listAlertRules(); setRules(res.list || []); } catch (e: any) { toast.error('加载预警规则失败', { description: e.message }); } finally { setRulesLoading(false); } }, []); useEffect(() => { loadStats(); }, [loadStats]); useEffect(() => { if (activeTab === 'records') loadRecords(); }, [activeTab, loadRecords]); useEffect(() => { if (activeTab === 'alerts') loadRules(); }, [activeTab, loadRules]); const openAddRecord = () => { setEditingRecord(null); setRecordForm({ ...emptyRecord, billing_date: new Date().toISOString().slice(0, 10) }); setRecordDialogOpen(true); }; const openEditRecord = (r: any) => { setEditingRecord(r); setRecordForm({ provider: r.provider, billing_date: r.billing_date, cost: r.cost, service: r.service || '', region: r.region || '', account_name: r.account_name || '', description: r.description || '' }); setRecordDialogOpen(true); }; const handleSaveRecord = async () => { if (!recordForm.provider || !recordForm.billing_date || !recordForm.cost) { toast.error('请填写必填字段'); return; } setSubmitting(true); try { const data = { ...recordForm, cost: parseFloat(recordForm.cost) }; if (editingRecord) { await billingApi.remove(editingRecord.id); await billingApi.create(data); } else { await billingApi.create(data); } setRecordDialogOpen(false); loadRecords(); loadStats(); toast('保存成功'); } catch (e: any) { toast.error('保存失败', { description: e.message }); } finally { setSubmitting(false); } }; const handleDeleteRecord = async (id: number) => { const ok = await confirm({ title: '删除账单记录', description: '确定删除这条成本记录?', variant: 'destructive' }); if (!ok) return; try { await billingApi.remove(id); loadRecords(); loadStats(); toast('删除成功'); } catch (e: any) { toast.error('删除失败', { description: e.message }); } }; const openAddRule = () => { setEditingRule(null); setRuleForm({ ...emptyRule }); setRuleDialogOpen(true); }; const openEditRule = (r: any) => { setEditingRule(r); setRuleForm({ name: r.name, provider: r.provider || '', metric: r.metric || 'daily', threshold: r.threshold, operator: r.operator || '>', enabled: r.enabled }); setRuleDialogOpen(true); }; const handleSaveRule = async () => { if (!ruleForm.name || !ruleForm.threshold) { toast.error('请填写规则名称和阈值'); return; } setSubmitting(true); try { const data = { ...ruleForm, threshold: parseFloat(ruleForm.threshold) }; if (editingRule) { await billingApi.updateAlertRule(editingRule.id, data); } else { await billingApi.createAlertRule(data); } setRuleDialogOpen(false); loadRules(); loadStats(); toast('保存成功'); } catch (e: any) { toast.error('保存失败', { description: e.message }); } finally { setSubmitting(false); } }; const handleDeleteRule = async (id: number) => { const ok = await confirm({ title: '删除预警规则', description: '确定删除此预警规则?', variant: 'destructive' }); if (!ok) return; try { await billingApi.deleteAlertRule(id); loadRules(); toast('删除成功'); } catch (e: any) { toast.error('删除失败', { description: e.message }); } }; const handleToggleRule = async (id: number) => { try { await billingApi.toggleAlertRule(id); loadRules(); loadStats(); } catch (e: any) { toast.error('操作失败', { description: e.message }); } }; return (

云账单管理

{activeTab === 'records' && ( )} {activeTab === 'alerts' && ( )}
{([ { key: 'overview', label: '概览统计' }, { key: 'records', label: '成本记录' }, { key: 'alerts', label: '预警规则' }, ] as const).map(tab => ( ))}
{activeTab === 'overview' && (
{statsLoading ? (
加载中...
) : stats ? ( <>
} label="今日成本" value={formatCurrency(stats.today_cost || 0)} trend={stats.today_cost - (stats.yesterday_cost || 0)} trendLabel={`昨日 ${formatCurrency(stats.yesterday_cost || 0)}`} /> } label="本月成本" value={formatCurrency(stats.month_cost || 0)} /> } label="年度成本" value={formatCurrency(stats.year_cost || 0)} /> } label="活跃预警" value={`${stats.alerts?.length || 0} 条`} alert={stats.alerts?.length > 0} />
{stats.alerts?.length > 0 && (
成本预警
{stats.alerts.map((a: any, i: number) => (
{a.rule_name} {a.provider} · {a.metric === 'daily' ? '日' : '月'}成本
当前 {formatCurrency(a.current)} {' '}{a.operator}{' '} 阈值 {formatCurrency(a.threshold)}
))}
)}

近 30 天成本趋势

本月厂商成本分布

{(stats.provider_dist || []).length === 0 ? (

暂无数据

) : (
{stats.provider_dist.map((p: any, i: number) => (
{p.percent > 8 ? (providerLabels[p.provider] || p.provider) : ''}
))}
{stats.provider_dist.map((p: any, i: number) => (
{providerLabels[p.provider] || p.provider}
{formatCurrency(p.cost)} {p.percent.toFixed(1)}%
))}
)} ) : null}
)} {activeTab === 'records' && (
setFilterStart(e.target.value)} />
setFilterEnd(e.target.value)} />
{recordsLoading ? (
加载中...
) : records.length === 0 ? (

暂无成本记录

) : (
{records.map((r) => ( ))}
日期 厂商 服务 区域 账号 费用 说明 操作
{r.billing_date} {providerLabels[r.provider] || r.provider} {r.service || '-'} {r.region || '-'} {r.account_name || '-'} {formatCurrency(r.cost)} {r.description || '-'}
)}
)} {activeTab === 'alerts' && (
{rulesLoading ? (
加载中...
) : rules.length === 0 ? (

暂无预警规则

) : (
{rules.map((r) => (
{r.name}
handleToggleRule(r.id)} />
厂商 {r.provider ? (providerLabels[r.provider] || r.provider) : '全部'}
指标 {r.metric === 'daily' ? '日成本' : '月累计'}
阈值 {r.operator} {formatCurrency(r.threshold)}
{r.last_triggered && (
最后触发 {new Date(r.last_triggered).toLocaleString('zh-CN')}
)}
))}
)}
)} {editingRecord ? '编辑成本记录' : '记录成本'} 记录一条每日云资源成本
setRecordForm({ ...recordForm, billing_date: e.target.value })} />
setRecordForm({ ...recordForm, cost: e.target.value })} />
setRecordForm({ ...recordForm, service: e.target.value })} />
setRecordForm({ ...recordForm, region: e.target.value })} />
setRecordForm({ ...recordForm, account_name: e.target.value })} />
setRecordForm({ ...recordForm, description: e.target.value })} />
{editingRule ? '编辑预警规则' : '添加预警规则'} 设置成本阈值,超标时自动预警
setRuleForm({ ...ruleForm, name: e.target.value })} />
setRuleForm({ ...ruleForm, threshold: e.target.value })} />
setRuleForm({ ...ruleForm, enabled: v })} />
); } function StatCard({ icon, label, value, trend, trendLabel, alert }: { icon: React.ReactNode; label: string; value: string; trend?: number; trendLabel?: string; alert?: boolean; }) { return (
{label} {icon}
{value}
{trendLabel && (
{trend !== undefined && trend > 0 && } {trend !== undefined && trend < 0 && } {trendLabel}
)}
); } function TrendChart({ data }: { data: { date: string; cost: number }[] }) { if (data.length === 0) { return

暂无数据

; } const max = Math.max(...data.map((d) => d.cost), 1); const recent = data.slice(-30); return (
{recent.map((d, i) => { const h = Math.max((d.cost / max) * 100, 2); return (
{d.date}: {formatCurrency(d.cost)}
); })}
{recent[0]?.date} {recent[recent.length - 1]?.date}
); }