批量转换 163 个文件,3204 处中文替换为 t() 调用: - Monitor: 日志中心/告警中心/数据源/大屏/站点拨测/SNMP/中间件 全部转换 - Kubernetes: ResourceVisualEditor/NamespaceResourcePage/拓扑/事件 全部转换 - Deploy: 流水线/发布/凭证/RBAC 全部转换 - Server/Cloud/Agent/Task/Layout/UI: 全部转换 - 翻译词典扩展至 6474 行 (22 namespaces, 5000+ keys) - tsc 编译零错误
113 lines
4.0 KiB
TypeScript
113 lines
4.0 KiB
TypeScript
import { type ReactNode } from 'react';
|
|
import { TrendingUp, TrendingDown } from 'lucide-react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import i18n from '@/i18n';
|
|
|
|
// 云厂商中文标签(主组件 + 弹窗共享) // Logan.Li
|
|
export const providerLabels: Record<string, string> = {
|
|
aliyun: i18n.t('cloud.aliyun'),
|
|
tencent: i18n.t('cloud.tencent'),
|
|
aws: 'AWS',
|
|
huawei: i18n.t('cloud.huawei'),
|
|
azure: 'Azure',
|
|
};
|
|
|
|
// 云厂商品牌色(跨主题固定,用 hex 避免 Tailwind 硬编码) // Logan.Li
|
|
export const providerHex: Record<string, string> = {
|
|
aliyun: '#FF6A00',
|
|
tencent: '#006EFF',
|
|
aws: '#FF9900',
|
|
huawei: '#CF0A2C',
|
|
azure: '#0078D4',
|
|
};
|
|
|
|
/** @deprecated 请改用 providerHex + style */
|
|
export 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',
|
|
};
|
|
|
|
export function ProviderDot({ provider, className = 'w-2 h-2' }: { provider: string; className?: string }) {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<span
|
|
className={`rounded-full shrink-0 ${className}`}
|
|
style={{ backgroundColor: providerHex[provider] || 'var(--surface-active)' }}
|
|
/>
|
|
);
|
|
}
|
|
|
|
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;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<div className={alert ? 'dg-alert-banner' : 'rounded-lg border border-[var(--border-default)] bg-[var(--surface-raised)]/50 p-4'}>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="dg-text-secondary text-sm">{label}</span>
|
|
<span className={alert ? 'dg-text-yellow' : 'dg-text-muted'}>{icon}</span>
|
|
</div>
|
|
<div className={`text-2xl font-bold ${alert ? 'dg-text-yellow' : 'text-[var(--text-primary)]'}`}>{value}</div>
|
|
{trendLabel && (
|
|
<div className="flex items-center gap-1 mt-1 text-xs">
|
|
{trend !== undefined && trend > 0 && <TrendingUp className="h-3 w-3 dg-text-red" />}
|
|
{trend !== undefined && trend < 0 && <TrendingDown className="h-3 w-3 dg-text-green" />}
|
|
<span className="dg-text-muted">{trendLabel}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function TrendChart({ data }: { data: { date: string; cost: number }[] }) {
|
|
const { t } = useTranslation();
|
|
if (data.length === 0) {
|
|
return <p className="text-sm dg-text-muted py-8 text-center">{t('common.noData')}</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 transition-opacity group-hover:opacity-90"
|
|
style={{
|
|
height: `${h}%`,
|
|
background: `linear-gradient(to top, color-mix(in srgb, var(--brand) 55%, transparent), color-mix(in srgb, var(--brand) 85%, transparent))`,
|
|
}}
|
|
/>
|
|
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover:block whitespace-nowrap text-xs bg-[var(--surface-inset)] text-[var(--text-primary)] px-2 py-1 rounded border border-[var(--border-default)] z-10">
|
|
{d.date}: {formatCurrency(d.cost)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="flex justify-between mt-2 text-xs dg-text-muted">
|
|
<span>{recent[0]?.date}</span>
|
|
<span>{recent[recent.length - 1]?.date}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |