feat(i18n): 全量中英文国际化改造

- 翻译 en-US.json 中 1006 条未翻译的中文条目为英文
- 提取 30+ 组件文件的硬编码中文到 locale JSON
- 新增 900+ 翻译键覆盖所有业务模块:
  - Layout (Sidebar, Header, Breadcrumb)
  - Server (ServerForm, CloudServerImport, WebShell, RdpTools)
  - Deploy (ReleaseManager, PipelineCreate, AppRelease)
  - Kubernetes (DeploymentEdit, PodExec, PodLogs, ResourceEditor)
  - Monitor (UnifiedQueryPanel, AlertCenter, Prometheus/Grafana Dashboard)
  - AIOps (predictUtils, PredictiveCostTab, AIPredictive)
  - System (useLdapManagement, AgentMonitor, AgentWizard)
- tsc --noEmit 类型检查通过
This commit is contained in:
attacker committed 2026-07-04 03:42:27 +08:00
1 parent 76ec9387f0
commit a9513c2056
44 files changed
+4342 -1792

No files matched your search

@@ -22,6 +22,7 @@ import {
Server,
RefreshCw, Check, MapPin, Search, AlertCircle,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { cloudApi, credentialApi } from '@/api';
import { CloudBrandIcon, getProviderColor } from '@/components/icons/CloudBrandIcon';
import { ImportCredentialSettings } from './ImportCredentialSettings';
@@ -43,6 +44,7 @@ const cloudProviders = [
];
export function CloudServerImport({ open, onClose, initialAccountId }: CloudServerImportProps) {
const { t } = useTranslation();
const [cloudAccounts, setCloudAccounts] = useState<any[]>([]);
const [step, setStep] = useState(1);
const [selectedProvider, setSelectedProvider] = useState<string>('');
@@ -282,18 +284,18 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
);
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.cloudProvider'), icon: Cloud },
{ id: 2, title: t('server.cloudAccount'), icon: Key },
{ id: 3, title: t('server.selectRegion'), icon: MapPin },
{ id: 4, title: t('server.importConfirm'), 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>从云账号批量导入主机资产,可选关联 Linux SSH 和 Windows RDP 凭证</DialogDescription>
<DialogTitle className="text-xl">{t('server.importCloudServers')}</DialogTitle>
<DialogDescription>{t('server.importCloudServersDesc')}</DialogDescription>
</DialogHeader>
{/* Step Indicator */}
@@ -321,7 +323,7 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
{/* 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.selectCloudProvider')}</p>
<div className="grid grid-cols-3 gap-4">
{cloudProviders.map((provider) => (
<Card
@@ -351,7 +353,7 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
{/* 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.selectAccount')}</p>
{filteredAccounts.length > 0 ? (
<div className="space-y-2 max-h-[300px] overflow-y-auto">
{filteredAccounts.map((account) => (
@@ -381,9 +383,9 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
</div>
<div className="text-right">
<Badge className="bg-emerald-500/20 dg-text-green border-0">
{account.region_count ?? account.regionCount ?? 0} 区域
{account.region_count ?? account.regionCount ?? 0} {t('server.regionCountLabel', { count: account.region_count ?? account.regionCount ?? 0 })}
</Badge>
<p className="text-xs text-[var(--dg-text-muted)] mt-1">{account.server_count ?? account.serverCount ?? 0} 服务器</p>
<p className="text-xs text-[var(--dg-text-muted)] mt-1">{account.server_count ?? account.serverCount ?? 0} {t('server.serverCountLabel', { count: account.server_count ?? account.serverCount ?? 0 })}</p>
</div>
</CardContent>
</Card>
@@ -392,7 +394,7 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
) : (
<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.noValidAccount')}</p>
</div>
)}
</div>
@@ -403,9 +405,9 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
<div className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-[var(--dg-text-secondary)] text-sm">
请选择要同步的区域
{t('server.selectSyncRegion')}
{regions.length > 0 && (
<span className="ml-2 text-[var(--dg-text-muted)]">(共 {regions.length} 个区域,已选 {selectedRegions.length} 个)</span>
<span className="ml-2 text-[var(--dg-text-muted)]">{t('server.regionStats', { total: regions.length, selected: selectedRegions.length })}</span>
)}
</p>
<Button
@@ -416,19 +418,19 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
disabled={isSyncingRegions}
>
<RefreshCw className={`h-3 w-3 mr-1 ${isSyncingRegions ? 'animate-spin' : ''}`} />
{isSyncingRegions ? '同步中...' : '刷新区域'}
{isSyncingRegions ? t('server.syncing') : t('server.refreshRegions')}
</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)]">正在从云 API 同步区域列表...</p>
<p className="text-[var(--dg-text-secondary)]">{t('server.syncingRegionList')}</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('server.noRegionData')}</p>
<Button
variant="outline"
size="sm"
@@ -436,7 +438,7 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
onClick={() => selectedAccount && syncRegions(selectedAccount.id)}
>
<RefreshCw className="h-3 w-3 mr-1" />
立即同步
{t('server.syncNow')}
</Button>
</div>
) : (
@@ -446,14 +448,14 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
<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('server.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>
@@ -468,8 +470,8 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
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('server.regionCode')}</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('server.regionName')}</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">Endpoint</th>
</tr>
</thead>
@@ -510,14 +512,14 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
/* 导入结果 */
<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('server.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} 台
{t('server.importedCount', { count: importResult.imported })}
</Badge>
{importResult.skipped > 0 && (
<Badge className="bg-amber-500/20 dg-text-yellow border-0 text-base px-4 py-1">
跳过 {importResult.skipped} 台(已存在)
{t('server.skippedCount', { count: importResult.skipped })}
</Badge>
)}
</div>
@@ -526,19 +528,19 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
<>
<div className="flex items-center justify-between">
<p className="text-[var(--dg-text-secondary)] text-sm">
已选 {selectedRegions.length} 个区域
{t('server.selectedRegionsCount', { count: selectedRegions.length })}
{cloudServers.length > 0 && (
<span className="ml-2">· 发现 {cloudServers.length} 台主机</span>
<span className="ml-2">· {t('server.discoveredHosts', { count: cloudServers.length })}</span>
)}
</p>
<div className="flex items-center gap-2">
{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} 台
{t('server.selectedServersCount', { count: selectedServers.length })}
</Badge>
</>
)}
@@ -550,7 +552,7 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
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.discovering') : cloudServers.length > 0 ? t('server.rediscover') : t('server.discoverHosts')}
</Button>
</div>
</div>
@@ -573,7 +575,7 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
<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.partialRegionFailed')}</p>
{discoverErrors.map((err, i) => (
<p key={i} className="text-xs mt-1 opacity-80">{err}</p>
))}
@@ -585,13 +587,13 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
{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.discoveringCloudServers')}</p>
<p className="text-xs text-[var(--dg-text-muted)] mt-1">{t('server.scanningRegions')}</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.clickDiscoverHint')}</p>
</div>
) : (
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden max-h-[300px] overflow-y-auto">
@@ -604,8 +606,8 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
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.excelHostname')}</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('server.excelIpAddress')}</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--dg-border)]">
@@ -641,19 +643,19 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
{step > 1 && !importResult && (
<Button variant="outline" onClick={handleBack} className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
<ChevronLeft className="h-4 w-4 mr-1" />
上一步
{t('common.previous')}
</Button>
)}
</div>
<div className="flex gap-2">
{importResult ? (
<Button onClick={handleClose} className="bg-blue-600 hover:bg-blue-700">
完成
{t('common.finish')}
</Button>
) : (
<>
<Button variant="outline" onClick={handleClose} className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
取消
{t('common.cancel')}
</Button>
{step < 4 ? (
<Button
@@ -665,7 +667,7 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
}
className="bg-blue-600 hover:bg-blue-700"
>
下一步
{t('common.next')}
<ChevronRight className="h-4 w-4 ml-1" />
</Button>
) : (
@@ -677,12 +679,12 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
{isImporting ? (
<>
<RefreshCw className="h-4 w-4 mr-1 animate-spin" />
导入中...
{t('server.importing')}
</>
) : (
<>
<Check className="h-4 w-4 mr-1" />
确认导入 ({selectedServers.length})
{t('server.confirmImport', { count: selectedServers.length })}
</>
)}
</Button>
@@ -23,6 +23,7 @@ import {
ChevronLeft,
Loader2,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { regionApi, serverApi, credentialApi } from '@/api';
import type { JsonData } from '@/types/common';
import { ImportCredentialSettings } from './ImportCredentialSettings';
@@ -110,6 +111,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');
@@ -163,7 +165,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.fileNoData'));
setIsUploading(false);
return;
}
@@ -188,14 +190,14 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
disk: Number(r.disk) || 0,
tags: r.tags ?? '',
status: 'valid',
message: '有效',
message: t('server.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.excelHostname') + ' missing');
if (!ip) errs.push(t('server.excelIpAddress') + ' missing');
else if (!isValidIP(ip)) errs.push(t('server.excelIpAddress') + ' format error');
const key = `${hostname}|${ip}`;
if (hostname && ip && seen.has(key)) errs.push('重复行');
if (hostname && ip && seen.has(key)) errs.push('Duplicate row');
else seen.add(key);
if (errs.length > 0) {
row.status = 'error';
@@ -206,7 +208,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
setPreviewData(preview);
setStep(step + 1);
} catch {
toast.error('文件解析失败,请确认是 CSV 格式');
toast.error(t('server.fileParseError'));
} finally {
setIsUploading(false);
}
@@ -267,11 +269,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.csvImportComplete', { success, skipped, failed }));
resetState();
onClose();
} catch (e) {
toast.error(e?.message || '导入失败,请重试');
toast.error(e?.message || t('server.importFailed'));
} finally {
setIsImporting(false);
}
@@ -287,17 +289,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('server.excelConfig') },
{ id: 2, title: t('server.excelUpload') },
{ 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.csvBatchImport')}</DialogTitle>
<DialogDescription className="sr-only">{t('server.csvBatchImportDesc')}</DialogDescription>
</DialogHeader>
{/* Step Indicator */}
@@ -335,13 +337,13 @@ 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 模板,Linux 默认 SSH,Windows 默认 RDP</p>
<p className="font-medium text-[var(--dg-text-primary)]">{t('server.importTemplate')}</p>
<p className="text-xs text-[var(--dg-text-muted)]">{t('server.importTemplateDesc')}</p>
</div>
</div>
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={downloadTemplate}>
<Download className="h-4 w-4 mr-2" />
下载模板
{t('server.downloadTemplate')}
</Button>
</div>
</CardContent>
@@ -349,13 +351,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.selectRegion')}</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.selectRegionPlaceholder')}</option>
{regions.map((region) => (
<option key={region.id} value={region.id}>
{region.name} ({region.code})
@@ -384,8 +386,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.uploadHint')}</p>
<p className="text-xs text-[var(--dg-text-muted)] mb-4">{t('server.uploadFormatHint')}</p>
<Input
type="file"
accept=".csv,text/csv"
@@ -395,7 +397,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.selectFile')}</span>
</Button>
</label>
</div>
@@ -425,7 +427,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.parsingFile')}</p>
</div>
)}
</div>
@@ -435,22 +437,22 @@ 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.dataPreview')}</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" />
{validCount} 有效
{validCount} {t('server.valid')}
</Badge>
{warningCount > 0 && (
<Badge className="bg-amber-500/20 dg-text-yellow border-0">
<AlertCircle className="h-3 w-3 mr-1" />
{warningCount} 警告
{warningCount} {t('server.warning')}
</Badge>
)}
{errorCount > 0 && (
<Badge className="bg-red-500/20 dg-text-red border-0">
<X className="h-3 w-3 mr-1" />
{errorCount} 错误
{errorCount} {t('server.error')}
</Badge>
)}
</div>
@@ -460,9 +462,9 @@ 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)]">{t('server.rowNumber')}</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('server.excelHostname')}</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('server.excelIpAddress')}</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>
</tr>
@@ -506,7 +508,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/30">
<div className="flex items-center gap-2 dg-text-red">
<AlertCircle className="h-4 w-4" />
<span className="text-sm">存在 {errorCount} 条错误数据,请修正后重新上传</span>
<span className="text-sm">{t('server.errorDataHint', { count: errorCount })}</span>
</div>
</div>
)}
@@ -519,13 +521,13 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
{step > 1 && (
<Button variant="outline" onClick={handleBack} className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
<ChevronLeft className="h-4 w-4 mr-1" />
上一步
{t('common.previous')}
</Button>
)}
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handleClose} className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
取消
{t('common.cancel')}
</Button>
{step < 3 ? (
<Button
@@ -533,7 +535,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
disabled={(step === 1 && !selectedRegion) || (step === 2 && !selectedFile)}
className="bg-blue-600 hover:bg-blue-700"
>
下一步
{t('common.next')}
<ChevronRight className="h-4 w-4 ml-1" />
</Button>
) : (
@@ -547,7 +549,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
) : (
<Check className="h-4 w-4 mr-1" />
)}
{isImporting ? '导入中...' : `确认导入 (${validCount})`}
{isImporting ? t('server.importing') : t('server.confirmImport', { count: validCount })}
</Button>
)}
</div>
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import { readFromClipboard } from '@/utils/clipboard';
import { rdpFilesApi, type RdpFileItem } from '@/api/server/rdp-files';
@@ -29,6 +30,7 @@ export function RdpToolsPanel({
onSyncToRemote,
onAutoExec,
}: RdpToolsPanelProps) {
const { t } = useTranslation();
const [tab, setTab] = useState<ToolsTab>('general');
const [localClipDraft, setLocalClipDraft] = useState('');
const [files, setFiles] = useState<RdpFileItem[]>([]);
@@ -43,7 +45,7 @@ export function RdpToolsPanel({
setFiles(data?.list ?? []);
} catch (err) {
setFiles([]);
toast.error(err instanceof Error ? err.message : '无法列出远端文件(SMB 445 可能未开放)');
toast.error(err instanceof Error ? err.message : t('server.listFileError'));
} finally {
setLoadingFiles(false);
}
@@ -59,17 +61,17 @@ export function RdpToolsPanel({
setLocalClipDraft(text);
return;
}
toast.info('无法自动读取剪贴板,请 Ctrl+V 粘贴(HTTPS 下可自动读取)');
toast.info(t('server.clipboardHint'));
};
const syncLocalClip = () => {
const trimmed = localClipDraft.trim();
if (!trimmed) {
toast.info('内容为空');
toast.info(t('server.emptyContent'));
return;
}
onSyncToRemote(trimmed);
toast.success('已同步到远端');
toast.success(t('server.syncedToRemote'));
};
const uploadWithFallback = async (file: File) => {
@@ -77,7 +79,7 @@ export function RdpToolsPanel({
try {
try {
await rdpFilesApi.upload(serverId, file);
toast.success(`${file.name} 已上传到 Download`);
toast.success(t('server.uploadedToDownload', { name: file.name }));
void refreshFiles();
return;
} catch {
@@ -85,9 +87,9 @@ export function RdpToolsPanel({
}
const staged = await rdpFilesApi.stage(serverId, file);
onAutoExec(staged.deploy_command);
toast.success(`${file.name} 已自动下发(SMB 不可用时的回退模式)`);
toast.success(t('server.autoSent', { name: file.name }));
} catch (err) {
toast.error(err instanceof Error ? err.message : '上传失败');
toast.error(err instanceof Error ? err.message : t('server.rdpUploadFailed'));
} finally {
setUploading(false);
}
@@ -102,9 +104,9 @@ export function RdpToolsPanel({
const handleDownload = async (name: string) => {
try {
await rdpFilesApi.download(serverId, name);
toast.success(`已开始下载 ${name}`);
toast.success(t('server.downloadStarted', { name }));
} catch (err) {
toast.error(err instanceof Error ? err.message : '下载失败');
toast.error(err instanceof Error ? err.message : t('server.rdpDownloadFailed'));
}
};
@@ -122,7 +124,7 @@ export function RdpToolsPanel({
)}
>
<Settings2 className="h-3.5 w-3.5" />
通用
{t('server.general')}
</button>
<button
type="button"
@@ -135,24 +137,24 @@ export function RdpToolsPanel({
)}
>
<FolderOpen className="h-3.5 w-3.5" />
文件
{t('server.rdpFiles')}
</button>
</div>
{tab === 'general' && (
<div className="p-3 space-y-3">
<div className="space-y-1.5">
<label className="text-xs text-[var(--dg-text-muted)]">远端剪贴板</label>
<label className="text-xs text-[var(--dg-text-muted)]">{t('server.remoteClipboard')}</label>
<textarea
readOnly
value={remoteClipboard}
rows={3}
placeholder="远端复制内容将显示在此"
placeholder={t('server.remoteClipPlaceholder')}
className="w-full resize-none rounded border border-[var(--dg-border)] bg-[var(--dg-bg-primary)] px-2 py-1.5 text-xs text-[var(--dg-text-secondary)] outline-none"
/>
</div>
<div className="space-y-1.5">
<label className="text-xs text-[var(--dg-text-muted)]">同步到远端</label>
<label className="text-xs text-[var(--dg-text-muted)]">{t('server.syncToRemote')}</label>
<textarea
value={localClipDraft}
onChange={(e) => setLocalClipDraft(e.target.value)}
@@ -162,11 +164,11 @@ export function RdpToolsPanel({
if (pasted) setLocalClipDraft(pasted);
}}
rows={3}
placeholder="点击输入框自动粘贴本地剪贴板"
placeholder={t('server.syncPlaceholder')}
className="w-full resize-none rounded border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] px-2 py-1.5 text-xs text-[var(--dg-text-primary)] outline-none"
/>
<Button type="button" size="sm" className="w-full" onClick={syncLocalClip}>
同步到远端
{t('server.syncButton')}
</Button>
</div>
</div>
@@ -194,12 +196,12 @@ export function RdpToolsPanel({
/>
<Button type="button" size="sm" className="w-full" disabled={uploading} onClick={() => fileInputRef.current?.click()}>
<Upload className="h-3.5 w-3.5 mr-1.5" />
{uploading ? '上传中...' : '上传文件'}
{uploading ? t('server.rdpUploading') : t('server.uploadFile')}
</Button>
<div className="max-h-40 overflow-y-auto rounded border border-[var(--dg-border)]">
{files.length === 0 ? (
<p className="px-2 py-3 text-[10px] text-[var(--dg-text-muted)] text-center">
{loadingFiles ? '加载中...' : '暂无文件(需 SMB 445 可达才能列出)'}
{loadingFiles ? t('server.rdpLoading') : t('server.noRdpFiles')}
</p>
) : (
files.map((f) => (
@@ -216,7 +218,7 @@ export function RdpToolsPanel({
)}
</div>
<p className="text-[10px] text-[var(--dg-text-muted)] leading-relaxed">
优先 SMB 直传(一键);若 445 不通则自动暂存到 AIOps 并由 RDP 执行下载命令。
{t('server.smbHint')}
</p>
</div>
)}
+33 -31
View File
@@ -1,5 +1,6 @@
import { cn } from '@/lib/utils';
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -107,6 +108,7 @@ export function ServerForm({
onSave,
formSaving,
}: ServerFormProps) {
const { t } = useTranslation();
const navigate = useNavigate();
const [credentials, setCredentials] = useState<Credential[]>([]);
@@ -171,10 +173,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.editServer') : t('server.addServer')}
</DialogTitle>
<DialogDescription className="text-[var(--dg-text-muted)]">
{editingId ? '修改服务器资产信息' : '录入新的服务器资产到 CMDB'}
{editingId ? t('server.editServerDesc') : t('server.addServerDesc')}
</DialogDescription>
</DialogHeader>
@@ -183,7 +185,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.formHostname')} <span className="dg-text-red">*</span>
</Label>
<Input
placeholder="web-server-01"
@@ -194,7 +196,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.formIpAddress')} <span className="dg-text-red">*</span>
</Label>
<Input
placeholder="10.0.0.1"
@@ -208,7 +210,7 @@ export function ServerForm({
{/* 连接协议 */}
<div className="grid grid-cols-3 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.osType')}</Label>
<Select
value={formData.os_type}
onValueChange={(v) => setFormData({ ...formData, os_type: v, protocol: v === 'windows' ? 'rdp' : 'ssh' })}
@@ -223,7 +225,7 @@ export function ServerForm({
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">连接协议</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('server.connectionProtocol')}</Label>
<Select value={formData.protocol} onValueChange={handleProtocolChange}>
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
<SelectValue />
@@ -235,7 +237,7 @@ export function ServerForm({
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">{formData.protocol === 'rdp' ? 'RDP端口' : 'SSH端口'}</Label>
<Label className="text-[var(--dg-text-secondary)]">{formData.protocol === 'rdp' ? t('server.rdpPort') : t('server.sshPort')}</Label>
<Input
type="number"
placeholder={formData.protocol === 'rdp' ? '3389' : '22'}
@@ -251,16 +253,16 @@ export function ServerForm({
{/* 连接配置 */}
<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.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)]">登录用户</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('server.loginUser')}</Label>
<Input
placeholder={formData.protocol === 'rdp' ? 'Administrator' : 'root'}
value={formData.username}
@@ -273,7 +275,7 @@ export function ServerForm({
{/* 凭证关联 — SSH/RDP 按协议过滤 */}
<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" /> {formData.protocol === 'rdp' ? 'RDP 凭证' : 'SSH 凭证'}
<Key className="h-3.5 w-3.5" /> {formData.protocol === 'rdp' ? t('server.rdpCredential') : t('server.sshCredential')}
{!editingId && !formData.credential_id && <span className="dg-text-red ml-1">*</span>}
</Label>
<Select
@@ -281,7 +283,7 @@ export function ServerForm({
onValueChange={handleCredentialSelect}
>
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
<SelectValue placeholder={formData.protocol === 'rdp' ? '选择 Windows RDP 密码...' : '选择 SSH 凭证...'} />
<SelectValue placeholder={formData.protocol === 'rdp' ? t('server.selectWindowsRdp') : t('server.selectSshCred')} />
</SelectTrigger>
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
{selectableCredentials.map((cred) => (
@@ -297,7 +299,7 @@ export function ServerForm({
{selectableCredentials.length === 0 && (
<p className="text-xs text-[var(--dg-text-muted)]">
暂无可用的 {formData.protocol === 'rdp' ? 'Windows RDP 密码' : 'SSH 凭证'}。
{formData.protocol === 'rdp' ? t('server.noAvailableRdp') : t('server.noAvailableSsh')}。
<span
className="text-[var(--dg-blue)] hover:underline cursor-pointer ml-1"
onClick={() => {
@@ -305,13 +307,13 @@ export function ServerForm({
navigate('/credentials');
}}
>
去系统管理 - 凭证配置创建
{t('server.goToCredential')}
</span>
</p>
)}
{formData.credential_id && (
<p className="text-[10px] dg-text-green/70">已关联凭证,登录信息从凭证自动获取(登录用户和认证方式由凭证决定)</p>
<p className="text-[10px] dg-text-green/70">{t('server.credentialLinked')}</p>
)}
</div>
@@ -325,7 +327,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>
@@ -333,16 +335,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.os')}</Label>
<Input
placeholder="如 CentOS 7.9"
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)]">运行状态</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('server.runningStatus')}</Label>
<Select
value={formData.status}
onValueChange={(v) => setFormData({ ...formData, status: v })}
@@ -351,17 +353,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('server.formRunning')}</SelectItem>
<SelectItem value="stopped">{t('server.formStopped')}</SelectItem>
<SelectItem value="maintenance">{t('server.formMaintenance')}</SelectItem>
<SelectItem value="offline">{t('server.formOffline')}</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.cpuCores')}</Label>
<Input
type="number"
placeholder="4"
@@ -371,7 +373,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.formMemory')}</Label>
<Input
type="number"
placeholder="8"
@@ -381,7 +383,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.formDisk')}</Label>
<Input
type="number"
placeholder="100"
@@ -392,7 +394,7 @@ 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('server.formTags')}</Label>
<Input
placeholder='JSON 格式,如 {"project":"web","team":"ops"}'
value={formData.tags}
@@ -401,9 +403,9 @@ export function ServerForm({
/>
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">描述</Label>
<Label className="text-[var(--dg-text-secondary)]">{t('server.formDescription')}</Label>
<Textarea
placeholder="补充说明..."
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]"
@@ -419,7 +421,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}
@@ -431,7 +433,7 @@ export function ServerForm({
) : (
<Save className="h-4 w-4 mr-2" />
)}
{editingId ? '保存修改' : '创建'}
{editingId ? t('server.saveEdit') : t('server.create')}
</Button>
</DialogFooter>
</DialogContent>
@@ -1,4 +1,5 @@
import { useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
Dialog,
DialogContent,
@@ -40,6 +41,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);
@@ -66,10 +68,10 @@ export function TerminalDialog({
try {
setAssigning(true);
await serverCredentialApi.assign(serverId, Number(selectedCredId));
toast.success('凭据已关联');
toast.success(t('common.operationSuccess'));
setAuthReady(true);
} catch (e) {
toast.error(e?.message || '关联失败');
toast.error(e?.message || t('common.operationFailed'));
} finally {
setAssigning(false);
}
@@ -141,12 +143,12 @@ export function TerminalDialog({
ws.onclose = () => {
setConnected(false);
setHasOutput(true);
term.write('\r\n\x1b[31m[连接已关闭]\x1b[0m\r\n');
term.write(`\r\n\x1b[31m[${t('server.connectionClosed')}]\r\n`);
};
ws.onerror = () => {
setHasOutput(true);
term.write('\r\n\x1b[31m[连接发生错误]\x1b[0m\r\n');
term.write(`\r\n\x1b[31m[${t('server.connectionError')}]\r\n`);
};
return () => {
@@ -233,18 +235,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.noAuthCred')}</p>
<p className="text-sm text-[var(--dg-text-muted)]">{t('server.selectCredHint')}</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.noCredAvailable')}</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.selectCred')}</option>
{credentials.map((c: JsonData) => (
<option key={c.id} value={String(c.id)}>
{c.type === 'ssh_key' ? '🔑' : '🔒'} {c.name}
@@ -257,7 +259,7 @@ export function TerminalDialog({
disabled={!selectedCredId || assigning}
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
>
{assigning ? '关联中...' : '确认连接'}
{assigning ? t('server.linking') : t('server.confirmConnect')}
</Button>
</div>
)}
@@ -265,7 +267,7 @@ export function TerminalDialog({
{authReady && (
<div className="relative bg-[#0a0e14]" style={{ height: fullscreen ? 'calc(100vh - 49px)' : '520px' }}>
<div ref={containerRef} className="xterm-terminal-host h-full" />
<TerminalLoadingOverlay show={!hasOutput} label="正在登录..." />
<TerminalLoadingOverlay show={!hasOutput} label={t('server.terminalLoggingIn')} />
</div>
)}
</DialogContent>
+20 -18
View File
@@ -1,4 +1,5 @@
import { useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -53,6 +54,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);
@@ -70,7 +72,7 @@ export function WebShell() {
const res: JsonData = await serverApi.getServers();
setServers(res?.list || []);
} catch (e) {
console.error('加载服务器列表失败:', e);
console.error('loadServers error:', e);
setServers([]);
}
};
@@ -150,11 +152,11 @@ export function WebShell() {
};
ws.onopen = () => {
term.write(`\x1b[32m正在连接 ${server.hostname} (${server.ip})...\x1b[0m\r\n`);
term.write(`\x1b[32m${t('server.connecting', { name: server.hostname, ip: server.ip })}\x1b[0m\r\n`);
};
ws.onclose = () => {
term.write('\r\n\x1b[31m[连接已关闭]\x1b[0m\r\n');
term.write(`\r\n\x1b[31m[${t('server.connectionClosed')}]\r\n`);
setSessions((prev) =>
prev.map((s) => (s.id === sessionId ? { ...s, connected: false, ready: true } : s))
);
@@ -162,7 +164,7 @@ export function WebShell() {
ws.onerror = () => {
setSessions((prev) => prev.map((s) => (s.id === sessionId ? { ...s, ready: true } : s)));
term.write('\r\n\x1b[31m[连接发生错误]\x1b[0m\r\n');
term.write(`\r\n\x1b[31m[${t('server.connectionError')}]\r\n`);
};
termsRef.current.set(sessionId, { term, ws, fit, opened: false });
@@ -256,7 +258,7 @@ export function WebShell() {
entry.opened = true;
entry.resizeCleanup = attachTerminalResize(el, entry.fit, entry.term, entry.ws);
} catch (e) {
console.error('打开终端失败:', e);
console.error('openTerminal error:', e);
}
}
} else {
@@ -276,33 +278,33 @@ 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.remoteTerminal')}</p>
</div>
<div className="flex items-center gap-2">
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button className="bg-[var(--dg-blue)] hover:opacity-90 text-white">
<Plus className="h-4 w-4 mr-2" />
新建连接
{t('server.newConnection')}
</Button>
</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.newTerminal')}</DialogTitle>
<DialogDescription className="text-[var(--dg-text-secondary)]">
选择要连接的服务器
{t('server.selectServer')}
</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.selectServer')}</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.selectServer')} />
</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.noRunningServers')}</SelectItem>
) : (
runningServers.map((server) => (
<SelectItem key={server.id} value={server.id}>
@@ -325,7 +327,7 @@ export function WebShell() {
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
>
<Terminal className="h-4 w-4 mr-2" />
连接
{t('server.webshellConnect')}
</Button>
</DialogFooter>
</DialogContent>
@@ -372,7 +374,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.noConnections')}</span>
)}
</div>
<div className="flex items-center gap-1">
@@ -410,7 +412,7 @@ export function WebShell() {
<TerminalLoadingOverlay
key={`${session.id}-loading`}
show={activeSession === session.id && !session.ready}
label="正在登录..."
label={t('server.webshellLoggingIn')}
/>
))}
</div>
@@ -420,8 +422,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.noActiveConnections')}</p>
<p className="text-sm mt-1">{t('server.newConnectionHint')}</p>
</div>
)}
</CardContent>
@@ -431,7 +433,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.quickCommands')}</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">