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