refactor: 拆分CloudBilling.tsx(663→467行)提取BillingRecordDialog+BillingRuleDialog子模块

This commit is contained in:
attacker committed 2026-06-16 19:09:13 +08:00
1 parent f865d41547
commit accbfb53c3
4 files changed
+340 -220

No files matched your search

+24 -220
View File
@@ -5,14 +5,6 @@ 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,
@@ -27,7 +19,6 @@ import {
RefreshCw,
Loader2,
TrendingUp,
TrendingDown,
Wallet,
CalendarDays,
AlertTriangle,
@@ -36,23 +27,9 @@ import {
import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { billingApi } from '@/services/api';
import { formatCurrency, StatCard, TrendChart } from './CloudBillingHelpers';
const providerLabels: Record<string, string> = {
aliyun: '阿里云',
tencent: '腾讯云',
aws: 'AWS',
huawei: '华为云',
azure: 'Azure',
};
const providerColors: Record<string, string> = {
aliyun: 'bg-orange-500',
tencent: 'bg-blue-500',
aws: 'bg-amber-500',
huawei: 'bg-red-500',
azure: 'bg-sky-500',
};
import { formatCurrency, StatCard, TrendChart, providerLabels, providerColors } from './CloudBillingHelpers';
import { BillingRecordDialog } from './BillingRecordDialog';
import { BillingRuleDialog } from './BillingRuleDialog';
export function CloudBilling() {
const confirm = useConfirm();
@@ -71,16 +48,11 @@ export function CloudBilling() {
const [rules, setRules] = useState<any[]>([]);
const [rulesLoading, setRulesLoading] = useState(true);
// 弹窗状态(open + 编辑目标) // Logan.Li
const [recordDialogOpen, setRecordDialogOpen] = useState(false);
const [editingRecord, setEditingRecord] = useState<any>(null);
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
const [editingRule, setEditingRule] = useState<any>(null);
const [submitting, setSubmitting] = useState(false);
const emptyRecord = { provider: 'aliyun', billing_date: '', cost: '', service: '', region: '', account_name: '', description: '' };
const [recordForm, setRecordForm] = useState<any>(emptyRecord);
const emptyRule = { name: '', provider: '', metric: 'daily', threshold: '', operator: '>', enabled: true };
const [ruleForm, setRuleForm] = useState<any>(emptyRule);
const loadStats = useCallback(async () => {
setStatsLoading(true);
@@ -136,39 +108,22 @@ export function CloudBilling() {
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 openAddRule = () => {
setEditingRule(null);
setRuleDialogOpen(true);
};
const openEditRule = (r: any) => {
setEditingRule(r);
setRuleDialogOpen(true);
};
const handleDeleteRecord = async (id: number) => {
@@ -184,42 +139,6 @@ export function CloudBilling() {
}
};
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;
@@ -531,133 +450,18 @@ export function CloudBilling() {
</div>
)}
<Dialog open={recordDialogOpen} onOpenChange={setRecordDialogOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{editingRecord ? '编辑成本记录' : '记录成本'}</DialogTitle>
<DialogDescription>记录一条每日云资源成本</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-sm text-slate-300">厂商 *</Label>
<Select value={recordForm.provider} onValueChange={(v) => setRecordForm({ ...recordForm, provider: v })}>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
<SelectContent>
{Object.entries(providerLabels).map(([k, v]) => (
<SelectItem key={k} value={k}>{v}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-sm text-slate-300">日期 *</Label>
<Input type="date" className="mt-1" value={recordForm.billing_date} onChange={(e) => setRecordForm({ ...recordForm, billing_date: e.target.value })} />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-sm text-slate-300">费用(元) *</Label>
<Input type="number" step="0.01" className="mt-1" placeholder="0.00" value={recordForm.cost} onChange={(e) => setRecordForm({ ...recordForm, cost: e.target.value })} />
</div>
<div>
<Label className="text-sm text-slate-300">服务</Label>
<Input className="mt-1" placeholder="ECS, RDS..." value={recordForm.service} onChange={(e) => setRecordForm({ ...recordForm, service: e.target.value })} />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-sm text-slate-300">区域</Label>
<Input className="mt-1" placeholder="cn-hangzhou" value={recordForm.region} onChange={(e) => setRecordForm({ ...recordForm, region: e.target.value })} />
</div>
<div>
<Label className="text-sm text-slate-300">账号名称</Label>
<Input className="mt-1" placeholder="主账号" value={recordForm.account_name} onChange={(e) => setRecordForm({ ...recordForm, account_name: e.target.value })} />
</div>
</div>
<div>
<Label className="text-sm text-slate-300">备注</Label>
<Input className="mt-1" placeholder="可选" value={recordForm.description} onChange={(e) => setRecordForm({ ...recordForm, description: e.target.value })} />
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setRecordDialogOpen(false)}>取消</Button>
<Button onClick={handleSaveRecord} disabled={submitting}>
{submitting && <Loader2 className="h-4 w-4 mr-1 animate-spin" />}
保存
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={ruleDialogOpen} onOpenChange={setRuleDialogOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{editingRule ? '编辑预警规则' : '添加预警规则'}</DialogTitle>
<DialogDescription>设置成本阈值,超标时自动预警</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div>
<Label className="text-sm text-slate-300">规则名称 *</Label>
<Input className="mt-1" placeholder="如:日成本超 500 元预警" value={ruleForm.name} onChange={(e) => setRuleForm({ ...ruleForm, name: e.target.value })} />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-sm text-slate-300">厂商</Label>
<Select value={ruleForm.provider || 'all'} onValueChange={(v) => setRuleForm({ ...ruleForm, provider: v === 'all' ? '' : v })}>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="all">全部厂商</SelectItem>
{Object.entries(providerLabels).map(([k, v]) => (
<SelectItem key={k} value={k}>{v}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-sm text-slate-300">指标</Label>
<Select value={ruleForm.metric} onValueChange={(v) => setRuleForm({ ...ruleForm, metric: v })}>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="daily">日成本</SelectItem>
<SelectItem value="monthly">月累计</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-sm text-slate-300">比较</Label>
<Select value={ruleForm.operator} onValueChange={(v) => setRuleForm({ ...ruleForm, operator: v })}>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value=">">大于 &gt;</SelectItem>
<SelectItem value=">=">大于等于 &ge;</SelectItem>
<SelectItem value="<">小于 &lt;</SelectItem>
<SelectItem value="<=">小于等于 &le;</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="text-sm text-slate-300">阈值(元) *</Label>
<Input type="number" step="0.01" className="mt-1" placeholder="500.00" value={ruleForm.threshold} onChange={(e) => setRuleForm({ ...ruleForm, threshold: e.target.value })} />
</div>
</div>
<div className="flex items-center gap-2">
<Switch checked={ruleForm.enabled} onCheckedChange={(v) => setRuleForm({ ...ruleForm, enabled: v })} />
<Label className="text-sm text-slate-300">启用</Label>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setRuleDialogOpen(false)}>取消</Button>
<Button onClick={handleSaveRule} disabled={submitting}>
{submitting && <Loader2 className="h-4 w-4 mr-1 animate-spin" />}
保存
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<BillingRecordDialog
open={recordDialogOpen}
onOpenChange={setRecordDialogOpen}
editingRecord={editingRecord}
onSuccess={() => { loadRecords(); loadStats(); }}
/>
<BillingRuleDialog
open={ruleDialogOpen}
onOpenChange={setRuleDialogOpen}
editingRule={editingRule}
onSuccess={() => { loadRules(); loadStats(); }}
/>
</div>
);
}