+
+ {/* 表头 */}
+
名称
状态
@@ -39,130 +61,155 @@ export function PodsTab({
操作
-
- {pods.map(pod => {
- const expanded = expandedPod === pod.name;
- const nodeName = pod.node_name || pod.node;
- const sc = statusColor(pod.status);
- const containers = (pod as Record
).containers as any[] | undefined;
+ {/* 虚拟滚动列表 */}
+
+
+ {virtualRows.map((vr) => {
+ const pod = pods[vr.index];
+ if (!pod) return null;
+ const expanded = expandedPod === pod.name;
+ const nodeName = pod.node_name || pod.node;
+ const sc = statusColor(pod.status);
+ const containers = (pod as unknown as Record
).containers as Record[] | undefined;
- return (
-
- {/* 列表行:紧凑信息,不重复展示完整时间线 // Logan.Li */}
+ return (
-
+
+ {/* 行数统计 */}
+ {pods.length > 0 && (
+
+ 共 {pods.length} 个容器组 {pods.length > 200 && '· 虚拟滚动渲染'}
+
+ )}
);
-}
\ No newline at end of file
+}
diff --git a/frontend/src/hooks/useVirtualRows.ts b/frontend/src/hooks/useVirtualRows.ts
new file mode 100644
index 0000000..737589d1
--- /dev/null
+++ b/frontend/src/hooks/useVirtualRows.ts
@@ -0,0 +1,74 @@
+// useVirtualRows — 虚拟滚动 Hook,基于 @tanstack/react-virtual
+// 用于大列表(1000+ 行)场景的性能优化,只渲染可见区域的行
+// 用法:
+// const { scrollRef, virtualRows, totalHeight } = useVirtualRows({
+// count: data.length, estimateSize: () => 40, overscan: 10,
+// });
+import { useRef, useCallback } from 'react';
+import { useVirtualizer } from '@tanstack/react-virtual';
+
+export interface VirtualRowsOptions {
+ count: number;
+ estimateSize: (index: number) => number;
+ overscan?: number;
+ /** 滚动容器的 padding 顶部像素(影响虚拟化偏移计算) */
+ paddingTop?: number;
+}
+
+export interface VirtualRowsResult {
+ /** 绑定到滚动容器 div 的 ref */
+ scrollRef: React.RefObject;
+ /** 当前可见+overscan 范围内的虚拟行 */
+ virtualRows: ReturnType['getVirtualItems']>;
+ /** 虚拟容器的总高度(用于设置 spacer 高度) */
+ totalHeight: number;
+ /** 滚动到指定 index */
+ scrollToIndex: (index: number) => void;
+}
+
+/**
+ * 虚拟滚动 Hook
+ *
+ * ```tsx
+ * const { scrollRef, virtualRows, totalHeight } = useVirtualRows({
+ * count: items.length, estimateSize: () => 40, overscan: 8,
+ * });
+ * return (
+ *
+ *
+ * {virtualRows.map(vr => (
+ *
+ * {renderRow(items[vr.index])}
+ *
+ * ))}
+ *
+ *
+ * );
+ * ```
+ */
+export function useVirtualRows({
+ count,
+ estimateSize,
+ overscan = 8,
+}: VirtualRowsOptions): VirtualRowsResult {
+ const scrollRef = useRef(null);
+
+ const virtualizer = useVirtualizer({
+ count,
+ getScrollElement: () => scrollRef.current,
+ estimateSize,
+ overscan,
+ });
+
+ const virtualRows = virtualizer.getVirtualItems();
+ const totalHeight = virtualizer.getTotalSize();
+
+ const scrollToIndex = useCallback(
+ (index: number) => {
+ virtualizer.scrollToIndex(index, { align: 'start' });
+ },
+ [virtualizer],
+ );
+
+ return { scrollRef, virtualRows, totalHeight, scrollToIndex };
+}
diff --git a/frontend/src/pages/AuditOperationsTab.tsx b/frontend/src/pages/AuditOperationsTab.tsx
index ed689f7..057f34e 100644
--- a/frontend/src/pages/AuditOperationsTab.tsx
+++ b/frontend/src/pages/AuditOperationsTab.tsx
@@ -1,17 +1,11 @@
// 操作审计 Tab — 增强版审计日志列表,含筛选/导出/统计/清除
-import { useState, useEffect, useCallback, useMemo } from 'react';
+// v2: 虚拟滚动渲染,支持万级日志流畅查看
+import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
+import { useVirtualizer } from '@tanstack/react-virtual';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from '@/components/ui/table';
import {
Search,
RefreshCw,
@@ -148,6 +142,20 @@ export function AuditOperationsTab() {
setDetailLog(log);
};
+ // 虚拟滚动
+ const scrollRef = useRef(null);
+ const GRID_COLS = 'minmax(140px,1fr) minmax(80px,1fr) minmax(100px,1fr) minmax(100px,1fr) minmax(140px,1fr) minmax(100px,1fr) minmax(80px,0.8fr) 48px';
+
+ const virtualizer = useVirtualizer({
+ count: filteredLogs.length,
+ getScrollElement: () => scrollRef.current,
+ estimateSize: () => 52,
+ overscan: 10,
+ });
+
+ const virtualRows = virtualizer.getVirtualItems();
+ const totalHeight = virtualizer.getTotalSize();
+
return (
{/* Filters */}
@@ -208,7 +216,7 @@ export function AuditOperationsTab() {
- {/* Logs Table */}
+ {/* Logs Table — 虚拟滚动 */}
@@ -222,64 +230,82 @@ export function AuditOperationsTab() {
暂无操作日志
) : (
-
-
-
- 时间
- 用户
- 模块
- 操作类型
- 资源
- IP
- 结果
- 详情
-
-
-
- {filteredLogs.map((log) => (
-
-
-
- {log.createdAt ? new Date(log.createdAt).toLocaleString() : '--'}
-
-
-
- {log.username || '--'}
-
-
- {log.module || '--'}
-
-
-
- {log.action || '--'}
-
-
-
-
- {log.resource || '--'}
-
-
-
- {log.ip || '--'}
-
- {getLogResultBadge(log.result)}
-
-
-
viewDetail(log)}
- title="查看详情"
- >
-
-
+ <>
+ {/* 表头 */}
+
+ 时间
+ 用户
+ 模块
+ 操作类型
+ 资源
+ IP
+ 结果
+ 详情
+
+
+ {/* 虚拟滚动列表 */}
+
+
+ {virtualRows.map((vr) => {
+ const log = filteredLogs[vr.index];
+ if (!log) return null;
+ return (
+
+
+ {log.createdAt ? new Date(log.createdAt).toLocaleString() : '--'}
+
+
{log.username || '--'}
+
{log.module || '--'}
+
+
+ {log.action || '--'}
+
+
+
+ {log.resource || '--'}
+
+
{log.ip || '--'}
+
{getLogResultBadge(log.result)}
+
+ viewDetail(log)}
+ title="查看详情"
+ >
+
+
+
-
-
- ))}
-
-
+ );
+ })}
+
+
+
+ {/* 行数统计 */}
+
+ 共 {filteredLogs.length} 条日志 {filteredLogs.length > 200 && '· 虚拟滚动渲染'}
+
+ >
)}
diff --git a/frontend/src/pages/terminal-audit/SessionCommandsPanel.tsx b/frontend/src/pages/terminal-audit/SessionCommandsPanel.tsx
index 3850e36..59bf5f3 100644
--- a/frontend/src/pages/terminal-audit/SessionCommandsPanel.tsx
+++ b/frontend/src/pages/terminal-audit/SessionCommandsPanel.tsx
@@ -1,21 +1,17 @@
// 终端审计 — 会话命令子面板 // Logan.Li
+// v2: 虚拟滚动,支持万级命令记录流畅查看
-import { useMemo, useState } from 'react';
+import { useMemo, useState, useRef } from 'react';
+import { useVirtualizer } from '@tanstack/react-virtual';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from '@/components/ui/table';
import { Search, RefreshCw } from 'lucide-react';
import { useSessionCommands } from '@/hooks/api';
import { formatSize } from './helpers';
+const GRID_COLS = 'minmax(150px,1fr) minmax(80px,0.8fr) minmax(200px,2fr) minmax(100px,0.8fr) minmax(80px,0.5fr)';
+
export function SessionCommandsPanel() {
const [keyword, setKeyword] = useState('');
const [sessionIdInput, setSessionIdInput] = useState('');
@@ -23,7 +19,7 @@ export function SessionCommandsPanel() {
const queryParams = useMemo(
() => ({
page: 1,
- page_size: 200,
+ page_size: 500,
...(sessionIdInput ? { session_id: Number(sessionIdInput) } : {}),
}),
[sessionIdInput],
@@ -40,6 +36,17 @@ export function SessionCommandsPanel() {
);
});
+ // 虚拟滚动
+ const scrollRef = useRef
(null);
+ const virtualizer = useVirtualizer({
+ count: filtered.length,
+ getScrollElement: () => scrollRef.current,
+ estimateSize: () => 48,
+ overscan: 10,
+ });
+ const virtualRows = virtualizer.getVirtualItems();
+ const totalHeight = virtualizer.getTotalSize();
+
return (
<>
@@ -81,46 +88,66 @@ export function SessionCommandsPanel() {
暂无会话命令记录
) : (
-
-
-
- 时间
- 用户
- 命令
- 输出大小
- 关联会话
-
-
-
- {filtered.map((c) => (
-
-
-
- {c.created_at ? new Date(c.created_at).toLocaleString() : '--'}
-
-
-
- {c.username || '--'}
-
-
-
- {c.command || '--'}
-
-
-
- {formatSize(c.output_size)}
-
-
- #{c.session_id}
-
-
- ))}
-
-
+ <>
+ {/* 表头 */}
+
+ 时间
+ 用户
+ 命令
+ 输出大小
+ 关联会话
+
+
+ {/* 虚拟滚动列表 */}
+
+
+ {virtualRows.map((vr) => {
+ const c = filtered[vr.index];
+ if (!c) return null;
+ return (
+
+
+ {c.created_at ? new Date(c.created_at).toLocaleString() : '--'}
+
+ {c.username || '--'}
+
+ {c.command || '--'}
+
+ {formatSize(c.output_size)}
+ #{c.session_id}
+
+ );
+ })}
+
+
+
+ {/* 行数统计 */}
+
+ 共 {filtered.length} 条命令 {filtered.length > 200 && '· 虚拟滚动渲染'}
+
+ >
)}