import { useTranslation } from 'react-i18next'; import i18n from '@/i18n'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, BarChart, Bar, } from 'recharts'; import { Cpu, HardDrive, Activity, MemoryStick, Database } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { JsonData } from '@/types/common'; // Cloud platform color mapping (theme-aware) const CLOUD_COLORS: Record = { '阿里云': '#ff6a00', aliyun: '#ff6a00', AWS: '#ff9900', aws: '#ff9900', '腾讯云': '#006eff', tencent: '#006eff', '华为云': '#ff0000', huawei: '#ff0000', Azure: '#0078d4', azure: '#0078d4', }; const FALLBACK_CLOUD_COLORS = ['#ff6a00', '#ff9900', '#006eff', '#ff0000', '#0078d4', '#10b981', '#a855f7', '#ec4899']; export function computeMonthChange( monthTrend: JsonData[]): { pct: number; label: string; direction: 'up' | 'down' | 'neutral'; } { if (!monthTrend || monthTrend.length < 2) { return { pct: 0, label: i18n.t('dashboard.cost.noData'), direction: 'neutral' }; } const cur = Number(monthTrend[monthTrend.length - 1].cost ?? 0); const prev = Number(monthTrend[monthTrend.length - 2].cost ?? 0); if (prev === 0) { return { pct: 0, label: cur > 0 ? i18n.t('dashboard.cost.newCost') : i18n.t('dashboard.cost.flat'), direction: 'neutral' }; } const pct = ((cur - prev) / prev) * 100; const direction = pct > 0.05 ? 'up' : pct < -0.05 ? 'down' : 'neutral'; const label = `${pct > 0 ? '+' : ''}${pct.toFixed(1)}% ${i18n.t('dashboard.cost.vsLastMonth')}`; return { pct, label, direction }; } function generateMetricsData(avgCpu: number, avgMemory: number, avgDisk: number, points = 24) { const data = []; for (let i = points - 1; i >= 0; i--) { const d = new Date(Date.now() - i * 60 * 60 * 1000); data.push({ time: `${d.getHours().toString().padStart(2, '0')}:00`, cpu: Number(Math.max(0, Math.min(100, avgCpu + (Math.random() - 0.5) * 15)).toFixed(1)), memory: Number(Math.max(0, Math.min(100, avgMemory + (Math.random() - 0.5) * 10)).toFixed(1)), disk: Number(Math.max(0, Math.min(100, avgDisk + (Math.random() - 0.5) * 5)).toFixed(1)), }); } return data; } function buildMetricsFromReal( metrics: JsonData[], avgDisk: number) { if (!metrics || metrics.length === 0) return []; const buckets: Record = {}; for (const m of metrics) { const ts = m.timestamp; if (!ts) continue; const d = new Date(ts); const label = `${d.getHours().toString().padStart(2, '0')}:00`; if (!buckets[label]) buckets[label] = {}; if (m.name === 'cpu') buckets[label].cpu = Number(m.value) || 0; if (m.name === 'memory') buckets[label].memory = Number(m.value) || 0; } return Object.entries(buckets) .sort((a, b) => a[0].localeCompare(b[0])) .map(([time, v]) => ({ time, cpu: Number((v.cpu ?? 0).toFixed(1)), memory: Number((v.memory ?? 0).toFixed(1)), disk: Number(avgDisk.toFixed(1)), })); } // Custom tooltip for charts (theme-aware) function CustomTooltip({ active, payload, label }: JsonData) { if (!active || !payload) return null; return (

{label}

{payload.map((entry: JsonData, index: number) => (
{entry.name}: {typeof entry.value === 'number' ? `${entry.value}%` : entry.value}
))}
); } export function CostOverviewCard({ costStats }: { costStats: JsonData }) { const { t } = useTranslation(); const cloudCosts: JsonData[] = costStats?.cloud_costs ?? []; const monthTrend: JsonData[] = costStats?.month_trend ?? []; const costData = cloudCosts.map((c, i) => ({ name: c.cloud, value: c.cost, color: CLOUD_COLORS[c.cloud] || FALLBACK_CLOUD_COLORS[i % FALLBACK_CLOUD_COLORS.length], })); const monthlyData = monthTrend.map((m) => ({ month: m.month, cost: m.cost })); const totalCost = costStats?.total_cost ?? costData.reduce((sum, item) => sum + item.value, 0); const costChange = computeMonthChange(monthTrend); return (
{t('dashboard.cost.title')} {t('dashboard.cost.description')}

¥{Number(totalCost).toLocaleString()}

{costChange.label}

{t('dashboard.cost.distribution')}

{costData.map((entry, index) => ( ))} } />

{t('dashboard.cost.monthlyTrend')}

} />
{costData.map((item) => (
{item.name}
¥{Number(item.value).toLocaleString()}
))}
); } export function TopResourceUsageCard({ topUsage }: { topUsage: JsonData}) { const { t } = useTranslation(); const topCpuServers = (topUsage?.cpu ?? []).slice(0, 5); const topMemoryServers = (topUsage?.memory ?? []).slice(0, 5); const topDiskServers = (topUsage?.disk ?? []).slice(0, 5); const renderList = ( items: JsonData[], color: string, BarColor: string, icon: React.ReactNode, label: string ) => (
{icon} {label}
{items.map((item, index) => { const usage = Number(item.usage ?? 0); return (
{index + 1}

{item.hostname}

80 ? 'bg-[var(--dg-red)]' : usage > 60 ? 'bg-[var(--dg-yellow)]' : BarColor )} style={{ width: `${Math.min(usage, 100)}%` }} />
80 ? 'text-[var(--dg-red)]' : usage > 60 ? 'text-[var(--dg-yellow)]' : 'text-[var(--dg-text-muted)]' )}> {usage.toFixed(1)}%
); })}
); return (
{t('dashboard.topUsage.title')} {t('dashboard.topUsage.description')}
{renderList(topCpuServers, 'blue', 'bg-[var(--dg-blue)]', , t('dashboard.topUsage.cpu'))} {renderList(topMemoryServers, 'emerald', 'bg-[var(--dg-green)]', , t('dashboard.topUsage.memory'))} {renderList(topDiskServers, 'purple', 'bg-[var(--dg-purple)]', , t('dashboard.topUsage.disk'))}
); } export function ResourceUsageCard({ avgCpu, avgMemory, avgDisk, totalCpu, totalMemory, usedMemory, totalDisk, usedDisk, metrics, }: { avgCpu: number; avgMemory: number; avgDisk: number; totalCpu: number; totalMemory: number; usedMemory: number; totalDisk: number; usedDisk: number; metrics?: JsonData[]; }) { const realData = buildMetricsFromReal(metrics ?? [], avgDisk); const data = realData.length > 0 ? realData : generateMetricsData(avgCpu, avgMemory, avgDisk, 24); const { t } = useTranslation(); return (
{t('dashboard.resourceUsage.title')} {t('dashboard.resourceUsage.description')}
{t('dashboard.resourceUsage.cpu')} {t('dashboard.resourceUsage.memory')} {t('dashboard.resourceUsage.disk')}
{/* 总资源概览:在百分比趋势旁补充具体容量,避免“只看到百分比” // Logan.Li */}
CPU {totalCpu} 核
内存 {usedMemory} / {totalMemory} GB
磁盘 {usedDisk} / {totalDisk} GB
} />
); }