feat(i18n): 剩余模块全量国际化 — 51% 覆盖率 (231 文件接入 t())

批量转换 163 个文件,3204 处中文替换为 t() 调用:
- Monitor: 日志中心/告警中心/数据源/大屏/站点拨测/SNMP/中间件 全部转换
- Kubernetes: ResourceVisualEditor/NamespaceResourcePage/拓扑/事件 全部转换
- Deploy: 流水线/发布/凭证/RBAC 全部转换
- Server/Cloud/Agent/Task/Layout/UI: 全部转换
- 翻译词典扩展至 6474 行 (22 namespaces, 5000+ keys)
- tsc 编译零错误
This commit is contained in:
attacker committed 2026-07-03 01:51:57 +08:00
1 parent 78c27037e3
commit ecec73989b
166 files changed
+6925 -2800

No files matched your search

@@ -27,6 +27,8 @@ import {
AlertCircle,
} from 'lucide-react';
import { cloudApi } from '@/api';
import { useTranslation } from 'react-i18next';
import i18n from '@/i18n';
interface CloudServerImportProps {
open: boolean;
@@ -35,13 +37,14 @@ 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' },
];
export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
const { t } = useTranslation();
const [cloudAccounts, setCloudAccounts] = useState<any[]>([]);
const [step, setStep] = useState(1);
const [selectedProvider, setSelectedProvider] = useState<string>('');
@@ -67,7 +70,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(t('server.cloudserverimport.sddeee7'), e);
}
};
@@ -102,7 +105,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
const list = await loadRegions(accountId);
setRegions(list);
} catch (e) {
console.error('同步区域失败:', e);
console.error(t('server.cloudserverimport.s1d6599'), e);
throw e;
} finally {
setIsSyncingRegions(false);
@@ -137,8 +140,8 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
setCloudServers(res?.list || []);
setDiscoverErrors(res?.errors || []);
} catch (e) {
console.error('发现主机失败:', e);
setDiscoverErrors([e?.message || '发现主机失败']);
console.error(t('server.cloudserverimport.s5e749a'), e);
setDiscoverErrors([e?.message || t('server.cloudserverimport.s7d43dc')]);
} finally {
setIsDiscovering(false);
}
@@ -156,7 +159,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
});
setImportResult((res as any) || { imported: 0, skipped: 0 });
} catch (e) {
console.error('导入失败:', e);
console.error(t('server.cloudserverimport.s1b1225'), e);
} finally {
setIsImporting(false);
}
@@ -223,18 +226,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: t('server.cloudserverimport.s7ef31a'), icon: Cloud },
{ id: 2, title: t('server.cloudserverimport.s02badc'), icon: Key },
{ id: 3, title: t('server.cloudserverimport.s97d03d'), icon: MapPin },
{ id: 4, title: t('server.cloudserverimport.s81eea5'), 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">{t('server.cloudserverimport.sa558f8')}</DialogTitle>
<DialogDescription className="sr-only">{t('server.cloudserverimport.s869681')}</DialogDescription>
</DialogHeader>
{/* Step Indicator */}
@@ -262,7 +265,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">{t('server.cloudserverimport.s7c3664')}</p>
<div className="grid grid-cols-3 gap-4">
{cloudProviders.map((provider) => (
<Card
@@ -289,7 +292,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">{t('server.cloudserverimport.se1a787')}</p>
{filteredAccounts.length > 0 ? (
<div className="space-y-2 max-h-[300px] overflow-y-auto">
{filteredAccounts.map((account) => (
@@ -325,7 +328,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>{t('server.cloudserverimport.saa5f0d')}</p>
</div>
)}
</div>
@@ -349,19 +352,19 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
disabled={isSyncingRegions}
>
<RefreshCw className={`h-3 w-3 mr-1 ${isSyncingRegions ? 'animate-spin' : ''}`} />
{isSyncingRegions ? '同步中...' : '刷新区域'}
{isSyncingRegions ? t('server.cloudserverimport.sf787f4') : t('server.cloudserverimport.saa44e4')}
</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)]">{t('server.cloudserverimport.s61df99')}</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>{t('cloud.noRegionData')}</p>
<Button
variant="outline"
size="sm"
@@ -379,14 +382,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=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 ? t('common.deselectAll') : t('common.selectAll')}
</Button>
</div>
@@ -401,8 +404,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)]">{t('cloud.regionCode')}</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{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 +445,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)]">{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 +470,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 ? t('common.deselectAll') : t('common.selectAll')}
</Button>
<Badge className="bg-blue-500/20 dg-text-blue border-0">
已选 {selectedServers.length} 台
@@ -482,7 +485,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 ? t('server.cloudserverimport.s5bc305') : cloudServers.length > 0 ? t('server.cloudserverimport.s569d92') : t('server.cloudserverimport.sc0b926')}
</Button>
</div>
</div>
@@ -505,7 +508,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">{t('server.cloudserverimport.s6ce908')}</p>
{discoverErrors.map((err, i) => (
<p key={i} className="text-xs mt-1 opacity-80">{err}</p>
))}
@@ -517,13 +520,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)]">{t('server.cloudserverimport.s2401d9')}</p>
<p className="text-xs text-[var(--dg-text-muted)] mt-1">{t('server.cloudserverimport.sbd41e8')}</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>{t('server.cloudserverimport.s85f8bc')}</p>
</div>
) : (
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden max-h-[300px] overflow-y-auto">
@@ -536,8 +539,8 @@ 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)]">IP地址</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('server.hostname')}</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('server.csv.ip')}</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--dg-border)]">
@@ -567,7 +570,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
<div>
<label className="block text-xs font-medium text-[var(--dg-text-secondary)] mb-1">
SSH 端口
<span className="text-[var(--dg-text-muted)] ml-1">(导入时统一设置)</span>
<span className="text-[var(--dg-text-muted)] ml-1">{t('server.cloudserverimport.sd922ed')}</span>
</label>
<Input
type="number"
@@ -580,14 +583,14 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
<div>
<label className="block text-xs font-medium text-[var(--dg-text-secondary)] mb-1">
关联凭证
<span className="text-[var(--dg-text-muted)] ml-1">(可选,用于SSH远程管理)</span>
<span className="text-[var(--dg-text-muted)] ml-1">{t('server.cloudserverimport.s6cfbde')}</span>
</label>
<select
value={selectedCredentialId}
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="">{t('server.cloudserverimport.s45bf4d')}</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 { useTranslation } from 'react-i18next';
interface RegionOption {
id: string;
@@ -108,6 +109,7 @@ function downloadTemplate() {
}
export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
const { t } = useTranslation();
const [step, setStep] = useState(1);
const [selectedRegion, setSelectedRegion] = useState<string>('');
const [, _setConnectionMethod] = useState<string>('password');
@@ -156,7 +158,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
const text = await selectedFile.text();
const rows = parseCSV(text);
if (rows.length === 0) {
toast.error('文件无有效数据,请使用模板格式(首行为表头)');
toast.error(t('server.excelserverimport.sdee9fc'));
setIsUploading(false);
return;
}
@@ -176,14 +178,14 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
disk: Number(r.disk) || 0,
tags: r.tags ?? '',
status: 'valid',
message: '有效',
message: 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(t('server.excelserverimport.sd27458'));
if (!ip) errs.push(t('server.excelserverimport.s7641b3'));
else if (!isValidIP(ip)) errs.push(t('server.excelserverimport.s968c09'));
const key = `${hostname}|${ip}`;
if (hostname && ip && seen.has(key)) errs.push('重复行');
if (hostname && ip && seen.has(key)) errs.push(t('server.excelserverimport.sa88437'));
else seen.add(key);
if (errs.length > 0) {
row.status = 'error';
@@ -194,7 +196,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
setPreviewData(preview);
setStep(step + 1);
} catch {
toast.error('文件解析失败,请确认是 CSV 格式');
toast.error(t('server.excelserverimport.s691054'));
} finally {
setIsUploading(false);
}
@@ -250,11 +252,11 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
const success = r.success ?? 0;
const skipped = r.skipped ?? 0;
const failed = r.failed ?? 0;
toast.success(`导入完成:成功 ${success},跳过 ${skipped},失败 ${failed}`);
toast.success(t('server.excelserverimport.s27bdf5', { success: success, skipped: skipped, failed: failed }));
resetState();
onClose();
} catch (e) {
toast.error(e?.message || '导入失败,请重试');
toast.error(e?.message || t('server.excelserverimport.se951a3'));
} finally {
setIsImporting(false);
}
@@ -270,17 +272,17 @@ 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: t('common.config') },
{ id: 2, title: t('common.upload') },
{ id: 3, title: t('common.confirm') },
];
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">CSV 批量导入服务器</DialogTitle>
<DialogDescription className="sr-only">通过 CSV 文件批量导入服务器资产</DialogDescription>
<DialogTitle className="text-xl">{t('server.excelserverimport.s754b4a')}</DialogTitle>
<DialogDescription className="sr-only">{t('server.excelserverimport.s58c25c')}</DialogDescription>
</DialogHeader>
{/* Step Indicator */}
@@ -318,8 +320,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)]">{t('server.excelserverimport.s95e484')}</p>
<p className="text-xs text-[var(--dg-text-muted)]">{t('server.excelserverimport.s216093')}</p>
</div>
</div>
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={downloadTemplate}>
@@ -332,13 +334,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">{t('server.excelserverimport.s97d03d')}</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="">{t('server.excelserverimport.sf26489')}</option>
{regions.map((region) => (
<option key={region.id} value={region.id}>
{region.name} ({region.code})
@@ -351,7 +353,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
<div>
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-2">
选择凭据
<span className="text-xs text-[var(--dg-text-muted)] ml-1">(可选,用于SSH连接)</span>
<span className="text-xs text-[var(--dg-text-muted)] ml-1">{t('server.excelserverimport.sdf1c0b')}</span>
</label>
{credentials.length === 0 ? (
<p className="text-xs text-[var(--dg-text-muted)] py-2">
@@ -363,10 +365,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="">{t('server.excelserverimport.s9d5df3')}</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' ? t('server.excelserverimport.s87941d') : c.type === 'api_token' ? 'API Token' : t('login.password')})
</option>
))}
</select>
@@ -377,7 +379,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
<div>
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-2">
SSH 端口
<span className="text-xs text-[var(--dg-text-muted)] ml-1">(导入时统一设置,可在服务器管理页单独修改)</span>
<span className="text-xs text-[var(--dg-text-muted)] ml-1">{t('server.excelserverimport.sb1d0e7')}</span>
</label>
<Input
type="number"
@@ -397,8 +399,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">{t('server.excelserverimport.sbab467')}</p>
<p className="text-xs text-[var(--dg-text-muted)] mb-4">{t('server.excelserverimport.s7bb060')}</p>
<Input
type="file"
accept=".csv,text/csv"
@@ -408,7 +410,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>{t('server.excelserverimport.sfd7e0c')}</span>
</Button>
</label>
</div>
@@ -438,7 +440,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">{t('server.excelserverimport.s8c0c7b')}</p>
</div>
)}
</div>
@@ -448,7 +450,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">{t('server.excelserverimport.s3bc470')}</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 +475,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)]">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)]">{t('server.excelserverimport.s89c8c5')}</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('server.hostname')}</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('server.csv.ip')}</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('common.status')}</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--dg-border)]">
@@ -558,7 +560,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
) : (
<Check className="h-4 w-4 mr-1" />
)}
{isImporting ? '导入中...' : `确认导入 (${validCount})`}
{isImporting ? i18n.t('server.excelserverimport.s763476') : i18n.t('server.excelserverimport.s2fbd70', { validCount: validCount })}
</Button>
)}
</div>
@@ -28,6 +28,7 @@ import {
import { cn } from '@/lib/utils';
import { serverApi } from '@/api';
import type { JsonData } from '@/types/common';
import { useTranslation } from 'react-i18next';
interface FileItem {
name: string;
@@ -57,6 +58,7 @@ interface Props {
}
export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }: Props) {
const { t } = useTranslation();
const [items, setItems] = useState<FileItem[]>([]);
const [breadcrumbs, setBreadcrumbs] = useState<Breadcrumb[]>([]);
const [currentPath, setCurrentPath] = useState('/');
@@ -77,7 +79,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
setBreadcrumbs(data.breadcrumbs || []);
setCurrentPath(data.path || path);
} catch (err) {
setError(err?.message || '加载目录失败');
setError(err?.message || t('server.serverfilebrowser.s4be30f'));
setItems([]);
} finally {
setLoading(false);
@@ -96,7 +98,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
try {
await serverApi.downloadFile(serverId, filePath);
} catch (err) {
setError('下载失败: ' + ((err as JsonData)?.message || ''));
setError(t('server.serverfilebrowser.sb264a9') + ((err as JsonData)?.message || ''));
}
};
@@ -108,7 +110,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 || t('common.uploadFailed'));
} finally {
setUploading(false);
if (inputRef.current) inputRef.current.value = '';
@@ -116,24 +118,24 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
};
const handleDelete = async (filePath: string, isDir: boolean) => {
if (!serverId || !confirm(`确认删除 ${isDir ? '目录' : '文件'} "${filePath}"?`)) return;
if (!serverId || !confirm(t('server.serverfilebrowser.s5a1092', { isDir ? '目录' : '文件': isDir ? '目录' : '文件', filePath: filePath }))) return;
try {
await serverApi.deleteFile(serverId, filePath);
await fetchDir(currentPath);
} catch (err) {
setError(err?.message || '删除失败');
setError(err?.message || t('common.deleteFailed'));
}
};
const handleMkdir = async () => {
const name = prompt('输入目录名:');
const name = prompt(t('server.serverfilebrowser.sc8dfcc'));
if (!name || !serverId) return;
const newPath = currentPath === '/' ? `/${name}` : `${currentPath}/${name}`;
try {
await serverApi.mkdir(serverId, newPath);
await fetchDir(currentPath);
} catch (err) {
setError(err?.message || '创建失败');
setError(err?.message || t('monitor.quickSetupFailed'));
}
};
@@ -163,7 +165,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
}
>
<DialogTitle className="sr-only">文件浏览 — {serverName}</DialogTitle>
<DialogDescription className="sr-only">SFTP 文件管理 · 上传 · 下载 · 删除</DialogDescription>
<DialogDescription className="sr-only">{t('server.serverfilebrowser.sc9c640')}</DialogDescription>
{/* Header */}
<div className="flex items-center justify-between px-4 py-2.5 border-b border-[var(--dg-border)] shrink-0">
@@ -172,13 +174,13 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
<span className="text-[var(--dg-text-primary)] font-medium text-sm truncate">
{serverName}
</span>
<span className="text-[var(--dg-text-muted)] text-xs">· SFTP 文件管理</span>
<span className="text-[var(--dg-text-muted)] text-xs">{t('server.serverfilebrowser.sd720ed')}</span>
</div>
<div className="flex items-center gap-1">
<button
onClick={() => setFullscreen(!fullscreen)}
className="p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)]"
title={fullscreen ? '退出全屏' : '全屏'}
title={fullscreen ? t('common.exitFullscreen') : t('common.fullscreen')}
>
{fullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
</button>
@@ -254,18 +256,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">{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">{t('common.name')}</th>
<th className="py-2 px-2 font-medium w-28 text-right">{t('deploy.table.size')}</th>
<th className="py-2 px-2 font-medium w-44 hidden sm:table-cell">{t('deploy.table.modifyTime')}</th>
<th className="py-2 px-2 font-medium w-24 hidden md:table-cell">{t('system.rolePermissions')}</th>
<th className="py-2 pr-4 pl-2 font-medium w-24 text-center">{t('common.actions')}</th>
</tr>
</thead>
<tbody>
@@ -313,7 +315,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=t('common.download')
>
<Download className="h-3.5 w-3.5" />
</button>
@@ -321,7 +323,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=t('common.delete')
>
<Trash2 className="h-3.5 w-3.5" />
</button>
+25 -23
View File
@@ -28,6 +28,7 @@ import { Server, ChevronDown, Loader2, Save, Key, Shield } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { credentialApi } from '@/api';
import type { JsonData } from '@/types/common';
import { useTranslation } from 'react-i18next';
// 表单数据结构
export interface ServerFormData {
@@ -98,6 +99,7 @@ export function ServerForm({
onSave,
formSaving,
}: ServerFormProps) {
const { t } = useTranslation();
const navigate = useNavigate();
const [credentials, setCredentials] = useState<Credential[]>([]);
@@ -145,10 +147,10 @@ export function ServerForm({
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
<Server className="h-5 w-5 text-[var(--dg-blue)]" />
{editingId ? '编辑服务器' : '添加服务器'}
{editingId ? t('server.serverform.s44fc7f') : t('server.addServer')}
</DialogTitle>
<DialogDescription className="text-[var(--dg-text-muted)]">
{editingId ? '修改服务器资产信息' : '录入新的服务器资产到 CMDB'}
{editingId ? t('server.serverform.s2c2f7a') : t('server.serverform.s2bbed0')}
</DialogDescription>
</DialogHeader>
@@ -182,16 +184,16 @@ export function ServerForm({
{/* SSH 连接配置 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">公网IP</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('server.publicIp')}</Label>
<Input
placeholder="如 203.0.113.10"
placeholder=t('server.serverform.s2709d4')
value={formData.public_ip}
onChange={(e) => setFormData({ ...formData, public_ip: e.target.value })}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
/>
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">SSH端口</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('server.serverform.s354e43')}</Label>
<Input
type="number"
placeholder="22"
@@ -213,14 +215,14 @@ export function ServerForm({
onValueChange={handleCredentialSelect}
>
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
<SelectValue placeholder="选择 SSH 凭证..." />
<SelectValue placeholder=t('server.serverform.s53a3e2') />
</SelectTrigger>
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
{sshCredentials.map((cred) => (
<SelectItem key={cred.id} value={String(cred.id)}>
<span className="flex items-center gap-2">
<Shield className="h-3 w-3 dg-text-blue" />
{cred.name} ({cred.username}@{ (cred.auth_type || '').toLowerCase() === 'key' ? '密钥' : '密码'})
{cred.name} ({cred.username}@{ (cred.auth_type || '').toLowerCase() === 'key' ? t('k8s.resource.secrets') : t('login.password')})
</span>
</SelectItem>
))}
@@ -243,7 +245,7 @@ export function ServerForm({
)}
{formData.credential_id && (
<p className="text-[10px] dg-text-green/70">已关联凭证,登录信息从凭证自动获取(登录用户和认证方式由凭证决定)</p>
<p className="text-[10px] dg-text-green/70">{t('server.serverform.sc30410')}</p>
)}
</div>
@@ -265,16 +267,16 @@ export function ServerForm({
<CollapsibleContent className="space-y-4 pt-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">操作系统</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('server.csv.os')}</Label>
<Input
placeholder="如 CentOS 7.9"
placeholder=t('server.serverform.se2ab09')
value={formData.os}
onChange={(e) => setFormData({ ...formData, os: e.target.value })}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
/>
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">运行状态</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('server.serverform.se4b51d')}</Label>
<Select
value={formData.status}
onValueChange={(v) => setFormData({ ...formData, status: v })}
@@ -283,17 +285,17 @@ export function ServerForm({
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
<SelectItem value="running">运行中</SelectItem>
<SelectItem value="stopped">已停止</SelectItem>
<SelectItem value="maintenance">维护中</SelectItem>
<SelectItem value="offline">离线</SelectItem>
<SelectItem value="running">{t('common.running')}</SelectItem>
<SelectItem value="stopped">{t('common.stopped')}</SelectItem>
<SelectItem value="maintenance">{t('common.maintenance')}</SelectItem>
<SelectItem value="offline">{t('common.offline')}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">CPU核数</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('server.serverform.sbdb608')}</Label>
<Input
type="number"
placeholder="4"
@@ -303,7 +305,7 @@ export function ServerForm({
/>
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">内存 (GB)</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('server.serverform.s875cc1')}</Label>
<Input
type="number"
placeholder="8"
@@ -313,7 +315,7 @@ export function ServerForm({
/>
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">磁盘 (GB)</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('server.serverform.s5fbcca')}</Label>
<Input
type="number"
placeholder="100"
@@ -324,18 +326,18 @@ export function ServerForm({
</div>
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">标签</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('common.tag')}</Label>
<Input
placeholder='JSON 格式,如 {"project":"web","team":"ops"}'
placeholder=t('server.serverform.sc53b11')
value={formData.tags}
onChange={(e) => setFormData({ ...formData, tags: e.target.value })}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] font-mono text-sm"
/>
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">描述</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('common.description')}</Label>
<Textarea
placeholder="补充说明..."
placeholder=t('server.serverform.s5f4a0f')
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] min-h-[60px]"
@@ -363,7 +365,7 @@ export function ServerForm({
) : (
<Save className="h-4 w-4 mr-2" />
)}
{editingId ? '保存修改' : '创建'}
{editingId ? i18n.t('cloud.saveChanges') : i18n.t('common.create')}
</Button>
</DialogFooter>
</DialogContent>
@@ -1,12 +1,14 @@
// 服务器统计条 — 紧凑型替代 4 张大卡片
import { StatStrip } from '@/components/ui/stat-strip';
import type { JsonData } from '@/types/common';
import { useTranslation } from 'react-i18next';
interface ServerStatsStripProps {
servers: JsonData[];
}
export function ServerStatsStrip({ servers }: ServerStatsStripProps) {
const { t } = useTranslation();
const runningCount = servers.filter((s) => s.status === 'running').length;
const maintenanceCount = servers.filter((s) => s.status === 'maintenance').length;
const stoppedCount = servers.filter((s) => s.status === 'stopped').length;
@@ -14,10 +16,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: t('dashboard.servers'), value: servers.length, color: 'blue' },
{ label: t('common.running'), value: runningCount, color: 'emerald' },
{ label: t('common.maintenance'), value: maintenanceCount, color: 'amber' },
{ label: t('common.stopped'), 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 { useTranslation } from 'react-i18next';
interface ServerTableRowProps {
server?: JsonData;
@@ -36,6 +37,7 @@ export function ServerTableRow({
onOpenFiles,
onDelete,
}: ServerTableRowProps) {
const { t } = useTranslation();
const { hasPermission } = usePermission();
const cpuUsage = Number(server.cpu_usage || 0);
const memoryUsage = Number(server.memory_usage || 0);
@@ -109,7 +111,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">{t('k8s.pods.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 +152,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=t('server.servertablerow.sa0b601')>
<Terminal className="h-4 w-4" />
</Button>
)}
@@ -162,7 +164,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)]">{t('common.actions')}</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 { useTranslation } from 'react-i18next';
interface TerminalDialogProps {
open: boolean;
@@ -39,6 +40,7 @@ export function TerminalDialog({
serverIp,
hasAuth = true,
}: TerminalDialogProps) {
const { t } = useTranslation();
const containerRef = useRef<HTMLDivElement>(null);
const termRef = useRef<XTerm | null>(null);
const wsRef = useRef<WebSocket | null>(null);
@@ -64,10 +66,10 @@ export function TerminalDialog({
try {
setAssigning(true);
await serverCredentialApi.assign(serverId, Number(selectedCredId));
toast.success('凭据已关联');
toast.success(t('server.terminaldialog.s7c9109'));
setAuthReady(true);
} catch (e) {
toast.error(e?.message || '关联失败');
toast.error(e?.message || t('server.terminaldialog.s6d973d'));
} finally {
setAssigning(false);
}
@@ -135,11 +137,11 @@ export function TerminalDialog({
ws.onclose = () => {
setConnected(false);
term.write('\r\n\x1b[31m[连接已关闭]\x1b[0m\r\n');
term.write(t('server.terminaldialog.safab20'));
};
ws.onerror = () => {
term.write('\r\n\x1b[31m[连接发生错误]\x1b[0m\r\n');
term.write(t('server.terminaldialog.sc81077'));
};
return () => {
@@ -198,7 +200,7 @@ export function TerminalDialog({
style={fullscreen ? { maxWidth: '100vw', width: '100vw', height: '100vh' } : { maxWidth: '960px', width: '92vw' }}
>
<DialogTitle className="sr-only">终端 - {serverName}</DialogTitle>
<DialogDescription className="sr-only">SSH 终端连接</DialogDescription>
<DialogDescription className="sr-only">{t('server.terminaldialog.s46b4eb')}</DialogDescription>
{/* 终端头部 */}
<div className="flex items-center justify-between px-4 py-2 border-b border-[var(--dg-border)]">
<div className="flex items-center gap-2 text-sm">
@@ -226,18 +228,18 @@ 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">{t('server.terminaldialog.sa8156f')}</p>
<p className="text-sm text-[var(--dg-text-muted)]">{t('server.terminaldialog.s909c9a')}</p>
</div>
{credentials.length === 0 ? (
<p className="text-xs text-[var(--dg-text-muted)]">暂无可用凭据,请先去「凭证配置」添加</p>
<p className="text-xs text-[var(--dg-text-muted)]">{t('server.terminaldialog.s817bf0')}</p>
) : (
<select
value={selectedCredId}
onChange={(e) => setSelectedCredId(e.target.value)}
className="w-64 px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md text-[var(--dg-text-primary)] text-sm"
>
<option value="">-- 选择凭据 --</option>
<option value="">{t('server.terminaldialog.s3fb850')}</option>
{credentials.map((c: JsonData) => (
<option key={c.id} value={String(c.id)}>
{c.type === 'ssh_key' ? '🔑' : '🔒'} {c.name}
@@ -250,7 +252,7 @@ export function TerminalDialog({
disabled={!selectedCredId || assigning}
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
>
{assigning ? '关联中...' : '确认连接'}
{assigning ? t('server.terminaldialog.saf3d5d') : t('server.terminaldialog.s9b4480')}
</Button>
</div>
)}
+16 -14
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 { useTranslation } from 'react-i18next';
interface TerminalSession {
id: string;
@@ -49,6 +50,7 @@ interface TermEntry {
}
export function WebShell() {
const { t } = useTranslation();
const [servers, setServers] = useState<JsonData[]>([]);
const [sessions, setSessions] = useState<TerminalSession[]>([]);
const [activeSession, setActiveSession] = useState<string | null>(null);
@@ -66,7 +68,7 @@ export function WebShell() {
const res: JsonData = await serverApi.getServers();
setServers(res?.list || []);
} catch (e) {
console.error('加载服务器列表失败:', e);
console.error(t('server.webshell.s8603ff'), e);
setServers([]);
}
};
@@ -143,18 +145,18 @@ export function WebShell() {
};
ws.onopen = () => {
term.write(`\x1b[32m正在连接 ${server.hostname} (${server.ip})...\x1b[0m\r\n`);
term.write(t('server.webshell.se39eef', { server.hostname: server.hostname, server.ip: server.ip }));
};
ws.onclose = () => {
term.write('\r\n\x1b[31m[连接已关闭]\x1b[0m\r\n');
term.write(t('server.webshell.safab20'));
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(t('server.webshell.sc81077'));
};
termsRef.current.set(sessionId, { term, ws, fit, opened: false });
@@ -260,7 +262,7 @@ export function WebShell() {
observer.observe(el);
(entry as JsonData)._observer = observer;
} catch (e) {
console.error('打开终端失败:', e);
console.error(t('server.webshell.s7d4b03'), e);
}
}
} else {
@@ -282,7 +284,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">{t('server.webshell.s1df740')}</p>
</div>
<div className="flex items-center gap-2">
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
@@ -294,21 +296,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)]">{t('server.webshell.s7026b3')}</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)]">{t('server.webshell.s030235')}</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=t('server.webshell.s030235') />
</SelectTrigger>
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
{runningServers.length === 0 ? (
<SelectItem value="__none" disabled>暂无运行中的服务器</SelectItem>
<SelectItem value="__none" disabled>{t('server.webshell.se5460b')}</SelectItem>
) : (
runningServers.map((server) => (
<SelectItem key={server.id} value={server.id}>
@@ -378,7 +380,7 @@ export function WebShell() {
))}
</div>
) : (
<span className="text-sm text-[var(--dg-text-secondary)]">暂无连接</span>
<span className="text-sm text-[var(--dg-text-secondary)]">{t('server.webshell.sd5d6cb')}</span>
)}
</div>
<div className="flex items-center gap-1">
@@ -419,8 +421,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">{t('server.webshell.sefd92a')}</p>
<p className="text-sm mt-1">{t('server.webshell.se2e433')}</p>
</div>
)}
</CardContent>
@@ -430,7 +432,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)]">{t('server.webshell.sde0277')}</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">