fix(i18n): 回退到稳定的 32% 覆盖率状态 (tsc + Vite 构建通过)

批量转换脚本引入的语法错误已回退,保留稳定的 i18n 转换:
- 翻译词典: 22 namespaces, 3600+ keys
- 已接入: 145+ 文件 (Dashboard/Login/K8s 主页面/Monitor/System/Deploy)
- 语言切换器在 Header 中可用 (🇨🇳/🇺🇸)
- tsc 零错误, Vite 构建成功
This commit is contained in:
attacker committed 2026-07-03 02:02:29 +08:00
1 parent ecec73989b
commit 72bfb0e12d
191 files changed
+5057 -7592

No files matched your search

@@ -27,8 +27,6 @@ import {
AlertCircle,
} from 'lucide-react';
import { cloudApi } from '@/api';
import { useTranslation } from 'react-i18next';
import i18n from '@/i18n';
interface CloudServerImportProps {
open: boolean;
@@ -37,14 +35,13 @@ interface CloudServerImportProps {
const cloudProviders = [
{ id: 'aws', name: 'AWS', color: 'bg-orange-500', icon: 'AWS' },
{ 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: '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: '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>('');
@@ -70,7 +67,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
const res: Record<string, any> = await cloudApi.getAccounts();
setCloudAccounts(res?.list || []);
} catch (e) {
console.error(t('server.cloudserverimport.sddeee7'), e);
console.error('加载云账号失败:', e);
}
};
@@ -105,7 +102,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
const list = await loadRegions(accountId);
setRegions(list);
} catch (e) {
console.error(t('server.cloudserverimport.s1d6599'), e);
console.error('同步区域失败:', e);
throw e;
} finally {
setIsSyncingRegions(false);
@@ -140,8 +137,8 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
setCloudServers(res?.list || []);
setDiscoverErrors(res?.errors || []);
} catch (e) {
console.error(t('server.cloudserverimport.s5e749a'), e);
setDiscoverErrors([e?.message || t('server.cloudserverimport.s7d43dc')]);
console.error('发现主机失败:', e);
setDiscoverErrors([e?.message || '发现主机失败']);
} finally {
setIsDiscovering(false);
}
@@ -159,7 +156,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
});
setImportResult((res as any) || { imported: 0, skipped: 0 });
} catch (e) {
console.error(t('server.cloudserverimport.s1b1225'), e);
console.error('导入失败:', e);
} finally {
setIsImporting(false);
}
@@ -226,18 +223,18 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
);
const steps = [
{ 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 },
{ id: 1, title: '公有云', icon: Cloud },
{ id: 2, title: '访问凭据', icon: Key },
{ id: 3, title: '选择区域', icon: MapPin },
{ id: 4, title: '导入确认', 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">{t('server.cloudserverimport.sa558f8')}</DialogTitle>
<DialogDescription className="sr-only">{t('server.cloudserverimport.s869681')}</DialogDescription>
<DialogTitle className="text-xl">导入云主机</DialogTitle>
<DialogDescription className="sr-only">从云平台批量导入主机资产</DialogDescription>
</DialogHeader>
{/* Step Indicator */}
@@ -265,7 +262,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">{t('server.cloudserverimport.s7c3664')}</p>
<p className="text-[var(--dg-text-secondary)] text-sm">请选择要导入云主机的云平台</p>
<div className="grid grid-cols-3 gap-4">
{cloudProviders.map((provider) => (
<Card
@@ -292,7 +289,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">{t('server.cloudserverimport.se1a787')}</p>
<p className="text-[var(--dg-text-secondary)] text-sm">请选择访问凭据(AccessKey)</p>
{filteredAccounts.length > 0 ? (
<div className="space-y-2 max-h-[300px] overflow-y-auto">
{filteredAccounts.map((account) => (
@@ -328,7 +325,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>{t('server.cloudserverimport.saa5f0d')}</p>
<p>该云平台暂无有效的访问凭据</p>
</div>
)}
</div>
@@ -352,19 +349,19 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
disabled={isSyncingRegions}
>
<RefreshCw className={`h-3 w-3 mr-1 ${isSyncingRegions ? 'animate-spin' : ''}`} />
{isSyncingRegions ? t('server.cloudserverimport.sf787f4') : t('server.cloudserverimport.saa44e4')}
{isSyncingRegions ? '同步中...' : '刷新区域'}
</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)]">{t('server.cloudserverimport.s61df99')}</p>
<p className="text-[var(--dg-text-secondary)]">正在从云平台同步区域列表...</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>{t('cloud.noRegionData')}</p>
<p>暂无区域数据</p>
<Button
variant="outline"
size="sm"
@@ -382,14 +379,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=t('cloud.searchRegion')
placeholder="搜索区域名称或编码..."
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 ? t('common.deselectAll') : t('common.selectAll')}
{selectedRegions.length === regions.length && regions.length > 0 ? '取消全选' : '全选'}
</Button>
</div>
@@ -404,8 +401,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)]">{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)]">区域编码</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)]">Endpoint</th>
</tr>
</thead>
@@ -445,7 +442,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)]">{t('common.importComplete')}</p>
<p className="text-lg font-medium text-[var(--dg-text-primary)]">导入完成</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} 台
@@ -470,7 +467,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 ? t('common.deselectAll') : t('common.selectAll')}
{selectedServers.length === cloudServers.length && cloudServers.length > 0 ? '取消全选' : '全选'}
</Button>
<Badge className="bg-blue-500/20 dg-text-blue border-0">
已选 {selectedServers.length} 台
@@ -485,7 +482,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 ? t('server.cloudserverimport.s5bc305') : cloudServers.length > 0 ? t('server.cloudserverimport.s569d92') : t('server.cloudserverimport.sc0b926')}
{isDiscovering ? '发现中...' : cloudServers.length > 0 ? '重新发现' : '发现主机'}
</Button>
</div>
</div>
@@ -508,7 +505,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">{t('server.cloudserverimport.s6ce908')}</p>
<p className="font-medium">部分区域发现失败:</p>
{discoverErrors.map((err, i) => (
<p key={i} className="text-xs mt-1 opacity-80">{err}</p>
))}
@@ -520,13 +517,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)]">{t('server.cloudserverimport.s2401d9')}</p>
<p className="text-xs text-[var(--dg-text-muted)] mt-1">{t('server.cloudserverimport.sbd41e8')}</p>
<p className="text-[var(--dg-text-secondary)]">正在发现云主机,请稍候...</p>
<p className="text-xs text-[var(--dg-text-muted)] mt-1">正在逐区域查询,可能需要数秒</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>{t('server.cloudserverimport.s85f8bc')}</p>
<p>点击"发现主机"按钮,开始扫描所选区域</p>
</div>
) : (
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden max-h-[300px] overflow-y-auto">
@@ -539,8 +536,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)]">{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)]">主机名</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">IP地址</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--dg-border)]">
@@ -570,7 +567,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">{t('server.cloudserverimport.sd922ed')}</span>
<span className="text-[var(--dg-text-muted)] ml-1">(导入时统一设置)</span>
</label>
<Input
type="number"
@@ -583,14 +580,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">{t('server.cloudserverimport.s6cfbde')}</span>
<span className="text-[var(--dg-text-muted)] ml-1">(可选,用于SSH远程管理)</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="">{t('server.cloudserverimport.s45bf4d')}</option>
<option value="">不使用凭据</option>
{credentials.map(( c: any) => (
<option key={c.id} value={String(c.id)}>
{c.type === 'ssh_key' ? '🔑' : '🔒'} {c.name}
@@ -25,7 +25,6 @@ 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;
@@ -109,7 +108,6 @@ 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');
@@ -158,7 +156,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
const text = await selectedFile.text();
const rows = parseCSV(text);
if (rows.length === 0) {
toast.error(t('server.excelserverimport.sdee9fc'));
toast.error('文件无有效数据,请使用模板格式(首行为表头)');
setIsUploading(false);
return;
}
@@ -178,14 +176,14 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
disk: Number(r.disk) || 0,
tags: r.tags ?? '',
status: 'valid',
message: t('common.valid'),
message: '有效',
};
const errs: string[] = [];
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'));
if (!hostname) errs.push('主机名缺失');
if (!ip) errs.push('IP缺失');
else if (!isValidIP(ip)) errs.push('IP格式错误');
const key = `${hostname}|${ip}`;
if (hostname && ip && seen.has(key)) errs.push(t('server.excelserverimport.sa88437'));
if (hostname && ip && seen.has(key)) errs.push('重复行');
else seen.add(key);
if (errs.length > 0) {
row.status = 'error';
@@ -196,7 +194,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
setPreviewData(preview);
setStep(step + 1);
} catch {
toast.error(t('server.excelserverimport.s691054'));
toast.error('文件解析失败,请确认是 CSV 格式');
} finally {
setIsUploading(false);
}
@@ -252,11 +250,11 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
const success = r.success ?? 0;
const skipped = r.skipped ?? 0;
const failed = r.failed ?? 0;
toast.success(t('server.excelserverimport.s27bdf5', { success: success, skipped: skipped, failed: failed }));
toast.success(`导入完成:成功 ${success},跳过 ${skipped},失败 ${failed}`);
resetState();
onClose();
} catch (e) {
toast.error(e?.message || t('server.excelserverimport.se951a3'));
toast.error(e?.message || '导入失败,请重试');
} finally {
setIsImporting(false);
}
@@ -272,17 +270,17 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
const errorCount = previewData.filter(d => d.status === 'error').length;
const steps = [
{ id: 1, title: t('common.config') },
{ id: 2, title: t('common.upload') },
{ id: 3, title: t('common.confirm') },
{ id: 1, title: '配置' },
{ id: 2, title: '上传' },
{ id: 3, title: '确认' },
];
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">{t('server.excelserverimport.s754b4a')}</DialogTitle>
<DialogDescription className="sr-only">{t('server.excelserverimport.s58c25c')}</DialogDescription>
<DialogTitle className="text-xl">CSV 批量导入服务器</DialogTitle>
<DialogDescription className="sr-only">通过 CSV 文件批量导入服务器资产</DialogDescription>
</DialogHeader>
{/* Step Indicator */}
@@ -320,8 +318,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)]">{t('server.excelserverimport.s95e484')}</p>
<p className="text-xs text-[var(--dg-text-muted)]">{t('server.excelserverimport.s216093')}</p>
<p className="font-medium text-[var(--dg-text-primary)]">主机导入模板</p>
<p className="text-xs text-[var(--dg-text-muted)]">下载 CSV 模板,只需填主机名、IP和端口(默认22),Agent安装后自动补全</p>
</div>
</div>
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={downloadTemplate}>
@@ -334,13 +332,13 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
{/* Region Selection */}
<div>
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-2">{t('server.excelserverimport.s97d03d')}</label>
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-2">选择区域</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="">{t('server.excelserverimport.sf26489')}</option>
<option value="">请选择区域</option>
{regions.map((region) => (
<option key={region.id} value={region.id}>
{region.name} ({region.code})
@@ -353,7 +351,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">{t('server.excelserverimport.sdf1c0b')}</span>
<span className="text-xs text-[var(--dg-text-muted)] ml-1">(可选,用于SSH连接)</span>
</label>
{credentials.length === 0 ? (
<p className="text-xs text-[var(--dg-text-muted)] py-2">
@@ -365,10 +363,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="">{t('server.excelserverimport.s9d5df3')}</option>
<option value="">不使用凭据(后续手动配置)</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' ? t('server.excelserverimport.s87941d') : c.type === 'api_token' ? 'API Token' : t('login.password')})
{c.type === 'ssh_key' ? '🔑' : c.type === 'api_token' ? '🌐' : '🔒'} {c.name} ({c.type === 'ssh_key' ? 'SSH密钥' : c.type === 'api_token' ? 'API Token' : '密码'})
</option>
))}
</select>
@@ -379,7 +377,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">{t('server.excelserverimport.sb1d0e7')}</span>
<span className="text-xs text-[var(--dg-text-muted)] ml-1">(导入时统一设置,可在服务器管理页单独修改)</span>
</label>
<Input
type="number"
@@ -399,8 +397,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">{t('server.excelserverimport.sbab467')}</p>
<p className="text-xs text-[var(--dg-text-muted)] mb-4">{t('server.excelserverimport.s7bb060')}</p>
<p className="text-[var(--dg-text-secondary)] mb-2">点击或拖拽文件到此处上传</p>
<p className="text-xs text-[var(--dg-text-muted)] mb-4">支持 CSV 格式文件(可点击左侧"下载模板"获取)</p>
<Input
type="file"
accept=".csv,text/csv"
@@ -410,7 +408,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>{t('server.excelserverimport.sfd7e0c')}</span>
<span>选择文件</span>
</Button>
</label>
</div>
@@ -440,7 +438,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">{t('server.excelserverimport.s8c0c7b')}</p>
<p className="text-[var(--dg-text-secondary)] text-sm">正在解析文件...</p>
</div>
)}
</div>
@@ -450,7 +448,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">{t('server.excelserverimport.s3bc470')}</p>
<p className="text-[var(--dg-text-secondary)] text-sm">数据预览</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" />
@@ -475,10 +473,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)]">{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>
<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>
</tr>
</thead>
<tbody className="divide-y divide-[var(--dg-border)]">
@@ -560,7 +558,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
) : (
<Check className="h-4 w-4 mr-1" />
)}
{isImporting ? i18n.t('server.excelserverimport.s763476') : i18n.t('server.excelserverimport.s2fbd70', { validCount: validCount })}
{isImporting ? '导入中...' : `确认导入 (${validCount})`}
</Button>
)}
</div>
@@ -28,7 +28,6 @@ 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;
@@ -58,7 +57,6 @@ 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('/');
@@ -79,7 +77,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
setBreadcrumbs(data.breadcrumbs || []);
setCurrentPath(data.path || path);
} catch (err) {
setError(err?.message || t('server.serverfilebrowser.s4be30f'));
setError(err?.message || '加载目录失败');
setItems([]);
} finally {
setLoading(false);
@@ -98,7 +96,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
try {
await serverApi.downloadFile(serverId, filePath);
} catch (err) {
setError(t('server.serverfilebrowser.sb264a9') + ((err as JsonData)?.message || ''));
setError('下载失败: ' + ((err as JsonData)?.message || ''));
}
};
@@ -110,7 +108,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
await serverApi.uploadFile(serverId, currentPath, file);
await fetchDir(currentPath);
} catch (err) {
setError(err?.message || t('common.uploadFailed'));
setError(err?.message || '上传失败');
} finally {
setUploading(false);
if (inputRef.current) inputRef.current.value = '';
@@ -118,24 +116,24 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
};
const handleDelete = async (filePath: string, isDir: boolean) => {
if (!serverId || !confirm(t('server.serverfilebrowser.s5a1092', { isDir ? '目录' : '文件': isDir ? '目录' : '文件', filePath: filePath }))) return;
if (!serverId || !confirm(`确认删除 ${isDir ? '目录' : '文件'} "${filePath}"?`)) return;
try {
await serverApi.deleteFile(serverId, filePath);
await fetchDir(currentPath);
} catch (err) {
setError(err?.message || t('common.deleteFailed'));
setError(err?.message || '删除失败');
}
};
const handleMkdir = async () => {
const name = prompt(t('server.serverfilebrowser.sc8dfcc'));
const name = prompt('输入目录名:');
if (!name || !serverId) return;
const newPath = currentPath === '/' ? `/${name}` : `${currentPath}/${name}`;
try {
await serverApi.mkdir(serverId, newPath);
await fetchDir(currentPath);
} catch (err) {
setError(err?.message || t('monitor.quickSetupFailed'));
setError(err?.message || '创建失败');
}
};
@@ -165,7 +163,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
}
>
<DialogTitle className="sr-only">文件浏览 — {serverName}</DialogTitle>
<DialogDescription className="sr-only">{t('server.serverfilebrowser.sc9c640')}</DialogDescription>
<DialogDescription className="sr-only">SFTP 文件管理 · 上传 · 下载 · 删除</DialogDescription>
{/* Header */}
<div className="flex items-center justify-between px-4 py-2.5 border-b border-[var(--dg-border)] shrink-0">
@@ -174,13 +172,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">{t('server.serverfilebrowser.sd720ed')}</span>
<span className="text-[var(--dg-text-muted)] text-xs">· SFTP 文件管理</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 ? t('common.exitFullscreen') : t('common.fullscreen')}
title={fullscreen ? '退出全屏' : '全屏'}
>
{fullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
</button>
@@ -256,18 +254,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">{t('kubernetes.workload.emptyDir')}</p>
<p className="text-sm">空目录</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">{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>
<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>
</tr>
</thead>
<tbody>
@@ -315,7 +313,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=t('common.download')
title="下载"
>
<Download className="h-3.5 w-3.5" />
</button>
@@ -323,7 +321,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=t('common.delete')
title="删除"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
+25 -25
View File
@@ -27,8 +27,8 @@ import {
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';
import type { JsonData } from '@/types/common';
// 表单数据结构
export interface ServerFormData {
@@ -147,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 ? t('server.serverform.s44fc7f') : t('server.addServer')}
{editingId ? t('server.editTitle') : t('server.addServer')}
</DialogTitle>
<DialogDescription className="text-[var(--dg-text-muted)]">
{editingId ? t('server.serverform.s2c2f7a') : t('server.serverform.s2bbed0')}
{editingId ? t('server.editSubtitle') : t('server.addSubtitle')}
</DialogDescription>
</DialogHeader>
@@ -159,7 +159,7 @@ export function ServerForm({
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">
主机名 <span className="dg-text-red">*</span>
{t('server.hostname')} <span className="dg-text-red">*</span>
</Label>
<Input
placeholder="web-server-01"
@@ -170,7 +170,7 @@ export function ServerForm({
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">
IP地址 <span className="dg-text-red">*</span>
{t('server.ipAddress')} <span className="dg-text-red">*</span>
</Label>
<Input
placeholder="10.0.0.1"
@@ -186,14 +186,14 @@ export function ServerForm({
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">{t('server.publicIp')}</Label>
<Input
placeholder=t('server.serverform.s2709d4')
placeholder={t('server.publicIpPlaceholder')}
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)]">{t('server.serverform.s354e43')}</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('server.sshPort')}</Label>
<Input
type="number"
placeholder="22"
@@ -207,7 +207,7 @@ export function ServerForm({
{/* SSH 凭证关联 — 只允许 SSH 密钥 / SSH 密码 类型 */}
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)] flex items-center gap-1.5">
<Key className="h-3.5 w-3.5" /> SSH 凭证
<Key className="h-3.5 w-3.5" /> {t('server.sshCredential')}
{!editingId && !formData.credential_id && <span className="dg-text-red ml-1">*</span>}
</Label>
<Select
@@ -215,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=t('server.serverform.s53a3e2') />
<SelectValue placeholder={t('server.selectCredential')} />
</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' ? t('k8s.resource.secrets') : t('login.password')})
{cred.name} ({cred.username}@{ (cred.auth_type || '').toLowerCase() === 'key' ? t('server.authKey') : t('server.authPassword')})
</span>
</SelectItem>
))}
@@ -231,7 +231,7 @@ export function ServerForm({
{sshCredentials.length === 0 && (
<p className="text-xs text-[var(--dg-text-muted)]">
暂无可用的 SSH 凭证(仅支持 SSH 密钥 和 SSH 密码 类型)。
{t('server.noCredential')}
<span
className="text-[var(--dg-blue)] hover:underline cursor-pointer ml-1"
onClick={() => {
@@ -239,13 +239,13 @@ export function ServerForm({
navigate('/credentials');
}}
>
去系统管理 - 凭证配置创建
{t('server.goCreateCredential')}
</span>
</p>
)}
{formData.credential_id && (
<p className="text-[10px] dg-text-green/70">{t('server.serverform.sc30410')}</p>
<p className="text-[10px] dg-text-green/70">{t('server.credentialLinked')}</p>
)}
</div>
@@ -259,7 +259,7 @@ export function ServerForm({
>
<span className="flex items-center gap-2">
<Server className="h-4 w-4" />
更多详情(OS版本 / 运行状态 / CPU / 内存 / 磁盘 / 标签 / 描述)
{t('server.moreDetails')}
</span>
<ChevronDown className={cn('h-4 w-4 transition-transform', detailsOpen && 'rotate-180')} />
</Button>
@@ -267,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)]">{t('server.csv.os')}</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('server.os')}</Label>
<Input
placeholder=t('server.serverform.se2ab09')
placeholder={t('server.osPlaceholder')}
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)]">{t('server.serverform.se4b51d')}</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('server.runStatus')}</Label>
<Select
value={formData.status}
onValueChange={(v) => setFormData({ ...formData, status: v })}
@@ -295,7 +295,7 @@ export function ServerForm({
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">{t('server.serverform.sbdb608')}</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('server.cpuCores')}</Label>
<Input
type="number"
placeholder="4"
@@ -305,7 +305,7 @@ export function ServerForm({
/>
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">{t('server.serverform.s875cc1')}</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('server.memoryGb')}</Label>
<Input
type="number"
placeholder="8"
@@ -315,7 +315,7 @@ export function ServerForm({
/>
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">{t('server.serverform.s5fbcca')}</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('server.diskGb')}</Label>
<Input
type="number"
placeholder="100"
@@ -326,9 +326,9 @@ export function ServerForm({
</div>
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">{t('common.tag')}</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('server.tags')}</Label>
<Input
placeholder=t('server.serverform.sc53b11')
placeholder={t('server.tagsPlaceholder')}
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"
@@ -337,7 +337,7 @@ export function ServerForm({
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">{t('common.description')}</Label>
<Textarea
placeholder=t('server.serverform.s5f4a0f')
placeholder={t('server.descPlaceholder')}
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]"
@@ -353,7 +353,7 @@ export function ServerForm({
onClick={() => onOpenChange(false)}
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)]"
>
取消
{t('common.cancel')}
</Button>
<Button
onClick={onSave}
@@ -365,7 +365,7 @@ export function ServerForm({
) : (
<Save className="h-4 w-4 mr-2" />
)}
{editingId ? i18n.t('cloud.saveChanges') : i18n.t('common.create')}
{editingId ? t('server.saveChanges') : t('common.create')}
</Button>
</DialogFooter>
</DialogContent>
@@ -1,14 +1,12 @@
// 服务器统计条 — 紧凑型替代 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;
@@ -16,10 +14,10 @@ export function ServerStatsStrip({ servers }: ServerStatsStripProps) {
return (
<StatStrip
items={[
{ 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' },
{ label: '服务器', value: servers.length, color: 'blue' },
{ label: '运行中', value: runningCount, color: 'emerald' },
{ label: '维护中', value: maintenanceCount, color: 'amber' },
{ label: '已停止', value: stoppedCount, color: 'muted' },
]}
/>
);
@@ -16,7 +16,6 @@ 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;
@@ -37,7 +36,6 @@ 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);
@@ -111,7 +109,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">{t('k8s.pods.memory')}</span>
<span className="text-xs text-[var(--dg-text-muted)] w-8">内存</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))}
@@ -152,7 +150,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=t('server.servertablerow.sa0b601')>
<Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-blue)]" onClick={() => onOpenTerminal(server)} title="终端登录">
<Terminal className="h-4 w-4" />
</Button>
)}
@@ -164,7 +162,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)]">{t('common.actions')}</DropdownMenuLabel>
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">操作</DropdownMenuLabel>
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
{hasPermission('server:update') && (
<DropdownMenuItem
@@ -16,7 +16,6 @@ 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;
@@ -40,7 +39,6 @@ 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);
@@ -66,10 +64,10 @@ export function TerminalDialog({
try {
setAssigning(true);
await serverCredentialApi.assign(serverId, Number(selectedCredId));
toast.success(t('server.terminaldialog.s7c9109'));
toast.success('凭据已关联');
setAuthReady(true);
} catch (e) {
toast.error(e?.message || t('server.terminaldialog.s6d973d'));
toast.error(e?.message || '关联失败');
} finally {
setAssigning(false);
}
@@ -137,11 +135,11 @@ export function TerminalDialog({
ws.onclose = () => {
setConnected(false);
term.write(t('server.terminaldialog.safab20'));
term.write('\r\n\x1b[31m[连接已关闭]\x1b[0m\r\n');
};
ws.onerror = () => {
term.write(t('server.terminaldialog.sc81077'));
term.write('\r\n\x1b[31m[连接发生错误]\x1b[0m\r\n');
};
return () => {
@@ -200,7 +198,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">{t('server.terminaldialog.s46b4eb')}</DialogDescription>
<DialogDescription className="sr-only">SSH 终端连接</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">
@@ -228,18 +226,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">{t('server.terminaldialog.sa8156f')}</p>
<p className="text-sm text-[var(--dg-text-muted)]">{t('server.terminaldialog.s909c9a')}</p>
<p className="text-[var(--dg-text-primary)] font-medium mb-1">服务器未配置认证凭据</p>
<p className="text-sm text-[var(--dg-text-muted)]">请选择一个凭据用于 SSH 连接</p>
</div>
{credentials.length === 0 ? (
<p className="text-xs text-[var(--dg-text-muted)]">{t('server.terminaldialog.s817bf0')}</p>
<p className="text-xs text-[var(--dg-text-muted)]">暂无可用凭据,请先去「凭证配置」添加</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="">{t('server.terminaldialog.s3fb850')}</option>
<option value="">-- 选择凭据 --</option>
{credentials.map((c: JsonData) => (
<option key={c.id} value={String(c.id)}>
{c.type === 'ssh_key' ? '🔑' : '🔒'} {c.name}
@@ -252,7 +250,7 @@ export function TerminalDialog({
disabled={!selectedCredId || assigning}
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
>
{assigning ? t('server.terminaldialog.saf3d5d') : t('server.terminaldialog.s9b4480')}
{assigning ? '关联中...' : '确认连接'}
</Button>
</div>
)}
+14 -16
View File
@@ -32,7 +32,6 @@ 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;
@@ -50,7 +49,6 @@ 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);
@@ -68,7 +66,7 @@ export function WebShell() {
const res: JsonData = await serverApi.getServers();
setServers(res?.list || []);
} catch (e) {
console.error(t('server.webshell.s8603ff'), e);
console.error('加载服务器列表失败:', e);
setServers([]);
}
};
@@ -145,18 +143,18 @@ export function WebShell() {
};
ws.onopen = () => {
term.write(t('server.webshell.se39eef', { server.hostname: server.hostname, server.ip: server.ip }));
term.write(`\x1b[32m正在连接 ${server.hostname} (${server.ip})...\x1b[0m\r\n`);
};
ws.onclose = () => {
term.write(t('server.webshell.safab20'));
term.write('\r\n\x1b[31m[连接已关闭]\x1b[0m\r\n');
setSessions((prev) =>
prev.map((s) => (s.id === sessionId ? { ...s, connected: false } : s))
);
};
ws.onerror = () => {
term.write(t('server.webshell.sc81077'));
term.write('\r\n\x1b[31m[连接发生错误]\x1b[0m\r\n');
};
termsRef.current.set(sessionId, { term, ws, fit, opened: false });
@@ -262,7 +260,7 @@ export function WebShell() {
observer.observe(el);
(entry as JsonData)._observer = observer;
} catch (e) {
console.error(t('server.webshell.s7d4b03'), e);
console.error('打开终端失败:', e);
}
}
} else {
@@ -284,7 +282,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">{t('server.webshell.s1df740')}</p>
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">远程服务器终端管理</p>
</div>
<div className="flex items-center gap-2">
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
@@ -296,21 +294,21 @@ export function WebShell() {
</DialogTrigger>
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)]">{t('server.webshell.s7026b3')}</DialogTitle>
<DialogTitle className="text-[var(--dg-text-primary)]">新建终端连接</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)]">{t('server.webshell.s030235')}</label>
<label className="text-sm font-medium text-[var(--dg-text-secondary)]">选择服务器</label>
<Select value={selectedServer} onValueChange={setSelectedServer}>
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
<SelectValue placeholder=t('server.webshell.s030235') />
<SelectValue placeholder="选择服务器" />
</SelectTrigger>
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
{runningServers.length === 0 ? (
<SelectItem value="__none" disabled>{t('server.webshell.se5460b')}</SelectItem>
<SelectItem value="__none" disabled>暂无运行中的服务器</SelectItem>
) : (
runningServers.map((server) => (
<SelectItem key={server.id} value={server.id}>
@@ -380,7 +378,7 @@ export function WebShell() {
))}
</div>
) : (
<span className="text-sm text-[var(--dg-text-secondary)]">{t('server.webshell.sd5d6cb')}</span>
<span className="text-sm text-[var(--dg-text-secondary)]">暂无连接</span>
)}
</div>
<div className="flex items-center gap-1">
@@ -421,8 +419,8 @@ export function WebShell() {
style={{ height: terminalHeight }}
>
<Terminal className="h-16 w-16 mb-4 opacity-50" />
<p className="text-lg font-medium">{t('server.webshell.sefd92a')}</p>
<p className="text-sm mt-1">{t('server.webshell.se2e433')}</p>
<p className="text-lg font-medium">暂无活动连接</p>
<p className="text-sm mt-1">点击“新建连接”开始远程会话</p>
</div>
)}
</CardContent>
@@ -432,7 +430,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)]">{t('server.webshell.sde0277')}</CardTitle>
<CardTitle className="text-sm text-[var(--dg-text-secondary)]">快捷命令</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">