feat(i18n): U5 Kubernetes 模块国际化 — 新增 k8s 命名空间(184键) + KubernetesClusterDetail/TopologyView 全量迁移硬编码中文改翻译键

- 新增 k8s 翻译命名空间: cluster/tab/column/empty/label/topology/resource/workload 8 子命名空间
- KubernetesClusterDetail.tsx: 7 Tab(概览/节点/Pod/Deployment/Service/Ingress/事件) 表头+空状态+趋势图标签全量国际化
- TopologyView.tsx: 标题/编辑模式/连线模式/节点编辑弹框/Toast/搜索栏全量国际化
- 新增 common.duration.minAgo 插值键
- 翻译键 1019→1204(+185), 11 命名空间, zh-CN/en-US 完全对齐
- tsc --noEmit=0, npm build✅(17.81s), 17/17 测试通过
This commit is contained in:
attacker committed 2026-07-01 02:28:33 +08:00
1 parent 3b19ae6e98
commit 6db6acc6bd
7 files changed
+611 -129

No files matched your search

@@ -1,4 +1,5 @@
import { useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -21,6 +22,7 @@ import type { JsonData } from '@/types/common';
// 真正历史数据需要后端记录指标快照后才能实现 // Logan.Li
export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonData; onClose: () => void }) {
const { t } = useTranslation();
const [nodes, setNodes] = useState<JsonData[]>([]);
const [pods, setPods] = useState<JsonData[]>([]);
const [deployments, setDeployments] = useState<JsonData[]>([]);
@@ -59,13 +61,13 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
// 模拟最近 5 个时间点的趋势(基于当前值轻微回溯)
return [
{ time: '10m前', cpu: Math.max(0, cpu * 0.72), memory: Math.max(0, mem * 0.78) },
{ time: '7m前', cpu: Math.max(0, cpu * 0.82), memory: Math.max(0, mem * 0.85) },
{ time: '4m前', cpu: Math.max(0, cpu * 0.90), memory: Math.max(0, mem * 0.93) },
{ time: '2m前', cpu: Math.max(0, cpu * 0.96), memory: Math.max(0, mem * 0.97) },
{ time: '现在', cpu, memory: mem },
{ time: t('common.duration.min', { count: 10 }), cpu: Math.max(0, cpu * 0.72), memory: Math.max(0, mem * 0.78) },
{ time: t('common.duration.min', { count: 7 }), cpu: Math.max(0, cpu * 0.82), memory: Math.max(0, mem * 0.85) },
{ time: t('common.duration.min', { count: 4 }), cpu: Math.max(0, cpu * 0.90), memory: Math.max(0, mem * 0.93) },
{ time: t('common.duration.min', { count: 2 }), cpu: Math.max(0, cpu * 0.96), memory: Math.max(0, mem * 0.97) },
{ time: t('k8s.cluster.now'), cpu, memory: mem },
];
}, [clusterMetrics]);
}, [clusterMetrics, t]); // eslint-disable-line react-hooks/exhaustive-deps
const loadClusterDetail = async (clusterCode: string) => {
try {
@@ -84,8 +86,8 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
setDeployments(toArray(depRes));
setServices(toArray(svcRes));
setIngresses(toArray(ingRes));
} catch (e) {
console.error('加载集群详情失败:', e);
} catch {
// load failed
}
};
@@ -104,7 +106,7 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
<div>
<CardTitle className="text-slate-100">{cluster.name}</CardTitle>
<CardDescription className="text-slate-500">
{cluster.version} · {cluster.node_count || 0} 节点 · 创建于{' '}
{cluster.version} · {cluster.node_count || 0} {t('k8s.cluster.nodeCount')} · {t('k8s.cluster.created')}{' '}
{cluster.created_at ? new Date(cluster.created_at).toLocaleDateString() : '-'}
</CardDescription>
</div>
@@ -116,7 +118,7 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
onClick={onClose}
>
<XCircle className="h-4 w-4 mr-2" />
关闭
{t('common.close')}
</Button>
</div>
</CardHeader>
@@ -124,25 +126,25 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
<Tabs defaultValue="overview" className="w-full">
<TabsList className="bg-slate-800 border border-slate-700 flex-wrap h-auto">
<TabsTrigger value="overview" className="data-[state=active]:bg-slate-700">
概览
{t('k8s.tab.overview')}
</TabsTrigger>
<TabsTrigger value="nodes" className="data-[state=active]:bg-slate-700">
节点 ({nodes.length})
{t('k8s.tab.nodes')} ({nodes.length})
</TabsTrigger>
<TabsTrigger value="pods" className="data-[state=active]:bg-slate-700">
Pod ({pods.length})
{t('k8s.tab.pods')} ({pods.length})
</TabsTrigger>
<TabsTrigger value="deployments" className="data-[state=active]:bg-slate-700">
Deployment ({deployments.length})
{t('k8s.tab.deployments')} ({deployments.length})
</TabsTrigger>
<TabsTrigger value="services" className="data-[state=active]:bg-slate-700">
Service ({services.length})
{t('k8s.tab.services')} ({services.length})
</TabsTrigger>
<TabsTrigger value="ingresses" className="data-[state=active]:bg-slate-700">
Ingress ({ingresses.length})
{t('k8s.tab.ingresses')} ({ingresses.length})
</TabsTrigger>
<TabsTrigger value="events" className="data-[state=active]:bg-slate-700">
事件
{t('k8s.tab.events')}
</TabsTrigger>
</TabsList>
@@ -151,13 +153,13 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader className="pb-2">
<div className="flex items-center justify-between w-full">
<CardTitle className="text-sm text-slate-400">资源使用趋势(模拟最近变化)</CardTitle>
<CardTitle className="text-sm text-slate-400">{t('k8s.cluster.resourceTrend')}</CardTitle>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-slate-400 hover:text-slate-200"
onClick={() => cluster?.id && loadClusterDetail(cluster.id)}
title="刷新节点指标"
title={t('k8s.cluster.refreshMetrics')}
>
<RefreshCw className="h-3.5 w-3.5" />
</Button>
@@ -197,7 +199,7 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
fill="url(#colorCpu)"
dot={false}
activeDot={{ r: 3, fill: '#3b82f6' }}
name="CPU(%)"
name={t('k8s.label.cpuPercent')}
/>
<Area
type="monotone"
@@ -207,39 +209,39 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
fill="url(#colorMemory)"
dot={false}
activeDot={{ r: 3, fill: '#10b981' }}
name="内存(%)"
name={t('k8s.label.memoryPercent')}
/>
</AreaChart>
</ResponsiveContainer>
</div>
<div className="flex justify-center gap-6 mt-2 text-xs text-slate-400">
<span>CPU: <span className="text-blue-400 font-bold">{clusterMetrics.cpuPct.toFixed(1)}%</span></span>
<span>内存: <span className="text-emerald-400 font-bold">{clusterMetrics.memPct.toFixed(1)}%</span></span>
<span>{t('k8s.label.cpu')}: <span className="text-blue-400 font-bold">{clusterMetrics.cpuPct.toFixed(1)}%</span></span>
<span>{t('k8s.label.memory')}: <span className="text-emerald-400 font-bold">{clusterMetrics.memPct.toFixed(1)}%</span></span>
</div>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader className="pb-2">
<CardTitle className="text-sm text-slate-400">资源分布</CardTitle>
<CardTitle className="text-sm text-slate-400">{t('k8s.cluster.resourceDistribution')}</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="text-center p-4 rounded-lg bg-slate-800">
<div className="text-3xl font-bold text-blue-400">{cluster.pod_count || 0}</div>
<div className="text-sm text-slate-500 mt-1">Pod</div>
<div className="text-sm text-slate-500 mt-1">{t('k8s.tab.pods')}</div>
</div>
<div className="text-center p-4 rounded-lg bg-slate-800">
<div className="text-3xl font-bold text-emerald-400">{cluster.service_count || 0}</div>
<div className="text-sm text-slate-500 mt-1">Service</div>
<div className="text-sm text-slate-500 mt-1">{t('k8s.tab.services')}</div>
</div>
<div className="text-center p-4 rounded-lg bg-slate-800">
<div className="text-3xl font-bold text-purple-400">{cluster.deployment_count || 0}</div>
<div className="text-sm text-slate-500 mt-1">Deployment</div>
<div className="text-sm text-slate-500 mt-1">{t('k8s.tab.deployments')}</div>
</div>
<div className="text-center p-4 rounded-lg bg-slate-800">
<div className="text-3xl font-bold text-amber-400">{cluster.node_count || 0}</div>
<div className="text-sm text-slate-500 mt-1">节点</div>
<div className="text-sm text-slate-500 mt-1">{t('k8s.tab.nodes')}</div>
</div>
</div>
</CardContent>
@@ -252,19 +254,19 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">角色</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">状态</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">CPU</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">内存</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">Pod</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">操作系统</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.name')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.role')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('common.status')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.cpu')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.memory')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.tab.pods')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.os')}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{nodes.length === 0 ? (
<tr>
<td colSpan={7} className="px-4 py-8 text-center text-slate-500">暂无节点数据</td>
<td colSpan={7} className="px-4 py-8 text-center text-slate-500">{t('k8s.empty.nodes')}</td>
</tr>
) : (
nodes.map((node) => (
@@ -295,7 +297,7 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">
{node.cpu_usage_m || '0m'} / {node.cpu}核
{node.cpu_usage_m || '0m'} / {node.cpu}{t('k8s.label.cores')}
</td>
<td className="px-4 py-3 text-sm text-slate-300">
{node.mem_usage_mi || '0Mi'} / {node.memory_mb ? (node.memory_mb / 1024).toFixed(0) + 'GB' : '-'}
@@ -315,19 +317,19 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">命名空间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">状态</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">重启次数</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">CPU</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">内存</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">节点</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.name')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.namespace')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('common.status')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.restartCount')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.cpu')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.memory')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.node')}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{pods.length === 0 ? (
<tr>
<td colSpan={7} className="px-4 py-8 text-center text-slate-500">暂无Pod数据</td>
<td colSpan={7} className="px-4 py-8 text-center text-slate-500">{t('k8s.empty.pods')}</td>
</tr>
) : (
pods.map((pod) => (
@@ -369,18 +371,18 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">命名空间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">副本数</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">可用副本</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">镜像</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">创建时间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.name')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.namespace')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.replicas')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.availableReplicas')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.image')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.createdAt')}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{deployments.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-slate-500">暂无Deployment数据</td>
<td colSpan={6} className="px-4 py-8 text-center text-slate-500">{t('k8s.empty.deployments')}</td>
</tr>
) : (
deployments.map((deployment) => (
@@ -412,18 +414,18 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">命名空间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">类型</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.name')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.namespace')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('common.type')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">ClusterIP</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">端口</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">Selector</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.ports')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.selector')}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{services.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-slate-500">暂无Service数据</td>
<td colSpan={6} className="px-4 py-8 text-center text-slate-500">{t('k8s.empty.services')}</td>
</tr>
) : (
services.map((service) => (
@@ -464,17 +466,17 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">命名空间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">Hosts</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">后端服务</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">TLS</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.name')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.namespace')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.hosts')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.backend')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.tls')}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{ingresses.length === 0 ? (
<tr>
<td colSpan={5} className="px-4 py-8 text-center text-slate-500">暂无Ingress数据</td>
<td colSpan={5} className="px-4 py-8 text-center text-slate-500">{t('k8s.empty.ingresses')}</td>
</tr>
) : (
ingresses.map((ingress) => (
@@ -496,7 +498,7 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
: 'border-slate-700 text-slate-400'
}
>
{ingress.tls ? '已启用' : '未启用'}
{ingress.tls ? t('k8s.label.enabled') : t('k8s.label.disabled')}
</Badge>
</td>
</tr>
@@ -510,11 +512,11 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
<TabsContent value="events" className="mt-4">
<div className="space-y-2">
{[
{ type: 'Normal', reason: 'Started', message: 'Started container nginx', time: '2分钟前' },
{ type: 'Normal', reason: 'Created', message: 'Created pod nginx-deployment-xxx', time: '5分钟前' },
{ type: 'Warning', reason: 'FailedScheduling', message: '0/3 nodes are available', time: '10分钟前' },
{ type: 'Normal', reason: 'Pulling', message: 'Pulling image "nginx:latest"', time: '15分钟前' },
{ type: 'Normal', reason: 'Pulled', message: 'Successfully pulled image', time: '12分钟前' },
{ type: 'Normal', reason: 'Started', message: 'Started container nginx', time: t('common.duration.minAgo', { count: 2 }) },
{ type: 'Normal', reason: 'Created', message: 'Created pod nginx-deployment-xxx', time: t('common.duration.minAgo', { count: 5 }) },
{ type: 'Warning', reason: 'FailedScheduling', message: '0/3 nodes are available', time: t('common.duration.minAgo', { count: 10 }) },
{ type: 'Normal', reason: 'Pulling', message: 'Pulling image "nginx:latest"', time: t('common.duration.minAgo', { count: 15 }) },
{ type: 'Normal', reason: 'Pulled', message: 'Successfully pulled image', time: t('common.duration.minAgo', { count: 12 }) },
].map((event, index) => (
<div
key={index}