feat(i18n): cloud 模块全量国际化迁移 — 4 组件 + 90 新增翻译键
迁移组件:
- RegionManager.tsx — providerNames→getProviderNames(t) 函数模式, 全量 t() 替换
- CloudAccountManager.tsx — providerLabels→getProviderLabels(t), 卡片/弹框/表单全量迁移
- CloudBilling.tsx — 概览/记录/预警三 Tab 全量迁移, TrendChart/StatStrip 标签国际化
- DomainManager.tsx — 域名卡片/状态徽章/创建编辑弹框全量迁移
翻译键: cloud 命名空间 145→235 键 (+90), common 新增 deleteFailed/deleteSuccess
验证: tsc -b=0, npm build✅ (21.21s), 17/17 集成测试通过, 0 缺失键, 0 残留 UI 中文
This commit is contained in:
1 parent
63e4c220d4
commit
8cbe3aadec
6 files changed
+409
-215
No files matched your search
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -40,6 +41,7 @@ import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
export function CloudBilling() {
|
||||
const { t } = useTranslation();
|
||||
const confirm = useConfirm();
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'records' | 'alerts'>('overview');
|
||||
@@ -67,11 +69,11 @@ export function CloudBilling() {
|
||||
const res = await billingApi.getStats();
|
||||
setStats(res);
|
||||
} catch (e) {
|
||||
toast.error('加载统计失败', { description: (e as JsonData).message });
|
||||
toast.error(t('cloud.loadStatsFailed'), { description: (e as JsonData).message });
|
||||
} finally {
|
||||
setStatsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
const loadRecords = useCallback(async () => {
|
||||
setRecordsLoading(true);
|
||||
@@ -83,11 +85,11 @@ export function CloudBilling() {
|
||||
const res = await billingApi.list(params);
|
||||
setRecords(res.list || []);
|
||||
} catch (e) {
|
||||
toast.error('加载账单失败', { description: (e as JsonData).message });
|
||||
toast.error(t('cloud.loadBillingFailed'), { description: (e as JsonData).message });
|
||||
} finally {
|
||||
setRecordsLoading(false);
|
||||
}
|
||||
}, [filterProvider, filterStart, filterEnd]);
|
||||
}, [filterProvider, filterStart, filterEnd, t]);
|
||||
|
||||
const loadRules = useCallback(async () => {
|
||||
setRulesLoading(true);
|
||||
@@ -95,11 +97,11 @@ export function CloudBilling() {
|
||||
const res = await billingApi.listAlertRules();
|
||||
setRules(res.list || []);
|
||||
} catch (e) {
|
||||
toast.error('加载预警规则失败', { description: (e as JsonData).message });
|
||||
toast.error(t('cloud.loadAlertRulesFailed'), { description: (e as JsonData).message });
|
||||
} finally {
|
||||
setRulesLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
loadStats();
|
||||
@@ -140,27 +142,27 @@ export function CloudBilling() {
|
||||
};
|
||||
|
||||
const handleDeleteRecord = async (id: number) => {
|
||||
const ok = await confirm({ title: '删除账单记录', description: '确定删除这条成本记录?', variant: 'destructive' });
|
||||
const ok = await confirm({ title: t('cloud.deleteBillingRecord'), description: t('cloud.deleteBillingConfirm'), variant: 'destructive' });
|
||||
if (!ok) return;
|
||||
try {
|
||||
await billingApi.remove(id);
|
||||
loadRecords();
|
||||
loadStats();
|
||||
toast('删除成功');
|
||||
toast(t('common.deleteSuccess'));
|
||||
} catch (e) {
|
||||
toast.error('删除失败', { description: (e as JsonData).message });
|
||||
toast.error(t('common.deleteFailed'), { description: (e as JsonData).message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRule = async (id: number) => {
|
||||
const ok = await confirm({ title: '删除预警规则', description: '确定删除此预警规则?', variant: 'destructive' });
|
||||
const ok = await confirm({ title: t('cloud.deleteAlertRule'), description: t('cloud.deleteAlertRuleConfirm'), variant: 'destructive' });
|
||||
if (!ok) return;
|
||||
try {
|
||||
await billingApi.deleteAlertRule(id);
|
||||
loadRules();
|
||||
toast('删除成功');
|
||||
toast(t('common.deleteSuccess'));
|
||||
} catch (e) {
|
||||
toast.error('删除失败', { description: (e as JsonData).message });
|
||||
toast.error(t('common.deleteFailed'), { description: (e as JsonData).message });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -170,24 +172,24 @@ export function CloudBilling() {
|
||||
loadRules();
|
||||
loadStats();
|
||||
} catch (e) {
|
||||
toast.error('操作失败', { description: (e as JsonData).message });
|
||||
toast.error(t('cloud.operationFailed'), { description: (e as JsonData).message });
|
||||
}
|
||||
};
|
||||
|
||||
const pageTabs = [
|
||||
{ id: 'overview', label: '概览统计' },
|
||||
{ id: 'records', label: '成本记录' },
|
||||
{ id: 'alerts', label: '预警规则', badge: rules.length > 0 ? rules.length : undefined },
|
||||
{ id: 'overview', label: t('cloud.overviewStats') },
|
||||
{ id: 'records', label: t('cloud.costRecords') },
|
||||
{ id: 'alerts', label: t('cloud.billingAlert'), badge: rules.length > 0 ? rules.length : undefined },
|
||||
];
|
||||
|
||||
const recordsToolbar = (
|
||||
<div className="flex flex-wrap items-end gap-3">
|
||||
<div>
|
||||
<Label className="text-xs dg-text-secondary mb-1">厂商</Label>
|
||||
<Label className="text-xs dg-text-secondary mb-1">{t('cloud.vendor')}</Label>
|
||||
<Select value={filterProvider || 'all'} onValueChange={(v) => setFilterProvider(v === 'all' ? '' : v)}>
|
||||
<SelectTrigger className="w-32 h-9 dg-input"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部</SelectItem>
|
||||
<SelectItem value="all">{t('common.all')}</SelectItem>
|
||||
{Object.entries(providerLabels).map(([k, v]) => (
|
||||
<SelectItem key={k} value={k}>{v}</SelectItem>
|
||||
))}
|
||||
@@ -195,15 +197,15 @@ export function CloudBilling() {
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs dg-text-secondary mb-1">开始日期</Label>
|
||||
<Label className="text-xs dg-text-secondary mb-1">{t('cloud.startDate')}</Label>
|
||||
<Input type="date" className="w-40 h-9 dg-input" value={filterStart} onChange={(e) => setFilterStart(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs dg-text-secondary mb-1">结束日期</Label>
|
||||
<Label className="text-xs dg-text-secondary mb-1">{t('cloud.endDate')}</Label>
|
||||
<Input type="date" className="w-40 h-9 dg-input" value={filterEnd} onChange={(e) => setFilterEnd(e.target.value)} />
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={loadRecords}>
|
||||
<RefreshCw className="h-4 w-4 mr-1" />查询
|
||||
<RefreshCw className="h-4 w-4 mr-1" />{t('cloud.query')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -211,8 +213,8 @@ export function CloudBilling() {
|
||||
return (
|
||||
<PageShell
|
||||
variant="list"
|
||||
title="云账单管理"
|
||||
subtitle="多云成本统计、记录与预警"
|
||||
title={t('cloud.billingTitle')}
|
||||
subtitle={t('cloud.billingSubtitle')}
|
||||
icon={<Wallet className="h-5 w-5" />}
|
||||
tabs={pageTabs}
|
||||
activeTab={activeTab}
|
||||
@@ -222,10 +224,10 @@ export function CloudBilling() {
|
||||
stats && !statsLoading ? (
|
||||
<StatStrip
|
||||
items={[
|
||||
{ label: '今日', value: formatCurrency(stats.today_cost || 0), color: 'blue' },
|
||||
{ label: '本月', value: formatCurrency(stats.month_cost || 0), color: 'amber' },
|
||||
{ label: '年度', value: formatCurrency(stats.year_cost || 0), color: 'purple' },
|
||||
{ label: '预警', value: `${stats.alerts?.length || 0} 条`, color: stats.alerts?.length > 0 ? 'red' : 'muted' },
|
||||
{ label: t('cloud.today'), value: formatCurrency(stats.today_cost || 0), color: 'blue' },
|
||||
{ label: t('cloud.thisMonth'), value: formatCurrency(stats.month_cost || 0), color: 'amber' },
|
||||
{ label: t('cloud.yearly'), value: formatCurrency(stats.year_cost || 0), color: 'purple' },
|
||||
{ label: t('cloud.costAlert'), value: t('cloud.alertCount', { count: stats.alerts?.length || 0 }), color: stats.alerts?.length > 0 ? 'red' : 'muted' },
|
||||
]}
|
||||
/>
|
||||
) : undefined
|
||||
@@ -233,16 +235,16 @@ export function CloudBilling() {
|
||||
actions={(
|
||||
<>
|
||||
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={refreshAll}>
|
||||
<RefreshCw className="h-4 w-4 mr-1" />刷新
|
||||
<RefreshCw className="h-4 w-4 mr-1" />{t('common.refresh')}
|
||||
</Button>
|
||||
{activeTab === 'records' && (
|
||||
<Button size="sm" className="dg-btn-primary" onClick={openAddRecord}>
|
||||
<Plus className="h-4 w-4 mr-1" />记录成本
|
||||
<Plus className="h-4 w-4 mr-1" />{t('cloud.recordCost')}
|
||||
</Button>
|
||||
)}
|
||||
{activeTab === 'alerts' && (
|
||||
<Button size="sm" className="dg-btn-primary" onClick={openAddRule}>
|
||||
<Plus className="h-4 w-4 mr-1" />添加规则
|
||||
<Plus className="h-4 w-4 mr-1" />{t('cloud.addAlertRule')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
@@ -258,7 +260,7 @@ export function CloudBilling() {
|
||||
<div className="dg-alert-banner">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Bell className="h-4 w-4 dg-text-yellow" />
|
||||
<span className="dg-alert-banner-title">成本预警</span>
|
||||
<span className="dg-alert-banner-title">{t('cloud.costAlert')}</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{stats.alerts.map((a: JsonData, i: number) => (
|
||||
@@ -266,13 +268,13 @@ export function CloudBilling() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="dg-badge-yellow">{a.rule_name}</Badge>
|
||||
<span className="dg-text-secondary">
|
||||
{a.provider} · {a.metric === 'daily' ? '日' : '月'}成本
|
||||
{a.provider} · {a.metric === 'daily' ? t('cloud.dailyCost') : t('cloud.monthlyAccumulated')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[var(--text-secondary)]">
|
||||
当前 <span className="dg-text-yellow font-medium">{formatCurrency(a.current)}</span>
|
||||
{t('cloud.current')} <span className="dg-text-yellow font-medium">{formatCurrency(a.current)}</span>
|
||||
{' '}{a.operator}{' '}
|
||||
阈值 <span className="dg-text-red font-medium">{formatCurrency(a.threshold)}</span>
|
||||
{t('cloud.alertThreshold')} <span className="dg-text-red font-medium">{formatCurrency(a.threshold)}</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -282,16 +284,16 @@ export function CloudBilling() {
|
||||
|
||||
<Card className="dg-card">
|
||||
<CardContent className="pt-4">
|
||||
<h3 className="dg-section-title">近 30 天成本趋势</h3>
|
||||
<h3 className="dg-section-title">{t('cloud.trend30d')}</h3>
|
||||
<TrendChart data={stats.month_trend || []} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="dg-card">
|
||||
<CardContent className="pt-4">
|
||||
<h3 className="dg-section-title">本月厂商成本分布</h3>
|
||||
<h3 className="dg-section-title">{t('cloud.monthVendorDistribution')}</h3>
|
||||
{(stats.provider_dist || []).length === 0 ? (
|
||||
<PageEmpty title="暂无分布数据" className="py-8" />
|
||||
<PageEmpty title={t('cloud.noDistributionData')} className="py-8" />
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex h-8 rounded-lg overflow-hidden">
|
||||
@@ -329,7 +331,7 @@ export function CloudBilling() {
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<PageEmpty icon={<Wallet className="h-12 w-12" />} title="暂无统计数据" />
|
||||
<PageEmpty icon={<Wallet className="h-12 w-12" />} title={t('cloud.noStatsData')} />
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -341,10 +343,10 @@ export function CloudBilling() {
|
||||
) : records.length === 0 ? (
|
||||
<PageEmpty
|
||||
icon={<Wallet className="h-12 w-12" />}
|
||||
title="暂无成本记录"
|
||||
title={t('cloud.noCostRecords')}
|
||||
action={(
|
||||
<Button size="sm" className="dg-btn-primary" onClick={openAddRecord}>
|
||||
<Plus className="h-4 w-4 mr-1" />记录成本
|
||||
<Plus className="h-4 w-4 mr-1" />{t('cloud.recordCost')}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
@@ -353,14 +355,14 @@ export function CloudBilling() {
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-[var(--surface-raised)] dg-text-secondary">
|
||||
<tr>
|
||||
<th className="px-4 py-2.5 text-left font-medium">日期</th>
|
||||
<th className="px-4 py-2.5 text-left font-medium">厂商</th>
|
||||
<th className="px-4 py-2.5 text-left font-medium">服务</th>
|
||||
<th className="px-4 py-2.5 text-left font-medium">区域</th>
|
||||
<th className="px-4 py-2.5 text-left font-medium">账号</th>
|
||||
<th className="px-4 py-2.5 text-right font-medium">费用</th>
|
||||
<th className="px-4 py-2.5 text-left font-medium">说明</th>
|
||||
<th className="px-4 py-2.5 text-center font-medium">操作</th>
|
||||
<th className="px-4 py-2.5 text-left font-medium">{t('cloud.colDate')}</th>
|
||||
<th className="px-4 py-2.5 text-left font-medium">{t('cloud.vendor')}</th>
|
||||
<th className="px-4 py-2.5 text-left font-medium">{t('cloud.colService')}</th>
|
||||
<th className="px-4 py-2.5 text-left font-medium">{t('cloud.colRegion')}</th>
|
||||
<th className="px-4 py-2.5 text-left font-medium">{t('cloud.colAccount')}</th>
|
||||
<th className="px-4 py-2.5 text-right font-medium">{t('cloud.colCost')}</th>
|
||||
<th className="px-4 py-2.5 text-left font-medium">{t('cloud.colDescription')}</th>
|
||||
<th className="px-4 py-2.5 text-center font-medium">{t('common.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -404,11 +406,11 @@ export function CloudBilling() {
|
||||
) : rules.length === 0 ? (
|
||||
<PageEmpty
|
||||
icon={<Bell className="h-12 w-12" />}
|
||||
title="暂无预警规则"
|
||||
description="添加规则后,成本超阈值将在此页管理"
|
||||
title={t('cloud.noAlertRules')}
|
||||
description={t('cloud.alertRuleHint')}
|
||||
action={(
|
||||
<Button size="sm" className="dg-btn-primary" onClick={openAddRule}>
|
||||
<Plus className="h-4 w-4 mr-1" />添加规则
|
||||
<Plus className="h-4 w-4 mr-1" />{t('cloud.addAlertRule')}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
@@ -426,20 +428,20 @@ export function CloudBilling() {
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<span className="dg-text-muted">厂商</span>
|
||||
<span className="ml-2 text-[var(--text-secondary)]">{r.provider ? (providerLabels[r.provider] || r.provider) : '全部'}</span>
|
||||
<span className="dg-text-muted">{t('cloud.vendor')}</span>
|
||||
<span className="ml-2 text-[var(--text-secondary)]">{r.provider ? (providerLabels[r.provider] || r.provider) : t('common.all')}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="dg-text-muted">指标</span>
|
||||
<span className="ml-2 text-[var(--text-secondary)]">{r.metric === 'daily' ? '日成本' : '月累计'}</span>
|
||||
<span className="dg-text-muted">{t('cloud.colMetric')}</span>
|
||||
<span className="ml-2 text-[var(--text-secondary)]">{r.metric === 'daily' ? t('cloud.dailyCost') : t('cloud.monthlyAccumulated')}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="dg-text-muted">阈值</span>
|
||||
<span className="dg-text-muted">{t('cloud.alertThreshold')}</span>
|
||||
<span className="ml-2 dg-text-yellow font-medium">{r.operator} {formatCurrency(r.threshold)}</span>
|
||||
</div>
|
||||
{r.last_triggered && (
|
||||
<div>
|
||||
<span className="dg-text-muted">最后触发</span>
|
||||
<span className="dg-text-muted">{t('cloud.lastTriggered')}</span>
|
||||
<span className="ml-2 dg-text-secondary">{new Date(r.last_triggered).toLocaleString('zh-CN')}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -473,4 +475,4 @@ export function CloudBilling() {
|
||||
/>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user