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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Plus, Trash2, Pencil, RefreshCw, Loader2, Wallet, Bell, } from 'lucide-react'; import { toast } from 'sonner'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { billingApi } from '@/api'; import { formatCurrency, TrendChart, providerLabels, providerColors } from './CloudBillingHelpers'; import { StatStrip } from '@/components/ui/stat-strip'; import { BillingRecordDialog } from './BillingRecordDialog'; import { BillingRuleDialog } from './BillingRuleDialog'; import type { JsonData } from '@/types/common'; 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); // 弹窗状态(open + 编辑目标) // Logan.Li const [recordDialogOpen, setRecordDialogOpen] = useState(false); const [editingRecord, setEditingRecord] = useState(null); const [ruleDialogOpen, setRuleDialogOpen] = useState(false); const [editingRule, setEditingRule] = useState(null); const loadStats = useCallback(async () => { setStatsLoading(true); try { const res = await billingApi.getStats(); setStats(res); } catch (e) { toast.error('加载统计失败', { description: (e as JsonData).message }); } finally { setStatsLoading(false); } }, []); const loadRecords = useCallback(async () => { setRecordsLoading(true); try { const params: JsonData = {}; 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) { toast.error('加载账单失败', { description: (e as JsonData).message }); } finally { setRecordsLoading(false); } }, [filterProvider, filterStart, filterEnd]); const loadRules = useCallback(async () => { setRulesLoading(true); try { const res = await billingApi.listAlertRules(); setRules(res.list || []); } catch (e) { toast.error('加载预警规则失败', { description: (e as JsonData).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); setRecordDialogOpen(true); }; const openEditRecord = (r: JsonData) => { setEditingRecord(r); setRecordDialogOpen(true); }; const openAddRule = () => { setEditingRule(null); setRuleDialogOpen(true); }; const openEditRule = (r: JsonData) => { setEditingRule(r); setRuleDialogOpen(true); }; 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) { toast.error('删除失败', { description: (e as JsonData).message }); } }; 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) { toast.error('删除失败', { description: (e as JsonData).message }); } }; const handleToggleRule = async (id: number) => { try { await billingApi.toggleAlertRule(id); loadRules(); loadStats(); } catch (e) { toast.error('操作失败', { description: (e as JsonData).message }); } }; return (

云账单管理

{activeTab === 'records' && ( )} {activeTab === 'alerts' && ( )}
{([ { key: 'overview', label: '概览统计' }, { key: 'records', label: '成本记录' }, { key: 'alerts', label: '预警规则' }, ] as const).map(tab => ( ))}
{activeTab === 'overview' && (
{statsLoading ? (
加载中...
) : stats ? ( <> 0 ? 'red' : 'muted' }, ]} /> {stats.alerts?.length > 0 && (
成本预警
{stats.alerts.map((a: JsonData, 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: JsonData, i: number) => (
{p.percent > 8 ? (providerLabels[p.provider] || p.provider) : ''}
))}
{stats.provider_dist.map((p: JsonData, 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')}
)}
))}
)}
)} { loadRecords(); loadStats(); }} /> { loadRules(); loadStats(); }} />
); }