From 45f7342d1ed0b031b3d32d5c78d593fbb46c29e1 Mon Sep 17 00:00:00 2001 From: attacker Date: Thu, 9 Jul 2026 18:36:13 +0800 Subject: [PATCH] =?UTF-8?q?refactor(quality):=20Q30=20=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E5=B7=A8=E5=9E=8B=E7=BB=84=E4=BB=B6=E6=8B=86=E5=88=86=20?= =?UTF-8?q?=E2=80=94=20CloudBilling=20534L=E2=86=92295L=20(-45%),=20?= =?UTF-8?q?=E6=8F=90=E5=8F=96=20cloud-billing/=203=20=E4=B8=AA=E6=96=87?= =?UTF-8?q?=E4=BB=B6=20(OverviewTab=20110L=20=E6=A6=82=E8=A7=88=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1:=E5=90=8C=E6=AD=A5=E4=BF=A1=E6=81=AF+=E6=88=90?= =?UTF-8?q?=E6=9C=AC=E5=91=8A=E8=AD=A6=E6=A8=AA=E5=B9=85+=E8=B6=8B?= =?UTF-8?q?=E5=8A=BF=E5=9B=BE+=E5=8E=82=E5=95=86=E5=88=86=E5=B8=83=20+=20R?= =?UTF-8?q?ecordsTab=20146L=20=E6=88=90=E6=9C=AC=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E8=A1=A8=E6=A0=BC+RecordsFilterToolbar=E7=AD=9B=E9=80=89?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E6=A0=8F=20+=20AlertRulesTab=2091L=20?= =?UTF-8?q?=E9=A2=84=E8=AD=A6=E8=A7=84=E5=88=99=E5=8D=A1=E7=89=87=E7=BD=91?= =?UTF-8?q?=E6=A0=BC),=20=E7=AD=9B=E9=80=89=E5=B7=A5=E5=85=B7=E6=A0=8F?= =?UTF-8?q?=E6=8F=90=E5=8F=96=E4=B8=BA=E7=8B=AC=E7=AB=8B=E5=8F=97=E6=8E=A7?= =?UTF-8?q?=E7=BB=84=E4=BB=B6,=203=E4=B8=AATab=E5=85=A8=E9=83=A8=E5=8F=97?= =?UTF-8?q?=E6=8E=A7=E5=B1=95=E7=A4=BA=E7=BB=84=E4=BB=B6=E9=9B=B6=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E7=8A=B6=E6=80=81,=20=E9=9B=B6=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=8F=98=E6=9B=B4,=20ESLint=20errors=3D0/warnings=3D65/any=3D0?= =?UTF-8?q?,=20tsc=20-b=3D0,=20npm=20build=E2=9C=93=20(17.81s),=20go=20bui?= =?UTF-8?q?ld=3D0,=2017/17=20=E6=B5=8B=E8=AF=95=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PROJECT_EVALUATION.md | 2 +- ROADMAP.md | 35 ++ .../src/components/cloud/CloudBilling.tsx | 303 ++---------------- .../cloud/cloud-billing/AlertRulesTab.tsx | 91 ++++++ .../cloud/cloud-billing/OverviewTab.tsx | 110 +++++++ .../cloud/cloud-billing/RecordsTab.tsx | 146 +++++++++ 6 files changed, 415 insertions(+), 272 deletions(-) create mode 100644 frontend/src/components/cloud/cloud-billing/AlertRulesTab.tsx create mode 100644 frontend/src/components/cloud/cloud-billing/OverviewTab.tsx create mode 100644 frontend/src/components/cloud/cloud-billing/RecordsTab.tsx diff --git a/PROJECT_EVALUATION.md b/PROJECT_EVALUATION.md index d25f5a2..f9820bb 100644 --- a/PROJECT_EVALUATION.md +++ b/PROJECT_EVALUATION.md @@ -129,7 +129,7 @@ | ~~`api.ts` 1324 行/352 方法~~ → 已拆分为 12 个域文件 (api/kubernetes.ts, deploy.ts, monitor.ts 等) | ✅ 已解决 | 前端 | ~~P0~~ done | | 前端 **1026 处** `any` 类型 | 类型安全缺失,重构风险高 | 前端全局 | ~~P0~~ **done** (any 1076→40, unused 25→6, console 93→1, tsc --noEmit=0) | | ~~K8s 双路由体系(集群域+Kuboard 域)~~ → code 路由统一为主,ID 路由标记 deprecated 向后兼容 | ✅ 已解决 | 后端 | ~~P1~~ done | -| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-Q27 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%) + NamespaceResourcePage 719L→300L (-58%) + ApprovalWorkflowPage 705L→167L (-76%) + PipelineRunDetail 678L→273L (-60%) + CloudServerImport 699L→404L (-42%,any[]→具体接口) + UnifiedQueryPanel 682L→529L (-22%) + ServerManager 652L→442L (-32%) + PipelineEditor 676L→406L (-40%) + RdpDialog 642L→442L (-31%) + DeploymentEditService 627L→394L (-37%) + TraceManager 605L→331L (-45%,修复 SpanTreeNode 内联定义反模式) + GrafanaDashboard 624L→78L (-87%,DRY 合并 Zabbix/Prometheus Tab + 移除文件级 eslint-disable + 类型安全升级) + TopologyView 596L→472L (-21%) + DeploymentEditAdvanced 590L→289L (-51%) + WorkloadViewPage 572L→348L (-39%) + CloudAccountManager 572L→308L + ExcelServerImport 560L→385L + PipelineCreateDialog 559L→498L + PodLogsViewer 553L→331L (-40%) + HostMonitor 547L→224L (-59%) + KubernetesClusterDetail 547L→130L (-76%) + **AlertCenter 542L→312L (-42%,提取 4 个受控组件: AlertsToolbar + AlertsTab + ContactsTab + ContactGroupsTab,客户端搜索过滤+删除确认逻辑下沉到子组件)**,累计提取 103 个独立组件文件** | +| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-Q27 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%) + NamespaceResourcePage 719L→300L (-58%) + ApprovalWorkflowPage 705L→167L (-76%) + PipelineRunDetail 678L→273L (-60%) + CloudServerImport 699L→404L (-42%,any[]→具体接口) + UnifiedQueryPanel 682L→529L (-22%) + ServerManager 652L→442L (-32%) + PipelineEditor 676L→406L (-40%) + RdpDialog 642L→442L (-31%) + DeploymentEditService 627L→394L (-37%) + TraceManager 605L→331L (-45%,修复 SpanTreeNode 内联定义反模式) + GrafanaDashboard 624L→78L (-87%,DRY 合并 Zabbix/Prometheus Tab + 移除文件级 eslint-disable + 类型安全升级) + TopologyView 596L→472L (-21%) + DeploymentEditAdvanced 590L→289L (-51%) + WorkloadViewPage 572L→348L (-39%) + CloudAccountManager 572L→308L + ExcelServerImport 560L→385L + PipelineCreateDialog 559L→498L + PodLogsViewer 553L→331L (-40%) + HostMonitor 547L→224L (-59%) + KubernetesClusterDetail 547L→130L (-76%) + **AlertCenter 542L→312L (-42%,提取 4 个受控组件: AlertsToolbar + AlertsTab + ContactsTab + ContactGroupsTab,客户端搜索过滤+删除确认逻辑下沉到子组件)** + **CloudBilling 534L→295L (-45%,提取 cloud-billing/ 3 个文件: OverviewTab + RecordsTab(含 RecordsFilterToolbar) + AlertRulesTab)**,累计提取 106 个独立组件文件** | ### 3.2 🟡 中优先级 diff --git a/ROADMAP.md b/ROADMAP.md index 1cfae85..9dc5dd3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -501,6 +501,41 @@ services/api.ts (1324L) | Q27 | 前端巨型组件拆分(HostMonitor 547L→224L (-59%),提取 host-monitor/tabs/ 8 个文件 (BasicTab + DiskTab + NetworkTab + PortMonitorTab + ProcessTable + FDUsageCard + ContextSwitchCard + ProcessTab),TABS 常量数组提取为模块级,PCT_REF_LINES 常量提升到 types.ts,端口/进程数组类型 JsonData→JsonData[] 类型安全升级) | P0 | **done** | | Q28 | 前端巨型组件拆分(KubernetesClusterDetail 547L→130L (-76%),提取 cluster-detail/ 8 个文件 (types(computeClusterMetrics/buildTrendData 纯函数) + OverviewTab + NodesTab + PodsTab + DeploymentsTab + ServicesTab + IngressesTab + EventsTab)) | P0 | **done** | | Q29 | 前端巨型组件拆分(AlertCenter 542L→312L (-42%),提取 4 个受控组件 (AlertsToolbar 89L 筛选工具栏 + AlertsTab 149L 告警列表表格+分页 + ContactsTab 87L 联系人卡片网格 + ContactGroupsTab 88L 联系组卡片网格),客户端搜索过滤逻辑移入 AlertsTab,删除确认逻辑下沉到子组件) | P0 | **done** | +| Q30 | 前端巨型组件拆分(CloudBilling 534L→295L (-45%),提取 cloud-billing/ 3 个文件 (OverviewTab 110L 概览统计:同步信息+成本告警横幅+趋势图+厂商分布 + RecordsTab 146L 成本记录表格+筛选工具栏 + AlertRulesTab 91L 预警规则卡片网格)) | P0 | **done** | + +--- + +## Q30: 前端巨型组件拆分 — CloudBilling [done] + +**问题**: `CloudBilling.tsx`(534 行)是 Q29 拆分后项目最大的云管理业务组件之一,包含云账单管理页面的全部逻辑:3 个 Tab(概览统计/成本记录/预警规则)、状态管理(~15 个 useState)、数据加载(loadStats/loadRecords/loadRules)、业务逻辑(同步账单/删除/切换规则)、以及 3 个 Tab 的完整内联 JSX(概览 ~93 行含同步信息横幅+成本告警横幅+趋势图+厂商分布堆叠条形图+明细网格 / 记录 ~62 行含筛选工具栏+表格 / 预警 ~60 行含规则卡片网格)。单文件过大导致维护困难。 + +**已完成 (2026-07-09)**: + +将 3 个 Tab 内容区提取为 `cloud-billing/` 子目录下的独立文件: + +| 新文件 | 行数 | 职责 | +|--------|------|------| +| `OverviewTab.tsx` | 110 | 概览统计 Tab(同步信息横幅 + 成本告警横幅 + 30 天趋势图 + 云厂商分布堆叠条形图+明细网格),受控展示组件接收 stats + statsLoading,内部用 useTranslation + formatCurrency + TrendChart + providerLabels/providerHex/ProviderDot | +| `RecordsTab.tsx` | 146 | 成本记录 Tab(含 `RecordsFilterToolbar` 导出组件: 云厂商下拉+日期范围+查询按钮 + `RecordsTab` 表格组件: 表头+表体+加载/空状态),RecordsFilterToolbar 接收 7 个 props(filter 值+setter+onQuery),RecordsTab 接收 records/loading + onEdit/onDelete/onAdd 回调 | +| `AlertRulesTab.tsx` | 91 | 预警规则 Tab(规则卡片网格: 名称+启用 Switch+厂商+指标+阈值+最近触发时间+编辑/删除按钮 + 加载/空状态),受控展示组件接收 rules/loading + onEdit/onDelete/onToggle/onAdd 回调 | + +主文件 `CloudBilling.tsx` 从 **534 行降至 295 行**(-45%),仅保留:~15 个 useState 状态声明、数据加载(loadStats/loadRecords/loadRules useCallback + useEffect 触发)、业务逻辑(handleSyncYesterday 账单同步 + handleDeleteRecord/handleDeleteRule 带 useConfirm 二次确认 + handleToggleRule)、openAdd/openEdit 回调、pageTabs 配置、PageShell 布局(StatStrip 统计条 + actions 按钮组 + RecordsFilterToolbar 条件传入 toolbar + 3 个 Tab 子组件编排 + 2 个 Dialog 挂载)。 + +**关键改进**: +- **筛选工具栏提取**: 原 `recordsToolbar` 内联 JSX(27 行: Select 下拉+2 个 date Input+查询 Button)提取为 `RecordsFilterToolbar` 独立组件,主组件通过 7 个 props(filter 值+setter+onQuery)控制,零逻辑变更。 +- **受控组件架构**: 3 个 Tab 子组件全部为受控展示组件(接收数据 + 回调 props),无独立状态。所有状态管理保留在主组件,拆分为纯机械操作(移动 JSX + 添加 props 透传),零逻辑变更。 + +**验收标准**: +- [x] CloudBilling.tsx 从 534 行降至 **295 行**(-45%) +- [x] 3 个提取的文件全部 < 150 行(最大 RecordsTab 146 行含 toolbar+table 双导出) +- [x] `tsc -b` 零错误 +- [x] `npm run build` 零错误(✓ built in 17.81s) +- [x] ESLint errors 保持 **0**(CloudBilling + cloud-billing/ 目录零 errors/warnings) +- [x] ESLint warnings 保持 **65**(无新增) +- [x] 全局 ESLint any 保持 **0** +- [x] `go build` 零错误 +- [x] 17/17 集成回归测试通过(零回归) +- [x] 云账单页功能不变(3 个 Tab:概览统计趋势图+厂商分布/成本记录筛选+表格/预警规则卡片+开关全部正常) --- diff --git a/frontend/src/components/cloud/CloudBilling.tsx b/frontend/src/components/cloud/CloudBilling.tsx index f2699f2..478856b 100644 --- a/frontend/src/components/cloud/CloudBilling.tsx +++ b/frontend/src/components/cloud/CloudBilling.tsx @@ -1,46 +1,18 @@ import { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; 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, - Wallet, - Bell, - CloudDownload, - Loader2, - Info, -} from 'lucide-react'; +import { Plus, RefreshCw, Wallet, CloudDownload, Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { billingApi } from '@/api'; -import { - formatCurrency, - TrendChart, - providerLabels, - providerHex, - ProviderDot, -} from './CloudBillingHelpers'; +import { formatCurrency } from './CloudBillingHelpers'; import { StatStrip } from '@/components/ui/stat-strip'; import { BillingRecordDialog } from './BillingRecordDialog'; import { BillingRuleDialog } from './BillingRuleDialog'; import { PageShell } from '@/components/layout/PageShell'; -import { ContentPanel } from '@/components/layout/ContentPanel'; -import { PageLoading } from '@/components/layout/PageLoading'; -import { PageEmpty } from '@/components/layout/PageEmpty'; +import { OverviewTab } from './cloud-billing/OverviewTab'; +import { RecordsTab, RecordsFilterToolbar } from './cloud-billing/RecordsTab'; +import { AlertRulesTab } from './cloud-billing/AlertRulesTab'; import type { JsonData } from '@/types/common'; export function CloudBilling() { @@ -217,34 +189,6 @@ export function CloudBilling() { { id: 'alerts', label: t('cloud.billingAlert'), badge: rules.length > 0 ? rules.length : undefined }, ]; - const recordsToolbar = ( -
-
- - -
-
- - setFilterStart(e.target.value)} /> -
-
- - setFilterEnd(e.target.value)} /> -
- -
- ); - return ( setActiveTab(id as typeof activeTab)} - toolbar={activeTab === 'records' ? recordsToolbar : undefined} + toolbar={activeTab === 'records' ? ( + + ) : undefined} metrics={ stats && !statsLoading ? ( {/* Tab 1:概览统计 */} {activeTab === 'overview' && ( - statsLoading ? ( - - ) : stats ? ( -
-
-
- -
-

{t('cloud.syncBillingInfo')}

-

{t('cloud.syncBillingTiming')}

-
-
-
- - {stats.alerts?.length > 0 && ( -
-
- - {t('cloud.costAlert')} -
-
- {stats.alerts.map((a: JsonData, i: number) => ( -
-
- {a.rule_name} - - {a.provider} · {a.metric === 'daily' ? t('cloud.dailyCost') : t('cloud.monthlyAccumulated')} - -
- - {t('cloud.current')} {formatCurrency(a.current)} - {' '}{a.operator}{' '} - {t('cloud.alertThreshold')} {formatCurrency(a.threshold)} - -
- ))} -
-
- )} - - - -

{t('cloud.trend30d')}

- -
-
- - - -

{t('cloud.monthVendorDistribution')}

- {(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)}% -
-
- ))} -
-
- )} -
-
-
- ) : ( - } title={t('cloud.noStatsData')} /> - ) + )} {/* Tab 2:成本记录 */} {activeTab === 'records' && ( - - {recordsLoading ? ( - - ) : records.length === 0 ? ( - } - title={t('cloud.noCostRecords')} - action={( - - )} - /> - ) : ( -
- - - - - - - - - - - - - - - {records.map((r) => ( - - - - - - - - - - - ))} - -
{t('cloud.colDate')}{t('cloud.vendor')}{t('cloud.colService')}{t('cloud.colRegion')}{t('cloud.colAccount')}{t('cloud.colCost')}{t('cloud.colDescription')}{t('common.actions')}
{r.billing_date} - - - {providerLabels[r.provider] || r.provider} - - {r.service || '-'}{r.region || '-'}{r.account_name || '-'}{formatCurrency(r.cost)}{r.description || '-'} -
- - -
-
-
- )} -
+ )} {/* Tab 3:预警规则 */} {activeTab === 'alerts' && ( - rulesLoading ? ( - - ) : rules.length === 0 ? ( - } - title={t('cloud.noAlertRules')} - description={t('cloud.alertRuleHint')} - action={( - - )} - /> - ) : ( -
- {rules.map((r) => ( - - -
-
- - {r.name} -
- handleToggleRule(r.id)} /> -
-
-
- {t('cloud.vendor')} - {r.provider ? (providerLabels[r.provider] || r.provider) : t('common.all')} -
-
- {t('cloud.colMetric')} - {r.metric === 'daily' ? t('cloud.dailyCost') : t('cloud.monthlyAccumulated')} -
-
- {t('cloud.alertThreshold')} - {r.operator} {formatCurrency(r.threshold)} -
- {r.last_triggered && ( -
- {t('cloud.lastTriggered')} - {new Date(r.last_triggered).toLocaleString('zh-CN')} -
- )} -
-
- - -
-
-
- ))} -
- ) + )} void; + onDelete: (id: number) => void; + onToggle: (id: number) => void; + onAdd: () => void; +}) { + const { t } = useTranslation(); + + if (loading) return ; + + if (rules.length === 0) { + return ( + } + title={t('cloud.noAlertRules')} + description={t('cloud.alertRuleHint')} + action={( + + )} + /> + ); + } + + return ( +
+ {rules.map((r) => ( + + +
+
+ + {r.name} +
+ onToggle(r.id)} /> +
+
+
+ {t('cloud.vendor')} + {r.provider ? (providerLabels[r.provider] || r.provider) : t('common.all')} +
+
+ {t('cloud.colMetric')} + {r.metric === 'daily' ? t('cloud.dailyCost') : t('cloud.monthlyAccumulated')} +
+
+ {t('cloud.alertThreshold')} + {r.operator} {formatCurrency(r.threshold)} +
+ {r.last_triggered && ( +
+ {t('cloud.lastTriggered')} + {new Date(r.last_triggered).toLocaleString('zh-CN')} +
+ )} +
+
+ + +
+
+
+ ))} +
+ ); +} diff --git a/frontend/src/components/cloud/cloud-billing/OverviewTab.tsx b/frontend/src/components/cloud/cloud-billing/OverviewTab.tsx new file mode 100644 index 0000000..f484f17 --- /dev/null +++ b/frontend/src/components/cloud/cloud-billing/OverviewTab.tsx @@ -0,0 +1,110 @@ +import { useTranslation } from 'react-i18next'; +import { Card, CardContent } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Wallet, Bell, Info } from 'lucide-react'; +import { formatCurrency, TrendChart, providerLabels, providerHex, ProviderDot } from '../CloudBillingHelpers'; +import { PageLoading } from '@/components/layout/PageLoading'; +import { PageEmpty } from '@/components/layout/PageEmpty'; +import type { JsonData } from '@/types/common'; + +/** 概览统计 Tab — 同步信息横幅 + 成本告警横幅 + 30 天趋势图 + 云厂商分布 // Logan.Li */ +export function OverviewTab({ stats, statsLoading }: { stats: JsonData; statsLoading: boolean }) { + const { t } = useTranslation(); + + if (statsLoading) return ; + if (!stats) return } title={t('cloud.noStatsData')} />; + + return ( +
+ {/* 同步信息提示 */} +
+
+ +
+

{t('cloud.syncBillingInfo')}

+

{t('cloud.syncBillingTiming')}

+
+
+
+ + {/* 成本告警横幅 */} + {stats.alerts?.length > 0 && ( +
+
+ + {t('cloud.costAlert')} +
+
+ {stats.alerts.map((a: JsonData, i: number) => ( +
+
+ {a.rule_name} + + {a.provider} · {a.metric === 'daily' ? t('cloud.dailyCost') : t('cloud.monthlyAccumulated')} + +
+ + {t('cloud.current')} {formatCurrency(a.current)} + {' '}{a.operator}{' '} + {t('cloud.alertThreshold')} {formatCurrency(a.threshold)} + +
+ ))} +
+
+ )} + + {/* 30 天趋势图 */} + + +

{t('cloud.trend30d')}

+ +
+
+ + {/* 云厂商成本分布 */} + + +

{t('cloud.monthVendorDistribution')}

+ {(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)}% +
+
+ ))} +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/src/components/cloud/cloud-billing/RecordsTab.tsx b/frontend/src/components/cloud/cloud-billing/RecordsTab.tsx new file mode 100644 index 0000000..2ea7d67 --- /dev/null +++ b/frontend/src/components/cloud/cloud-billing/RecordsTab.tsx @@ -0,0 +1,146 @@ +import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Plus, Trash2, Pencil, RefreshCw, Wallet } from 'lucide-react'; +import { ContentPanel } from '@/components/layout/ContentPanel'; +import { PageLoading } from '@/components/layout/PageLoading'; +import { PageEmpty } from '@/components/layout/PageEmpty'; +import { formatCurrency, providerLabels, ProviderDot } from '../CloudBillingHelpers'; +import type { JsonData } from '@/types/common'; + +/** 成本记录筛选工具栏 — 云厂商下拉 + 日期范围 + 查询按钮 // Logan.Li */ +export function RecordsFilterToolbar({ + filterProvider, + filterStart, + filterEnd, + onProviderChange, + onStartChange, + onEndChange, + onQuery, +}: { + filterProvider: string; + filterStart: string; + filterEnd: string; + onProviderChange: (v: string) => void; + onStartChange: (v: string) => void; + onEndChange: (v: string) => void; + onQuery: () => void; +}) { + const { t } = useTranslation(); + + return ( +
+
+ + +
+
+ + onStartChange(e.target.value)} /> +
+
+ + onEndChange(e.target.value)} /> +
+ +
+ ); +} + +/** 成本记录 Tab — 表格 + 加载/空状态 // Logan.Li */ +export function RecordsTab({ + records, + loading, + onEdit, + onDelete, + onAdd, +}: { + records: JsonData[]; + loading: boolean; + onEdit: (r: JsonData) => void; + onDelete: (id: number) => void; + onAdd: () => void; +}) { + const { t } = useTranslation(); + + return ( + + {loading ? ( + + ) : records.length === 0 ? ( + } + title={t('cloud.noCostRecords')} + action={( + + )} + /> + ) : ( +
+ + + + + + + + + + + + + + + {records.map((r) => ( + + + + + + + + + + + ))} + +
{t('cloud.colDate')}{t('cloud.vendor')}{t('cloud.colService')}{t('cloud.colRegion')}{t('cloud.colAccount')}{t('cloud.colCost')}{t('cloud.colDescription')}{t('common.actions')}
{r.billing_date} + + + {providerLabels[r.provider] || r.provider} + + {r.service || '-'}{r.region || '-'}{r.account_name || '-'}{formatCurrency(r.cost)}{r.description || '-'} +
+ + +
+
+
+ )} +
+ ); +}