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:
attacker committed 2026-07-02 12:50:05 +08:00
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 { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
@@ -32,16 +33,19 @@ import { PageEmpty } from '@/components/layout/PageEmpty';
import { StatStrip } from '@/components/ui/stat-strip'; import { StatStrip } from '@/components/ui/stat-strip';
import type { JsonData } from '@/types/common'; import type { JsonData } from '@/types/common';
const providerLabels: Record<string, string> = { function getProviderLabels(t: (key: string) => string): Record<string, string> {
aws: 'AWS', return {
aliyun: '阿里云', aws: 'AWS',
tencent: '腾讯云', aliyun: t('cloud.aliyun'),
huawei: '华为云', tencent: t('cloud.tencent'),
azure: 'Azure', huawei: t('cloud.huawei'),
gcp: 'GCP', azure: 'Azure',
}; gcp: 'GCP',
};
}
export function CloudAccountManager() { export function CloudAccountManager() {
const { t } = useTranslation();
const [accounts, setAccounts] = useState<JsonData[]>([]); const [accounts, setAccounts] = useState<JsonData[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
@@ -60,6 +64,8 @@ export function CloudAccountManager() {
status: 'active', status: 'active',
}); });
const providerLabels = getProviderLabels(t);
const resetForm = () => { const resetForm = () => {
setEditingAccount(null); setEditingAccount(null);
setFormData({ setFormData({
@@ -110,7 +116,7 @@ export function CloudAccountManager() {
setFormOpen(false); setFormOpen(false);
loadData(); loadData();
} catch (e) { } catch (e) {
console.error(editingAccount ? '更新云账号失败:' : '创建云账号失败:', e); console.error(editingAccount ? 'update account' : 'create account', e);
} finally { } finally {
setSubmitting(false); setSubmitting(false);
} }
@@ -121,8 +127,7 @@ export function CloudAccountManager() {
setLoading(true); setLoading(true);
const res: JsonData = await cloudApi.getAccounts(); const res: JsonData = await cloudApi.getAccounts();
setAccounts(res?.list || []); setAccounts(res?.list || []);
} catch (e) { } catch (_e) {
console.error('加载云账号列表失败:', e);
setAccounts([]); setAccounts([]);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -137,8 +142,8 @@ export function CloudAccountManager() {
try { try {
await cloudApi.deleteAccount(id); await cloudApi.deleteAccount(id);
loadData(); loadData();
} catch (e) { } catch (_e) {
console.error('删除云账号失败:', e); // silently ignored
} }
}; };
@@ -162,7 +167,7 @@ export function CloudAccountManager() {
<div className="relative flex-1 max-w-md"> <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" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 dg-text-muted" />
<Input <Input
placeholder="搜索账号名称或AccessKey..." placeholder={t('cloud.searchAccount')}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 h-9 dg-input" className="pl-10 h-9 dg-input"
@@ -173,11 +178,11 @@ export function CloudAccountManager() {
onChange={(e) => setSelectedProvider(e.target.value)} onChange={(e) => setSelectedProvider(e.target.value)}
className="px-3 py-2 h-9 rounded-md text-sm dg-input" 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="aws">AWS</option>
<option value="aliyun">阿里云</option> <option value="aliyun">{t('cloud.aliyun')}</option>
<option value="tencent">腾讯云</option> <option value="tencent">{t('cloud.tencent')}</option>
<option value="huawei">华为云</option> <option value="huawei">{t('cloud.huawei')}</option>
<option value="azure">Azure</option> <option value="azure">Azure</option>
</select> </select>
</div> </div>
@@ -186,17 +191,17 @@ export function CloudAccountManager() {
return ( return (
<PageShell <PageShell
variant="list" variant="list"
title="云账号管理" title={t('cloud.cloudAccounts')}
subtitle="管理多云平台的访问凭证和账号信息" subtitle={t('cloud.accountsSubtitle')}
icon={<Key className="h-5 w-5" />} icon={<Key className="h-5 w-5" />}
metrics={ metrics={
!loading ? ( !loading ? (
<StatStrip <StatStrip
items={[ items={[
{ label: '总计', value: stats.total, color: 'blue' }, { label: t('common.total'), value: stats.total, color: 'blue' },
{ label: '正常', value: stats.active, color: 'green' }, { label: t('cloud.normal'), value: stats.active, color: 'green' },
{ label: '停用', value: stats.inactive, color: 'muted' }, { label: t('cloud.disabled'), value: stats.inactive, color: 'muted' },
{ label: '异常', value: stats.error, color: stats.error > 0 ? 'red' : 'muted' }, { label: t('cloud.error'), value: stats.error, color: stats.error > 0 ? 'red' : 'muted' },
]} ]}
/> />
) : undefined ) : undefined
@@ -206,11 +211,11 @@ export function CloudAccountManager() {
<> <>
<Button variant="outline" size="sm" onClick={loadData} className="dg-btn-secondary"> <Button variant="outline" size="sm" onClick={loadData} className="dg-btn-secondary">
<RefreshCw className={`h-4 w-4 mr-1 ${loading ? 'animate-spin' : ''}`} /> <RefreshCw className={`h-4 w-4 mr-1 ${loading ? 'animate-spin' : ''}`} />
刷新 {t('common.refresh')}
</Button> </Button>
<Button size="sm" className="dg-btn-primary" onClick={handleOpenCreate}> <Button size="sm" className="dg-btn-primary" onClick={handleOpenCreate}>
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
添加账号 {t('cloud.addAccount')}
</Button> </Button>
</> </>
)} )}
@@ -220,11 +225,11 @@ export function CloudAccountManager() {
) : filteredAccounts.length === 0 ? ( ) : filteredAccounts.length === 0 ? (
<PageEmpty <PageEmpty
icon={<Key className="h-12 w-12" />} icon={<Key className="h-12 w-12" />}
title="暂无云账号数据" title={t('cloud.noAccountData')}
action={( action={(
<Button size="sm" className="dg-btn-primary" onClick={handleOpenCreate}> <Button size="sm" className="dg-btn-primary" onClick={handleOpenCreate}>
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
添加账号 {t('cloud.addAccount')}
</Button> </Button>
)} )}
/> />
@@ -256,19 +261,19 @@ export function CloudAccountManager() {
{account.status === 'active' && ( {account.status === 'active' && (
<Badge className="dg-status-running border-0"> <Badge className="dg-status-running border-0">
<CheckCircle2 className="h-3 w-3 mr-1" /> <CheckCircle2 className="h-3 w-3 mr-1" />
正常 {t('cloud.normal')}
</Badge> </Badge>
)} )}
{account.status === 'inactive' && ( {account.status === 'inactive' && (
<Badge className="dg-status-stopped border-0"> <Badge className="dg-status-stopped border-0">
<XCircle className="h-3 w-3 mr-1" /> <XCircle className="h-3 w-3 mr-1" />
已停用 {t('cloud.stopped')}
</Badge> </Badge>
)} )}
{account.status === 'error' && ( {account.status === 'error' && (
<Badge className="dg-status-error border-0"> <Badge className="dg-status-error border-0">
<Key className="h-3 w-3 mr-1" /> <Key className="h-3 w-3 mr-1" />
异常 {t('cloud.error')}
</Badge> </Badge>
)} )}
</div> </div>
@@ -295,15 +300,15 @@ export function CloudAccountManager() {
</div> </div>
<div className="grid grid-cols-3 gap-4 mt-4 pt-4 border-t border-[var(--dg-border)]"> <div className="grid grid-cols-3 gap-4 mt-4 pt-4 border-t border-[var(--dg-border)]">
<div> <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> <p className="text-lg font-medium text-[var(--dg-green)]">¥{Number(monthlyCost).toLocaleString()}</p>
</div> </div>
<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> <p className="text-lg font-medium text-[var(--dg-text-primary)]">{serverCount}</p>
</div> </div>
<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)]"> <p className="text-sm text-[var(--dg-text-secondary)]">
{lastSync !== '-' ? new Date(lastSync).toLocaleString() : '-'} {lastSync !== '-' ? new Date(lastSync).toLocaleString() : '-'}
</p> </p>
@@ -322,10 +327,10 @@ export function CloudAccountManager() {
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
<Cloud className="h-5 w-5 text-[var(--dg-blue)]" /> <Cloud className="h-5 w-5 text-[var(--dg-blue)]" />
{editingAccount ? '编辑云账号' : '添加云账号'} {editingAccount ? t('cloud.editAccount') : t('cloud.addAccount')}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
{editingAccount ? '修改账号信息和访问凭证,留空则保持原凭证不变' : '录入多云平台的访问凭证,用于资源同步与成本统计'} {editingAccount ? t('cloud.editAccountDesc') : t('cloud.addAccountDesc')}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -333,10 +338,10 @@ export function CloudAccountManager() {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label> <Label>
账号名称 <span className="text-[var(--dg-red)]">*</span> {t('cloud.accountName')} <span className="text-[var(--dg-red)]">*</span>
</Label> </Label>
<Input <Input
placeholder="阿里云生产账号" placeholder={t('cloud.accountNamePlaceholder')}
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="dg-input" className="dg-input"
@@ -344,17 +349,17 @@ export function CloudAccountManager() {
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label> <Label>
云平台 <span className="text-[var(--dg-red)]">*</span> {t('cloud.platformLabel')} <span className="text-[var(--dg-red)]">*</span>
</Label> </Label>
<select <select
value={formData.provider} value={formData.provider}
onChange={(e) => setFormData({ ...formData, provider: e.target.value })} onChange={(e) => setFormData({ ...formData, provider: e.target.value })}
className="w-full px-3 py-2 rounded-md text-sm dg-input" className="w-full px-3 py-2 rounded-md text-sm dg-input"
> >
<option value="aliyun">阿里云</option> <option value="aliyun">{t('cloud.aliyun')}</option>
<option value="tencent">腾讯云</option> <option value="tencent">{t('cloud.tencent')}</option>
<option value="aws">AWS</option> <option value="aws">AWS</option>
<option value="huawei">华为云</option> <option value="huawei">{t('cloud.huawei')}</option>
<option value="azure">Azure</option> <option value="azure">Azure</option>
</select> </select>
</div> </div>
@@ -363,7 +368,7 @@ export function CloudAccountManager() {
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label> <Label>
AccessKey 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> </Label>
<Input <Input
placeholder="LTAI..." placeholder="LTAI..."
@@ -376,7 +381,7 @@ export function CloudAccountManager() {
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label> <Label>
SecretKey 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> </Label>
<Input <Input
type="password" type="password"
@@ -389,7 +394,7 @@ export function CloudAccountManager() {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label>月费用 (元)</Label> <Label>{t('cloud.monthlyFeeUnit')}</Label>
<Input <Input
type="number" type="number"
min="0" min="0"
@@ -400,15 +405,15 @@ export function CloudAccountManager() {
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label>状态</Label> <Label>{t('common.status')}</Label>
<select <select
value={formData.status} value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value })} onChange={(e) => setFormData({ ...formData, status: e.target.value })}
className="w-full px-3 py-2 rounded-md text-sm dg-input" className="w-full px-3 py-2 rounded-md text-sm dg-input"
> >
<option value="active">正常</option> <option value="active">{t('cloud.normal')}</option>
<option value="inactive">已停用</option> <option value="inactive">{t('cloud.stopped')}</option>
<option value="error">异常</option> <option value="error">{t('cloud.error')}</option>
</select> </select>
</div> </div>
</div> </div>
@@ -416,7 +421,7 @@ export function CloudAccountManager() {
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setFormOpen(false)} className="dg-btn-secondary"> <Button variant="outline" onClick={() => setFormOpen(false)} className="dg-btn-secondary">
取消 {t('common.cancel')}
</Button> </Button>
<Button <Button
className="dg-btn-primary" className="dg-btn-primary"
@@ -424,7 +429,7 @@ export function CloudAccountManager() {
onClick={handleSubmit} 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" />} {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> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
+60 -58
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
@@ -40,6 +41,7 @@ import { PageEmpty } from '@/components/layout/PageEmpty';
import type { JsonData } from '@/types/common'; import type { JsonData } from '@/types/common';
export function CloudBilling() { export function CloudBilling() {
const { t } = useTranslation();
const confirm = useConfirm(); const confirm = useConfirm();
const [activeTab, setActiveTab] = useState<'overview' | 'records' | 'alerts'>('overview'); const [activeTab, setActiveTab] = useState<'overview' | 'records' | 'alerts'>('overview');
@@ -67,11 +69,11 @@ export function CloudBilling() {
const res = await billingApi.getStats(); const res = await billingApi.getStats();
setStats(res); setStats(res);
} catch (e) { } catch (e) {
toast.error('加载统计失败', { description: (e as JsonData).message }); toast.error(t('cloud.loadStatsFailed'), { description: (e as JsonData).message });
} finally { } finally {
setStatsLoading(false); setStatsLoading(false);
} }
}, []); }, [t]);
const loadRecords = useCallback(async () => { const loadRecords = useCallback(async () => {
setRecordsLoading(true); setRecordsLoading(true);
@@ -83,11 +85,11 @@ export function CloudBilling() {
const res = await billingApi.list(params); const res = await billingApi.list(params);
setRecords(res.list || []); setRecords(res.list || []);
} catch (e) { } catch (e) {
toast.error('加载账单失败', { description: (e as JsonData).message }); toast.error(t('cloud.loadBillingFailed'), { description: (e as JsonData).message });
} finally { } finally {
setRecordsLoading(false); setRecordsLoading(false);
} }
}, [filterProvider, filterStart, filterEnd]); }, [filterProvider, filterStart, filterEnd, t]);
const loadRules = useCallback(async () => { const loadRules = useCallback(async () => {
setRulesLoading(true); setRulesLoading(true);
@@ -95,11 +97,11 @@ export function CloudBilling() {
const res = await billingApi.listAlertRules(); const res = await billingApi.listAlertRules();
setRules(res.list || []); setRules(res.list || []);
} catch (e) { } catch (e) {
toast.error('加载预警规则失败', { description: (e as JsonData).message }); toast.error(t('cloud.loadAlertRulesFailed'), { description: (e as JsonData).message });
} finally { } finally {
setRulesLoading(false); setRulesLoading(false);
} }
}, []); }, [t]);
useEffect(() => { useEffect(() => {
loadStats(); loadStats();
@@ -140,27 +142,27 @@ export function CloudBilling() {
}; };
const handleDeleteRecord = async (id: number) => { 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; if (!ok) return;
try { try {
await billingApi.remove(id); await billingApi.remove(id);
loadRecords(); loadRecords();
loadStats(); loadStats();
toast('删除成功'); toast(t('common.deleteSuccess'));
} catch (e) { } 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 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; if (!ok) return;
try { try {
await billingApi.deleteAlertRule(id); await billingApi.deleteAlertRule(id);
loadRules(); loadRules();
toast('删除成功'); toast(t('common.deleteSuccess'));
} catch (e) { } 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(); loadRules();
loadStats(); loadStats();
} catch (e) { } catch (e) {
toast.error('操作失败', { description: (e as JsonData).message }); toast.error(t('cloud.operationFailed'), { description: (e as JsonData).message });
} }
}; };
const pageTabs = [ const pageTabs = [
{ id: 'overview', label: '概览统计' }, { id: 'overview', label: t('cloud.overviewStats') },
{ id: 'records', label: '成本记录' }, { id: 'records', label: t('cloud.costRecords') },
{ id: 'alerts', label: '预警规则', badge: rules.length > 0 ? rules.length : undefined }, { id: 'alerts', label: t('cloud.billingAlert'), badge: rules.length > 0 ? rules.length : undefined },
]; ];
const recordsToolbar = ( const recordsToolbar = (
<div className="flex flex-wrap items-end gap-3"> <div className="flex flex-wrap items-end gap-3">
<div> <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)}> <Select value={filterProvider || 'all'} onValueChange={(v) => setFilterProvider(v === 'all' ? '' : v)}>
<SelectTrigger className="w-32 h-9 dg-input"><SelectValue /></SelectTrigger> <SelectTrigger className="w-32 h-9 dg-input"><SelectValue /></SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">全部</SelectItem> <SelectItem value="all">{t('common.all')}</SelectItem>
{Object.entries(providerLabels).map(([k, v]) => ( {Object.entries(providerLabels).map(([k, v]) => (
<SelectItem key={k} value={k}>{v}</SelectItem> <SelectItem key={k} value={k}>{v}</SelectItem>
))} ))}
@@ -195,15 +197,15 @@ export function CloudBilling() {
</Select> </Select>
</div> </div>
<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)} /> <Input type="date" className="w-40 h-9 dg-input" value={filterStart} onChange={(e) => setFilterStart(e.target.value)} />
</div> </div>
<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)} /> <Input type="date" className="w-40 h-9 dg-input" value={filterEnd} onChange={(e) => setFilterEnd(e.target.value)} />
</div> </div>
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={loadRecords}> <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> </Button>
</div> </div>
); );
@@ -211,8 +213,8 @@ export function CloudBilling() {
return ( return (
<PageShell <PageShell
variant="list" variant="list"
title="云账单管理" title={t('cloud.billingTitle')}
subtitle="多云成本统计、记录与预警" subtitle={t('cloud.billingSubtitle')}
icon={<Wallet className="h-5 w-5" />} icon={<Wallet className="h-5 w-5" />}
tabs={pageTabs} tabs={pageTabs}
activeTab={activeTab} activeTab={activeTab}
@@ -222,10 +224,10 @@ export function CloudBilling() {
stats && !statsLoading ? ( stats && !statsLoading ? (
<StatStrip <StatStrip
items={[ items={[
{ label: '今日', value: formatCurrency(stats.today_cost || 0), color: 'blue' }, { label: t('cloud.today'), value: formatCurrency(stats.today_cost || 0), color: 'blue' },
{ label: '本月', value: formatCurrency(stats.month_cost || 0), color: 'amber' }, { label: t('cloud.thisMonth'), value: formatCurrency(stats.month_cost || 0), color: 'amber' },
{ label: '年度', value: formatCurrency(stats.year_cost || 0), color: 'purple' }, { label: t('cloud.yearly'), value: formatCurrency(stats.year_cost || 0), color: 'purple' },
{ label: '预警', value: `${stats.alerts?.length || 0} 条`, color: stats.alerts?.length > 0 ? 'red' : 'muted' }, { label: t('cloud.costAlert'), value: t('cloud.alertCount', { count: stats.alerts?.length || 0 }), color: stats.alerts?.length > 0 ? 'red' : 'muted' },
]} ]}
/> />
) : undefined ) : undefined
@@ -233,16 +235,16 @@ export function CloudBilling() {
actions={( actions={(
<> <>
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={refreshAll}> <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> </Button>
{activeTab === 'records' && ( {activeTab === 'records' && (
<Button size="sm" className="dg-btn-primary" onClick={openAddRecord}> <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> </Button>
)} )}
{activeTab === 'alerts' && ( {activeTab === 'alerts' && (
<Button size="sm" className="dg-btn-primary" onClick={openAddRule}> <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> </Button>
)} )}
</> </>
@@ -258,7 +260,7 @@ export function CloudBilling() {
<div className="dg-alert-banner"> <div className="dg-alert-banner">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<Bell className="h-4 w-4 dg-text-yellow" /> <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>
<div className="space-y-2"> <div className="space-y-2">
{stats.alerts.map((a: JsonData, i: number) => ( {stats.alerts.map((a: JsonData, i: number) => (
@@ -266,13 +268,13 @@ export function CloudBilling() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant="outline" className="dg-badge-yellow">{a.rule_name}</Badge> <Badge variant="outline" className="dg-badge-yellow">{a.rule_name}</Badge>
<span className="dg-text-secondary"> <span className="dg-text-secondary">
{a.provider} · {a.metric === 'daily' ? '日' : '月'}成本 {a.provider} · {a.metric === 'daily' ? t('cloud.dailyCost') : t('cloud.monthlyAccumulated')}
</span> </span>
</div> </div>
<span className="text-[var(--text-secondary)]"> <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}{' '} {' '}{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> </span>
</div> </div>
))} ))}
@@ -282,16 +284,16 @@ export function CloudBilling() {
<Card className="dg-card"> <Card className="dg-card">
<CardContent className="pt-4"> <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 || []} /> <TrendChart data={stats.month_trend || []} />
</CardContent> </CardContent>
</Card> </Card>
<Card className="dg-card"> <Card className="dg-card">
<CardContent className="pt-4"> <CardContent className="pt-4">
<h3 className="dg-section-title">本月厂商成本分布</h3> <h3 className="dg-section-title">{t('cloud.monthVendorDistribution')}</h3>
{(stats.provider_dist || []).length === 0 ? ( {(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="space-y-3">
<div className="flex h-8 rounded-lg overflow-hidden"> <div className="flex h-8 rounded-lg overflow-hidden">
@@ -329,7 +331,7 @@ export function CloudBilling() {
</Card> </Card>
</div> </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 ? ( ) : records.length === 0 ? (
<PageEmpty <PageEmpty
icon={<Wallet className="h-12 w-12" />} icon={<Wallet className="h-12 w-12" />}
title="暂无成本记录" title={t('cloud.noCostRecords')}
action={( action={(
<Button size="sm" className="dg-btn-primary" onClick={openAddRecord}> <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> </Button>
)} )}
/> />
@@ -353,14 +355,14 @@ export function CloudBilling() {
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead className="bg-[var(--surface-raised)] dg-text-secondary"> <thead className="bg-[var(--surface-raised)] dg-text-secondary">
<tr> <tr>
<th className="px-4 py-2.5 text-left 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">厂商</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">服务</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">区域</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">账号</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">费用</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">说明</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">操作</th> <th className="px-4 py-2.5 text-center font-medium">{t('common.actions')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -404,11 +406,11 @@ export function CloudBilling() {
) : rules.length === 0 ? ( ) : rules.length === 0 ? (
<PageEmpty <PageEmpty
icon={<Bell className="h-12 w-12" />} icon={<Bell className="h-12 w-12" />}
title="暂无预警规则" title={t('cloud.noAlertRules')}
description="添加规则后,成本超阈值将在此页管理" description={t('cloud.alertRuleHint')}
action={( action={(
<Button size="sm" className="dg-btn-primary" onClick={openAddRule}> <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> </Button>
)} )}
/> />
@@ -426,20 +428,20 @@ export function CloudBilling() {
</div> </div>
<div className="grid grid-cols-2 gap-2 text-sm"> <div className="grid grid-cols-2 gap-2 text-sm">
<div> <div>
<span className="dg-text-muted">厂商</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) : '全部'}</span> <span className="ml-2 text-[var(--text-secondary)]">{r.provider ? (providerLabels[r.provider] || r.provider) : t('common.all')}</span>
</div> </div>
<div> <div>
<span className="dg-text-muted">指标</span> <span className="dg-text-muted">{t('cloud.colMetric')}</span>
<span className="ml-2 text-[var(--text-secondary)]">{r.metric === 'daily' ? '日成本' : '月累计'}</span> <span className="ml-2 text-[var(--text-secondary)]">{r.metric === 'daily' ? t('cloud.dailyCost') : t('cloud.monthlyAccumulated')}</span>
</div> </div>
<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> <span className="ml-2 dg-text-yellow font-medium">{r.operator} {formatCurrency(r.threshold)}</span>
</div> </div>
{r.last_triggered && ( {r.last_triggered && (
<div> <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> <span className="ml-2 dg-text-secondary">{new Date(r.last_triggered).toLocaleString('zh-CN')}</span>
</div> </div>
)} )}
@@ -473,4 +475,4 @@ export function CloudBilling() {
/> />
</PageShell> </PageShell>
); );
} }
+44 -43
View File
@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
@@ -32,6 +33,7 @@ import { StatStrip } from '@/components/ui/stat-strip';
import type { JsonData } from '@/types/common'; import type { JsonData } from '@/types/common';
export function DomainManager() { export function DomainManager() {
const { t } = useTranslation();
const [domains, setDomains] = useState<JsonData[]>([]); const [domains, setDomains] = useState<JsonData[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
@@ -109,8 +111,8 @@ export function DomainManager() {
} }
setFormOpen(false); setFormOpen(false);
loadData(); loadData();
} catch (e) { } catch (_e) {
console.error(editingDomain ? '更新域名失败:' : '创建域名失败:', e); // silently ignored
} finally { } finally {
setSubmitting(false); setSubmitting(false);
} }
@@ -121,8 +123,7 @@ export function DomainManager() {
setLoading(true); setLoading(true);
const res: JsonData = await cloudApi.getDomains(); const res: JsonData = await cloudApi.getDomains();
setDomains(res?.list || []); setDomains(res?.list || []);
} catch (e) { } catch (_e) {
console.error('加载域名列表失败:', e);
setDomains([]); setDomains([]);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -137,8 +138,8 @@ export function DomainManager() {
try { try {
await cloudApi.deleteDomain(id); await cloudApi.deleteDomain(id);
loadData(); loadData();
} catch (e) { } catch (_e) {
console.error('删除域名失败:', e); // silently ignored
} }
}; };
@@ -168,7 +169,7 @@ export function DomainManager() {
<div className="relative flex-1 max-w-md"> <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" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 dg-text-muted" />
<Input <Input
placeholder="搜索域名..." placeholder={t('cloud.searchDomain')}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 h-9 dg-input" className="pl-10 h-9 dg-input"
@@ -179,10 +180,10 @@ export function DomainManager() {
onChange={(e) => setSelectedStatus(e.target.value)} onChange={(e) => setSelectedStatus(e.target.value)}
className="px-3 py-2 h-9 rounded-md text-sm dg-input" className="px-3 py-2 h-9 rounded-md text-sm dg-input"
> >
<option value="all">全部状态</option> <option value="all">{t('cloud.allStatus')}</option>
<option value="active">正常</option> <option value="active">{t('cloud.normal')}</option>
<option value="expired">已过期</option> <option value="expired">{t('cloud.domainExpired')}</option>
<option value="pending">待审核</option> <option value="pending">{t('cloud.pending')}</option>
</select> </select>
</div> </div>
); );
@@ -190,16 +191,16 @@ export function DomainManager() {
return ( return (
<PageShell <PageShell
variant="list" variant="list"
title="域名管理" title={t('cloud.domains')}
subtitle="管理域名注册信息和DNS解析记录" subtitle={t('cloud.domainsSubtitle')}
icon={<Globe className="h-5 w-5" />} icon={<Globe className="h-5 w-5" />}
metrics={ metrics={
!loading ? ( !loading ? (
<StatStrip <StatStrip
items={[ items={[
{ label: '总计', value: stats.total, color: 'blue' }, { label: t('common.total'), value: stats.total, color: 'blue' },
{ label: '正常', value: stats.active, color: 'green' }, { label: t('cloud.normal'), value: stats.active, color: 'green' },
{ label: '已过期', value: stats.expired, color: stats.expired > 0 ? 'red' : 'muted' }, { label: t('cloud.domainExpired'), value: stats.expired, color: stats.expired > 0 ? 'red' : 'muted' },
{ label: 'SSL', value: stats.sslEnabled, color: 'cyan' }, { 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"> <Button variant="outline" size="sm" onClick={loadData} className="dg-btn-secondary">
<RefreshCw className={`h-4 w-4 mr-1 ${loading ? 'animate-spin' : ''}`} /> <RefreshCw className={`h-4 w-4 mr-1 ${loading ? 'animate-spin' : ''}`} />
刷新 {t('common.refresh')}
</Button> </Button>
<Button size="sm" className="dg-btn-primary" onClick={handleOpenCreate}> <Button size="sm" className="dg-btn-primary" onClick={handleOpenCreate}>
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
添加域名 {t('cloud.addDomain')}
</Button> </Button>
</> </>
)} )}
@@ -224,11 +225,11 @@ export function DomainManager() {
) : filteredDomains.length === 0 ? ( ) : filteredDomains.length === 0 ? (
<PageEmpty <PageEmpty
icon={<Globe className="h-12 w-12" />} icon={<Globe className="h-12 w-12" />}
title="暂无域名数据" title={t('cloud.noDomainData')}
action={( action={(
<Button size="sm" className="dg-btn-primary" onClick={handleOpenCreate}> <Button size="sm" className="dg-btn-primary" onClick={handleOpenCreate}>
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
添加域名 {t('cloud.addDomain')}
</Button> </Button>
)} )}
/> />
@@ -253,19 +254,19 @@ export function DomainManager() {
{domain.status === 'active' && ( {domain.status === 'active' && (
<Badge className="dg-status-running border-0"> <Badge className="dg-status-running border-0">
<CheckCircle2 className="h-3 w-3 mr-1" /> <CheckCircle2 className="h-3 w-3 mr-1" />
正常 {t('cloud.normal')}
</Badge> </Badge>
)} )}
{domain.status === 'expired' && ( {domain.status === 'expired' && (
<Badge className="dg-status-error border-0"> <Badge className="dg-status-error border-0">
<XCircle className="h-3 w-3 mr-1" /> <XCircle className="h-3 w-3 mr-1" />
已过期 {t('cloud.domainExpired')}
</Badge> </Badge>
)} )}
{domain.status === 'pending' && ( {domain.status === 'pending' && (
<Badge className="dg-status-warning border-0"> <Badge className="dg-status-warning border-0">
<Clock className="h-3 w-3 mr-1" /> <Clock className="h-3 w-3 mr-1" />
待审核 {t('cloud.pending')}
</Badge> </Badge>
)} )}
{sslEnabled && ( {sslEnabled && (
@@ -276,17 +277,17 @@ export function DomainManager() {
)} )}
</div> </div>
<div className="flex items-center gap-4 mt-1 text-sm dg-text-secondary"> <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>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>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="text-right"> <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)]'}`}> <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 && ( {sslEnabled && sslExpireDate && (
<span className="dg-text-muted"> · SSL: {sslExpireDate.substring(0, 10)}</span> <span className="dg-text-muted"> · SSL: {sslExpireDate.substring(0, 10)}</span>
)} )}
@@ -325,16 +326,16 @@ export function DomainManager() {
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
<Globe className="h-5 w-5 text-[var(--dg-blue)]" /> <Globe className="h-5 w-5 text-[var(--dg-blue)]" />
{editingDomain ? '编辑域名' : '添加域名'} {editingDomain ? t('cloud.editDomain') : t('cloud.addDomain')}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
{editingDomain ? '修改域名注册信息、DNS 配置和 SSL 证书状态' : '录入域名信息,包括注册商、DNS 服务商和到期时间'} {editingDomain ? t('cloud.editDomainDesc') : t('cloud.addDomainDesc')}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-2"> <div className="space-y-4 py-2">
<div className="space-y-2"> <div className="space-y-2">
<Label>域名 *</Label> <Label>{t('cloud.domainName')} *</Label>
<Input <Input
placeholder="example.com" placeholder="example.com"
value={formData.name} value={formData.name}
@@ -345,31 +346,31 @@ export function DomainManager() {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>注册商</Label> <Label>{t('cloud.domainRegistrar')}</Label>
<Input <Input
placeholder="阿里云 / Cloudflare ..." placeholder={t('cloud.aliyun') + ' / Cloudflare ...'}
value={formData.registrar} value={formData.registrar}
onChange={(e) => setFormData({ ...formData, registrar: e.target.value })} onChange={(e) => setFormData({ ...formData, registrar: e.target.value })}
className="dg-input" className="dg-input"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>状态</Label> <Label>{t('common.status')}</Label>
<select <select
value={formData.status} value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value })} onChange={(e) => setFormData({ ...formData, status: e.target.value })}
className="w-full px-3 py-2 rounded-md text-sm dg-input" className="w-full px-3 py-2 rounded-md text-sm dg-input"
> >
<option value="active">正常</option> <option value="active">{t('cloud.normal')}</option>
<option value="expired">已过期</option> <option value="expired">{t('cloud.domainExpired')}</option>
<option value="pending">待审核</option> <option value="pending">{t('cloud.pending')}</option>
</select> </select>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>到期时间</Label> <Label>{t('cloud.domainExpireDate')}</Label>
<Input <Input
type="date" type="date"
value={formData.expire_date} value={formData.expire_date}
@@ -378,7 +379,7 @@ export function DomainManager() {
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>DNS 服务商</Label> <Label>{t('cloud.domainDnsServer')}</Label>
<Input <Input
placeholder="Cloudflare / DNSPod ..." placeholder="Cloudflare / DNSPod ..."
value={formData.dns_provider} value={formData.dns_provider}
@@ -389,7 +390,7 @@ export function DomainManager() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>解析记录数</Label> <Label>{t('cloud.recordCountLabel')}</Label>
<Input <Input
type="number" type="number"
placeholder="0" placeholder="0"
@@ -401,7 +402,7 @@ export function DomainManager() {
<div className="grid grid-cols-2 gap-4 items-end"> <div className="grid grid-cols-2 gap-4 items-end">
<div className="space-y-2"> <div className="space-y-2">
<Label>SSL 证书到期</Label> <Label>{t('cloud.domainSslExpire')}</Label>
<Input <Input
type="date" type="date"
value={formData.ssl_expire_date} value={formData.ssl_expire_date}
@@ -417,14 +418,14 @@ export function DomainManager() {
onChange={(e) => setFormData({ ...formData, ssl_enabled: e.target.checked })} onChange={(e) => setFormData({ ...formData, ssl_enabled: e.target.checked })}
className="h-4 w-4 rounded border-[var(--dg-border)] accent-[var(--dg-cyan)]" 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> </label>
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setFormOpen(false)} className="dg-btn-secondary"> <Button variant="outline" onClick={() => setFormOpen(false)} className="dg-btn-secondary">
取消 {t('common.cancel')}
</Button> </Button>
<Button <Button
className="dg-btn-primary" className="dg-btn-primary"
@@ -432,7 +433,7 @@ export function DomainManager() {
onClick={handleSubmit} 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" />} {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> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
+65 -59
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
@@ -39,13 +40,15 @@ import { PageEmpty } from '@/components/layout/PageEmpty';
import { StatStrip } from '@/components/ui/stat-strip'; import { StatStrip } from '@/components/ui/stat-strip';
import type { JsonData } from '@/types/common'; import type { JsonData } from '@/types/common';
const providerNames: Record<string, string> = { function getProviderNames(t: (key: string) => string): Record<string, string> {
aws: 'AWS', return {
aliyun: '阿里云', aws: 'AWS',
tencent: '腾讯云', aliyun: t('cloud.aliyun'),
huawei: '华为云', tencent: t('cloud.tencent'),
azure: 'Azure', huawei: t('cloud.huawei'),
}; azure: 'Azure',
};
}
const providerColors: Record<string, string> = { const providerColors: Record<string, string> = {
aws: 'dg-text-orange', aws: 'dg-text-orange',
@@ -56,6 +59,7 @@ const providerColors: Record<string, string> = {
}; };
export function RegionManager() { export function RegionManager() {
const { t } = useTranslation();
const confirm = useConfirm(); const confirm = useConfirm();
const [regions, setRegions] = useState<JsonData[]>([]); const [regions, setRegions] = useState<JsonData[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -69,6 +73,8 @@ export function RegionManager() {
status: 'active', endpoint: '', status: 'active', endpoint: '',
}); });
const providerNames = getProviderNames(t);
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
@@ -126,20 +132,20 @@ export function RegionManager() {
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
const region = regions.find(r => r.id === id); const region = regions.find(r => r.id === id);
const ok = await confirm({ const ok = await confirm({
title: '删除区域', title: t('cloud.deleteRegion'),
description: `确定删除「${region?.name || id}」吗?此操作不可撤销。`, description: t('cloud.deleteRegionConfirm', { name: region?.name || id }),
confirmText: '删除', confirmText: t('common.delete'),
variant: 'destructive', variant: 'destructive',
}); });
if (!ok) return; if (!ok) return;
try { try {
await regionApi.delete(id); await regionApi.delete(id);
toast.success('区域已删除'); toast.success(t('cloud.regionDeleted'));
setSelectedIds(prev => prev.filter(i => i !== id)); setSelectedIds(prev => prev.filter(i => i !== id));
loadData(); loadData();
} catch (e) { } catch (e) {
console.error('删除区域失败:', e); console.error(t('cloud.deleteRegion'), e);
toast.error('删除失败'); toast.error(t('common.deleteFailed'));
} }
}; };
@@ -147,43 +153,43 @@ export function RegionManager() {
const handleBatchDelete = async () => { const handleBatchDelete = async () => {
if (selectedIds.length === 0) return; if (selectedIds.length === 0) return;
const ok = await confirm({ const ok = await confirm({
title: '批量删除区域', title: t('cloud.batchDeleteRegion'),
description: `确定删除选中的 ${selectedIds.length} 个区域吗?此操作不可撤销。`, description: t('cloud.batchDeleteRegionConfirm', { count: selectedIds.length }),
confirmText: '批量删除', confirmText: t('cloud.batchDelete'),
variant: 'destructive', variant: 'destructive',
}); });
if (!ok) return; if (!ok) return;
try { try {
await regionApi.batchDelete(selectedIds.map(Number)); await regionApi.batchDelete(selectedIds.map(Number));
toast.success(`已删除 ${selectedIds.length} 个区域`); toast.success(t('cloud.batchDeletedRegions', { count: selectedIds.length }));
setSelectedIds([]); setSelectedIds([]);
loadData(); loadData();
} catch (e) { } catch (e) {
console.error('批量删除失败:', e); console.error(t('cloud.batchDeleteFailed'), e);
toast.error('批量删除失败'); toast.error(t('cloud.batchDeleteFailed'));
} }
}; };
// DataTable 列定义 // DataTable 列定义
const columns: DataTableColumn<JsonData>[] = [ const columns: DataTableColumn<JsonData>[] = [
{ key: 'name', header: '区域名称', sortable: true, render: (r) => <span className="font-medium text-[var(--dg-text-primary)]">{r.name}</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: '编码', sortable: true, render: (r) => <span className="font-mono text-sm text-[var(--dg-text-secondary)]">{r.code}</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>, 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: 'location', header: t('cloud.location'), 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: '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' ? ( 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) => ( render: (r) => (
<div className="flex justify-end gap-1"> <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)]"> <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 /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">全部云商</SelectItem> <SelectItem value="all">{t('cloud.allVendors')}</SelectItem>
{Object.entries(providerNames).map(([k, v]) => ( {Object.entries(providerNames).map(([k, v]) => (
<SelectItem key={k} value={k}>{v}</SelectItem> <SelectItem key={k} value={k}>{v}</SelectItem>
))} ))}
@@ -220,14 +226,14 @@ export function RegionManager() {
const batchToolbar = selectedIds.length > 0 ? ( const batchToolbar = selectedIds.length > 0 ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm dg-text-secondary"> <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> </span>
<Button variant="ghost" size="sm" onClick={() => setSelectedIds([])} className="dg-text-muted"> <Button variant="ghost" size="sm" onClick={() => setSelectedIds([])} className="dg-text-muted">
取消选择 {t('cloud.clearSelection')}
</Button> </Button>
<Button variant="outline" size="sm" className="dg-btn-danger" onClick={handleBatchDelete}> <Button variant="outline" size="sm" className="dg-btn-danger" onClick={handleBatchDelete}>
<Trash2 className="h-4 w-4 mr-1" /> <Trash2 className="h-4 w-4 mr-1" />
批量删除 {t('cloud.batchDelete')}
</Button> </Button>
</div> </div>
) : undefined; ) : undefined;
@@ -235,16 +241,16 @@ export function RegionManager() {
return ( return (
<PageShell <PageShell
variant="list" variant="list"
title="区域管理" title={t('cloud.regions')}
subtitle="管理各云厂商的区域与接入端点" subtitle={t('cloud.regionsSubtitle')}
icon={<MapPin className="h-5 w-5" />} icon={<MapPin className="h-5 w-5" />}
metrics={ metrics={
!loading ? ( !loading ? (
<StatStrip <StatStrip
items={[ items={[
{ label: '总计', value: stats.total, color: 'blue' }, { label: t('common.total'), value: stats.total, color: 'blue' },
{ label: '启用', value: stats.active, color: 'green' }, { label: t('cloud.enabled'), value: stats.active, color: 'green' },
{ label: '禁用', value: stats.inactive, color: 'muted' }, { label: t('cloud.disabled'), value: stats.inactive, color: 'muted' },
]} ]}
/> />
) : undefined ) : undefined
@@ -254,11 +260,11 @@ export function RegionManager() {
<> <>
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={loadData}> <Button variant="outline" size="sm" className="dg-btn-secondary" onClick={loadData}>
<RefreshCw className={`h-4 w-4 mr-1 ${loading ? 'animate-spin' : ''}`} /> <RefreshCw className={`h-4 w-4 mr-1 ${loading ? 'animate-spin' : ''}`} />
刷新 {t('common.refresh')}
</Button> </Button>
<Button size="sm" className="dg-btn-primary" onClick={openCreate}> <Button size="sm" className="dg-btn-primary" onClick={openCreate}>
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
添加区域 {t('cloud.addRegion')}
</Button> </Button>
</> </>
)} )}
@@ -268,11 +274,11 @@ export function RegionManager() {
) : filtered.length === 0 ? ( ) : filtered.length === 0 ? (
<PageEmpty <PageEmpty
icon={<MapPin className="h-12 w-12" />} icon={<MapPin className="h-12 w-12" />}
title="暂无区域数据" title={t('cloud.noRegionData')}
action={( action={(
<Button size="sm" className="dg-btn-primary" onClick={openCreate}> <Button size="sm" className="dg-btn-primary" onClick={openCreate}>
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
添加区域 {t('cloud.addRegion')}
</Button> </Button>
)} )}
/> />
@@ -283,7 +289,7 @@ export function RegionManager() {
data={filtered} data={filtered}
rowKey={(r) => r.id} rowKey={(r) => r.id}
searchable searchable
searchPlaceholder="搜索区域名称或编码..." searchPlaceholder={t('cloud.searchRegion')}
searchKeys={['name', 'code']} searchKeys={['name', 'code']}
sortable sortable
selectable selectable
@@ -292,7 +298,7 @@ export function RegionManager() {
columnToggle columnToggle
exportable exportable
exportFilename="regions.csv" exportFilename="regions.csv"
emptyText="暂无匹配区域" emptyText={t('cloud.noMatchRegion')}
/> />
</ContentPanel> </ContentPanel>
)} )}
@@ -301,26 +307,26 @@ export function RegionManager() {
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}> <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>{editingItem ? '编辑区域' : '添加区域'}</DialogTitle> <DialogTitle>{editingItem ? t('cloud.editRegion') : t('cloud.addRegion')}</DialogTitle>
<DialogDescription className="sr-only">描述</DialogDescription> <DialogDescription className="sr-only">description</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-2"> <div className="space-y-4 py-2">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label>区域名称 *</Label> <Label>{t('cloud.regionName')} *</Label>
<Input <Input
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="如:华北-北京" placeholder={t('cloud.regionNamePlaceholder')}
className="dg-input" className="dg-input"
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label>编码 *</Label> <Label>{t('cloud.code')} *</Label>
<Input <Input
value={formData.code} value={formData.code}
onChange={(e) => setFormData({ ...formData, code: e.target.value })} onChange={(e) => setFormData({ ...formData, code: e.target.value })}
placeholder="如:cn-north-1" placeholder={t('cloud.regionCodePlaceholder')}
className="dg-input font-mono" className="dg-input font-mono"
disabled={!!editingItem} disabled={!!editingItem}
/> />
@@ -328,7 +334,7 @@ export function RegionManager() {
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label>云商</Label> <Label>{t('cloud.vendor')}</Label>
<Select value={formData.provider} onValueChange={(v) => setFormData({ ...formData, provider: v })}> <Select value={formData.provider} onValueChange={(v) => setFormData({ ...formData, provider: v })}>
<SelectTrigger className="dg-input"> <SelectTrigger className="dg-input">
<SelectValue /> <SelectValue />
@@ -341,42 +347,42 @@ export function RegionManager() {
</Select> </Select>
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label>状态</Label> <Label>{t('common.status')}</Label>
<Select value={formData.status} onValueChange={(v) => setFormData({ ...formData, status: v })}> <Select value={formData.status} onValueChange={(v) => setFormData({ ...formData, status: v })}>
<SelectTrigger className="dg-input"> <SelectTrigger className="dg-input">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="active">启用</SelectItem> <SelectItem value="active">{t('cloud.enabled')}</SelectItem>
<SelectItem value="inactive">禁用</SelectItem> <SelectItem value="inactive">{t('cloud.disabled')}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label>地理位置</Label> <Label>{t('cloud.geoLocation')}</Label>
<Input <Input
value={formData.location} value={formData.location}
onChange={(e) => setFormData({ ...formData, location: e.target.value })} onChange={(e) => setFormData({ ...formData, location: e.target.value })}
placeholder="如:中国北京" placeholder={t('cloud.locationPlaceholder')}
className="dg-input" className="dg-input"
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label>接入端点</Label> <Label>{t('cloud.regionEndpoint')}</Label>
<Input <Input
value={formData.endpoint} value={formData.endpoint}
onChange={(e) => setFormData({ ...formData, endpoint: e.target.value })} 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" className="dg-input font-mono text-sm"
/> />
</div> </div>
</div> </div>
<DialogFooter> <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()}> <Button onClick={handleSubmit} disabled={submitting || !formData.name.trim() || !formData.code.trim()}>
{submitting && <Loader2 className="h-4 w-4 mr-1 animate-spin" />} {submitting && <Loader2 className="h-4 w-4 mr-1 animate-spin" />}
{editingItem ? '保存' : '创建'} {editingItem ? t('common.save') : t('common.create')}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
+92 -2
View File
@@ -471,7 +471,9 @@
"channel": "Channel", "channel": "Channel",
"contact": "Contact", "contact": "Contact",
"true": "Yes", "true": "Yes",
"false": "No" "false": "No",
"deleteFailed": "Delete failed",
"deleteSuccess": "Deleted successfully"
}, },
"menu": { "menu": {
"dashboard": "Dashboard", "dashboard": "Dashboard",
@@ -3822,7 +3824,95 @@
"setBudget": "Set Budget", "setBudget": "Set Budget",
"budgetAlert": "Budget Alert", "budgetAlert": "Budget Alert",
"budgetUsage": "Budget Usage", "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": { "agent": {
"title": "Agent Management", "title": "Agent Management",
+92 -2
View File
@@ -471,7 +471,9 @@
"channel": "渠道", "channel": "渠道",
"contact": "联系人", "contact": "联系人",
"true": "是", "true": "是",
"false": "否" "false": "否",
"deleteFailed": "删除失败",
"deleteSuccess": "删除成功"
}, },
"menu": { "menu": {
"dashboard": "仪表盘", "dashboard": "仪表盘",
@@ -3822,7 +3824,95 @@
"setBudget": "设置预算", "setBudget": "设置预算",
"budgetAlert": "预算预警", "budgetAlert": "预算预警",
"budgetUsage": "预算使用率", "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": { "agent": {
"title": "Agent 管理", "title": "Agent 管理",