refactor: 全局代码模块化重构 — 所有文件<700行
后端 handler 拆分: - automation.go(1067→4文件), server.go(1011→5文件), ldap.go(815→3文件), user.go(704→2文件) 前端组件拆分: - SystemManager(1782→569), AlertManager(1133→220), K8sClusters(1062→4文件) - Dashboard(941→4文件), ServerManager(905→640), ApprovalFlow(830→548) - AlertCenter(775→693), AIPredictiveAnalytics(760→670) - CloudBilling(728→663), GrafanaDashboard(717→643), sidebar(726→689) Mock 数据模块化: - mocks/data.ts(2552行→barrel+11个domain模块) 新增 CLAUDE.md 项目全局开发规范 修复: AlertCenter/LogManager ListResponse.data→.list 类型不匹配 验证: go build ✅ + tsc --noEmit ✅ + npm run build ✅
This commit is contained in:
1 parent
e8dbe65955
commit
73b5a48df9
37 files changed
+5148
-5237
No files matched your search
@@ -36,6 +36,7 @@ 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: '阿里云',
|
||||
@@ -53,10 +54,6 @@ const providerColors: Record<string, string> = {
|
||||
azure: 'bg-sky-500',
|
||||
};
|
||||
|
||||
function formatCurrency(n: number) {
|
||||
return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
export function CloudBilling() {
|
||||
const confirm = useConfirm();
|
||||
|
||||
@@ -664,65 +661,3 @@ export function CloudBilling() {
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({ icon, label, value, trend, trendLabel, alert }: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: string;
|
||||
trend?: number;
|
||||
trendLabel?: string;
|
||||
alert?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className={`rounded-lg border p-4 ${alert ? 'border-amber-500/30 bg-amber-500/5' : 'border-slate-800 bg-slate-900/50'}`}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-slate-400 text-sm">{label}</span>
|
||||
<span className={alert ? 'text-amber-400' : 'text-slate-500'}>{icon}</span>
|
||||
</div>
|
||||
<div className={`text-2xl font-bold ${alert ? 'text-amber-300' : 'text-slate-100'}`}>{value}</div>
|
||||
{trendLabel && (
|
||||
<div className="flex items-center gap-1 mt-1 text-xs">
|
||||
{trend !== undefined && trend > 0 && <TrendingUp className="h-3 w-3 text-red-400" />}
|
||||
{trend !== undefined && trend < 0 && <TrendingDown className="h-3 w-3 text-green-400" />}
|
||||
<span className="text-slate-500">{trendLabel}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TrendChart({ data }: { data: { date: string; cost: number }[] }) {
|
||||
if (data.length === 0) {
|
||||
return <p className="text-sm text-slate-500 py-8 text-center">暂无数据</p>;
|
||||
}
|
||||
const max = Math.max(...data.map((d) => d.cost), 1);
|
||||
const recent = data.slice(-30);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-end gap-0.5 h-40">
|
||||
{recent.map((d, i) => {
|
||||
const h = Math.max((d.cost / max) * 100, 2);
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 group relative"
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<div
|
||||
className="absolute bottom-0 w-full rounded-t-sm bg-gradient-to-t from-blue-600/60 to-blue-400/80 group-hover:from-blue-500 group-hover:to-blue-300 transition-colors"
|
||||
style={{ height: `${h}%` }}
|
||||
/>
|
||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover:block whitespace-nowrap text-xs bg-slate-800 text-slate-200 px-2 py-1 rounded border border-slate-700 z-10">
|
||||
{d.date}: {formatCurrency(d.cost)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex justify-between mt-2 text-xs text-slate-500">
|
||||
<span>{recent[0]?.date}</span>
|
||||
<span>{recent[recent.length - 1]?.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import { TrendingUp, TrendingDown } from 'lucide-react';
|
||||
|
||||
export function formatCurrency(n: number) {
|
||||
return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
export function StatCard({ icon, label, value, trend, trendLabel, alert }: {
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
value: string;
|
||||
trend?: number;
|
||||
trendLabel?: string;
|
||||
alert?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className={`rounded-lg border p-4 ${alert ? 'border-amber-500/30 bg-amber-500/5' : 'border-slate-800 bg-slate-900/50'}`}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-slate-400 text-sm">{label}</span>
|
||||
<span className={alert ? 'text-amber-400' : 'text-slate-500'}>{icon}</span>
|
||||
</div>
|
||||
<div className={`text-2xl font-bold ${alert ? 'text-amber-300' : 'text-slate-100'}`}>{value}</div>
|
||||
{trendLabel && (
|
||||
<div className="flex items-center gap-1 mt-1 text-xs">
|
||||
{trend !== undefined && trend > 0 && <TrendingUp className="h-3 w-3 text-red-400" />}
|
||||
{trend !== undefined && trend < 0 && <TrendingDown className="h-3 w-3 text-green-400" />}
|
||||
<span className="text-slate-500">{trendLabel}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TrendChart({ data }: { data: { date: string; cost: number }[] }) {
|
||||
if (data.length === 0) {
|
||||
return <p className="text-sm text-slate-500 py-8 text-center">暂无数据</p>;
|
||||
}
|
||||
const max = Math.max(...data.map((d) => d.cost), 1);
|
||||
const recent = data.slice(-30);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-end gap-0.5 h-40">
|
||||
{recent.map((d, i) => {
|
||||
const h = Math.max((d.cost / max) * 100, 2);
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 group relative"
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<div
|
||||
className="absolute bottom-0 w-full rounded-t-sm bg-gradient-to-t from-blue-600/60 to-blue-400/80 group-hover:from-blue-500 group-hover:to-blue-300 transition-colors"
|
||||
style={{ height: `${h}%` }}
|
||||
/>
|
||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover:block whitespace-nowrap text-xs bg-slate-800 text-slate-200 px-2 py-1 rounded border border-slate-700 z-10">
|
||||
{d.date}: {formatCurrency(d.cost)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex justify-between mt-2 text-xs text-slate-500">
|
||||
<span>{recent[0]?.date}</span>
|
||||
<span>{recent[recent.length - 1]?.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user