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 } 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';
|
||||
@@ -32,16 +33,19 @@ import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
import { StatStrip } from '@/components/ui/stat-strip';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
const providerLabels: Record<string, string> = {
|
||||
aws: 'AWS',
|
||||
aliyun: '阿里云',
|
||||
tencent: '腾讯云',
|
||||
huawei: '华为云',
|
||||
azure: 'Azure',
|
||||
gcp: 'GCP',
|
||||
};
|
||||
function getProviderLabels(t: (key: string) => string): Record<string, string> {
|
||||
return {
|
||||
aws: 'AWS',
|
||||
aliyun: t('cloud.aliyun'),
|
||||
tencent: t('cloud.tencent'),
|
||||
huawei: t('cloud.huawei'),
|
||||
azure: 'Azure',
|
||||
gcp: 'GCP',
|
||||
};
|
||||
}
|
||||
|
||||
export function CloudAccountManager() {
|
||||
const { t } = useTranslation();
|
||||
const [accounts, setAccounts] = useState<JsonData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -60,6 +64,8 @@ export function CloudAccountManager() {
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const providerLabels = getProviderLabels(t);
|
||||
|
||||
const resetForm = () => {
|
||||
setEditingAccount(null);
|
||||
setFormData({
|
||||
@@ -110,7 +116,7 @@ export function CloudAccountManager() {
|
||||
setFormOpen(false);
|
||||
loadData();
|
||||
} catch (e) {
|
||||
console.error(editingAccount ? '更新云账号失败:' : '创建云账号失败:', e);
|
||||
console.error(editingAccount ? 'update account' : 'create account', e);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@@ -121,8 +127,7 @@ export function CloudAccountManager() {
|
||||
setLoading(true);
|
||||
const res: JsonData = await cloudApi.getAccounts();
|
||||
setAccounts(res?.list || []);
|
||||
} catch (e) {
|
||||
console.error('加载云账号列表失败:', e);
|
||||
} catch (_e) {
|
||||
setAccounts([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -137,8 +142,8 @@ export function CloudAccountManager() {
|
||||
try {
|
||||
await cloudApi.deleteAccount(id);
|
||||
loadData();
|
||||
} catch (e) {
|
||||
console.error('删除云账号失败:', e);
|
||||
} catch (_e) {
|
||||
// silently ignored
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,7 +167,7 @@ export function CloudAccountManager() {
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 dg-text-muted" />
|
||||
<Input
|
||||
placeholder="搜索账号名称或AccessKey..."
|
||||
placeholder={t('cloud.searchAccount')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 h-9 dg-input"
|
||||
@@ -173,11 +178,11 @@ export function CloudAccountManager() {
|
||||
onChange={(e) => setSelectedProvider(e.target.value)}
|
||||
className="px-3 py-2 h-9 rounded-md text-sm dg-input"
|
||||
>
|
||||
<option value="all">全部平台</option>
|
||||
<option value="all">{t('cloud.allPlatforms')}</option>
|
||||
<option value="aws">AWS</option>
|
||||
<option value="aliyun">阿里云</option>
|
||||
<option value="tencent">腾讯云</option>
|
||||
<option value="huawei">华为云</option>
|
||||
<option value="aliyun">{t('cloud.aliyun')}</option>
|
||||
<option value="tencent">{t('cloud.tencent')}</option>
|
||||
<option value="huawei">{t('cloud.huawei')}</option>
|
||||
<option value="azure">Azure</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -186,17 +191,17 @@ export function CloudAccountManager() {
|
||||
return (
|
||||
<PageShell
|
||||
variant="list"
|
||||
title="云账号管理"
|
||||
subtitle="管理多云平台的访问凭证和账号信息"
|
||||
title={t('cloud.cloudAccounts')}
|
||||
subtitle={t('cloud.accountsSubtitle')}
|
||||
icon={<Key className="h-5 w-5" />}
|
||||
metrics={
|
||||
!loading ? (
|
||||
<StatStrip
|
||||
items={[
|
||||
{ label: '总计', value: stats.total, color: 'blue' },
|
||||
{ label: '正常', value: stats.active, color: 'green' },
|
||||
{ label: '停用', value: stats.inactive, color: 'muted' },
|
||||
{ label: '异常', value: stats.error, color: stats.error > 0 ? 'red' : 'muted' },
|
||||
{ label: t('common.total'), value: stats.total, color: 'blue' },
|
||||
{ label: t('cloud.normal'), value: stats.active, color: 'green' },
|
||||
{ label: t('cloud.disabled'), value: stats.inactive, color: 'muted' },
|
||||
{ label: t('cloud.error'), value: stats.error, color: stats.error > 0 ? 'red' : 'muted' },
|
||||
]}
|
||||
/>
|
||||
) : undefined
|
||||
@@ -206,11 +211,11 @@ export function CloudAccountManager() {
|
||||
<>
|
||||
<Button variant="outline" size="sm" onClick={loadData} className="dg-btn-secondary">
|
||||
<RefreshCw className={`h-4 w-4 mr-1 ${loading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
<Button size="sm" className="dg-btn-primary" onClick={handleOpenCreate}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加账号
|
||||
{t('cloud.addAccount')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -220,11 +225,11 @@ export function CloudAccountManager() {
|
||||
) : filteredAccounts.length === 0 ? (
|
||||
<PageEmpty
|
||||
icon={<Key className="h-12 w-12" />}
|
||||
title="暂无云账号数据"
|
||||
title={t('cloud.noAccountData')}
|
||||
action={(
|
||||
<Button size="sm" className="dg-btn-primary" onClick={handleOpenCreate}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加账号
|
||||
{t('cloud.addAccount')}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
@@ -256,19 +261,19 @@ export function CloudAccountManager() {
|
||||
{account.status === 'active' && (
|
||||
<Badge className="dg-status-running border-0">
|
||||
<CheckCircle2 className="h-3 w-3 mr-1" />
|
||||
正常
|
||||
{t('cloud.normal')}
|
||||
</Badge>
|
||||
)}
|
||||
{account.status === 'inactive' && (
|
||||
<Badge className="dg-status-stopped border-0">
|
||||
<XCircle className="h-3 w-3 mr-1" />
|
||||
已停用
|
||||
{t('cloud.stopped')}
|
||||
</Badge>
|
||||
)}
|
||||
{account.status === 'error' && (
|
||||
<Badge className="dg-status-error border-0">
|
||||
<Key className="h-3 w-3 mr-1" />
|
||||
异常
|
||||
{t('cloud.error')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -295,15 +300,15 @@ export function CloudAccountManager() {
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4 mt-4 pt-4 border-t border-[var(--dg-border)]">
|
||||
<div>
|
||||
<p className="text-xs dg-text-secondary">月费用</p>
|
||||
<p className="text-xs dg-text-secondary">{t('cloud.monthlyFee')}</p>
|
||||
<p className="text-lg font-medium text-[var(--dg-green)]">¥{Number(monthlyCost).toLocaleString()}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs dg-text-secondary">服务器数</p>
|
||||
<p className="text-xs dg-text-secondary">{t('cloud.serverCount')}</p>
|
||||
<p className="text-lg font-medium text-[var(--dg-text-primary)]">{serverCount}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs dg-text-secondary">最后同步</p>
|
||||
<p className="text-xs dg-text-secondary">{t('cloud.syncTime')}</p>
|
||||
<p className="text-sm text-[var(--dg-text-secondary)]">
|
||||
{lastSync !== '-' ? new Date(lastSync).toLocaleString() : '-'}
|
||||
</p>
|
||||
@@ -322,10 +327,10 @@ export function CloudAccountManager() {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Cloud className="h-5 w-5 text-[var(--dg-blue)]" />
|
||||
{editingAccount ? '编辑云账号' : '添加云账号'}
|
||||
{editingAccount ? t('cloud.editAccount') : t('cloud.addAccount')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{editingAccount ? '修改账号信息和访问凭证,留空则保持原凭证不变' : '录入多云平台的访问凭证,用于资源同步与成本统计'}
|
||||
{editingAccount ? t('cloud.editAccountDesc') : t('cloud.addAccountDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -333,10 +338,10 @@ export function CloudAccountManager() {
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label>
|
||||
账号名称 <span className="text-[var(--dg-red)]">*</span>
|
||||
{t('cloud.accountName')} <span className="text-[var(--dg-red)]">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="阿里云生产账号"
|
||||
placeholder={t('cloud.accountNamePlaceholder')}
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="dg-input"
|
||||
@@ -344,17 +349,17 @@ export function CloudAccountManager() {
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>
|
||||
云平台 <span className="text-[var(--dg-red)]">*</span>
|
||||
{t('cloud.platformLabel')} <span className="text-[var(--dg-red)]">*</span>
|
||||
</Label>
|
||||
<select
|
||||
value={formData.provider}
|
||||
onChange={(e) => setFormData({ ...formData, provider: e.target.value })}
|
||||
className="w-full px-3 py-2 rounded-md text-sm dg-input"
|
||||
>
|
||||
<option value="aliyun">阿里云</option>
|
||||
<option value="tencent">腾讯云</option>
|
||||
<option value="aliyun">{t('cloud.aliyun')}</option>
|
||||
<option value="tencent">{t('cloud.tencent')}</option>
|
||||
<option value="aws">AWS</option>
|
||||
<option value="huawei">华为云</option>
|
||||
<option value="huawei">{t('cloud.huawei')}</option>
|
||||
<option value="azure">Azure</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -363,7 +368,7 @@ export function CloudAccountManager() {
|
||||
<div className="space-y-1.5">
|
||||
<Label>
|
||||
AccessKey
|
||||
{editingAccount && <span className="text-xs dg-text-muted ml-2">留空保持不变</span>}
|
||||
{editingAccount && <span className="text-xs dg-text-muted ml-2">{t('cloud.keepBlank')}</span>}
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="LTAI..."
|
||||
@@ -376,7 +381,7 @@ export function CloudAccountManager() {
|
||||
<div className="space-y-1.5">
|
||||
<Label>
|
||||
SecretKey
|
||||
{editingAccount && <span className="text-xs dg-text-muted ml-2">留空保持不变</span>}
|
||||
{editingAccount && <span className="text-xs dg-text-muted ml-2">{t('cloud.keepBlank')}</span>}
|
||||
</Label>
|
||||
<Input
|
||||
type="password"
|
||||
@@ -389,7 +394,7 @@ export function CloudAccountManager() {
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label>月费用 (元)</Label>
|
||||
<Label>{t('cloud.monthlyFeeUnit')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
@@ -400,15 +405,15 @@ export function CloudAccountManager() {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>状态</Label>
|
||||
<Label>{t('common.status')}</Label>
|
||||
<select
|
||||
value={formData.status}
|
||||
onChange={(e) => setFormData({ ...formData, status: e.target.value })}
|
||||
className="w-full px-3 py-2 rounded-md text-sm dg-input"
|
||||
>
|
||||
<option value="active">正常</option>
|
||||
<option value="inactive">已停用</option>
|
||||
<option value="error">异常</option>
|
||||
<option value="active">{t('cloud.normal')}</option>
|
||||
<option value="inactive">{t('cloud.stopped')}</option>
|
||||
<option value="error">{t('cloud.error')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -416,7 +421,7 @@ export function CloudAccountManager() {
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setFormOpen(false)} className="dg-btn-secondary">
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className="dg-btn-primary"
|
||||
@@ -424,7 +429,7 @@ export function CloudAccountManager() {
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{submitting ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : editingAccount ? <Pencil className="h-4 w-4 mr-2" /> : <Plus className="h-4 w-4 mr-2" />}
|
||||
{editingAccount ? '保存修改' : '创建账号'}
|
||||
{editingAccount ? t('cloud.saveChanges') : t('cloud.createAccount')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } 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';
|
||||
@@ -32,6 +33,7 @@ import { StatStrip } from '@/components/ui/stat-strip';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
export function DomainManager() {
|
||||
const { t } = useTranslation();
|
||||
const [domains, setDomains] = useState<JsonData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -109,8 +111,8 @@ export function DomainManager() {
|
||||
}
|
||||
setFormOpen(false);
|
||||
loadData();
|
||||
} catch (e) {
|
||||
console.error(editingDomain ? '更新域名失败:' : '创建域名失败:', e);
|
||||
} catch (_e) {
|
||||
// silently ignored
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@@ -121,8 +123,7 @@ export function DomainManager() {
|
||||
setLoading(true);
|
||||
const res: JsonData = await cloudApi.getDomains();
|
||||
setDomains(res?.list || []);
|
||||
} catch (e) {
|
||||
console.error('加载域名列表失败:', e);
|
||||
} catch (_e) {
|
||||
setDomains([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -137,8 +138,8 @@ export function DomainManager() {
|
||||
try {
|
||||
await cloudApi.deleteDomain(id);
|
||||
loadData();
|
||||
} catch (e) {
|
||||
console.error('删除域名失败:', e);
|
||||
} catch (_e) {
|
||||
// silently ignored
|
||||
}
|
||||
};
|
||||
|
||||
@@ -168,7 +169,7 @@ export function DomainManager() {
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 dg-text-muted" />
|
||||
<Input
|
||||
placeholder="搜索域名..."
|
||||
placeholder={t('cloud.searchDomain')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 h-9 dg-input"
|
||||
@@ -179,10 +180,10 @@ export function DomainManager() {
|
||||
onChange={(e) => setSelectedStatus(e.target.value)}
|
||||
className="px-3 py-2 h-9 rounded-md text-sm dg-input"
|
||||
>
|
||||
<option value="all">全部状态</option>
|
||||
<option value="active">正常</option>
|
||||
<option value="expired">已过期</option>
|
||||
<option value="pending">待审核</option>
|
||||
<option value="all">{t('cloud.allStatus')}</option>
|
||||
<option value="active">{t('cloud.normal')}</option>
|
||||
<option value="expired">{t('cloud.domainExpired')}</option>
|
||||
<option value="pending">{t('cloud.pending')}</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
@@ -190,16 +191,16 @@ export function DomainManager() {
|
||||
return (
|
||||
<PageShell
|
||||
variant="list"
|
||||
title="域名管理"
|
||||
subtitle="管理域名注册信息和DNS解析记录"
|
||||
title={t('cloud.domains')}
|
||||
subtitle={t('cloud.domainsSubtitle')}
|
||||
icon={<Globe className="h-5 w-5" />}
|
||||
metrics={
|
||||
!loading ? (
|
||||
<StatStrip
|
||||
items={[
|
||||
{ label: '总计', value: stats.total, color: 'blue' },
|
||||
{ label: '正常', value: stats.active, color: 'green' },
|
||||
{ label: '已过期', value: stats.expired, color: stats.expired > 0 ? 'red' : 'muted' },
|
||||
{ label: t('common.total'), value: stats.total, color: 'blue' },
|
||||
{ label: t('cloud.normal'), value: stats.active, color: 'green' },
|
||||
{ label: t('cloud.domainExpired'), value: stats.expired, color: stats.expired > 0 ? 'red' : 'muted' },
|
||||
{ label: 'SSL', value: stats.sslEnabled, color: 'cyan' },
|
||||
]}
|
||||
/>
|
||||
@@ -210,11 +211,11 @@ export function DomainManager() {
|
||||
<>
|
||||
<Button variant="outline" size="sm" onClick={loadData} className="dg-btn-secondary">
|
||||
<RefreshCw className={`h-4 w-4 mr-1 ${loading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
<Button size="sm" className="dg-btn-primary" onClick={handleOpenCreate}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加域名
|
||||
{t('cloud.addDomain')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -224,11 +225,11 @@ export function DomainManager() {
|
||||
) : filteredDomains.length === 0 ? (
|
||||
<PageEmpty
|
||||
icon={<Globe className="h-12 w-12" />}
|
||||
title="暂无域名数据"
|
||||
title={t('cloud.noDomainData')}
|
||||
action={(
|
||||
<Button size="sm" className="dg-btn-primary" onClick={handleOpenCreate}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加域名
|
||||
{t('cloud.addDomain')}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
@@ -253,19 +254,19 @@ export function DomainManager() {
|
||||
{domain.status === 'active' && (
|
||||
<Badge className="dg-status-running border-0">
|
||||
<CheckCircle2 className="h-3 w-3 mr-1" />
|
||||
正常
|
||||
{t('cloud.normal')}
|
||||
</Badge>
|
||||
)}
|
||||
{domain.status === 'expired' && (
|
||||
<Badge className="dg-status-error border-0">
|
||||
<XCircle className="h-3 w-3 mr-1" />
|
||||
已过期
|
||||
{t('cloud.domainExpired')}
|
||||
</Badge>
|
||||
)}
|
||||
{domain.status === 'pending' && (
|
||||
<Badge className="dg-status-warning border-0">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
待审核
|
||||
{t('cloud.pending')}
|
||||
</Badge>
|
||||
)}
|
||||
{sslEnabled && (
|
||||
@@ -276,17 +277,17 @@ export function DomainManager() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-1 text-sm dg-text-secondary">
|
||||
<span>注册商: {domain.registrar || '-'}</span>
|
||||
<span>{t('cloud.domainRegistrar')}: {domain.registrar || '-'}</span>
|
||||
<span>DNS: {domain.dns_provider || domain.dnsProvider || '-'}</span>
|
||||
<span>解析记录: {domain.record_count ?? domain.recordCount ?? 0}条</span>
|
||||
<span>{t('cloud.domainRecord')}: {domain.record_count ?? domain.recordCount ?? 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<p className="text-sm dg-text-secondary">到期时间</p>
|
||||
<p className="text-sm dg-text-secondary">{t('cloud.domainExpireDate')}</p>
|
||||
<p className={`text-sm font-medium ${daysUntilExpiry < 30 ? 'text-[var(--dg-red)]' : 'text-[var(--dg-text-primary)]'}`}>
|
||||
{expireDate ? expireDate.substring(0, 10) : '-'}{expireDate && ` (${daysUntilExpiry}天)`}
|
||||
{expireDate ? expireDate.substring(0, 10) : '-'}{expireDate && ` (${t('cloud.daysLeft', { count: daysUntilExpiry })})`}
|
||||
{sslEnabled && sslExpireDate && (
|
||||
<span className="dg-text-muted"> · SSL: {sslExpireDate.substring(0, 10)}</span>
|
||||
)}
|
||||
@@ -325,16 +326,16 @@ export function DomainManager() {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Globe className="h-5 w-5 text-[var(--dg-blue)]" />
|
||||
{editingDomain ? '编辑域名' : '添加域名'}
|
||||
{editingDomain ? t('cloud.editDomain') : t('cloud.addDomain')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{editingDomain ? '修改域名注册信息、DNS 配置和 SSL 证书状态' : '录入域名信息,包括注册商、DNS 服务商和到期时间'}
|
||||
{editingDomain ? t('cloud.editDomainDesc') : t('cloud.addDomainDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
<Label>域名 *</Label>
|
||||
<Label>{t('cloud.domainName')} *</Label>
|
||||
<Input
|
||||
placeholder="example.com"
|
||||
value={formData.name}
|
||||
@@ -345,31 +346,31 @@ export function DomainManager() {
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>注册商</Label>
|
||||
<Label>{t('cloud.domainRegistrar')}</Label>
|
||||
<Input
|
||||
placeholder="阿里云 / Cloudflare ..."
|
||||
placeholder={t('cloud.aliyun') + ' / Cloudflare ...'}
|
||||
value={formData.registrar}
|
||||
onChange={(e) => setFormData({ ...formData, registrar: e.target.value })}
|
||||
className="dg-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>状态</Label>
|
||||
<Label>{t('common.status')}</Label>
|
||||
<select
|
||||
value={formData.status}
|
||||
onChange={(e) => setFormData({ ...formData, status: e.target.value })}
|
||||
className="w-full px-3 py-2 rounded-md text-sm dg-input"
|
||||
>
|
||||
<option value="active">正常</option>
|
||||
<option value="expired">已过期</option>
|
||||
<option value="pending">待审核</option>
|
||||
<option value="active">{t('cloud.normal')}</option>
|
||||
<option value="expired">{t('cloud.domainExpired')}</option>
|
||||
<option value="pending">{t('cloud.pending')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>到期时间</Label>
|
||||
<Label>{t('cloud.domainExpireDate')}</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={formData.expire_date}
|
||||
@@ -378,7 +379,7 @@ export function DomainManager() {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>DNS 服务商</Label>
|
||||
<Label>{t('cloud.domainDnsServer')}</Label>
|
||||
<Input
|
||||
placeholder="Cloudflare / DNSPod ..."
|
||||
value={formData.dns_provider}
|
||||
@@ -389,7 +390,7 @@ export function DomainManager() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>解析记录数</Label>
|
||||
<Label>{t('cloud.recordCountLabel')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="0"
|
||||
@@ -401,7 +402,7 @@ export function DomainManager() {
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 items-end">
|
||||
<div className="space-y-2">
|
||||
<Label>SSL 证书到期</Label>
|
||||
<Label>{t('cloud.domainSslExpire')}</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={formData.ssl_expire_date}
|
||||
@@ -417,14 +418,14 @@ export function DomainManager() {
|
||||
onChange={(e) => setFormData({ ...formData, ssl_enabled: e.target.checked })}
|
||||
className="h-4 w-4 rounded border-[var(--dg-border)] accent-[var(--dg-cyan)]"
|
||||
/>
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">启用 SSL</span>
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">{t('cloud.enableSsl')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setFormOpen(false)} className="dg-btn-secondary">
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className="dg-btn-primary"
|
||||
@@ -432,7 +433,7 @@ export function DomainManager() {
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{submitting ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : editingDomain ? <Edit2 className="h-4 w-4 mr-2" /> : <Plus className="h-4 w-4 mr-2" />}
|
||||
{editingDomain ? '保存修改' : '创建域名'}
|
||||
{editingDomain ? t('cloud.saveChanges') : t('cloud.createDomain')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -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';
|
||||
@@ -39,13 +40,15 @@ import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
import { StatStrip } from '@/components/ui/stat-strip';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
const providerNames: Record<string, string> = {
|
||||
aws: 'AWS',
|
||||
aliyun: '阿里云',
|
||||
tencent: '腾讯云',
|
||||
huawei: '华为云',
|
||||
azure: 'Azure',
|
||||
};
|
||||
function getProviderNames(t: (key: string) => string): Record<string, string> {
|
||||
return {
|
||||
aws: 'AWS',
|
||||
aliyun: t('cloud.aliyun'),
|
||||
tencent: t('cloud.tencent'),
|
||||
huawei: t('cloud.huawei'),
|
||||
azure: 'Azure',
|
||||
};
|
||||
}
|
||||
|
||||
const providerColors: Record<string, string> = {
|
||||
aws: 'dg-text-orange',
|
||||
@@ -56,6 +59,7 @@ const providerColors: Record<string, string> = {
|
||||
};
|
||||
|
||||
export function RegionManager() {
|
||||
const { t } = useTranslation();
|
||||
const confirm = useConfirm();
|
||||
const [regions, setRegions] = useState<JsonData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -69,6 +73,8 @@ export function RegionManager() {
|
||||
status: 'active', endpoint: '',
|
||||
});
|
||||
|
||||
const providerNames = getProviderNames(t);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -126,20 +132,20 @@ export function RegionManager() {
|
||||
const handleDelete = async (id: number) => {
|
||||
const region = regions.find(r => r.id === id);
|
||||
const ok = await confirm({
|
||||
title: '删除区域',
|
||||
description: `确定删除「${region?.name || id}」吗?此操作不可撤销。`,
|
||||
confirmText: '删除',
|
||||
title: t('cloud.deleteRegion'),
|
||||
description: t('cloud.deleteRegionConfirm', { name: region?.name || id }),
|
||||
confirmText: t('common.delete'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
await regionApi.delete(id);
|
||||
toast.success('区域已删除');
|
||||
toast.success(t('cloud.regionDeleted'));
|
||||
setSelectedIds(prev => prev.filter(i => i !== id));
|
||||
loadData();
|
||||
} catch (e) {
|
||||
console.error('删除区域失败:', e);
|
||||
toast.error('删除失败');
|
||||
console.error(t('cloud.deleteRegion'), e);
|
||||
toast.error(t('common.deleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -147,43 +153,43 @@ export function RegionManager() {
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedIds.length === 0) return;
|
||||
const ok = await confirm({
|
||||
title: '批量删除区域',
|
||||
description: `确定删除选中的 ${selectedIds.length} 个区域吗?此操作不可撤销。`,
|
||||
confirmText: '批量删除',
|
||||
title: t('cloud.batchDeleteRegion'),
|
||||
description: t('cloud.batchDeleteRegionConfirm', { count: selectedIds.length }),
|
||||
confirmText: t('cloud.batchDelete'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
await regionApi.batchDelete(selectedIds.map(Number));
|
||||
toast.success(`已删除 ${selectedIds.length} 个区域`);
|
||||
toast.success(t('cloud.batchDeletedRegions', { count: selectedIds.length }));
|
||||
setSelectedIds([]);
|
||||
loadData();
|
||||
} catch (e) {
|
||||
console.error('批量删除失败:', e);
|
||||
toast.error('批量删除失败');
|
||||
console.error(t('cloud.batchDeleteFailed'), e);
|
||||
toast.error(t('cloud.batchDeleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
// DataTable 列定义
|
||||
const columns: DataTableColumn<JsonData>[] = [
|
||||
{ key: 'name', header: '区域名称', sortable: true, render: (r) => <span className="font-medium text-[var(--dg-text-primary)]">{r.name}</span> },
|
||||
{ key: 'code', header: '编码', sortable: true, render: (r) => <span className="font-mono text-sm text-[var(--dg-text-secondary)]">{r.code}</span> },
|
||||
{ key: 'name', header: t('cloud.regionName'), sortable: true, render: (r) => <span className="font-medium text-[var(--dg-text-primary)]">{r.name}</span> },
|
||||
{ key: 'code', header: t('cloud.code'), sortable: true, render: (r) => <span className="font-mono text-sm text-[var(--dg-text-secondary)]">{r.code}</span> },
|
||||
{
|
||||
key: 'provider', header: '云商', sortable: true, hideable: false,
|
||||
key: 'provider', header: t('cloud.vendor'), sortable: true, hideable: false,
|
||||
render: (r) => <span className={providerColors[r.provider] || 'text-[var(--dg-text-secondary)]'}>{providerNames[r.provider] || r.provider}</span>,
|
||||
},
|
||||
{ key: 'location', header: '位置', render: (r) => r.location || '—' },
|
||||
{ key: 'endpoint', header: '接入端点', render: (r) => <span className="max-w-[200px] truncate block font-mono text-xs text-[var(--dg-text-secondary)]" title={r.endpoint}>{r.endpoint || '—'}</span> },
|
||||
{ key: 'location', header: t('cloud.location'), render: (r) => r.location || '—' },
|
||||
{ key: 'endpoint', header: t('cloud.regionEndpoint'), render: (r) => <span className="max-w-[200px] truncate block font-mono text-xs text-[var(--dg-text-secondary)]" title={r.endpoint}>{r.endpoint || '—'}</span> },
|
||||
{
|
||||
key: 'status', header: '状态', sortable: true,
|
||||
key: 'status', header: t('cloud.regionStatus'), sortable: true,
|
||||
render: (r) => r.status === 'active' ? (
|
||||
<Badge className="dg-status-running gap-1 border-0"><CheckCircle2 className="h-3 w-3" /> 启用</Badge>
|
||||
<Badge className="dg-status-running gap-1 border-0"><CheckCircle2 className="h-3 w-3" /> {t('cloud.enabled')}</Badge>
|
||||
) : (
|
||||
<Badge className="dg-status-stopped gap-1 border-0"><XCircle className="h-3 w-3" /> 禁用</Badge>
|
||||
<Badge className="dg-status-stopped gap-1 border-0"><XCircle className="h-3 w-3" /> {t('cloud.disabled')}</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'actions', header: '操作', align: 'right', hideable: false,
|
||||
key: 'actions', header: t('common.actions'), align: 'right', hideable: false,
|
||||
render: (r) => (
|
||||
<div className="flex justify-end gap-1">
|
||||
<button onClick={() => openEdit(r)} className="p-1.5 rounded hover:bg-[var(--dg-bg-active)] text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]">
|
||||
@@ -209,7 +215,7 @@ export function RegionManager() {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部云商</SelectItem>
|
||||
<SelectItem value="all">{t('cloud.allVendors')}</SelectItem>
|
||||
{Object.entries(providerNames).map(([k, v]) => (
|
||||
<SelectItem key={k} value={k}>{v}</SelectItem>
|
||||
))}
|
||||
@@ -220,14 +226,14 @@ export function RegionManager() {
|
||||
const batchToolbar = selectedIds.length > 0 ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm dg-text-secondary">
|
||||
已选择 <span className="font-semibold text-[var(--text-primary)]">{selectedIds.length}</span> 个区域
|
||||
{t('cloud.selectedRegionCount', { count: selectedIds.length })}
|
||||
</span>
|
||||
<Button variant="ghost" size="sm" onClick={() => setSelectedIds([])} className="dg-text-muted">
|
||||
取消选择
|
||||
{t('cloud.clearSelection')}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="dg-btn-danger" onClick={handleBatchDelete}>
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
批量删除
|
||||
{t('cloud.batchDelete')}
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined;
|
||||
@@ -235,16 +241,16 @@ export function RegionManager() {
|
||||
return (
|
||||
<PageShell
|
||||
variant="list"
|
||||
title="区域管理"
|
||||
subtitle="管理各云厂商的区域与接入端点"
|
||||
title={t('cloud.regions')}
|
||||
subtitle={t('cloud.regionsSubtitle')}
|
||||
icon={<MapPin className="h-5 w-5" />}
|
||||
metrics={
|
||||
!loading ? (
|
||||
<StatStrip
|
||||
items={[
|
||||
{ label: '总计', value: stats.total, color: 'blue' },
|
||||
{ label: '启用', value: stats.active, color: 'green' },
|
||||
{ label: '禁用', value: stats.inactive, color: 'muted' },
|
||||
{ label: t('common.total'), value: stats.total, color: 'blue' },
|
||||
{ label: t('cloud.enabled'), value: stats.active, color: 'green' },
|
||||
{ label: t('cloud.disabled'), value: stats.inactive, color: 'muted' },
|
||||
]}
|
||||
/>
|
||||
) : undefined
|
||||
@@ -254,11 +260,11 @@ export function RegionManager() {
|
||||
<>
|
||||
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={loadData}>
|
||||
<RefreshCw className={`h-4 w-4 mr-1 ${loading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
<Button size="sm" className="dg-btn-primary" onClick={openCreate}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加区域
|
||||
{t('cloud.addRegion')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -268,11 +274,11 @@ export function RegionManager() {
|
||||
) : filtered.length === 0 ? (
|
||||
<PageEmpty
|
||||
icon={<MapPin className="h-12 w-12" />}
|
||||
title="暂无区域数据"
|
||||
title={t('cloud.noRegionData')}
|
||||
action={(
|
||||
<Button size="sm" className="dg-btn-primary" onClick={openCreate}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加区域
|
||||
{t('cloud.addRegion')}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
@@ -283,7 +289,7 @@ export function RegionManager() {
|
||||
data={filtered}
|
||||
rowKey={(r) => r.id}
|
||||
searchable
|
||||
searchPlaceholder="搜索区域名称或编码..."
|
||||
searchPlaceholder={t('cloud.searchRegion')}
|
||||
searchKeys={['name', 'code']}
|
||||
sortable
|
||||
selectable
|
||||
@@ -292,7 +298,7 @@ export function RegionManager() {
|
||||
columnToggle
|
||||
exportable
|
||||
exportFilename="regions.csv"
|
||||
emptyText="暂无匹配区域"
|
||||
emptyText={t('cloud.noMatchRegion')}
|
||||
/>
|
||||
</ContentPanel>
|
||||
)}
|
||||
@@ -301,26 +307,26 @@ export function RegionManager() {
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingItem ? '编辑区域' : '添加区域'}</DialogTitle>
|
||||
<DialogDescription className="sr-only">描述</DialogDescription>
|
||||
<DialogTitle>{editingItem ? t('cloud.editRegion') : t('cloud.addRegion')}</DialogTitle>
|
||||
<DialogDescription className="sr-only">description</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label>区域名称 *</Label>
|
||||
<Label>{t('cloud.regionName')} *</Label>
|
||||
<Input
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="如:华北-北京"
|
||||
placeholder={t('cloud.regionNamePlaceholder')}
|
||||
className="dg-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>编码 *</Label>
|
||||
<Label>{t('cloud.code')} *</Label>
|
||||
<Input
|
||||
value={formData.code}
|
||||
onChange={(e) => setFormData({ ...formData, code: e.target.value })}
|
||||
placeholder="如:cn-north-1"
|
||||
placeholder={t('cloud.regionCodePlaceholder')}
|
||||
className="dg-input font-mono"
|
||||
disabled={!!editingItem}
|
||||
/>
|
||||
@@ -328,7 +334,7 @@ export function RegionManager() {
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label>云商</Label>
|
||||
<Label>{t('cloud.vendor')}</Label>
|
||||
<Select value={formData.provider} onValueChange={(v) => setFormData({ ...formData, provider: v })}>
|
||||
<SelectTrigger className="dg-input">
|
||||
<SelectValue />
|
||||
@@ -341,42 +347,42 @@ export function RegionManager() {
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>状态</Label>
|
||||
<Label>{t('common.status')}</Label>
|
||||
<Select value={formData.status} onValueChange={(v) => setFormData({ ...formData, status: v })}>
|
||||
<SelectTrigger className="dg-input">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="active">启用</SelectItem>
|
||||
<SelectItem value="inactive">禁用</SelectItem>
|
||||
<SelectItem value="active">{t('cloud.enabled')}</SelectItem>
|
||||
<SelectItem value="inactive">{t('cloud.disabled')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>地理位置</Label>
|
||||
<Label>{t('cloud.geoLocation')}</Label>
|
||||
<Input
|
||||
value={formData.location}
|
||||
onChange={(e) => setFormData({ ...formData, location: e.target.value })}
|
||||
placeholder="如:中国北京"
|
||||
placeholder={t('cloud.locationPlaceholder')}
|
||||
className="dg-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>接入端点</Label>
|
||||
<Label>{t('cloud.regionEndpoint')}</Label>
|
||||
<Input
|
||||
value={formData.endpoint}
|
||||
onChange={(e) => setFormData({ ...formData, endpoint: e.target.value })}
|
||||
placeholder="如:ecs.cn-north-1.aliyuncs.com"
|
||||
placeholder={t('cloud.endpointPlaceholder')}
|
||||
className="dg-input font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => setDialogOpen(false)}>取消</Button>
|
||||
<Button variant="ghost" onClick={() => setDialogOpen(false)}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleSubmit} disabled={submitting || !formData.name.trim() || !formData.code.trim()}>
|
||||
{submitting && <Loader2 className="h-4 w-4 mr-1 animate-spin" />}
|
||||
{editingItem ? '保存' : '创建'}
|
||||
{editingItem ? t('common.save') : t('common.create')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -471,7 +471,9 @@
|
||||
"channel": "Channel",
|
||||
"contact": "Contact",
|
||||
"true": "Yes",
|
||||
"false": "No"
|
||||
"false": "No",
|
||||
"deleteFailed": "Delete failed",
|
||||
"deleteSuccess": "Deleted successfully"
|
||||
},
|
||||
"menu": {
|
||||
"dashboard": "Dashboard",
|
||||
@@ -3822,7 +3824,95 @@
|
||||
"setBudget": "Set Budget",
|
||||
"budgetAlert": "Budget Alert",
|
||||
"budgetUsage": "Budget Usage",
|
||||
"budgetRemaining": "Budget Remaining"
|
||||
"budgetRemaining": "Budget Remaining",
|
||||
"code": "Code",
|
||||
"vendor": "Vendor",
|
||||
"location": "Location",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"allVendors": "All Vendors",
|
||||
"selectedRegionCount": "{{count}} regions selected",
|
||||
"clearSelection": "Clear Selection",
|
||||
"batchDelete": "Batch Delete",
|
||||
"batchDeleteRegion": "Batch Delete Regions",
|
||||
"batchDeleteRegionConfirm": "Are you sure you want to delete the selected {{count}} regions? This action cannot be undone.",
|
||||
"batchDeletedRegions": "{{count}} regions deleted",
|
||||
"batchDeleteFailed": "Batch delete failed",
|
||||
"deleteRegionConfirm": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
|
||||
"regionDeleted": "Region deleted",
|
||||
"regionsSubtitle": "Manage cloud provider regions and endpoints",
|
||||
"noRegionData": "No region data",
|
||||
"searchRegion": "Search region name or code...",
|
||||
"noMatchRegion": "No matching regions",
|
||||
"geoLocation": "Geographic Location",
|
||||
"regionNamePlaceholder": "e.g. North China-Beijing",
|
||||
"regionCodePlaceholder": "e.g. cn-north-1",
|
||||
"locationPlaceholder": "e.g. Beijing, China",
|
||||
"endpointPlaceholder": "e.g. ecs.cn-north-1.aliyuncs.com",
|
||||
"normal": "Normal",
|
||||
"stopped": "Stopped",
|
||||
"error": "Error",
|
||||
"searchAccount": "Search account name or AccessKey...",
|
||||
"accountsSubtitle": "Manage multi-cloud access credentials and account info",
|
||||
"noAccountData": "No cloud account data",
|
||||
"monthlyFee": "Monthly Fee",
|
||||
"serverCount": "Servers",
|
||||
"keepBlank": "Leave blank to keep unchanged",
|
||||
"monthlyFeeUnit": "Monthly Fee (CNY)",
|
||||
"saveChanges": "Save Changes",
|
||||
"createAccount": "Create Account",
|
||||
"editAccountDesc": "Modify account info and credentials. Leave blank to keep existing.",
|
||||
"addAccountDesc": "Enter multi-cloud credentials for resource sync and cost tracking",
|
||||
"accountNamePlaceholder": "e.g. Aliyun Production",
|
||||
"platformLabel": "Cloud Platform",
|
||||
"allPlatforms": "All Platforms",
|
||||
"loadStatsFailed": "Failed to load statistics",
|
||||
"loadBillingFailed": "Failed to load billing",
|
||||
"loadAlertRulesFailed": "Failed to load alert rules",
|
||||
"deleteBillingConfirm": "Are you sure you want to delete this cost record?",
|
||||
"operationFailed": "Operation failed",
|
||||
"overviewStats": "Overview",
|
||||
"costRecords": "Cost Records",
|
||||
"startDate": "Start Date",
|
||||
"endDate": "End Date",
|
||||
"query": "Query",
|
||||
"billingSubtitle": "Multi-cloud cost statistics, records and alerts",
|
||||
"today": "Today",
|
||||
"thisMonth": "This Month",
|
||||
"yearly": "Yearly",
|
||||
"alertCount": "{{count}} alerts",
|
||||
"recordCost": "Record Cost",
|
||||
"costAlert": "Cost Alerts",
|
||||
"monthlyAccumulated": "Monthly Accumulated",
|
||||
"current": "Current",
|
||||
"monthVendorDistribution": "Monthly Vendor Cost Distribution",
|
||||
"noDistributionData": "No distribution data",
|
||||
"noStatsData": "No statistics data",
|
||||
"noCostRecords": "No cost records",
|
||||
"colDate": "Date",
|
||||
"colService": "Service",
|
||||
"colRegion": "Region",
|
||||
"colAccount": "Account",
|
||||
"colCost": "Cost",
|
||||
"colDescription": "Description",
|
||||
"noAlertRules": "No alert rules",
|
||||
"alertRuleHint": "Add rules to manage cost threshold alerts",
|
||||
"colMetric": "Metric",
|
||||
"lastTriggered": "Last Triggered",
|
||||
"deleteBillingRecord": "Delete Billing Record",
|
||||
"deleteAlertRule": "Delete Alert Rule",
|
||||
"deleteAlertRuleConfirm": "Are you sure you want to delete this alert rule?",
|
||||
"pending": "Pending",
|
||||
"searchDomain": "Search domains...",
|
||||
"allStatus": "All Status",
|
||||
"domainsSubtitle": "Manage domain registration and DNS records",
|
||||
"noDomainData": "No domain data",
|
||||
"recordCountLabel": "DNS Record Count",
|
||||
"enableSsl": "Enable SSL",
|
||||
"createDomain": "Create Domain",
|
||||
"editDomainDesc": "Modify domain registration, DNS config and SSL certificate status",
|
||||
"addDomainDesc": "Enter domain info including registrar, DNS provider and expiry date",
|
||||
"daysLeft": "{{count}} days"
|
||||
},
|
||||
"agent": {
|
||||
"title": "Agent Management",
|
||||
|
||||
@@ -471,7 +471,9 @@
|
||||
"channel": "渠道",
|
||||
"contact": "联系人",
|
||||
"true": "是",
|
||||
"false": "否"
|
||||
"false": "否",
|
||||
"deleteFailed": "删除失败",
|
||||
"deleteSuccess": "删除成功"
|
||||
},
|
||||
"menu": {
|
||||
"dashboard": "仪表盘",
|
||||
@@ -3822,7 +3824,95 @@
|
||||
"setBudget": "设置预算",
|
||||
"budgetAlert": "预算预警",
|
||||
"budgetUsage": "预算使用率",
|
||||
"budgetRemaining": "剩余预算"
|
||||
"budgetRemaining": "剩余预算",
|
||||
"code": "编码",
|
||||
"vendor": "云商",
|
||||
"location": "位置",
|
||||
"enabled": "启用",
|
||||
"disabled": "禁用",
|
||||
"allVendors": "全部云商",
|
||||
"selectedRegionCount": "已选择 {{count}} 个区域",
|
||||
"clearSelection": "取消选择",
|
||||
"batchDelete": "批量删除",
|
||||
"batchDeleteRegion": "批量删除区域",
|
||||
"batchDeleteRegionConfirm": "确定删除选中的 {{count}} 个区域吗?此操作不可撤销。",
|
||||
"batchDeletedRegions": "已删除 {{count}} 个区域",
|
||||
"batchDeleteFailed": "批量删除失败",
|
||||
"deleteRegionConfirm": "确定删除「{{name}}」吗?此操作不可撤销。",
|
||||
"regionDeleted": "区域已删除",
|
||||
"regionsSubtitle": "管理各云厂商的区域与接入端点",
|
||||
"noRegionData": "暂无区域数据",
|
||||
"searchRegion": "搜索区域名称或编码...",
|
||||
"noMatchRegion": "暂无匹配区域",
|
||||
"geoLocation": "地理位置",
|
||||
"regionNamePlaceholder": "如:华北-北京",
|
||||
"regionCodePlaceholder": "如:cn-north-1",
|
||||
"locationPlaceholder": "如:中国北京",
|
||||
"endpointPlaceholder": "如:ecs.cn-north-1.aliyuncs.com",
|
||||
"normal": "正常",
|
||||
"stopped": "已停用",
|
||||
"error": "异常",
|
||||
"searchAccount": "搜索账号名称或AccessKey...",
|
||||
"accountsSubtitle": "管理多云平台的访问凭证和账号信息",
|
||||
"noAccountData": "暂无云账号数据",
|
||||
"monthlyFee": "月费用",
|
||||
"serverCount": "服务器数",
|
||||
"keepBlank": "留空保持不变",
|
||||
"monthlyFeeUnit": "月费用 (元)",
|
||||
"saveChanges": "保存修改",
|
||||
"createAccount": "创建账号",
|
||||
"editAccountDesc": "修改账号信息和访问凭证,留空则保持原凭证不变",
|
||||
"addAccountDesc": "录入多云平台的访问凭证,用于资源同步与成本统计",
|
||||
"accountNamePlaceholder": "阿里云生产账号",
|
||||
"platformLabel": "云平台",
|
||||
"allPlatforms": "全部平台",
|
||||
"loadStatsFailed": "加载统计失败",
|
||||
"loadBillingFailed": "加载账单失败",
|
||||
"loadAlertRulesFailed": "加载预警规则失败",
|
||||
"deleteBillingConfirm": "确定删除这条成本记录?",
|
||||
"operationFailed": "操作失败",
|
||||
"overviewStats": "概览统计",
|
||||
"costRecords": "成本记录",
|
||||
"startDate": "开始日期",
|
||||
"endDate": "结束日期",
|
||||
"query": "查询",
|
||||
"billingSubtitle": "多云成本统计、记录与预警",
|
||||
"today": "今日",
|
||||
"thisMonth": "本月",
|
||||
"yearly": "年度",
|
||||
"alertCount": "{{count}} 条",
|
||||
"recordCost": "记录成本",
|
||||
"costAlert": "成本预警",
|
||||
"monthlyAccumulated": "月累计",
|
||||
"current": "当前",
|
||||
"monthVendorDistribution": "本月厂商成本分布",
|
||||
"noDistributionData": "暂无分布数据",
|
||||
"noStatsData": "暂无统计数据",
|
||||
"noCostRecords": "暂无成本记录",
|
||||
"colDate": "日期",
|
||||
"colService": "服务",
|
||||
"colRegion": "区域",
|
||||
"colAccount": "账号",
|
||||
"colCost": "费用",
|
||||
"colDescription": "说明",
|
||||
"noAlertRules": "暂无预警规则",
|
||||
"alertRuleHint": "添加规则后,成本超阈值将在此页管理",
|
||||
"colMetric": "指标",
|
||||
"lastTriggered": "最后触发",
|
||||
"deleteBillingRecord": "删除账单记录",
|
||||
"deleteAlertRule": "删除预警规则",
|
||||
"deleteAlertRuleConfirm": "确定删除此预警规则?",
|
||||
"pending": "待审核",
|
||||
"searchDomain": "搜索域名...",
|
||||
"allStatus": "全部状态",
|
||||
"domainsSubtitle": "管理域名注册信息和DNS解析记录",
|
||||
"noDomainData": "暂无域名数据",
|
||||
"recordCountLabel": "解析记录数",
|
||||
"enableSsl": "启用 SSL",
|
||||
"createDomain": "创建域名",
|
||||
"editDomainDesc": "修改域名注册信息、DNS 配置和 SSL 证书状态",
|
||||
"addDomainDesc": "录入域名信息,包括注册商、DNS 服务商和到期时间",
|
||||
"daysLeft": "{{count}}天"
|
||||
},
|
||||
"agent": {
|
||||
"title": "Agent 管理",
|
||||
|
||||
Reference in new issue
Block a user