feat(i18n): 安全批量转换 135 文件 — 覆盖率 34%→62%
使用 i18n.t() 全局方式(避免 hook 定位问题): - 288 目标文件中 135 个成功转换 - 2228 处中文替换为 i18n.t() 调用 - 1006 个新翻译 key 添加到 zh-CN.json + en-US.json - 词典总量: 3554 keys (22 namespaces) - tsc 零错误 + Vite 构建成功 - 6 个含代码示例/数据字符串的文件安全回退
This commit is contained in:
1 parent
72bfb0e12d
commit
55f216cfbb
131 files changed
+4049
-1908
No files matched your search
@@ -15,6 +15,7 @@ import { serverApi, remoteExecApi, type RemoteExecResult, type RemoteUploadResul
|
||||
import { PageShell } from '@/components/layout/PageShell';
|
||||
import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
interface RemoteServer {
|
||||
id: number; hostname: string; ip: string; username: string; port: number;
|
||||
@@ -41,7 +42,7 @@ export function RemoteExecPage() {
|
||||
try {
|
||||
const res = await serverApi.getServers({ page: 1, page_size: 500, keyword });
|
||||
setServers(((res as JsonData)?.list as RemoteServer[]) || []);
|
||||
} catch { toast.error('加载服务器列表失败'); }
|
||||
} catch { toast.error(i18n.t('task.remoteexecpaget.s68755a4d')); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
useEffect(() => { load(); }, []);
|
||||
@@ -58,12 +59,12 @@ export function RemoteExecPage() {
|
||||
};
|
||||
|
||||
const doExec = async () => {
|
||||
if (!command.trim()) { toast.error('请输入要执行的命令'); return; }
|
||||
if (selCount === 0) { toast.error('请至少选择一台主机'); return; }
|
||||
if (!command.trim()) { toast.error(i18n.t('agent.agentmonitortsx.s9358aada')); return; }
|
||||
if (selCount === 0) { toast.error(i18n.t('task.remoteexecpaget.se62f41c4')); return; }
|
||||
const ok = await _confirm({
|
||||
title: '确认批量执行命令',
|
||||
title: i18n.t('task.remoteexecpaget.s110b7142'),
|
||||
description: `将在 ${selCount} 台主机上执行该命令,请确认命令安全:\n\n${command}`,
|
||||
confirmText: '执行', variant: 'destructive',
|
||||
confirmText: i18n.t('deploy.label.execute'), variant: 'destructive',
|
||||
});
|
||||
if (!ok) return;
|
||||
setRunning(true); setExecResults([]);
|
||||
@@ -72,14 +73,14 @@ export function RemoteExecPage() {
|
||||
setExecResults(res || []);
|
||||
const succ = (res || []).filter(r => r.success).length;
|
||||
toast.success(`执行完成:${succ}/${selCount} 成功`);
|
||||
} catch { toast.error('执行失败'); }
|
||||
} catch { toast.error(i18n.t('automation.systemTaskFailed')); }
|
||||
finally { setRunning(false); }
|
||||
};
|
||||
|
||||
const doUpload = async () => {
|
||||
if (!file) { toast.error('请选择要上传的文件'); return; }
|
||||
if (!remotePath.trim()) { toast.error('请填写远程目标路径'); return; }
|
||||
if (selCount === 0) { toast.error('请至少选择一台主机'); return; }
|
||||
if (!file) { toast.error(i18n.t('task.remoteexecpaget.s68ea13ee')); return; }
|
||||
if (!remotePath.trim()) { toast.error(i18n.t('task.remoteexecpaget.sb68231b8')); return; }
|
||||
if (selCount === 0) { toast.error(i18n.t('task.remoteexecpaget.se62f41c4')); return; }
|
||||
const dst = remotePath.trim().endsWith('/') ? remotePath.trim() + file.name : remotePath.trim();
|
||||
setRunning(true); setUploadResults([]);
|
||||
try {
|
||||
@@ -87,15 +88,15 @@ export function RemoteExecPage() {
|
||||
setUploadResults(res || []);
|
||||
const succ = (res || []).filter(r => r.success).length;
|
||||
toast.success(`上传完成:${succ}/${selCount} 成功`);
|
||||
} catch { toast.error('上传失败'); }
|
||||
} catch { toast.error(i18n.t('common.uploadFailed')); }
|
||||
finally { setRunning(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<PageShell
|
||||
variant="workspace"
|
||||
title="远程执行"
|
||||
subtitle="关联主机后批量执行命令 / 上传文件(Go-native SSH)"
|
||||
title={i18n.t('automation.remoteExec')}
|
||||
subtitle={i18n.t('task.remoteexecpaget.s123ad5f6')}
|
||||
icon={<Terminal className="h-5 w-5" />}
|
||||
contentClassName="flex-1 min-h-0"
|
||||
loading={loading}
|
||||
@@ -111,9 +112,9 @@ export function RemoteExecPage() {
|
||||
<Card className="col-span-5 dg-card">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm flex items-center justify-between text-[var(--dg-text-primary)]">
|
||||
<span className="flex items-center gap-2"><ServerIcon className="h-4 w-4 dg-text-blue" />选择目标主机</span>
|
||||
<span className="flex items-center gap-2"><ServerIcon className="h-4 w-4 dg-text-blue" /> {i18n.t('automation.remoteSelectHost')} </span>
|
||||
<Input
|
||||
placeholder="搜索 主机名/IP"
|
||||
placeholder={i18n.t('task.remoteexecpaget.s253acd21')}
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && load()}
|
||||
@@ -124,8 +125,8 @@ export function RemoteExecPage() {
|
||||
<CardContent className="max-h-[520px] overflow-y-auto space-y-1">
|
||||
{servers.length === 0 ? (
|
||||
<PageEmpty
|
||||
title="暂无服务器"
|
||||
description="请先在「服务器管理」添加"
|
||||
title={i18n.t('dashboard.serversStatus.empty')}
|
||||
description={i18n.t('task.remoteexecpaget.s0afbf1fc')}
|
||||
className="py-10"
|
||||
/>
|
||||
) : (
|
||||
@@ -143,8 +144,8 @@ export function RemoteExecPage() {
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)] font-mono">{s.ip} · {s.username || 'root'}</div>
|
||||
</div>
|
||||
{s.has_auth === false
|
||||
? <Badge className="bg-amber-500/20 dg-text-yellow text-[10px]">无凭据</Badge>
|
||||
: <Badge className="bg-emerald-500/20 dg-text-green text-[10px]">{s.auth_type === 'key' ? '密钥' : '密码'}</Badge>}
|
||||
? <Badge className="bg-amber-500/20 dg-text-yellow text-[10px]"> {i18n.t('task.remoteexecpaget.s152f5300')} </Badge>
|
||||
: <Badge className="bg-emerald-500/20 dg-text-green text-[10px]">{s.auth_type === 'key' ? i18n.t('system.credentialKey') : i18n.t('system.passwordLabel')}</Badge>}
|
||||
</label>
|
||||
))}
|
||||
</>
|
||||
@@ -175,17 +176,17 @@ export function RemoteExecPage() {
|
||||
|
||||
{tab === 'exec' ? (
|
||||
<Card className="dg-card">
|
||||
<CardHeader className="pb-3"><CardTitle className="text-sm text-[var(--dg-text-primary)]">批量执行命令</CardTitle></CardHeader>
|
||||
<CardHeader className="pb-3"><CardTitle className="text-sm text-[var(--dg-text-primary)]"> {i18n.t('task.remoteexecpaget.seb6e67f1')} </CardTitle></CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<textarea
|
||||
value={command}
|
||||
onChange={(e) => setCommand(e.target.value)}
|
||||
rows={5}
|
||||
placeholder="输入要在选中主机上执行的 shell 命令,例如:df -h | head -5"
|
||||
placeholder={i18n.t('task.remoteexecpaget.s76628808')}
|
||||
className="w-full bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md px-3 py-2 text-xs font-mono text-[var(--dg-text-primary)] resize-none"
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs dg-text-secondary">超时(秒)</span>
|
||||
<span className="text-xs dg-text-secondary"> {i18n.t('automation.remoteTimeout')} </span>
|
||||
<Input type="number" min={1} value={timeoutSec} onChange={(e) => setTimeoutSec(Number(e.target.value) || 60)}
|
||||
className="h-8 w-24 text-xs dg-input" />
|
||||
<Button onClick={doExec} disabled={running} className="ml-auto dg-btn-primary h-8 text-xs">
|
||||
@@ -197,17 +198,17 @@ export function RemoteExecPage() {
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="dg-card">
|
||||
<CardHeader className="pb-3"><CardTitle className="text-sm text-[var(--dg-text-primary)]">批量上传文件</CardTitle></CardHeader>
|
||||
<CardHeader className="pb-3"><CardTitle className="text-sm text-[var(--dg-text-primary)]"> {i18n.t('task.remoteexecpaget.s30048caa')} </CardTitle></CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-xs dg-text-secondary">选择本地文件</span>
|
||||
<span className="text-xs dg-text-secondary"> {i18n.t('task.remoteexecpaget.sb7e21a1e')} </span>
|
||||
<Input type="file" onChange={(e) => setFile(e.target.files?.[0] || null)}
|
||||
className="h-8 text-xs dg-input" />
|
||||
{file && <span className="text-[10px] text-[var(--dg-text-muted)]">{file.name} ({(file.size / 1024).toFixed(1)} KB)</span>}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-xs dg-text-secondary">远程目标路径(目录则自动追加文件名)</span>
|
||||
<Input value={remotePath} onChange={(e) => setRemotePath(e.target.value)} placeholder="/tmp/ 或 /opt/app/config.yaml"
|
||||
<span className="text-xs dg-text-secondary"> {i18n.t('task.remoteexecpaget.sb47f0323')} </span>
|
||||
<Input value={remotePath} onChange={(e) => setRemotePath(e.target.value)} placeholder={i18n.t('task.remoteexecpaget.sd4b44a36')}
|
||||
className="h-8 text-xs font-mono dg-input" />
|
||||
</div>
|
||||
<Button onClick={doUpload} disabled={running} className="dg-btn-primary h-8 text-xs">
|
||||
@@ -220,7 +221,7 @@ export function RemoteExecPage() {
|
||||
|
||||
{(execResults.length > 0 || uploadResults.length > 0) && (
|
||||
<Card className="dg-card">
|
||||
<CardHeader className="pb-3"><CardTitle className="text-sm text-[var(--dg-text-primary)]">执行结果</CardTitle></CardHeader>
|
||||
<CardHeader className="pb-3"><CardTitle className="text-sm text-[var(--dg-text-primary)]"> {i18n.t('automation.remoteExecResult')} </CardTitle></CardHeader>
|
||||
<CardContent className="space-y-2 max-h-[300px] overflow-y-auto">
|
||||
{execResults.map((r, i) => (
|
||||
<div key={i} className="p-2 rounded border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]">
|
||||
@@ -232,7 +233,7 @@ export function RemoteExecPage() {
|
||||
</Badge>
|
||||
</div>
|
||||
<pre className="text-[10px] font-mono dg-text-secondary whitespace-pre-wrap break-all max-h-32 overflow-y-auto">
|
||||
{r.error || r.output || '(无输出)'}
|
||||
{r.error || r.output || i18n.t('task.remoteexecpaget.s1a63831c')}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in new issue
Block a user