From accbfb53c335ef74f5de3d4667c10f8266501ef9 Mon Sep 17 00:00:00 2001 From: attacker Date: Tue, 16 Jun 2026 19:09:13 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8B=86=E5=88=86CloudBilling.tsx(?= =?UTF-8?q?663=E2=86=92467=E8=A1=8C)=E6=8F=90=E5=8F=96BillingRecordDialog+?= =?UTF-8?q?BillingRuleDialog=E5=AD=90=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/cloud/BillingRecordDialog.tsx | 149 +++++++++++ .../components/cloud/BillingRuleDialog.tsx | 149 +++++++++++ .../src/components/cloud/CloudBilling.tsx | 244 ++---------------- .../components/cloud/CloudBillingHelpers.tsx | 18 ++ 4 files changed, 340 insertions(+), 220 deletions(-) create mode 100644 frontend/src/components/cloud/BillingRecordDialog.tsx create mode 100644 frontend/src/components/cloud/BillingRuleDialog.tsx diff --git a/frontend/src/components/cloud/BillingRecordDialog.tsx b/frontend/src/components/cloud/BillingRecordDialog.tsx new file mode 100644 index 0000000..d7bd3fd --- /dev/null +++ b/frontend/src/components/cloud/BillingRecordDialog.tsx @@ -0,0 +1,149 @@ +// 云账单成本记录弹窗(新建/编辑)— 自包含状态,从 CloudBilling 提取 // Logan.Li +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Loader2 } from 'lucide-react'; +import { toast } from 'sonner'; +import { billingApi } from '@/services/api'; +import { providerLabels } from './CloudBillingHelpers'; + +const emptyRecord = { + provider: 'aliyun', + billing_date: '', + cost: '', + service: '', + region: '', + account_name: '', + description: '', +}; + +interface Props { + open: boolean; + onOpenChange: (open: boolean) => void; + editingRecord: any | null; + onSuccess: () => void; +} + +export function BillingRecordDialog({ open, onOpenChange, editingRecord, onSuccess }: Props) { + const [form, setForm] = useState(emptyRecord); + const [submitting, setSubmitting] = useState(false); + + // 弹窗打开时初始化表单(新建预填今日日期,编辑回填数据) // Logan.Li + useEffect(() => { + if (!open) return; + if (editingRecord) { + setForm({ + provider: editingRecord.provider, + billing_date: editingRecord.billing_date, + cost: editingRecord.cost, + service: editingRecord.service || '', + region: editingRecord.region || '', + account_name: editingRecord.account_name || '', + description: editingRecord.description || '', + }); + } else { + setForm({ ...emptyRecord, billing_date: new Date().toISOString().slice(0, 10) }); + } + }, [open, editingRecord]); + + const handleSave = async () => { + if (!form.provider || !form.billing_date || !form.cost) { + toast.error('请填写必填字段'); + return; + } + setSubmitting(true); + try { + const data = { ...form, cost: parseFloat(form.cost) }; + if (editingRecord) { + await billingApi.remove(editingRecord.id); + await billingApi.create(data); + } else { + await billingApi.create(data); + } + onOpenChange(false); + onSuccess(); + toast('保存成功'); + } catch (e: any) { + toast.error('保存失败', { description: e.message }); + } finally { + setSubmitting(false); + } + }; + + return ( + + + + {editingRecord ? '编辑成本记录' : '记录成本'} + 记录一条每日云资源成本 + +
+
+
+ + +
+
+ + setForm({ ...form, billing_date: e.target.value })} /> +
+
+
+
+ + setForm({ ...form, cost: e.target.value })} /> +
+
+ + setForm({ ...form, service: e.target.value })} /> +
+
+
+
+ + setForm({ ...form, region: e.target.value })} /> +
+
+ + setForm({ ...form, account_name: e.target.value })} /> +
+
+
+ + setForm({ ...form, description: e.target.value })} /> +
+
+ + + + +
+
+ ); +} diff --git a/frontend/src/components/cloud/BillingRuleDialog.tsx b/frontend/src/components/cloud/BillingRuleDialog.tsx new file mode 100644 index 0000000..29a6dd6 --- /dev/null +++ b/frontend/src/components/cloud/BillingRuleDialog.tsx @@ -0,0 +1,149 @@ +// 云账单预警规则弹窗(新建/编辑)— 自包含状态,从 CloudBilling 提取 // Logan.Li +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +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 { Loader2 } from 'lucide-react'; +import { toast } from 'sonner'; +import { billingApi } from '@/services/api'; +import { providerLabels } from './CloudBillingHelpers'; + +const emptyRule = { name: '', provider: '', metric: 'daily', threshold: '', operator: '>', enabled: true }; + +interface Props { + open: boolean; + onOpenChange: (open: boolean) => void; + editingRule: any | null; + onSuccess: () => void; +} + +export function BillingRuleDialog({ open, onOpenChange, editingRule, onSuccess }: Props) { + const [form, setForm] = useState(emptyRule); + const [submitting, setSubmitting] = useState(false); + + // 弹窗打开时初始化表单 // Logan.Li + useEffect(() => { + if (!open) return; + if (editingRule) { + setForm({ + name: editingRule.name, + provider: editingRule.provider || '', + metric: editingRule.metric || 'daily', + threshold: editingRule.threshold, + operator: editingRule.operator || '>', + enabled: editingRule.enabled, + }); + } else { + setForm({ ...emptyRule }); + } + }, [open, editingRule]); + + const handleSave = async () => { + if (!form.name || !form.threshold) { + toast.error('请填写规则名称和阈值'); + return; + } + setSubmitting(true); + try { + const data = { ...form, threshold: parseFloat(form.threshold) }; + if (editingRule) { + await billingApi.updateAlertRule(editingRule.id, data); + } else { + await billingApi.createAlertRule(data); + } + onOpenChange(false); + onSuccess(); + toast('保存成功'); + } catch (e: any) { + toast.error('保存失败', { description: e.message }); + } finally { + setSubmitting(false); + } + }; + + return ( + + + + {editingRule ? '编辑预警规则' : '添加预警规则'} + 设置成本阈值,超标时自动预警 + +
+
+ + setForm({ ...form, name: e.target.value })} /> +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + setForm({ ...form, threshold: e.target.value })} /> +
+
+
+ setForm({ ...form, enabled: v })} /> + +
+
+ + + + +
+
+ ); +} diff --git a/frontend/src/components/cloud/CloudBilling.tsx b/frontend/src/components/cloud/CloudBilling.tsx index 45f8e56..9697a34 100644 --- a/frontend/src/components/cloud/CloudBilling.tsx +++ b/frontend/src/components/cloud/CloudBilling.tsx @@ -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 = { - 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', -}; +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([]); 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 [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); @@ -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() { )} - - - - {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 })} /> - -
-
- - - - -
-
+ { loadRecords(); loadStats(); }} + /> + { loadRules(); loadStats(); }} + /> ); } - diff --git a/frontend/src/components/cloud/CloudBillingHelpers.tsx b/frontend/src/components/cloud/CloudBillingHelpers.tsx index c7cc8c4..15f2b11 100644 --- a/frontend/src/components/cloud/CloudBillingHelpers.tsx +++ b/frontend/src/components/cloud/CloudBillingHelpers.tsx @@ -1,6 +1,24 @@ import { type ReactNode } from 'react'; import { TrendingUp, TrendingDown } from 'lucide-react'; +// 云厂商中文标签(主组件 + 弹窗共享) // Logan.Li +export const providerLabels: Record = { + aliyun: '阿里云', + tencent: '腾讯云', + aws: 'AWS', + huawei: '华为云', + azure: 'Azure', +}; + +// 云厂商品牌色(主组件 + 弹窗共享) // Logan.Li +export const providerColors: Record = { + aliyun: 'bg-orange-500', + tencent: 'bg-blue-500', + aws: 'bg-amber-500', + huawei: 'bg-red-500', + azure: 'bg-sky-500', +}; + export function formatCurrency(n: number) { return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; }