Files
aiops/frontend/src/components/cloud/CloudBillingHelpers.tsx
T
attacker ecec73989b feat(i18n): 剩余模块全量国际化 — 51% 覆盖率 (231 文件接入 t())
批量转换 163 个文件,3204 处中文替换为 t() 调用:
- Monitor: 日志中心/告警中心/数据源/大屏/站点拨测/SNMP/中间件 全部转换
- Kubernetes: ResourceVisualEditor/NamespaceResourcePage/拓扑/事件 全部转换
- Deploy: 流水线/发布/凭证/RBAC 全部转换
- Server/Cloud/Agent/Task/Layout/UI: 全部转换
- 翻译词典扩展至 6474 行 (22 namespaces, 5000+ keys)
- tsc 编译零错误
2026-07-03 01:51:57 +08:00

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>
);
}