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:
attacker committed 2026-07-03 02:14:20 +08:00
1 parent 72bfb0e12d
commit 55f216cfbb
131 files changed
+4049 -1908

No files matched your search

@@ -27,6 +27,7 @@ import {
AlertCircle,
} from 'lucide-react';
import { cloudApi } from '@/api';
import i18n from '@/i18n';
interface CloudServerImportProps {
open: boolean;
@@ -35,9 +36,9 @@ interface CloudServerImportProps {
const cloudProviders = [
{ id: 'aws', name: 'AWS', color: 'bg-orange-500', icon: 'AWS' },
{ id: 'aliyun', name: '阿里云', color: 'bg-orange-600', icon: 'AL' },
{ id: 'tencent', name: '腾讯云', color: 'bg-blue-500', icon: 'TX' },
{ id: 'huawei', name: '华为云', color: 'bg-red-500', icon: 'HW' },
{ id: 'aliyun', name: i18n.t('cloud.aliyun'), color: 'bg-orange-600', icon: 'AL' },
{ id: 'tencent', name: i18n.t('cloud.tencent'), color: 'bg-blue-500', icon: 'TX' },
{ id: 'huawei', name: i18n.t('cloud.huawei'), color: 'bg-red-500', icon: 'HW' },
{ id: 'azure', name: 'Azure', color: 'bg-blue-600', icon: 'AZ' },
];
@@ -67,7 +68,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
const res: Record<string, any> = await cloudApi.getAccounts();
setCloudAccounts(res?.list || []);
} catch (e) {
console.error('加载云账号失败:', e);
console.error(i18n.t('server.cloudserverimpo.sddeee730'), e);
}
};
@@ -102,7 +103,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
const list = await loadRegions(accountId);
setRegions(list);
} catch (e) {
console.error('同步区域失败:', e);
console.error(i18n.t('server.cloudserverimpo.s1d65993d'), e);
throw e;
} finally {
setIsSyncingRegions(false);
@@ -137,8 +138,8 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
setCloudServers(res?.list || []);
setDiscoverErrors(res?.errors || []);
} catch (e) {
console.error('发现主机失败:', e);
setDiscoverErrors([e?.message || '发现主机失败']);
console.error(i18n.t('server.cloudserverimpo.s5e749a34'), e);
setDiscoverErrors([e?.message || i18n.t('server.cloudserverimpo.s7d43dcbe')]);
} finally {
setIsDiscovering(false);
}
@@ -156,7 +157,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
});
setImportResult((res as any) || { imported: 0, skipped: 0 });
} catch (e) {
console.error('导入失败:', e);
console.error(i18n.t('server.cloudserverimpo.s1b122554'), e);
} finally {
setIsImporting(false);
}
@@ -223,18 +224,18 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
);
const steps = [
{ id: 1, title: '公有云', icon: Cloud },
{ id: 2, title: '访问凭据', icon: Key },
{ id: 3, title: '选择区域', icon: MapPin },
{ id: 4, title: '导入确认', icon: CheckCircle },
{ id: 1, title: i18n.t('server.cloudserverimpo.s7ef31abb'), icon: Cloud },
{ id: 2, title: i18n.t('server.cloudserverimpo.s02badc25'), icon: Key },
{ id: 3, title: i18n.t('server.cloudserverimpo.s97d03d46'), icon: MapPin },
{ id: 4, title: i18n.t('server.cloudserverimpo.s81eea51a'), icon: CheckCircle },
];
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-3xl bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
<DialogHeader>
<DialogTitle className="text-xl">导入云主机</DialogTitle>
<DialogDescription className="sr-only">从云平台批量导入主机资产</DialogDescription>
<DialogTitle className="text-xl"> {i18n.t('server.cloudserverimpo.sa558f8a8')} </DialogTitle>
<DialogDescription className="sr-only"> {i18n.t('server.cloudserverimpo.s86968159')} </DialogDescription>
</DialogHeader>
{/* Step Indicator */}
@@ -262,7 +263,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
{/* Step 1: Select Cloud Provider */}
{step === 1 && (
<div className="space-y-4">
<p className="text-[var(--dg-text-secondary)] text-sm">请选择要导入云主机的云平台</p>
<p className="text-[var(--dg-text-secondary)] text-sm"> {i18n.t('server.cloudserverimpo.s7c366475')} </p>
<div className="grid grid-cols-3 gap-4">
{cloudProviders.map((provider) => (
<Card
@@ -289,7 +290,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
{/* Step 2: Select Account */}
{step === 2 && (
<div className="space-y-4">
<p className="text-[var(--dg-text-secondary)] text-sm">请选择访问凭据(AccessKey)</p>
<p className="text-[var(--dg-text-secondary)] text-sm"> {i18n.t('server.cloudserverimpo.se1a7870b')} </p>
{filteredAccounts.length > 0 ? (
<div className="space-y-2 max-h-[300px] overflow-y-auto">
{filteredAccounts.map((account) => (
@@ -325,7 +326,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
) : (
<div className="text-center py-8 text-[var(--dg-text-muted)]">
<Key className="h-12 w-12 mx-auto mb-3 opacity-50" />
<p>该云平台暂无有效的访问凭据</p>
<p> {i18n.t('server.cloudserverimpo.saa5f0df7')} </p>
</div>
)}
</div>
@@ -349,19 +350,19 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
disabled={isSyncingRegions}
>
<RefreshCw className={`h-3 w-3 mr-1 ${isSyncingRegions ? 'animate-spin' : ''}`} />
{isSyncingRegions ? '同步中...' : '刷新区域'}
{isSyncingRegions ? i18n.t('server.cloudserverimpo.sf787f452') : i18n.t('server.cloudserverimpo.saa44e4f6')}
</Button>
</div>
{isSyncingRegions ? (
<div className="text-center py-12">
<RefreshCw className="h-8 w-8 mx-auto mb-3 animate-spin dg-text-blue" />
<p className="text-[var(--dg-text-secondary)]">正在从云平台同步区域列表...</p>
<p className="text-[var(--dg-text-secondary)]"> {i18n.t('server.cloudserverimpo.s61df99e0')} </p>
</div>
) : regions.length === 0 ? (
<div className="text-center py-8 text-[var(--dg-text-muted)]">
<MapPin className="h-10 w-10 mx-auto mb-3 opacity-50" />
<p>暂无区域数据</p>
<p> {i18n.t('cloud.noRegionData')} </p>
<Button
variant="outline"
size="sm"
@@ -379,14 +380,14 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
<div className="relative flex-1">
<Search className="h-4 w-4 absolute left-3 top-1/2 -translate-y-1/2 text-[var(--dg-text-muted)]" />
<Input
placeholder="搜索区域名称或编码..."
placeholder={i18n.t('cloud.searchRegion')}
value={regionSearch}
onChange={(e) => setRegionSearch(e.target.value)}
className="pl-9 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"
/>
</div>
<Button variant="outline" size="sm" onClick={handleSelectAllRegions} className="border-[var(--dg-border)]">
{selectedRegions.length === regions.length && regions.length > 0 ? '取消全选' : '全选'}
{selectedRegions.length === regions.length && regions.length > 0 ? i18n.t('approval.editor.deselectAll') : i18n.t('system.selectAll')}
</Button>
</div>
@@ -401,8 +402,8 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
onCheckedChange={handleSelectAllRegions}
/>
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">区域编码</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">区域名称</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]"> {i18n.t('cloud.regionCode')} </th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]"> {i18n.t('cloud.regionName')} </th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">Endpoint</th>
</tr>
</thead>
@@ -442,7 +443,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
/* 导入结果 */
<div className="text-center py-12">
<CheckCircle className="h-12 w-12 mx-auto mb-3 dg-text-green" />
<p className="text-lg font-medium text-[var(--dg-text-primary)]">导入完成</p>
<p className="text-lg font-medium text-[var(--dg-text-primary)]"> {i18n.t('common.importComplete')} </p>
<div className="flex items-center justify-center gap-6 mt-4">
<Badge className="bg-emerald-500/20 dg-text-green border-0 text-base px-4 py-1">
成功导入 {importResult.imported} 台
@@ -467,7 +468,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
{cloudServers.length > 0 && (
<>
<Button variant="outline" size="sm" onClick={handleSelectAllServers} className="border-[var(--dg-border)]">
{selectedServers.length === cloudServers.length && cloudServers.length > 0 ? '取消全选' : '全选'}
{selectedServers.length === cloudServers.length && cloudServers.length > 0 ? i18n.t('approval.editor.deselectAll') : i18n.t('system.selectAll')}
</Button>
<Badge className="bg-blue-500/20 dg-text-blue border-0">
已选 {selectedServers.length} 台
@@ -482,7 +483,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
className="border-[var(--dg-border)] dg-text-blue"
>
<RefreshCw className={`h-3 w-3 mr-1 ${isDiscovering ? 'animate-spin' : ''}`} />
{isDiscovering ? '发现中...' : cloudServers.length > 0 ? '重新发现' : '发现主机'}
{isDiscovering ? i18n.t('server.cloudserverimpo.s5bc3059d') : cloudServers.length > 0 ? i18n.t('server.cloudserverimpo.s569d92b0') : i18n.t('server.cloudserverimpo.sc0b92674')}
</Button>
</div>
</div>
@@ -505,7 +506,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
<div className="flex items-start gap-2 p-3 rounded-lg bg-amber-500/10 border border-amber-500/30 dg-text-yellow text-sm">
<AlertCircle className="h-4 w-4 mt-0.5 flex-shrink-0" />
<div>
<p className="font-medium">部分区域发现失败:</p>
<p className="font-medium"> {i18n.t('server.cloudserverimpo.s6ce90825')} </p>
{discoverErrors.map((err, i) => (
<p key={i} className="text-xs mt-1 opacity-80">{err}</p>
))}
@@ -517,13 +518,13 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
{isDiscovering ? (
<div className="text-center py-12">
<RefreshCw className="h-8 w-8 mx-auto mb-3 animate-spin dg-text-blue" />
<p className="text-[var(--dg-text-secondary)]">正在发现云主机,请稍候...</p>
<p className="text-xs text-[var(--dg-text-muted)] mt-1">正在逐区域查询,可能需要数秒</p>
<p className="text-[var(--dg-text-secondary)]"> {i18n.t('server.cloudserverimpo.s2401d988')} </p>
<p className="text-xs text-[var(--dg-text-muted)] mt-1"> {i18n.t('server.cloudserverimpo.sbd41e8f6')} </p>
</div>
) : cloudServers.length === 0 && !isDiscovering ? (
<div className="text-center py-8 text-[var(--dg-text-muted)]">
<Server className="h-10 w-10 mx-auto mb-3 opacity-50" />
<p>点击"发现主机"按钮,开始扫描所选区域</p>
<p> {i18n.t('server.cloudserverimpo.s85f8bc02')} </p>
</div>
) : (
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden max-h-[300px] overflow-y-auto">
@@ -536,7 +537,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
onCheckedChange={handleSelectAllServers}
/>
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">主机名</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]"> {i18n.t('kubernetes.workload.hostname')} </th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">IP地址</th>
</tr>
</thead>
@@ -587,7 +588,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
onChange={(e) => setSelectedCredentialId(e.target.value)}
className="w-full px-3 py-1.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md text-[var(--dg-text-primary)] text-xs"
>
<option value="">不使用凭据</option>
<option value=""> {i18n.t('server.cloudserverimpo.s45bf4d4b')} </option>
{credentials.map(( c: any) => (
<option key={c.id} value={String(c.id)}>
{c.type === 'ssh_key' ? '🔑' : '🔒'} {c.name}
@@ -25,6 +25,7 @@ import {
} from 'lucide-react';
import { regionApi, serverApi, credentialApi } from '@/api';
import type { JsonData } from '@/types/common';
import i18n from '@/i18n';
interface RegionOption {
id: string;
@@ -156,7 +157,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
const text = await selectedFile.text();
const rows = parseCSV(text);
if (rows.length === 0) {
toast.error('文件无有效数据,请使用模板格式(首行为表头)');
toast.error(i18n.t('server.excelserverimpo.sdee9fcbf'));
setIsUploading(false);
return;
}
@@ -176,14 +177,14 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
disk: Number(r.disk) || 0,
tags: r.tags ?? '',
status: 'valid',
message: '有效',
message: i18n.t('common.valid'),
};
const errs: string[] = [];
if (!hostname) errs.push('主机名缺失');
if (!ip) errs.push('IP缺失');
else if (!isValidIP(ip)) errs.push('IP格式错误');
if (!hostname) errs.push(i18n.t('server.excelserverimpo.sd274584b'));
if (!ip) errs.push(i18n.t('server.excelserverimpo.s7641b315'));
else if (!isValidIP(ip)) errs.push(i18n.t('server.excelserverimpo.s968c09ff'));
const key = `${hostname}|${ip}`;
if (hostname && ip && seen.has(key)) errs.push('重复行');
if (hostname && ip && seen.has(key)) errs.push(i18n.t('server.excelserverimpo.sa884370d'));
else seen.add(key);
if (errs.length > 0) {
row.status = 'error';
@@ -194,7 +195,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
setPreviewData(preview);
setStep(step + 1);
} catch {
toast.error('文件解析失败,请确认是 CSV 格式');
toast.error(i18n.t('server.excelserverimpo.s691054e3'));
} finally {
setIsUploading(false);
}
@@ -254,7 +255,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
resetState();
onClose();
} catch (e) {
toast.error(e?.message || '导入失败,请重试');
toast.error(e?.message || i18n.t('server.excelserverimpo.se951a3c7'));
} finally {
setIsImporting(false);
}
@@ -270,9 +271,9 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
const errorCount = previewData.filter(d => d.status === 'error').length;
const steps = [
{ id: 1, title: '配置' },
{ id: 2, title: '上传' },
{ id: 3, title: '确认' },
{ id: 1, title: i18n.t('deploy.detailDialog.config') },
{ id: 2, title: i18n.t('common.upload2') },
{ id: 3, title: i18n.t('monitor.alertEvents.acknowledge') },
];
return (
@@ -280,7 +281,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
<DialogContent className="max-w-3xl bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
<DialogHeader>
<DialogTitle className="text-xl">CSV 批量导入服务器</DialogTitle>
<DialogDescription className="sr-only">通过 CSV 文件批量导入服务器资产</DialogDescription>
<DialogDescription className="sr-only"> {i18n.t('server.excelserverimpo.s58c25c4b')} </DialogDescription>
</DialogHeader>
{/* Step Indicator */}
@@ -318,8 +319,8 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
<FileSpreadsheet className="h-5 w-5 dg-text-green" />
</div>
<div>
<p className="font-medium text-[var(--dg-text-primary)]">主机导入模板</p>
<p className="text-xs text-[var(--dg-text-muted)]">下载 CSV 模板,只需填主机名、IP和端口(默认22),Agent安装后自动补全</p>
<p className="font-medium text-[var(--dg-text-primary)]"> {i18n.t('server.excelserverimpo.s95e484bd')} </p>
<p className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('server.excelserverimpo.s21609324')} </p>
</div>
</div>
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={downloadTemplate}>
@@ -332,13 +333,13 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
{/* Region Selection */}
<div>
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-2">选择区域</label>
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-2"> {i18n.t('server.cloudserverimpo.s97d03d46')} </label>
<select
value={selectedRegion}
onChange={(e) => setSelectedRegion(e.target.value)}
className="w-full px-3 py-2 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-md text-[var(--dg-text-primary)] text-sm"
>
<option value="">请选择区域</option>
<option value=""> {i18n.t('server.excelserverimpo.sf2648916')} </option>
{regions.map((region) => (
<option key={region.id} value={region.id}>
{region.name} ({region.code})
@@ -363,10 +364,10 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
onChange={(e) => setSelectedCredentialId(e.target.value)}
className="w-full px-3 py-2 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-md text-[var(--dg-text-primary)] text-sm"
>
<option value="">不使用凭据(后续手动配置)</option>
<option value=""> {i18n.t('server.excelserverimpo.s9d5df376')} </option>
{credentials.map((c: JsonData) => (
<option key={c.id} value={String(c.id)}>
{c.type === 'ssh_key' ? '🔑' : c.type === 'api_token' ? '🌐' : '🔒'} {c.name} ({c.type === 'ssh_key' ? 'SSH密钥' : c.type === 'api_token' ? 'API Token' : '密码'})
{c.type === 'ssh_key' ? '🔑' : c.type === 'api_token' ? '🌐' : '🔒'} {c.name} ({c.type === 'ssh_key' ? i18n.t('server.excelserverimpo.s87941dca') : c.type === 'api_token' ? 'API Token' : i18n.t('system.passwordLabel')})
</option>
))}
</select>
@@ -397,8 +398,8 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
className="border-2 border-dashed border-[var(--dg-border)] rounded-lg p-8 text-center hover:border-blue-500/50 transition-colors"
>
<Upload className="h-12 w-12 mx-auto mb-4 text-[var(--dg-text-muted)]" />
<p className="text-[var(--dg-text-secondary)] mb-2">点击或拖拽文件到此处上传</p>
<p className="text-xs text-[var(--dg-text-muted)] mb-4">支持 CSV 格式文件(可点击左侧"下载模板"获取)</p>
<p className="text-[var(--dg-text-secondary)] mb-2"> {i18n.t('server.excelserverimpo.sbab467bd')} </p>
<p className="text-xs text-[var(--dg-text-muted)] mb-4"> {i18n.t('server.excelserverimpo.s7bb06035')} </p>
<Input
type="file"
accept=".csv,text/csv"
@@ -408,7 +409,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
/>
<label htmlFor="excel-upload">
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" asChild>
<span>选择文件</span>
<span> {i18n.t('server.excelserverimpo.sfd7e0c99')} </span>
</Button>
</label>
</div>
@@ -438,7 +439,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
{isUploading && (
<div className="text-center py-4">
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-2" />
<p className="text-[var(--dg-text-secondary)] text-sm">正在解析文件...</p>
<p className="text-[var(--dg-text-secondary)] text-sm"> {i18n.t('server.excelserverimpo.s8c0c7b6b')} </p>
</div>
)}
</div>
@@ -448,7 +449,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
{step === 3 && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-[var(--dg-text-secondary)] text-sm">数据预览</p>
<p className="text-[var(--dg-text-secondary)] text-sm"> {i18n.t('server.excelserverimpo.s3bc470c6')} </p>
<div className="flex items-center gap-2">
<Badge className="bg-emerald-500/20 dg-text-green border-0">
<Check className="h-3 w-3 mr-1" />
@@ -473,10 +474,10 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
<table className="w-full">
<thead className="bg-[var(--dg-bg-secondary)] sticky top-0">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">行号</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">主机名</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]"> {i18n.t('monitor.logPanel.logResults.lineNo')} </th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]"> {i18n.t('kubernetes.workload.hostname')} </th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">IP地址</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">状态</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]"> {i18n.t('system.terminalStatus')} </th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--dg-border)]">
@@ -558,7 +559,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
) : (
<Check className="h-4 w-4 mr-1" />
)}
{isImporting ? '导入中...' : `确认导入 (${validCount})`}
{isImporting ? i18n.t('system.importingDot') : `确认导入 (${validCount})`}
</Button>
)}
</div>
@@ -28,6 +28,7 @@ import {
import { cn } from '@/lib/utils';
import { serverApi } from '@/api';
import type { JsonData } from '@/types/common';
import i18n from '@/i18n';
interface FileItem {
name: string;
@@ -77,7 +78,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
setBreadcrumbs(data.breadcrumbs || []);
setCurrentPath(data.path || path);
} catch (err) {
setError(err?.message || '加载目录失败');
setError(err?.message || i18n.t('server.serverfilebrows.s4be30fcb'));
setItems([]);
} finally {
setLoading(false);
@@ -96,7 +97,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
try {
await serverApi.downloadFile(serverId, filePath);
} catch (err) {
setError('下载失败: ' + ((err as JsonData)?.message || ''));
setError(i18n.t('server.serverfilebrows.sb264a9b6') + ((err as JsonData)?.message || ''));
}
};
@@ -108,7 +109,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
await serverApi.uploadFile(serverId, currentPath, file);
await fetchDir(currentPath);
} catch (err) {
setError(err?.message || '上传失败');
setError(err?.message || i18n.t('common.uploadFailed'));
} finally {
setUploading(false);
if (inputRef.current) inputRef.current.value = '';
@@ -116,12 +117,12 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
};
const handleDelete = async (filePath: string, isDir: boolean) => {
if (!serverId || !confirm(`确认删除 ${isDir ? '目录' : '文件'} "${filePath}"?`)) return;
if (!serverId || !confirm(`确认删除 ${isDir ? i18n.t('server.serverfilebrows.s767fa455') : i18n.t('k8s.pods.action.file')} "${filePath}"?`)) return;
try {
await serverApi.deleteFile(serverId, filePath);
await fetchDir(currentPath);
} catch (err) {
setError(err?.message || '删除失败');
setError(err?.message || i18n.t('kubernetes.msg.deleteFailed2'));
}
};
@@ -133,7 +134,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
await serverApi.mkdir(serverId, newPath);
await fetchDir(currentPath);
} catch (err) {
setError(err?.message || '创建失败');
setError(err?.message || i18n.t('monitor.dashboard.custom.toastCreateFailed'));
}
};
@@ -178,7 +179,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
<button
onClick={() => setFullscreen(!fullscreen)}
className="p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)]"
title={fullscreen ? '退出全屏' : '全屏'}
title={fullscreen ? i18n.t('common.exitFullscreen2') : i18n.t('monitor.fullscreen')}
>
{fullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
</button>
@@ -254,18 +255,18 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
) : sortedItems.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-[var(--dg-text-muted)]">
<Folder className="h-10 w-10 mb-2 opacity-30" />
<p className="text-sm">空目录</p>
<p className="text-sm"> {i18n.t('kubernetes.workload.emptyDir')} </p>
</div>
) : (
<table className="w-full text-sm">
<thead className="sticky top-0 bg-[var(--dg-bg-primary)] border-b border-[var(--dg-border)] z-10">
<tr className="text-left text-[var(--dg-text-muted)] text-xs">
<th className="py-2 pl-4 w-8 font-medium"></th>
<th className="py-2 font-medium">名称</th>
<th className="py-2 px-2 font-medium w-28 text-right">大小</th>
<th className="py-2 px-2 font-medium w-44 hidden sm:table-cell">修改时间</th>
<th className="py-2 px-2 font-medium w-24 hidden md:table-cell">权限</th>
<th className="py-2 pr-4 pl-2 font-medium w-24 text-center">操作</th>
<th className="py-2 font-medium"> {i18n.t('kubernetes.secret.secretName')} </th>
<th className="py-2 px-2 font-medium w-28 text-right"> {i18n.t('kubernetes.storage.pvcSize')} </th>
<th className="py-2 px-2 font-medium w-44 hidden sm:table-cell"> {i18n.t('deploy.table.modifyTime')} </th>
<th className="py-2 px-2 font-medium w-24 hidden md:table-cell"> {i18n.t('system.rolePermissions')} </th>
<th className="py-2 pr-4 pl-2 font-medium w-24 text-center"> {i18n.t('system.auditAction')} </th>
</tr>
</thead>
<tbody>
@@ -313,7 +314,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
<button
onClick={(e) => { e.stopPropagation(); handleDownload(item.path); }}
className="p-1 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)] hover:text-white"
title="下载"
title={i18n.t('automation.scriptDownload')}
>
<Download className="h-3.5 w-3.5" />
</button>
@@ -321,7 +322,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
<button
onClick={(e) => { e.stopPropagation(); handleDelete(item.path, item.is_dir); }}
className="p-1 rounded hover:bg-[var(--dg-bg-hover)] dg-text-red hover:dg-text-red"
title="删除"
title={i18n.t('kubernetes.storage.delete2')}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
@@ -1,6 +1,7 @@
// 服务器统计条 — 紧凑型替代 4 张大卡片
import { StatStrip } from '@/components/ui/stat-strip';
import type { JsonData } from '@/types/common';
import i18n from '@/i18n';
interface ServerStatsStripProps {
servers: JsonData[];
@@ -14,10 +15,10 @@ export function ServerStatsStrip({ servers }: ServerStatsStripProps) {
return (
<StatStrip
items={[
{ label: '服务器', value: servers.length, color: 'blue' },
{ label: '运行中', value: runningCount, color: 'emerald' },
{ label: '维护中', value: maintenanceCount, color: 'amber' },
{ label: '已停止', value: stoppedCount, color: 'muted' },
{ label: i18n.t('system.terminalServer'), value: servers.length, color: 'blue' },
{ label: i18n.t('agent.agentServiceRunning'), value: runningCount, color: 'emerald' },
{ label: i18n.t('dashboard.serversStatus.maintenance'), value: maintenanceCount, color: 'amber' },
{ label: i18n.t('agent.agentServiceStopped'), value: stoppedCount, color: 'muted' },
]}
/>
);
@@ -16,6 +16,7 @@ import { Edit, MoreHorizontal, Server as ServerIcon, Terminal, Trash2, Network,
import { getStatusBadge, getTags, getUsageColor } from './serverHelpers';
import { usePermission } from '@/hooks/usePermission';
import type { JsonData } from '@/types/common';
import i18n from '@/i18n';
interface ServerTableRowProps {
server?: JsonData;
@@ -109,7 +110,7 @@ export function ServerTableRow({
<span className="text-xs text-[var(--dg-text-secondary)]">{cpuUsage.toFixed(1)}%</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-[var(--dg-text-muted)] w-8">内存</span>
<span className="text-xs text-[var(--dg-text-muted)] w-8"> {i18n.t('kubernetes.pod.memory')} </span>
<div className="w-16 h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
<div
className={cn('h-full rounded-full', getUsageColor(memoryUsage))}
@@ -150,7 +151,7 @@ export function ServerTableRow({
<TableCell className="text-right">
<div className="flex items-center justify-end gap-1">
{hasPermission('server:webshell') && (
<Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-blue)]" onClick={() => onOpenTerminal(server)} title="终端登录">
<Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-blue)]" onClick={() => onOpenTerminal(server)} title={i18n.t('server.servertablerowt.sa0b6010a')}>
<Terminal className="h-4 w-4" />
</Button>
)}
@@ -162,7 +163,7 @@ export function ServerTableRow({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">操作</DropdownMenuLabel>
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]"> {i18n.t('system.auditAction')} </DropdownMenuLabel>
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
{hasPermission('server:update') && (
<DropdownMenuItem
@@ -16,6 +16,7 @@ import { useUserStore } from '@/store';
import { credentialApi, serverCredentialApi } from '@/api';
import { toast } from 'sonner';
import type { JsonData } from '@/types/common';
import i18n from '@/i18n';
interface TerminalDialogProps {
open: boolean;
@@ -64,10 +65,10 @@ export function TerminalDialog({
try {
setAssigning(true);
await serverCredentialApi.assign(serverId, Number(selectedCredId));
toast.success('凭据已关联');
toast.success(i18n.t('server.terminaldialogt.s7c9109af'));
setAuthReady(true);
} catch (e) {
toast.error(e?.message || '关联失败');
toast.error(e?.message || i18n.t('server.terminaldialogt.s6d973dbe'));
} finally {
setAssigning(false);
}
@@ -135,11 +136,11 @@ export function TerminalDialog({
ws.onclose = () => {
setConnected(false);
term.write('\r\n\x1b[31m[连接已关闭]\x1b[0m\r\n');
term.write(i18n.t('server.terminaldialogt.safab2019'));
};
ws.onerror = () => {
term.write('\r\n\x1b[31m[连接发生错误]\x1b[0m\r\n');
term.write(i18n.t('kubernetes.podexecterminal.sc8107726'));
};
return () => {
@@ -226,11 +227,11 @@ export function TerminalDialog({
<div className="p-6 flex flex-col items-center justify-center gap-4" style={{ height: '480px' }}>
<Shield className="h-12 w-12 dg-text-yellow" />
<div className="text-center">
<p className="text-[var(--dg-text-primary)] font-medium mb-1">服务器未配置认证凭据</p>
<p className="text-sm text-[var(--dg-text-muted)]">请选择一个凭据用于 SSH 连接</p>
<p className="text-[var(--dg-text-primary)] font-medium mb-1"> {i18n.t('server.terminaldialogt.sa8156f58')} </p>
<p className="text-sm text-[var(--dg-text-muted)]"> {i18n.t('server.terminaldialogt.s909c9a65')} </p>
</div>
{credentials.length === 0 ? (
<p className="text-xs text-[var(--dg-text-muted)]">暂无可用凭据,请先去「凭证配置」添加</p>
<p className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('server.terminaldialogt.s817bf0a2')} </p>
) : (
<select
value={selectedCredId}
@@ -250,7 +251,7 @@ export function TerminalDialog({
disabled={!selectedCredId || assigning}
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
>
{assigning ? '关联中...' : '确认连接'}
{assigning ? i18n.t('server.terminaldialogt.saf3d5dd8') : i18n.t('server.terminaldialogt.s9b448012')}
</Button>
</div>
)}
+14 -13
View File
@@ -32,6 +32,7 @@ import 'xterm/css/xterm.css';
import { serverApi } from '@/api';
import { useUserStore } from '@/store';
import type { JsonData } from '@/types/common';
import i18n from '@/i18n';
interface TerminalSession {
id: string;
@@ -66,7 +67,7 @@ export function WebShell() {
const res: JsonData = await serverApi.getServers();
setServers(res?.list || []);
} catch (e) {
console.error('加载服务器列表失败:', e);
console.error(i18n.t('server.webshelltsx.s8603ff5b'), e);
setServers([]);
}
};
@@ -147,14 +148,14 @@ export function WebShell() {
};
ws.onclose = () => {
term.write('\r\n\x1b[31m[连接已关闭]\x1b[0m\r\n');
term.write(i18n.t('server.terminaldialogt.safab2019'));
setSessions((prev) =>
prev.map((s) => (s.id === sessionId ? { ...s, connected: false } : s))
);
};
ws.onerror = () => {
term.write('\r\n\x1b[31m[连接发生错误]\x1b[0m\r\n');
term.write(i18n.t('kubernetes.podexecterminal.sc8107726'));
};
termsRef.current.set(sessionId, { term, ws, fit, opened: false });
@@ -260,7 +261,7 @@ export function WebShell() {
observer.observe(el);
(entry as JsonData)._observer = observer;
} catch (e) {
console.error('打开终端失败:', e);
console.error(i18n.t('server.webshelltsx.s7d4b03bb'), e);
}
}
} else {
@@ -282,7 +283,7 @@ export function WebShell() {
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">WebShell</h1>
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">远程服务器终端管理</p>
<p className="text-sm text-[var(--dg-text-secondary)] mt-1"> {i18n.t('server.webshelltsx.s1df74087')} </p>
</div>
<div className="flex items-center gap-2">
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
@@ -294,21 +295,21 @@ export function WebShell() {
</DialogTrigger>
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)]">新建终端连接</DialogTitle>
<DialogTitle className="text-[var(--dg-text-primary)]"> {i18n.t('server.webshelltsx.s7026b318')} </DialogTitle>
<DialogDescription className="text-[var(--dg-text-secondary)]">
选择要连接的服务器
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium text-[var(--dg-text-secondary)]">选择服务器</label>
<label className="text-sm font-medium text-[var(--dg-text-secondary)]"> {i18n.t('server.webshelltsx.s030235d1')} </label>
<Select value={selectedServer} onValueChange={setSelectedServer}>
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
<SelectValue placeholder="选择服务器" />
<SelectValue placeholder={i18n.t('server.webshelltsx.s030235d1')} />
</SelectTrigger>
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
{runningServers.length === 0 ? (
<SelectItem value="__none" disabled>暂无运行中的服务器</SelectItem>
<SelectItem value="__none" disabled> {i18n.t('server.webshelltsx.se5460be8')} </SelectItem>
) : (
runningServers.map((server) => (
<SelectItem key={server.id} value={server.id}>
@@ -378,7 +379,7 @@ export function WebShell() {
))}
</div>
) : (
<span className="text-sm text-[var(--dg-text-secondary)]">暂无连接</span>
<span className="text-sm text-[var(--dg-text-secondary)]"> {i18n.t('server.webshelltsx.sd5d6cbbb')} </span>
)}
</div>
<div className="flex items-center gap-1">
@@ -419,8 +420,8 @@ export function WebShell() {
style={{ height: terminalHeight }}
>
<Terminal className="h-16 w-16 mb-4 opacity-50" />
<p className="text-lg font-medium">暂无活动连接</p>
<p className="text-sm mt-1">点击“新建连接”开始远程会话</p>
<p className="text-lg font-medium"> {i18n.t('server.webshelltsx.sefd92a06')} </p>
<p className="text-sm mt-1"> {i18n.t('server.webshelltsx.se2e43347')} </p>
</div>
)}
</CardContent>
@@ -430,7 +431,7 @@ export function WebShell() {
{activeSessionData && (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardHeader className="pb-2">
<CardTitle className="text-sm text-[var(--dg-text-secondary)]">快捷命令</CardTitle>
<CardTitle className="text-sm text-[var(--dg-text-secondary)]"> {i18n.t('server.webshelltsx.sde027789')} </CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">