diff --git a/PROJECT_EVALUATION.md b/PROJECT_EVALUATION.md
index 96bb0e8..9e96872 100644
--- a/PROJECT_EVALUATION.md
+++ b/PROJECT_EVALUATION.md
@@ -129,7 +129,7 @@
| ~~`api.ts` 1324 行/352 方法~~ → 已拆分为 12 个域文件 (api/kubernetes.ts, deploy.ts, monitor.ts 等) | ✅ 已解决 | 前端 | ~~P0~~ done |
| 前端 **1026 处** `any` 类型 | 类型安全缺失,重构风险高 | 前端全局 | ~~P0~~ **done** (any 1076→40, unused 25→6, console 93→1, tsc --noEmit=0) |
| ~~K8s 双路由体系(集群域+Kuboard 域)~~ → code 路由统一为主,ID 路由标记 deprecated 向后兼容 | ✅ 已解决 | 后端 | ~~P1~~ done |
-| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-Q26 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%) + NamespaceResourcePage 719L→300L (-58%) + ApprovalWorkflowPage 705L→167L (-76%) + PipelineRunDetail 678L→273L (-60%) + CloudServerImport 699L→404L (-42%,any[]→具体接口) + UnifiedQueryPanel 682L→529L (-22%) + ServerManager 652L→442L (-32%) + PipelineEditor 676L→406L (-40%) + RdpDialog 642L→442L (-31%) + DeploymentEditService 627L→394L (-37%) + TraceManager 605L→331L (-45%,修复 SpanTreeNode 内联定义反模式) + GrafanaDashboard 624L→78L (-87%,DRY 合并 Zabbix/Prometheus Tab + 移除文件级 eslint-disable + 类型安全升级) + **TopologyView 596L→472L (-21%,提取 TopologyCanvas/NodeDetailDialog/EditNodeDialog,移除文件级 eslint-disable + any→ClusterInfo 具体接口)** + **DeploymentEditAdvanced 590L→289L (-51%,提取 types(parser+builder 纯函数) + UpdateStrategyCard/TolerationsCard/PodMiscCard/PodSecurityCard/HostSettingsCard 5 个 Card 组件)** + **WorkloadViewPage 572L→348L (-39%,提取 types(parseWorkloadStats 纯函数) + shared-components(StatCard/LabelBadges/ContainerInfo) + UpdateStrategySelectorCards + PodInstancesTable + ScaleDialog)** + **CloudAccountManager 572L→308L + ExcelServerImport 560L→385L + PipelineCreateDialog 559L→498L** + **PodLogsViewer 553L→331L (-40%,提取 types(highlightKeyword 纯函数) + LogsHeader + ControlBar + LogContentArea + BottomStatusBar)**,累计提取 83 个独立组件文件** |
+| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-Q27 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%) + NamespaceResourcePage 719L→300L (-58%) + ApprovalWorkflowPage 705L→167L (-76%) + PipelineRunDetail 678L→273L (-60%) + CloudServerImport 699L→404L (-42%,any[]→具体接口) + UnifiedQueryPanel 682L→529L (-22%) + ServerManager 652L→442L (-32%) + PipelineEditor 676L→406L (-40%) + RdpDialog 642L→442L (-31%) + DeploymentEditService 627L→394L (-37%) + TraceManager 605L→331L (-45%,修复 SpanTreeNode 内联定义反模式) + GrafanaDashboard 624L→78L (-87%,DRY 合并 Zabbix/Prometheus Tab + 移除文件级 eslint-disable + 类型安全升级) + TopologyView 596L→472L (-21%) + DeploymentEditAdvanced 590L→289L (-51%) + WorkloadViewPage 572L→348L (-39%) + CloudAccountManager 572L→308L + ExcelServerImport 560L→385L + PipelineCreateDialog 559L→498L + PodLogsViewer 553L→331L (-40%) + **HostMonitor 547L→224L (-59%,提取 host-monitor/tabs/ 8 个文件 + TABS/PCT_REF_LINES 常量提取为模块级)**,累计提取 91 个独立组件文件** |
### 3.2 🟡 中优先级
diff --git a/ROADMAP.md b/ROADMAP.md
index e0b2f58..c343930 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -498,6 +498,47 @@ services/api.ts (1324L)
| Q24 | 前端巨型组件拆分(ExcelServerImport 560L→385L,提取 excel-import/types(6 个 CSV 纯函数) + StepIndicator + PreviewTable) | P0 | **done** |
| Q25 | 前端巨型组件拆分(PipelineCreateDialog 559L→498L,提取 pipeline-create/types(PRESETS/DEFAULT_TARGET) + AdvancedSettingsPanel(172L 受控展开面板)) | P0 | **done** |
| Q26 | 前端巨型组件拆分(PodLogsViewer 553L→331L,提取 pod-logs-viewer/types(highlightKeyword 纯函数) + LogsHeader + ControlBar + LogContentArea + BottomStatusBar) | P0 | **done** |
+| Q27 | 前端巨型组件拆分(HostMonitor 547L→224L (-59%),提取 host-monitor/tabs/ 8 个文件 (BasicTab + DiskTab + NetworkTab + PortMonitorTab + ProcessTable + FDUsageCard + ContextSwitchCard + ProcessTab),TABS 常量数组提取为模块级,PCT_REF_LINES 常量提升到 types.ts,端口/进程数组类型 JsonData→JsonData[] 类型安全升级) | P0 | **done** |
+
+---
+
+## Q27: 前端巨型组件拆分 — HostMonitor [done]
+
+**问题**: `HostMonitor.tsx`(547 行)是 Q26 拆分后项目最大的前端业务组件之一,包含主机监控详情页面的全部逻辑:7 个 Tab(基础监控/磁盘/网络/端口/进程/系统信息/健康探针)、~15 个 useState/useRef 状态管理、react-query 数据加载、端口策略 CRUD、以及 5 个 Tab 的内联 JSX(基础监控 6 MetricChartCard / 磁盘 5 MetricChartCard / 网络 8 MetricChartCard+DualLineChart / 端口监听表格+策略配置 / 进程 TOP 表格+FD 趋势图+CTX/IRQ 卡片)。虽然部分子组件已提取(MetricChartCard/DualLineChart/HealthProbeSection/SystemInfoSection/HostCoreMetricsStrip),但 Tab 内容区仍全部内联。单文件过大导致维护困难。
+
+**已完成 (2026-07-09)**:
+
+将 5 个 Tab 内容区提取为 `host-monitor/tabs/` 子目录下的独立文件:
+
+| 新文件 | 行数 | 职责 |
+|--------|------|------|
+| `BasicTab.tsx` | 35 | 基础监控 Tab(CPU/IO等待/内存/可用内存/Swap/系统负载 6 个 MetricChartCard),受控组件接收 metrics + latest + currentVal + aggType + pctRefLines |
+| `DiskTab.tsx` | 42 | 磁盘监控 Tab(磁盘使用/Inode/IO读写/IO繁忙度 5 个 MetricChartCard),受控组件接收 metrics + latest + currentVal + aggType + pctRefLines + diskIOInfo |
+| `NetworkTab.tsx` | 51 | 网络监控 Tab(TCP EST/TW/CW/总连接 4 MetricChartCard + DualLineChart 网卡带宽 + 接收/发送错误 2 MetricChartCard),受控组件接收 metrics + netInfo + currentVal + aggType |
+| `PortMonitorTab.tsx` | 73 | 端口监控 Tab(策略配置按钮 + 白名单/黑名单徽章 + 监听端口表格),受控组件接收 portInfo + portPolicyMode + portWhitelist + onOpenPortPolicy |
+| `ProcessTable.tsx` | 86 | 进程 TOP 表格(CMD/PID/内存%/CPU%/FDs 5 列 + 复制命令按钮 + clipboard API try/catch),纯展示组件接收 processTop |
+| `FDUsageCard.tsx` | 79 | FD 文件句柄卡片(已分配/Max/使用率 3 项指标 + ZoomableAreaChart 趋势图 + ChartToolbar 缩放控制),受控组件接收 fdInfo + fdMetrics + aggType + fdChartRef + fdChartZoomed + setFdChartZoomed |
+| `ContextSwitchCard.tsx` | 55 | 上下文切换/中断卡片(CTX/IRQ 大数字卡片 + CTX/IRQ 比例条),纯展示组件接收 csInfo |
+| `ProcessTab.tsx` | 39 | 进程监控 Tab 编排器(ProcessTable + FDUsageCard + ContextSwitchCard 两列布局),接收全部进程 Tab props 并分发 |
+
+主文件 `HostMonitor.tsx` 从 **547 行降至 224 行**(-59%),仅保留:状态管理(selectedHost/activeTab/timeRange/aggType/portPolicy 4 state + fdChartRef/fdChartZoomed 2 ref)、react-query 数据加载(useHostList/useHostHistory/usePortPolicy)、useEffect(portPolicy 同步 + hosts 自动选择)、handleSavePortPolicy 回调、useMemo 派生数据(history/spaMinutes/actualDataSpan/latest/metrics)、JSON 解析(netInfo/portInfo/diskIOInfo/swapInfo/processTop/fdInfo/csInfo/heartbeatInfo)、currentVal 闭包函数、toolbar JSX、TABS 常量数组、monitorBody Tabs 骨架(7 TabsContent 分发到子组件)、dialogs JSX、embedded/full 渲染分支。
+
+**关键改进**:
+- **TABS 常量提取**: 原 7 个 Tab 的 id/label/icon 定义内联在 `monitorBody` JSX 的 `.map()` 数组中(每次 render 重建),提取为模块级 `TABS` 常量数组,消除重复创建。
+- **PCT_REF_LINES 常量提升**: 原 `pctRefLines` 在组件函数体内定义(每次 render 重建),提升到 `types.ts` 作为模块级导出常量 `PCT_REF_LINES`,同时定义 `RefLine` 接口供 Tab 组件类型约束。
+- **类型安全升级**: `portInfo` 和 `processTop` 从 `JsonData`(单个对象)改为 `JsonData[]`(对象数组),并加 `as unknown as JsonData[]` 类型断言解决 `parseJSON` 返回类型与数组类型不兼容的 TS2345 错误。
+
+**验收标准**:
+- [x] HostMonitor.tsx 从 547 行降至 **224 行**(-59%)
+- [x] 8 个提取的文件全部 < 90 行(最大 ProcessTable 86 行,ProcessTab 编排器仅 39 行)
+- [x] `tsc -b` 零错误
+- [x] `npm run build` 零错误(✓ built in 17.64s)
+- [x] ESLint errors 保持 **0**(HostMonitor + host-monitor/tabs/ 目录零 errors/warnings)
+- [x] ESLint warnings 保持 **65**(无新增)
+- [x] 全局 ESLint any 保持 **0**
+- [x] `go build` 零错误
+- [x] 17/17 集成回归测试通过(零回归)
+- [x] 主机监控详情页功能不变(7 个 Tab 全部正常:基础监控/磁盘/网络/端口/进程/系统信息/健康探针)
---
diff --git a/frontend/src/pages/monitor/HostMonitor.tsx b/frontend/src/pages/monitor/HostMonitor.tsx
index 8d34d96..851c1a6 100644
--- a/frontend/src/pages/monitor/HostMonitor.tsx
+++ b/frontend/src/pages/monitor/HostMonitor.tsx
@@ -6,33 +6,38 @@ import { useQueryClient } from '@tanstack/react-query';
import { agentApi } from '@/api';
import { useHostList, useHostHistory, usePortPolicy } from '@/hooks/api/useHostMonitor';
import { queryKeys } from '@/hooks/api/queryKeys';
-import { Card, CardContent } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
-import { Badge } from '@/components/ui/badge';
-import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { PageShell } from '@/components/layout/PageShell';
import { PageLoading } from '@/components/layout/PageLoading';
-import {
- Cpu, HardDrive, Network, Activity, Server,
- Info, Monitor, Disc, Clock, List, HeartPulse, RefreshCw, XCircle,
-} from 'lucide-react';
-import { ZoomableAreaChart, type ZoomableChartHandle } from './host-monitor/ZoomableAreaChart';
-import { ChartToolbar } from './host-monitor/ChartToolbar';
-import { MetricIconBox } from './host-monitor/MetricIconBox';
-import { DEFAULT_TIME_RANGE, type AggType, type HostRecord, type HostMonitorTimeRange } from './host-monitor/types';
+import { Monitor, HardDrive, Network, Server, Info, List, HeartPulse } from 'lucide-react';
+import type { ZoomableChartHandle } from './host-monitor/ZoomableAreaChart';
+import { DEFAULT_TIME_RANGE, PCT_REF_LINES, type AggType, type HostRecord, type HostMonitorTimeRange } from './host-monitor/types';
import { HostTimeRangePicker } from './host-monitor/HostTimeRangePicker';
import { getRangeSpanMinutes } from './host-monitor/timeRange';
-import { parseJSON, formatBPS, buildMetricsFromHistory, getCurrentVal, filterHistoryByRange, getActualDataSpan } from './host-monitor/utils';
-import { MetricChartCard } from './host-monitor/MetricChartCard';
-import { DualLineChart } from './host-monitor/DualLineChart';
+import { parseJSON, buildMetricsFromHistory, getCurrentVal, filterHistoryByRange, getActualDataSpan } from './host-monitor/utils';
import { HealthProbeSection } from './host-monitor/HealthProbeSection';
import { PortPolicyDialog } from './host-monitor/PortPolicyDialog';
import { SystemInfoSection } from './host-monitor/SystemInfoSection';
import { HostCoreMetricsStrip } from './host-monitor/HostCoreMetricsStrip';
+import { BasicTab } from './host-monitor/tabs/BasicTab';
+import { DiskTab } from './host-monitor/tabs/DiskTab';
+import { NetworkTab } from './host-monitor/tabs/NetworkTab';
+import { PortMonitorTab } from './host-monitor/tabs/PortMonitorTab';
+import { ProcessTab } from './host-monitor/tabs/ProcessTab';
import type { JsonData } from '@/types/common';
import i18n from '@/i18n';
+const TABS = [
+ { id: 'basic', label: i18n.t('monitor.hostmonitortsx.s00303869'), icon: },
+ { id: 'disk', label: i18n.t('monitor.hostmonitortsx.s5dd0a090'), icon: },
+ { id: 'network', label: i18n.t('monitor.hostmonitortsx.s59805cc8'), icon: },
+ { id: 'ports', label: i18n.t('agent.portMonitor'), icon: },
+ { id: 'process', label: i18n.t('monitor.hostProcess'), icon:
},
+ { id: 'system', label: i18n.t('system.systemInfo'), icon: },
+ { id: 'health_probes', label: i18n.t('kubernetes.container.probes'), icon: },
+];
+
export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
const qc = useQueryClient();
const [searchParams] = useSearchParams();
@@ -82,19 +87,10 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
} finally { setPortPolicySaving(false); }
};
- const history = useMemo(
- () => filterHistoryByRange(rawHistory, timeRange),
- [rawHistory, timeRange],
- );
+ const history = useMemo(() => filterHistoryByRange(rawHistory, timeRange), [rawHistory, timeRange]);
const spanMinutes = getRangeSpanMinutes(timeRange);
- const timeConfig = useMemo(
- () => ({ buckets: timeRange.buckets, spanMinutes }),
- [timeRange.buckets, spanMinutes],
- );
- const actualDataSpan = useMemo(
- () => getActualDataSpan(history, timeRange),
- [history, timeRange],
- );
+ const timeConfig = useMemo(() => ({ buckets: timeRange.buckets, spanMinutes }), [timeRange.buckets, spanMinutes]);
+ const actualDataSpan = useMemo(() => getActualDataSpan(history, timeRange), [history, timeRange]);
const loading = hostsLoading || historyLoading;
const serverInfo = (hosts.find((h) => h.hostname === selectedHost) || null) as HostRecord | null;
const latest = useMemo((): HostRecord => {
@@ -106,10 +102,10 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
const metrics = useMemo(() => buildMetricsFromHistory(history, timeConfig), [history, timeConfig]);
const netInfo = parseJSON(latest?.net_info);
- const portInfo: JsonData = Array.isArray(latest?.port_info) ? latest.port_info : (Array.isArray(parseJSON(latest?.port_info)) ? parseJSON(latest?.port_info) : []);
+ const portInfo: JsonData[] = Array.isArray(latest?.port_info) ? latest.port_info as JsonData[] : (Array.isArray(parseJSON(latest?.port_info)) ? parseJSON(latest?.port_info) as unknown as JsonData[] : []);
const diskIOInfo = parseJSON(latest?.disk_io_info);
const swapInfo = parseJSON(latest?.swap_info);
- const processTop: JsonData = Array.isArray(parseJSON(latest?.process_top_info)) ? parseJSON(latest?.process_top_info) : [];
+ const processTop: JsonData[] = Array.isArray(parseJSON(latest?.process_top_info)) ? parseJSON(latest?.process_top_info) as unknown as JsonData[] : [];
const fdInfo = parseJSON(latest?.fd_info);
const csInfo = parseJSON(latest?.cs_info);
const heartbeatInfo = parseJSON(latest?.heartbeat_info);
@@ -117,11 +113,6 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
const currentVal = (key: string, fallback = '--') =>
getCurrentVal(latest, netInfo, diskIOInfo, swapInfo, fdInfo, key, fallback);
- const pctRefLines = [
- { value: 80, label: '80%', color: 'var(--dg-yellow)' },
- { value: 90, label: '90%', color: 'var(--dg-red)' },
- ];
-
const toolbar = (
@@ -143,9 +134,7 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {