feat(i18n): 剩余模块全量国际化 — 51% 覆盖率 (231 文件接入 t())
批量转换 163 个文件,3204 处中文替换为 t() 调用: - Monitor: 日志中心/告警中心/数据源/大屏/站点拨测/SNMP/中间件 全部转换 - Kubernetes: ResourceVisualEditor/NamespaceResourcePage/拓扑/事件 全部转换 - Deploy: 流水线/发布/凭证/RBAC 全部转换 - Server/Cloud/Agent/Task/Layout/UI: 全部转换 - 翻译词典扩展至 6474 行 (22 namespaces, 5000+ keys) - tsc 编译零错误
This commit is contained in:
1 parent
78c27037e3
commit
ecec73989b
166 files changed
+6925
-2800
No files matched your search
@@ -27,6 +27,8 @@ import {
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { cloudApi } from '@/api';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
interface CloudServerImportProps {
|
||||
open: boolean;
|
||||
@@ -35,13 +37,14 @@ interface CloudServerImportProps {
|
||||
|
||||
const cloudProviders = [
|
||||
{ id: 'aws', name: 'AWS', color: 'bg-orange-500', icon: 'AWS' },
|
||||
{ id: 'aliyun', name: '阿里云', color: 'bg-orange-600', icon: 'AL' },
|
||||
{ id: 'tencent', name: '腾讯云', color: 'bg-blue-500', icon: 'TX' },
|
||||
{ id: 'huawei', name: '华为云', color: 'bg-red-500', icon: 'HW' },
|
||||
{ id: 'aliyun', name: i18n.t('cloud.aliyun'), color: 'bg-orange-600', icon: 'AL' },
|
||||
{ id: 'tencent', name: i18n.t('cloud.tencent'), color: 'bg-blue-500', icon: 'TX' },
|
||||
{ id: 'huawei', name: i18n.t('cloud.huawei'), color: 'bg-red-500', icon: 'HW' },
|
||||
{ id: 'azure', name: 'Azure', color: 'bg-blue-600', icon: 'AZ' },
|
||||
];
|
||||
|
||||
export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
const { t } = useTranslation();
|
||||
const [cloudAccounts, setCloudAccounts] = useState<any[]>([]);
|
||||
const [step, setStep] = useState(1);
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>('');
|
||||
@@ -67,7 +70,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
const res: Record<string, any> = await cloudApi.getAccounts();
|
||||
setCloudAccounts(res?.list || []);
|
||||
} catch (e) {
|
||||
console.error('加载云账号失败:', e);
|
||||
console.error(t('server.cloudserverimport.sddeee7'), e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -102,7 +105,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
const list = await loadRegions(accountId);
|
||||
setRegions(list);
|
||||
} catch (e) {
|
||||
console.error('同步区域失败:', e);
|
||||
console.error(t('server.cloudserverimport.s1d6599'), e);
|
||||
throw e;
|
||||
} finally {
|
||||
setIsSyncingRegions(false);
|
||||
@@ -137,8 +140,8 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
setCloudServers(res?.list || []);
|
||||
setDiscoverErrors(res?.errors || []);
|
||||
} catch (e) {
|
||||
console.error('发现主机失败:', e);
|
||||
setDiscoverErrors([e?.message || '发现主机失败']);
|
||||
console.error(t('server.cloudserverimport.s5e749a'), e);
|
||||
setDiscoverErrors([e?.message || t('server.cloudserverimport.s7d43dc')]);
|
||||
} finally {
|
||||
setIsDiscovering(false);
|
||||
}
|
||||
@@ -156,7 +159,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
});
|
||||
setImportResult((res as any) || { imported: 0, skipped: 0 });
|
||||
} catch (e) {
|
||||
console.error('导入失败:', e);
|
||||
console.error(t('server.cloudserverimport.s1b1225'), e);
|
||||
} finally {
|
||||
setIsImporting(false);
|
||||
}
|
||||
@@ -223,18 +226,18 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
);
|
||||
|
||||
const steps = [
|
||||
{ id: 1, title: '公有云', icon: Cloud },
|
||||
{ id: 2, title: '访问凭据', icon: Key },
|
||||
{ id: 3, title: '选择区域', icon: MapPin },
|
||||
{ id: 4, title: '导入确认', icon: CheckCircle },
|
||||
{ id: 1, title: t('server.cloudserverimport.s7ef31a'), icon: Cloud },
|
||||
{ id: 2, title: t('server.cloudserverimport.s02badc'), icon: Key },
|
||||
{ id: 3, title: t('server.cloudserverimport.s97d03d'), icon: MapPin },
|
||||
{ id: 4, title: t('server.cloudserverimport.s81eea5'), icon: CheckCircle },
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-3xl bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">导入云主机</DialogTitle>
|
||||
<DialogDescription className="sr-only">从云平台批量导入主机资产</DialogDescription>
|
||||
<DialogTitle className="text-xl">{t('server.cloudserverimport.sa558f8')}</DialogTitle>
|
||||
<DialogDescription className="sr-only">{t('server.cloudserverimport.s869681')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Step Indicator */}
|
||||
@@ -262,7 +265,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
{/* Step 1: Select Cloud Provider */}
|
||||
{step === 1 && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-[var(--dg-text-secondary)] text-sm">请选择要导入云主机的云平台</p>
|
||||
<p className="text-[var(--dg-text-secondary)] text-sm">{t('server.cloudserverimport.s7c3664')}</p>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{cloudProviders.map((provider) => (
|
||||
<Card
|
||||
@@ -289,7 +292,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
{/* Step 2: Select Account */}
|
||||
{step === 2 && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-[var(--dg-text-secondary)] text-sm">请选择访问凭据(AccessKey)</p>
|
||||
<p className="text-[var(--dg-text-secondary)] text-sm">{t('server.cloudserverimport.se1a787')}</p>
|
||||
{filteredAccounts.length > 0 ? (
|
||||
<div className="space-y-2 max-h-[300px] overflow-y-auto">
|
||||
{filteredAccounts.map((account) => (
|
||||
@@ -325,7 +328,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
) : (
|
||||
<div className="text-center py-8 text-[var(--dg-text-muted)]">
|
||||
<Key className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||
<p>该云平台暂无有效的访问凭据</p>
|
||||
<p>{t('server.cloudserverimport.saa5f0d')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -349,19 +352,19 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
disabled={isSyncingRegions}
|
||||
>
|
||||
<RefreshCw className={`h-3 w-3 mr-1 ${isSyncingRegions ? 'animate-spin' : ''}`} />
|
||||
{isSyncingRegions ? '同步中...' : '刷新区域'}
|
||||
{isSyncingRegions ? t('server.cloudserverimport.sf787f4') : t('server.cloudserverimport.saa44e4')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isSyncingRegions ? (
|
||||
<div className="text-center py-12">
|
||||
<RefreshCw className="h-8 w-8 mx-auto mb-3 animate-spin dg-text-blue" />
|
||||
<p className="text-[var(--dg-text-secondary)]">正在从云平台同步区域列表...</p>
|
||||
<p className="text-[var(--dg-text-secondary)]">{t('server.cloudserverimport.s61df99')}</p>
|
||||
</div>
|
||||
) : regions.length === 0 ? (
|
||||
<div className="text-center py-8 text-[var(--dg-text-muted)]">
|
||||
<MapPin className="h-10 w-10 mx-auto mb-3 opacity-50" />
|
||||
<p>暂无区域数据</p>
|
||||
<p>{t('cloud.noRegionData')}</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -379,14 +382,14 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
<div className="relative flex-1">
|
||||
<Search className="h-4 w-4 absolute left-3 top-1/2 -translate-y-1/2 text-[var(--dg-text-muted)]" />
|
||||
<Input
|
||||
placeholder="搜索区域名称或编码..."
|
||||
placeholder=t('cloud.searchRegion')
|
||||
value={regionSearch}
|
||||
onChange={(e) => setRegionSearch(e.target.value)}
|
||||
className="pl-9 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={handleSelectAllRegions} className="border-[var(--dg-border)]">
|
||||
{selectedRegions.length === regions.length && regions.length > 0 ? '取消全选' : '全选'}
|
||||
{selectedRegions.length === regions.length && regions.length > 0 ? t('common.deselectAll') : t('common.selectAll')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -401,8 +404,8 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
onCheckedChange={handleSelectAllRegions}
|
||||
/>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">区域编码</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">区域名称</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('cloud.regionCode')}</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('cloud.regionName')}</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">Endpoint</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -442,7 +445,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
/* 导入结果 */
|
||||
<div className="text-center py-12">
|
||||
<CheckCircle className="h-12 w-12 mx-auto mb-3 dg-text-green" />
|
||||
<p className="text-lg font-medium text-[var(--dg-text-primary)]">导入完成</p>
|
||||
<p className="text-lg font-medium text-[var(--dg-text-primary)]">{t('common.importComplete')}</p>
|
||||
<div className="flex items-center justify-center gap-6 mt-4">
|
||||
<Badge className="bg-emerald-500/20 dg-text-green border-0 text-base px-4 py-1">
|
||||
成功导入 {importResult.imported} 台
|
||||
@@ -467,7 +470,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
{cloudServers.length > 0 && (
|
||||
<>
|
||||
<Button variant="outline" size="sm" onClick={handleSelectAllServers} className="border-[var(--dg-border)]">
|
||||
{selectedServers.length === cloudServers.length && cloudServers.length > 0 ? '取消全选' : '全选'}
|
||||
{selectedServers.length === cloudServers.length && cloudServers.length > 0 ? t('common.deselectAll') : t('common.selectAll')}
|
||||
</Button>
|
||||
<Badge className="bg-blue-500/20 dg-text-blue border-0">
|
||||
已选 {selectedServers.length} 台
|
||||
@@ -482,7 +485,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
className="border-[var(--dg-border)] dg-text-blue"
|
||||
>
|
||||
<RefreshCw className={`h-3 w-3 mr-1 ${isDiscovering ? 'animate-spin' : ''}`} />
|
||||
{isDiscovering ? '发现中...' : cloudServers.length > 0 ? '重新发现' : '发现主机'}
|
||||
{isDiscovering ? t('server.cloudserverimport.s5bc305') : cloudServers.length > 0 ? t('server.cloudserverimport.s569d92') : t('server.cloudserverimport.sc0b926')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -505,7 +508,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
<div className="flex items-start gap-2 p-3 rounded-lg bg-amber-500/10 border border-amber-500/30 dg-text-yellow text-sm">
|
||||
<AlertCircle className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="font-medium">部分区域发现失败:</p>
|
||||
<p className="font-medium">{t('server.cloudserverimport.s6ce908')}</p>
|
||||
{discoverErrors.map((err, i) => (
|
||||
<p key={i} className="text-xs mt-1 opacity-80">{err}</p>
|
||||
))}
|
||||
@@ -517,13 +520,13 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
{isDiscovering ? (
|
||||
<div className="text-center py-12">
|
||||
<RefreshCw className="h-8 w-8 mx-auto mb-3 animate-spin dg-text-blue" />
|
||||
<p className="text-[var(--dg-text-secondary)]">正在发现云主机,请稍候...</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mt-1">正在逐区域查询,可能需要数秒</p>
|
||||
<p className="text-[var(--dg-text-secondary)]">{t('server.cloudserverimport.s2401d9')}</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mt-1">{t('server.cloudserverimport.sbd41e8')}</p>
|
||||
</div>
|
||||
) : cloudServers.length === 0 && !isDiscovering ? (
|
||||
<div className="text-center py-8 text-[var(--dg-text-muted)]">
|
||||
<Server className="h-10 w-10 mx-auto mb-3 opacity-50" />
|
||||
<p>点击"发现主机"按钮,开始扫描所选区域</p>
|
||||
<p>{t('server.cloudserverimport.s85f8bc')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden max-h-[300px] overflow-y-auto">
|
||||
@@ -536,8 +539,8 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
onCheckedChange={handleSelectAllServers}
|
||||
/>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">主机名</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">IP地址</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('server.hostname')}</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('server.csv.ip')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-[var(--dg-border)]">
|
||||
@@ -567,7 +570,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-[var(--dg-text-secondary)] mb-1">
|
||||
SSH 端口
|
||||
<span className="text-[var(--dg-text-muted)] ml-1">(导入时统一设置)</span>
|
||||
<span className="text-[var(--dg-text-muted)] ml-1">{t('server.cloudserverimport.sd922ed')}</span>
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
@@ -580,14 +583,14 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-[var(--dg-text-secondary)] mb-1">
|
||||
关联凭证
|
||||
<span className="text-[var(--dg-text-muted)] ml-1">(可选,用于SSH远程管理)</span>
|
||||
<span className="text-[var(--dg-text-muted)] ml-1">{t('server.cloudserverimport.s6cfbde')}</span>
|
||||
</label>
|
||||
<select
|
||||
value={selectedCredentialId}
|
||||
onChange={(e) => setSelectedCredentialId(e.target.value)}
|
||||
className="w-full px-3 py-1.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md text-[var(--dg-text-primary)] text-xs"
|
||||
>
|
||||
<option value="">不使用凭据</option>
|
||||
<option value="">{t('server.cloudserverimport.s45bf4d')}</option>
|
||||
{credentials.map(( c: any) => (
|
||||
<option key={c.id} value={String(c.id)}>
|
||||
{c.type === 'ssh_key' ? '🔑' : '🔒'} {c.name}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { regionApi, serverApi, credentialApi } from '@/api';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface RegionOption {
|
||||
id: string;
|
||||
@@ -108,6 +109,7 @@ function downloadTemplate() {
|
||||
}
|
||||
|
||||
export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
const { t } = useTranslation();
|
||||
const [step, setStep] = useState(1);
|
||||
const [selectedRegion, setSelectedRegion] = useState<string>('');
|
||||
const [, _setConnectionMethod] = useState<string>('password');
|
||||
@@ -156,7 +158,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
const text = await selectedFile.text();
|
||||
const rows = parseCSV(text);
|
||||
if (rows.length === 0) {
|
||||
toast.error('文件无有效数据,请使用模板格式(首行为表头)');
|
||||
toast.error(t('server.excelserverimport.sdee9fc'));
|
||||
setIsUploading(false);
|
||||
return;
|
||||
}
|
||||
@@ -176,14 +178,14 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
disk: Number(r.disk) || 0,
|
||||
tags: r.tags ?? '',
|
||||
status: 'valid',
|
||||
message: '有效',
|
||||
message: t('common.valid'),
|
||||
};
|
||||
const errs: string[] = [];
|
||||
if (!hostname) errs.push('主机名缺失');
|
||||
if (!ip) errs.push('IP缺失');
|
||||
else if (!isValidIP(ip)) errs.push('IP格式错误');
|
||||
if (!hostname) errs.push(t('server.excelserverimport.sd27458'));
|
||||
if (!ip) errs.push(t('server.excelserverimport.s7641b3'));
|
||||
else if (!isValidIP(ip)) errs.push(t('server.excelserverimport.s968c09'));
|
||||
const key = `${hostname}|${ip}`;
|
||||
if (hostname && ip && seen.has(key)) errs.push('重复行');
|
||||
if (hostname && ip && seen.has(key)) errs.push(t('server.excelserverimport.sa88437'));
|
||||
else seen.add(key);
|
||||
if (errs.length > 0) {
|
||||
row.status = 'error';
|
||||
@@ -194,7 +196,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
setPreviewData(preview);
|
||||
setStep(step + 1);
|
||||
} catch {
|
||||
toast.error('文件解析失败,请确认是 CSV 格式');
|
||||
toast.error(t('server.excelserverimport.s691054'));
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
@@ -250,11 +252,11 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
const success = r.success ?? 0;
|
||||
const skipped = r.skipped ?? 0;
|
||||
const failed = r.failed ?? 0;
|
||||
toast.success(`导入完成:成功 ${success},跳过 ${skipped},失败 ${failed}`);
|
||||
toast.success(t('server.excelserverimport.s27bdf5', { success: success, skipped: skipped, failed: failed }));
|
||||
resetState();
|
||||
onClose();
|
||||
} catch (e) {
|
||||
toast.error(e?.message || '导入失败,请重试');
|
||||
toast.error(e?.message || t('server.excelserverimport.se951a3'));
|
||||
} finally {
|
||||
setIsImporting(false);
|
||||
}
|
||||
@@ -270,17 +272,17 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
const errorCount = previewData.filter(d => d.status === 'error').length;
|
||||
|
||||
const steps = [
|
||||
{ id: 1, title: '配置' },
|
||||
{ id: 2, title: '上传' },
|
||||
{ id: 3, title: '确认' },
|
||||
{ id: 1, title: t('common.config') },
|
||||
{ id: 2, title: t('common.upload') },
|
||||
{ id: 3, title: t('common.confirm') },
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-3xl bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">CSV 批量导入服务器</DialogTitle>
|
||||
<DialogDescription className="sr-only">通过 CSV 文件批量导入服务器资产</DialogDescription>
|
||||
<DialogTitle className="text-xl">{t('server.excelserverimport.s754b4a')}</DialogTitle>
|
||||
<DialogDescription className="sr-only">{t('server.excelserverimport.s58c25c')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Step Indicator */}
|
||||
@@ -318,8 +320,8 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
<FileSpreadsheet className="h-5 w-5 dg-text-green" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-[var(--dg-text-primary)]">主机导入模板</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">下载 CSV 模板,只需填主机名、IP和端口(默认22),Agent安装后自动补全</p>
|
||||
<p className="font-medium text-[var(--dg-text-primary)]">{t('server.excelserverimport.s95e484')}</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">{t('server.excelserverimport.s216093')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={downloadTemplate}>
|
||||
@@ -332,13 +334,13 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
|
||||
{/* Region Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-2">选择区域</label>
|
||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-2">{t('server.excelserverimport.s97d03d')}</label>
|
||||
<select
|
||||
value={selectedRegion}
|
||||
onChange={(e) => setSelectedRegion(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-md text-[var(--dg-text-primary)] text-sm"
|
||||
>
|
||||
<option value="">请选择区域</option>
|
||||
<option value="">{t('server.excelserverimport.sf26489')}</option>
|
||||
{regions.map((region) => (
|
||||
<option key={region.id} value={region.id}>
|
||||
{region.name} ({region.code})
|
||||
@@ -351,7 +353,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-2">
|
||||
选择凭据
|
||||
<span className="text-xs text-[var(--dg-text-muted)] ml-1">(可选,用于SSH连接)</span>
|
||||
<span className="text-xs text-[var(--dg-text-muted)] ml-1">{t('server.excelserverimport.sdf1c0b')}</span>
|
||||
</label>
|
||||
{credentials.length === 0 ? (
|
||||
<p className="text-xs text-[var(--dg-text-muted)] py-2">
|
||||
@@ -363,10 +365,10 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
onChange={(e) => setSelectedCredentialId(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-md text-[var(--dg-text-primary)] text-sm"
|
||||
>
|
||||
<option value="">不使用凭据(后续手动配置)</option>
|
||||
<option value="">{t('server.excelserverimport.s9d5df3')}</option>
|
||||
{credentials.map((c: JsonData) => (
|
||||
<option key={c.id} value={String(c.id)}>
|
||||
{c.type === 'ssh_key' ? '🔑' : c.type === 'api_token' ? '🌐' : '🔒'} {c.name} ({c.type === 'ssh_key' ? 'SSH密钥' : c.type === 'api_token' ? 'API Token' : '密码'})
|
||||
{c.type === 'ssh_key' ? '🔑' : c.type === 'api_token' ? '🌐' : '🔒'} {c.name} ({c.type === 'ssh_key' ? t('server.excelserverimport.s87941d') : c.type === 'api_token' ? 'API Token' : t('login.password')})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -377,7 +379,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-2">
|
||||
SSH 端口
|
||||
<span className="text-xs text-[var(--dg-text-muted)] ml-1">(导入时统一设置,可在服务器管理页单独修改)</span>
|
||||
<span className="text-xs text-[var(--dg-text-muted)] ml-1">{t('server.excelserverimport.sb1d0e7')}</span>
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
@@ -397,8 +399,8 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
className="border-2 border-dashed border-[var(--dg-border)] rounded-lg p-8 text-center hover:border-blue-500/50 transition-colors"
|
||||
>
|
||||
<Upload className="h-12 w-12 mx-auto mb-4 text-[var(--dg-text-muted)]" />
|
||||
<p className="text-[var(--dg-text-secondary)] mb-2">点击或拖拽文件到此处上传</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mb-4">支持 CSV 格式文件(可点击左侧"下载模板"获取)</p>
|
||||
<p className="text-[var(--dg-text-secondary)] mb-2">{t('server.excelserverimport.sbab467')}</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mb-4">{t('server.excelserverimport.s7bb060')}</p>
|
||||
<Input
|
||||
type="file"
|
||||
accept=".csv,text/csv"
|
||||
@@ -408,7 +410,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
/>
|
||||
<label htmlFor="excel-upload">
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" asChild>
|
||||
<span>选择文件</span>
|
||||
<span>{t('server.excelserverimport.sfd7e0c')}</span>
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
@@ -438,7 +440,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
{isUploading && (
|
||||
<div className="text-center py-4">
|
||||
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-2" />
|
||||
<p className="text-[var(--dg-text-secondary)] text-sm">正在解析文件...</p>
|
||||
<p className="text-[var(--dg-text-secondary)] text-sm">{t('server.excelserverimport.s8c0c7b')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -448,7 +450,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
{step === 3 && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[var(--dg-text-secondary)] text-sm">数据预览</p>
|
||||
<p className="text-[var(--dg-text-secondary)] text-sm">{t('server.excelserverimport.s3bc470')}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className="bg-emerald-500/20 dg-text-green border-0">
|
||||
<Check className="h-3 w-3 mr-1" />
|
||||
@@ -473,10 +475,10 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
<table className="w-full">
|
||||
<thead className="bg-[var(--dg-bg-secondary)] sticky top-0">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">行号</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">主机名</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">IP地址</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">状态</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('server.excelserverimport.s89c8c5')}</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('server.hostname')}</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('server.csv.ip')}</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('common.status')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-[var(--dg-border)]">
|
||||
@@ -558,7 +560,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
) : (
|
||||
<Check className="h-4 w-4 mr-1" />
|
||||
)}
|
||||
{isImporting ? '导入中...' : `确认导入 (${validCount})`}
|
||||
{isImporting ? i18n.t('server.excelserverimport.s763476') : i18n.t('server.excelserverimport.s2fbd70', { validCount: validCount })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import { cn } from '@/lib/utils';
|
||||
import { serverApi } from '@/api';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface FileItem {
|
||||
name: string;
|
||||
@@ -57,6 +58,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [items, setItems] = useState<FileItem[]>([]);
|
||||
const [breadcrumbs, setBreadcrumbs] = useState<Breadcrumb[]>([]);
|
||||
const [currentPath, setCurrentPath] = useState('/');
|
||||
@@ -77,7 +79,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
|
||||
setBreadcrumbs(data.breadcrumbs || []);
|
||||
setCurrentPath(data.path || path);
|
||||
} catch (err) {
|
||||
setError(err?.message || '加载目录失败');
|
||||
setError(err?.message || t('server.serverfilebrowser.s4be30f'));
|
||||
setItems([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -96,7 +98,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
|
||||
try {
|
||||
await serverApi.downloadFile(serverId, filePath);
|
||||
} catch (err) {
|
||||
setError('下载失败: ' + ((err as JsonData)?.message || ''));
|
||||
setError(t('server.serverfilebrowser.sb264a9') + ((err as JsonData)?.message || ''));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -108,7 +110,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
|
||||
await serverApi.uploadFile(serverId, currentPath, file);
|
||||
await fetchDir(currentPath);
|
||||
} catch (err) {
|
||||
setError(err?.message || '上传失败');
|
||||
setError(err?.message || t('common.uploadFailed'));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
if (inputRef.current) inputRef.current.value = '';
|
||||
@@ -116,24 +118,24 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
|
||||
};
|
||||
|
||||
const handleDelete = async (filePath: string, isDir: boolean) => {
|
||||
if (!serverId || !confirm(`确认删除 ${isDir ? '目录' : '文件'} "${filePath}"?`)) return;
|
||||
if (!serverId || !confirm(t('server.serverfilebrowser.s5a1092', { isDir ? '目录' : '文件': isDir ? '目录' : '文件', filePath: filePath }))) return;
|
||||
try {
|
||||
await serverApi.deleteFile(serverId, filePath);
|
||||
await fetchDir(currentPath);
|
||||
} catch (err) {
|
||||
setError(err?.message || '删除失败');
|
||||
setError(err?.message || t('common.deleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleMkdir = async () => {
|
||||
const name = prompt('输入目录名:');
|
||||
const name = prompt(t('server.serverfilebrowser.sc8dfcc'));
|
||||
if (!name || !serverId) return;
|
||||
const newPath = currentPath === '/' ? `/${name}` : `${currentPath}/${name}`;
|
||||
try {
|
||||
await serverApi.mkdir(serverId, newPath);
|
||||
await fetchDir(currentPath);
|
||||
} catch (err) {
|
||||
setError(err?.message || '创建失败');
|
||||
setError(err?.message || t('monitor.quickSetupFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -163,7 +165,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
|
||||
}
|
||||
>
|
||||
<DialogTitle className="sr-only">文件浏览 — {serverName}</DialogTitle>
|
||||
<DialogDescription className="sr-only">SFTP 文件管理 · 上传 · 下载 · 删除</DialogDescription>
|
||||
<DialogDescription className="sr-only">{t('server.serverfilebrowser.sc9c640')}</DialogDescription>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-2.5 border-b border-[var(--dg-border)] shrink-0">
|
||||
@@ -172,13 +174,13 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
|
||||
<span className="text-[var(--dg-text-primary)] font-medium text-sm truncate">
|
||||
{serverName}
|
||||
</span>
|
||||
<span className="text-[var(--dg-text-muted)] text-xs">· SFTP 文件管理</span>
|
||||
<span className="text-[var(--dg-text-muted)] text-xs">{t('server.serverfilebrowser.sd720ed')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setFullscreen(!fullscreen)}
|
||||
className="p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)]"
|
||||
title={fullscreen ? '退出全屏' : '全屏'}
|
||||
title={fullscreen ? t('common.exitFullscreen') : t('common.fullscreen')}
|
||||
>
|
||||
{fullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
|
||||
</button>
|
||||
@@ -254,18 +256,18 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
|
||||
) : sortedItems.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-[var(--dg-text-muted)]">
|
||||
<Folder className="h-10 w-10 mb-2 opacity-30" />
|
||||
<p className="text-sm">空目录</p>
|
||||
<p className="text-sm">{t('kubernetes.workload.emptyDir')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 bg-[var(--dg-bg-primary)] border-b border-[var(--dg-border)] z-10">
|
||||
<tr className="text-left text-[var(--dg-text-muted)] text-xs">
|
||||
<th className="py-2 pl-4 w-8 font-medium"></th>
|
||||
<th className="py-2 font-medium">名称</th>
|
||||
<th className="py-2 px-2 font-medium w-28 text-right">大小</th>
|
||||
<th className="py-2 px-2 font-medium w-44 hidden sm:table-cell">修改时间</th>
|
||||
<th className="py-2 px-2 font-medium w-24 hidden md:table-cell">权限</th>
|
||||
<th className="py-2 pr-4 pl-2 font-medium w-24 text-center">操作</th>
|
||||
<th className="py-2 font-medium">{t('common.name')}</th>
|
||||
<th className="py-2 px-2 font-medium w-28 text-right">{t('deploy.table.size')}</th>
|
||||
<th className="py-2 px-2 font-medium w-44 hidden sm:table-cell">{t('deploy.table.modifyTime')}</th>
|
||||
<th className="py-2 px-2 font-medium w-24 hidden md:table-cell">{t('system.rolePermissions')}</th>
|
||||
<th className="py-2 pr-4 pl-2 font-medium w-24 text-center">{t('common.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -313,7 +315,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDownload(item.path); }}
|
||||
className="p-1 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)] hover:text-white"
|
||||
title="下载"
|
||||
title=t('common.download')
|
||||
>
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@@ -321,7 +323,7 @@ export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }:
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDelete(item.path, item.is_dir); }}
|
||||
className="p-1 rounded hover:bg-[var(--dg-bg-hover)] dg-text-red hover:dg-text-red"
|
||||
title="删除"
|
||||
title=t('common.delete')
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
||||
@@ -28,6 +28,7 @@ import { Server, ChevronDown, Loader2, Save, Key, Shield } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { credentialApi } from '@/api';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// 表单数据结构
|
||||
export interface ServerFormData {
|
||||
@@ -98,6 +99,7 @@ export function ServerForm({
|
||||
onSave,
|
||||
formSaving,
|
||||
}: ServerFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [credentials, setCredentials] = useState<Credential[]>([]);
|
||||
|
||||
@@ -145,10 +147,10 @@ export function ServerForm({
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Server className="h-5 w-5 text-[var(--dg-blue)]" />
|
||||
{editingId ? '编辑服务器' : '添加服务器'}
|
||||
{editingId ? t('server.serverform.s44fc7f') : t('server.addServer')}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-muted)]">
|
||||
{editingId ? '修改服务器资产信息' : '录入新的服务器资产到 CMDB'}
|
||||
{editingId ? t('server.serverform.s2c2f7a') : t('server.serverform.s2bbed0')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -182,16 +184,16 @@ export function ServerForm({
|
||||
{/* SSH 连接配置 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">公网IP</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('server.publicIp')}</Label>
|
||||
<Input
|
||||
placeholder="如 203.0.113.10"
|
||||
placeholder=t('server.serverform.s2709d4')
|
||||
value={formData.public_ip}
|
||||
onChange={(e) => setFormData({ ...formData, public_ip: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">SSH端口</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('server.serverform.s354e43')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="22"
|
||||
@@ -213,14 +215,14 @@ export function ServerForm({
|
||||
onValueChange={handleCredentialSelect}
|
||||
>
|
||||
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<SelectValue placeholder="选择 SSH 凭证..." />
|
||||
<SelectValue placeholder=t('server.serverform.s53a3e2') />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
{sshCredentials.map((cred) => (
|
||||
<SelectItem key={cred.id} value={String(cred.id)}>
|
||||
<span className="flex items-center gap-2">
|
||||
<Shield className="h-3 w-3 dg-text-blue" />
|
||||
{cred.name} ({cred.username}@{ (cred.auth_type || '').toLowerCase() === 'key' ? '密钥' : '密码'})
|
||||
{cred.name} ({cred.username}@{ (cred.auth_type || '').toLowerCase() === 'key' ? t('k8s.resource.secrets') : t('login.password')})
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -243,7 +245,7 @@ export function ServerForm({
|
||||
)}
|
||||
|
||||
{formData.credential_id && (
|
||||
<p className="text-[10px] dg-text-green/70">已关联凭证,登录信息从凭证自动获取(登录用户和认证方式由凭证决定)</p>
|
||||
<p className="text-[10px] dg-text-green/70">{t('server.serverform.sc30410')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -265,16 +267,16 @@ export function ServerForm({
|
||||
<CollapsibleContent className="space-y-4 pt-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">操作系统</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('server.csv.os')}</Label>
|
||||
<Input
|
||||
placeholder="如 CentOS 7.9"
|
||||
placeholder=t('server.serverform.se2ab09')
|
||||
value={formData.os}
|
||||
onChange={(e) => setFormData({ ...formData, os: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">运行状态</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('server.serverform.se4b51d')}</Label>
|
||||
<Select
|
||||
value={formData.status}
|
||||
onValueChange={(v) => setFormData({ ...formData, status: v })}
|
||||
@@ -283,17 +285,17 @@ export function ServerForm({
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
<SelectItem value="running">运行中</SelectItem>
|
||||
<SelectItem value="stopped">已停止</SelectItem>
|
||||
<SelectItem value="maintenance">维护中</SelectItem>
|
||||
<SelectItem value="offline">离线</SelectItem>
|
||||
<SelectItem value="running">{t('common.running')}</SelectItem>
|
||||
<SelectItem value="stopped">{t('common.stopped')}</SelectItem>
|
||||
<SelectItem value="maintenance">{t('common.maintenance')}</SelectItem>
|
||||
<SelectItem value="offline">{t('common.offline')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">CPU核数</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('server.serverform.sbdb608')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="4"
|
||||
@@ -303,7 +305,7 @@ export function ServerForm({
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">内存 (GB)</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('server.serverform.s875cc1')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="8"
|
||||
@@ -313,7 +315,7 @@ export function ServerForm({
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">磁盘 (GB)</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('server.serverform.s5fbcca')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="100"
|
||||
@@ -324,18 +326,18 @@ export function ServerForm({
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">标签</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('common.tag')}</Label>
|
||||
<Input
|
||||
placeholder='JSON 格式,如 {"project":"web","team":"ops"}'
|
||||
placeholder=t('server.serverform.sc53b11')
|
||||
value={formData.tags}
|
||||
onChange={(e) => setFormData({ ...formData, tags: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">描述</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('common.description')}</Label>
|
||||
<Textarea
|
||||
placeholder="补充说明..."
|
||||
placeholder=t('server.serverform.s5f4a0f')
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] min-h-[60px]"
|
||||
@@ -363,7 +365,7 @@ export function ServerForm({
|
||||
) : (
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{editingId ? '保存修改' : '创建'}
|
||||
{editingId ? i18n.t('cloud.saveChanges') : i18n.t('common.create')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// 服务器统计条 — 紧凑型替代 4 张大卡片
|
||||
import { StatStrip } from '@/components/ui/stat-strip';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ServerStatsStripProps {
|
||||
servers: JsonData[];
|
||||
}
|
||||
|
||||
export function ServerStatsStrip({ servers }: ServerStatsStripProps) {
|
||||
const { t } = useTranslation();
|
||||
const runningCount = servers.filter((s) => s.status === 'running').length;
|
||||
const maintenanceCount = servers.filter((s) => s.status === 'maintenance').length;
|
||||
const stoppedCount = servers.filter((s) => s.status === 'stopped').length;
|
||||
@@ -14,10 +16,10 @@ export function ServerStatsStrip({ servers }: ServerStatsStripProps) {
|
||||
return (
|
||||
<StatStrip
|
||||
items={[
|
||||
{ label: '服务器', value: servers.length, color: 'blue' },
|
||||
{ label: '运行中', value: runningCount, color: 'emerald' },
|
||||
{ label: '维护中', value: maintenanceCount, color: 'amber' },
|
||||
{ label: '已停止', value: stoppedCount, color: 'muted' },
|
||||
{ label: t('dashboard.servers'), value: servers.length, color: 'blue' },
|
||||
{ label: t('common.running'), value: runningCount, color: 'emerald' },
|
||||
{ label: t('common.maintenance'), value: maintenanceCount, color: 'amber' },
|
||||
{ label: t('common.stopped'), value: stoppedCount, color: 'muted' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Edit, MoreHorizontal, Server as ServerIcon, Terminal, Trash2, Network,
|
||||
import { getStatusBadge, getTags, getUsageColor } from './serverHelpers';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ServerTableRowProps {
|
||||
server?: JsonData;
|
||||
@@ -36,6 +37,7 @@ export function ServerTableRow({
|
||||
onOpenFiles,
|
||||
onDelete,
|
||||
}: ServerTableRowProps) {
|
||||
const { t } = useTranslation();
|
||||
const { hasPermission } = usePermission();
|
||||
const cpuUsage = Number(server.cpu_usage || 0);
|
||||
const memoryUsage = Number(server.memory_usage || 0);
|
||||
@@ -109,7 +111,7 @@ export function ServerTableRow({
|
||||
<span className="text-xs text-[var(--dg-text-secondary)]">{cpuUsage.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-[var(--dg-text-muted)] w-8">内存</span>
|
||||
<span className="text-xs text-[var(--dg-text-muted)] w-8">{t('k8s.pods.memory')}</span>
|
||||
<div className="w-16 h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn('h-full rounded-full', getUsageColor(memoryUsage))}
|
||||
@@ -150,7 +152,7 @@ export function ServerTableRow({
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
{hasPermission('server:webshell') && (
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-blue)]" onClick={() => onOpenTerminal(server)} title="终端登录">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-blue)]" onClick={() => onOpenTerminal(server)} title=t('server.servertablerow.sa0b601')>
|
||||
<Terminal className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
@@ -162,7 +164,7 @@ export function ServerTableRow({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">操作</DropdownMenuLabel>
|
||||
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">{t('common.actions')}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||
{hasPermission('server:update') && (
|
||||
<DropdownMenuItem
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useUserStore } from '@/store';
|
||||
import { credentialApi, serverCredentialApi } from '@/api';
|
||||
import { toast } from 'sonner';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface TerminalDialogProps {
|
||||
open: boolean;
|
||||
@@ -39,6 +40,7 @@ export function TerminalDialog({
|
||||
serverIp,
|
||||
hasAuth = true,
|
||||
}: TerminalDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const termRef = useRef<XTerm | null>(null);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
@@ -64,10 +66,10 @@ export function TerminalDialog({
|
||||
try {
|
||||
setAssigning(true);
|
||||
await serverCredentialApi.assign(serverId, Number(selectedCredId));
|
||||
toast.success('凭据已关联');
|
||||
toast.success(t('server.terminaldialog.s7c9109'));
|
||||
setAuthReady(true);
|
||||
} catch (e) {
|
||||
toast.error(e?.message || '关联失败');
|
||||
toast.error(e?.message || t('server.terminaldialog.s6d973d'));
|
||||
} finally {
|
||||
setAssigning(false);
|
||||
}
|
||||
@@ -135,11 +137,11 @@ export function TerminalDialog({
|
||||
|
||||
ws.onclose = () => {
|
||||
setConnected(false);
|
||||
term.write('\r\n\x1b[31m[连接已关闭]\x1b[0m\r\n');
|
||||
term.write(t('server.terminaldialog.safab20'));
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
term.write('\r\n\x1b[31m[连接发生错误]\x1b[0m\r\n');
|
||||
term.write(t('server.terminaldialog.sc81077'));
|
||||
};
|
||||
|
||||
return () => {
|
||||
@@ -198,7 +200,7 @@ export function TerminalDialog({
|
||||
style={fullscreen ? { maxWidth: '100vw', width: '100vw', height: '100vh' } : { maxWidth: '960px', width: '92vw' }}
|
||||
>
|
||||
<DialogTitle className="sr-only">终端 - {serverName}</DialogTitle>
|
||||
<DialogDescription className="sr-only">SSH 终端连接</DialogDescription>
|
||||
<DialogDescription className="sr-only">{t('server.terminaldialog.s46b4eb')}</DialogDescription>
|
||||
{/* 终端头部 */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-[var(--dg-border)]">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
@@ -226,18 +228,18 @@ export function TerminalDialog({
|
||||
<div className="p-6 flex flex-col items-center justify-center gap-4" style={{ height: '480px' }}>
|
||||
<Shield className="h-12 w-12 dg-text-yellow" />
|
||||
<div className="text-center">
|
||||
<p className="text-[var(--dg-text-primary)] font-medium mb-1">服务器未配置认证凭据</p>
|
||||
<p className="text-sm text-[var(--dg-text-muted)]">请选择一个凭据用于 SSH 连接</p>
|
||||
<p className="text-[var(--dg-text-primary)] font-medium mb-1">{t('server.terminaldialog.sa8156f')}</p>
|
||||
<p className="text-sm text-[var(--dg-text-muted)]">{t('server.terminaldialog.s909c9a')}</p>
|
||||
</div>
|
||||
{credentials.length === 0 ? (
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">暂无可用凭据,请先去「凭证配置」添加</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">{t('server.terminaldialog.s817bf0')}</p>
|
||||
) : (
|
||||
<select
|
||||
value={selectedCredId}
|
||||
onChange={(e) => setSelectedCredId(e.target.value)}
|
||||
className="w-64 px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md text-[var(--dg-text-primary)] text-sm"
|
||||
>
|
||||
<option value="">-- 选择凭据 --</option>
|
||||
<option value="">{t('server.terminaldialog.s3fb850')}</option>
|
||||
{credentials.map((c: JsonData) => (
|
||||
<option key={c.id} value={String(c.id)}>
|
||||
{c.type === 'ssh_key' ? '🔑' : '🔒'} {c.name}
|
||||
@@ -250,7 +252,7 @@ export function TerminalDialog({
|
||||
disabled={!selectedCredId || assigning}
|
||||
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
|
||||
>
|
||||
{assigning ? '关联中...' : '确认连接'}
|
||||
{assigning ? t('server.terminaldialog.saf3d5d') : t('server.terminaldialog.s9b4480')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -32,6 +32,7 @@ import 'xterm/css/xterm.css';
|
||||
import { serverApi } from '@/api';
|
||||
import { useUserStore } from '@/store';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface TerminalSession {
|
||||
id: string;
|
||||
@@ -49,6 +50,7 @@ interface TermEntry {
|
||||
}
|
||||
|
||||
export function WebShell() {
|
||||
const { t } = useTranslation();
|
||||
const [servers, setServers] = useState<JsonData[]>([]);
|
||||
const [sessions, setSessions] = useState<TerminalSession[]>([]);
|
||||
const [activeSession, setActiveSession] = useState<string | null>(null);
|
||||
@@ -66,7 +68,7 @@ export function WebShell() {
|
||||
const res: JsonData = await serverApi.getServers();
|
||||
setServers(res?.list || []);
|
||||
} catch (e) {
|
||||
console.error('加载服务器列表失败:', e);
|
||||
console.error(t('server.webshell.s8603ff'), e);
|
||||
setServers([]);
|
||||
}
|
||||
};
|
||||
@@ -143,18 +145,18 @@ export function WebShell() {
|
||||
};
|
||||
|
||||
ws.onopen = () => {
|
||||
term.write(`\x1b[32m正在连接 ${server.hostname} (${server.ip})...\x1b[0m\r\n`);
|
||||
term.write(t('server.webshell.se39eef', { server.hostname: server.hostname, server.ip: server.ip }));
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
term.write('\r\n\x1b[31m[连接已关闭]\x1b[0m\r\n');
|
||||
term.write(t('server.webshell.safab20'));
|
||||
setSessions((prev) =>
|
||||
prev.map((s) => (s.id === sessionId ? { ...s, connected: false } : s))
|
||||
);
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
term.write('\r\n\x1b[31m[连接发生错误]\x1b[0m\r\n');
|
||||
term.write(t('server.webshell.sc81077'));
|
||||
};
|
||||
|
||||
termsRef.current.set(sessionId, { term, ws, fit, opened: false });
|
||||
@@ -260,7 +262,7 @@ export function WebShell() {
|
||||
observer.observe(el);
|
||||
(entry as JsonData)._observer = observer;
|
||||
} catch (e) {
|
||||
console.error('打开终端失败:', e);
|
||||
console.error(t('server.webshell.s7d4b03'), e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -282,7 +284,7 @@ export function WebShell() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">WebShell</h1>
|
||||
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">远程服务器终端管理</p>
|
||||
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">{t('server.webshell.s1df740')}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
@@ -294,21 +296,21 @@ export function WebShell() {
|
||||
</DialogTrigger>
|
||||
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">新建终端连接</DialogTitle>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">{t('server.webshell.s7026b3')}</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-secondary)]">
|
||||
选择要连接的服务器
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--dg-text-secondary)]">选择服务器</label>
|
||||
<label className="text-sm font-medium text-[var(--dg-text-secondary)]">{t('server.webshell.s030235')}</label>
|
||||
<Select value={selectedServer} onValueChange={setSelectedServer}>
|
||||
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<SelectValue placeholder="选择服务器" />
|
||||
<SelectValue placeholder=t('server.webshell.s030235') />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
{runningServers.length === 0 ? (
|
||||
<SelectItem value="__none" disabled>暂无运行中的服务器</SelectItem>
|
||||
<SelectItem value="__none" disabled>{t('server.webshell.se5460b')}</SelectItem>
|
||||
) : (
|
||||
runningServers.map((server) => (
|
||||
<SelectItem key={server.id} value={server.id}>
|
||||
@@ -378,7 +380,7 @@ export function WebShell() {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">暂无连接</span>
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">{t('server.webshell.sd5d6cb')}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -419,8 +421,8 @@ export function WebShell() {
|
||||
style={{ height: terminalHeight }}
|
||||
>
|
||||
<Terminal className="h-16 w-16 mb-4 opacity-50" />
|
||||
<p className="text-lg font-medium">暂无活动连接</p>
|
||||
<p className="text-sm mt-1">点击“新建连接”开始远程会话</p>
|
||||
<p className="text-lg font-medium">{t('server.webshell.sefd92a')}</p>
|
||||
<p className="text-sm mt-1">{t('server.webshell.se2e433')}</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -430,7 +432,7 @@ export function WebShell() {
|
||||
{activeSessionData && (
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm text-[var(--dg-text-secondary)]">快捷命令</CardTitle>
|
||||
<CardTitle className="text-sm text-[var(--dg-text-secondary)]">{t('server.webshell.sde0277')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
||||
Reference in new issue
Block a user