mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-01 17:51:24 +08:00
⚡️ perf(ui): 优化数据页滚动与编辑响应
- 优化 DataGrid 虚拟滚动横向同步与外部滚动条宽度计算 - 降低 v2 数据表内容容器的重绘与持久化写入开销 - 拆分 Tab 内容渲染并收敛 QueryEditor 对活跃标签的订阅 - 修复虚拟编辑态与单元格右键菜单的共享渲染路径 - 调整 v2 数据表编辑态样式并补齐性能复现 harness 对照能力 - 补充 DataGrid 布局与滚动相关回归测试
This commit is contained in:
@@ -1,15 +1,111 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Alert, Button, Card, InputNumber, Select, Space, Typography } from 'antd';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Alert, Button, Card, InputNumber, Segmented, Select, Space, Typography } from 'antd';
|
||||
|
||||
import DataGrid, { GONAVI_ROW_KEY } from '../components/DataGrid';
|
||||
import { useStore } from '../store';
|
||||
import type { EditRowLocator } from '../utils/rowLocator';
|
||||
import type { DataTableDensity } from '../utils/dataGridDisplay';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
type HarnessUiVersion = 'legacy' | 'v2';
|
||||
type HarnessTheme = 'light' | 'dark';
|
||||
|
||||
type HarnessRow = Record<string, any> & {
|
||||
[GONAVI_ROW_KEY]: string;
|
||||
};
|
||||
|
||||
type HarnessRuntimeConfig = {
|
||||
uiVersion: HarnessUiVersion;
|
||||
density: DataTableDensity;
|
||||
theme: HarnessTheme;
|
||||
uiScale: number;
|
||||
fontSize: number;
|
||||
};
|
||||
|
||||
type HarnessRestoreSnapshot = {
|
||||
appearance: ReturnType<typeof useStore.getState>['appearance'];
|
||||
theme: ReturnType<typeof useStore.getState>['theme'];
|
||||
uiScale: number;
|
||||
fontSize: number;
|
||||
bodyUiVersion: string | null;
|
||||
bodyTheme: string | null;
|
||||
bodyFontSize: string;
|
||||
rootVars: Record<string, string>;
|
||||
};
|
||||
|
||||
const hasHarnessAppearanceDrift = (
|
||||
appearance: ReturnType<typeof useStore.getState>['appearance'],
|
||||
uiVersion: HarnessUiVersion,
|
||||
density: DataTableDensity,
|
||||
): boolean => (
|
||||
appearance.uiVersion !== uiVersion
|
||||
|| appearance.dataTableDensity !== density
|
||||
|| appearance.dataTableFontSize !== null
|
||||
|| appearance.dataTableFontSizeFollowGlobal !== true
|
||||
);
|
||||
|
||||
const DEFAULT_HARNESS_CONFIG: HarnessRuntimeConfig = {
|
||||
uiVersion: 'legacy',
|
||||
density: 'comfortable',
|
||||
theme: 'light',
|
||||
uiScale: 1,
|
||||
fontSize: 14,
|
||||
};
|
||||
|
||||
const clampHarnessUiScale = (value: unknown): number => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return DEFAULT_HARNESS_CONFIG.uiScale;
|
||||
}
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) return DEFAULT_HARNESS_CONFIG.uiScale;
|
||||
return Math.min(1.25, Math.max(0.8, numeric));
|
||||
};
|
||||
|
||||
const clampHarnessFontSize = (value: unknown): number => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return DEFAULT_HARNESS_CONFIG.fontSize;
|
||||
}
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) return DEFAULT_HARNESS_CONFIG.fontSize;
|
||||
return Math.min(20, Math.max(12, Math.round(numeric)));
|
||||
};
|
||||
|
||||
const readHarnessRuntimeConfig = (): HarnessRuntimeConfig => {
|
||||
if (typeof window === 'undefined') {
|
||||
return { ...DEFAULT_HARNESS_CONFIG };
|
||||
}
|
||||
try {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const uiVersion = searchParams.get('uiVersion') === 'v2' ? 'v2' : DEFAULT_HARNESS_CONFIG.uiVersion;
|
||||
const densityRaw = searchParams.get('density');
|
||||
const density: DataTableDensity = densityRaw === 'compact' || densityRaw === 'standard'
|
||||
? densityRaw
|
||||
: DEFAULT_HARNESS_CONFIG.density;
|
||||
const theme = searchParams.get('theme') === 'dark' ? 'dark' : DEFAULT_HARNESS_CONFIG.theme;
|
||||
return {
|
||||
uiVersion,
|
||||
density,
|
||||
theme,
|
||||
uiScale: clampHarnessUiScale(searchParams.get('uiScale')),
|
||||
fontSize: clampHarnessFontSize(searchParams.get('fontSize')),
|
||||
};
|
||||
} catch {
|
||||
return { ...DEFAULT_HARNESS_CONFIG };
|
||||
}
|
||||
};
|
||||
|
||||
const DOCUMENT_ROOT_VAR_KEYS = [
|
||||
'--gonavi-font-size',
|
||||
'--gn-ui-scale',
|
||||
'--gn-font-size',
|
||||
'--gn-font-size-sm',
|
||||
'--gn-font-size-xs',
|
||||
'--gn-font-size-mono',
|
||||
'--gn-data-table-font-size',
|
||||
'--gn-sidebar-tree-font-size',
|
||||
] as const;
|
||||
|
||||
const buildHarnessColumns = (count: number): string[] => {
|
||||
const safeCount = Math.max(8, Math.min(64, Math.trunc(count || 0)));
|
||||
return Array.from({ length: safeCount }, (_, index) => {
|
||||
@@ -62,12 +158,113 @@ const HARNESS_EDIT_LOCATOR: EditRowLocator = {
|
||||
};
|
||||
|
||||
const PerfDataGridHarness: React.FC = () => {
|
||||
const initialConfig = useMemo(() => readHarnessRuntimeConfig(), []);
|
||||
const setAppearance = useStore((state) => state.setAppearance);
|
||||
const setTheme = useStore((state) => state.setTheme);
|
||||
const setUiScale = useStore((state) => state.setUiScale);
|
||||
const setFontSize = useStore((state) => state.setFontSize);
|
||||
const [rowCount, setRowCount] = useState(10000);
|
||||
const [columnCount, setColumnCount] = useState(24);
|
||||
const [density, setDensity] = useState<'compact' | 'comfortable' | 'spacious'>('comfortable');
|
||||
const [uiVersion, setUiVersion] = useState<HarnessUiVersion>(initialConfig.uiVersion);
|
||||
const [density, setDensity] = useState<DataTableDensity>(initialConfig.density);
|
||||
const restoreSnapshotRef = useRef<HarnessRestoreSnapshot | null>(null);
|
||||
|
||||
const columnNames = useMemo(() => buildHarnessColumns(columnCount), [columnCount]);
|
||||
const data = useMemo(() => buildHarnessData(rowCount, columnNames), [rowCount, columnNames]);
|
||||
const effectiveUiScale = clampHarnessUiScale(initialConfig.uiScale);
|
||||
const effectiveFontSize = clampHarnessFontSize(initialConfig.fontSize);
|
||||
const effectiveDataTableFontSize = effectiveFontSize;
|
||||
|
||||
useEffect(() => {
|
||||
if (restoreSnapshotRef.current) return;
|
||||
const currentState = useStore.getState();
|
||||
restoreSnapshotRef.current = {
|
||||
appearance: { ...currentState.appearance },
|
||||
theme: currentState.theme,
|
||||
uiScale: currentState.uiScale,
|
||||
fontSize: currentState.fontSize,
|
||||
bodyUiVersion: document.body.getAttribute('data-ui-version'),
|
||||
bodyTheme: document.body.getAttribute('data-theme'),
|
||||
bodyFontSize: document.body.style.fontSize,
|
||||
rootVars: Object.fromEntries(
|
||||
DOCUMENT_ROOT_VAR_KEYS.map((key) => [key, document.documentElement.style.getPropertyValue(key)])
|
||||
),
|
||||
};
|
||||
|
||||
return () => {
|
||||
const snapshot = restoreSnapshotRef.current;
|
||||
if (!snapshot) return;
|
||||
useStore.getState().setAppearance(snapshot.appearance);
|
||||
useStore.getState().setTheme(snapshot.theme);
|
||||
useStore.getState().setUiScale(snapshot.uiScale);
|
||||
useStore.getState().setFontSize(snapshot.fontSize);
|
||||
if (snapshot.bodyUiVersion) {
|
||||
document.body.setAttribute('data-ui-version', snapshot.bodyUiVersion);
|
||||
} else {
|
||||
document.body.removeAttribute('data-ui-version');
|
||||
}
|
||||
if (snapshot.bodyTheme) {
|
||||
document.body.setAttribute('data-theme', snapshot.bodyTheme);
|
||||
} else {
|
||||
document.body.removeAttribute('data-theme');
|
||||
}
|
||||
document.body.style.fontSize = snapshot.bodyFontSize;
|
||||
DOCUMENT_ROOT_VAR_KEYS.forEach((key) => {
|
||||
const value = snapshot.rootVars[key];
|
||||
if (value) {
|
||||
document.documentElement.style.setProperty(key, value);
|
||||
return;
|
||||
}
|
||||
document.documentElement.style.removeProperty(key);
|
||||
});
|
||||
restoreSnapshotRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const currentState = useStore.getState();
|
||||
if (hasHarnessAppearanceDrift(currentState.appearance, uiVersion, density)) {
|
||||
setAppearance({
|
||||
uiVersion,
|
||||
dataTableDensity: density,
|
||||
dataTableFontSize: null,
|
||||
dataTableFontSizeFollowGlobal: true,
|
||||
});
|
||||
}
|
||||
if (currentState.theme !== initialConfig.theme) {
|
||||
setTheme(initialConfig.theme);
|
||||
}
|
||||
if (Math.abs(currentState.uiScale - initialConfig.uiScale) > 0.0001) {
|
||||
setUiScale(initialConfig.uiScale);
|
||||
}
|
||||
if (currentState.fontSize !== initialConfig.fontSize) {
|
||||
setFontSize(initialConfig.fontSize);
|
||||
}
|
||||
}, [
|
||||
density,
|
||||
initialConfig.fontSize,
|
||||
initialConfig.theme,
|
||||
initialConfig.uiScale,
|
||||
setAppearance,
|
||||
setFontSize,
|
||||
setTheme,
|
||||
setUiScale,
|
||||
uiVersion,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.setAttribute('data-theme', initialConfig.theme);
|
||||
document.body.setAttribute('data-ui-version', uiVersion);
|
||||
document.body.style.fontSize = `${effectiveFontSize}px`;
|
||||
document.documentElement.style.setProperty('--gonavi-font-size', `${effectiveFontSize}px`);
|
||||
document.documentElement.style.setProperty('--gn-ui-scale', `${effectiveUiScale}`);
|
||||
document.documentElement.style.setProperty('--gn-font-size', `${effectiveFontSize}px`);
|
||||
document.documentElement.style.setProperty('--gn-font-size-sm', `${Math.max(10, Math.round(effectiveFontSize * 0.86))}px`);
|
||||
document.documentElement.style.setProperty('--gn-font-size-xs', `${Math.max(9, Math.round(effectiveFontSize * 0.76))}px`);
|
||||
document.documentElement.style.setProperty('--gn-font-size-mono', `${Math.max(10, Math.round(effectiveDataTableFontSize * 0.92))}px`);
|
||||
document.documentElement.style.setProperty('--gn-data-table-font-size', `${effectiveDataTableFontSize}px`);
|
||||
document.documentElement.style.setProperty('--gn-sidebar-tree-font-size', `${effectiveFontSize}px`);
|
||||
}, [effectiveDataTableFontSize, effectiveFontSize, effectiveUiScale, initialConfig.theme, uiVersion]);
|
||||
|
||||
return (
|
||||
<div style={{ height: '100vh', overflow: 'hidden', background: '#0b1220', padding: 16, boxSizing: 'border-box' }}>
|
||||
@@ -90,6 +287,14 @@ const PerfDataGridHarness: React.FC = () => {
|
||||
>
|
||||
<Space wrap align="center" size={12}>
|
||||
<Text strong>DataGrid 性能复现页</Text>
|
||||
<Segmented
|
||||
value={uiVersion}
|
||||
onChange={(value) => setUiVersion(value as HarnessUiVersion)}
|
||||
options={[
|
||||
{ label: '旧版 UI', value: 'legacy' },
|
||||
{ label: '新版 UI', value: 'v2' },
|
||||
]}
|
||||
/>
|
||||
<InputNumber
|
||||
min={200}
|
||||
max={50000}
|
||||
@@ -111,9 +316,9 @@ const PerfDataGridHarness: React.FC = () => {
|
||||
style={{ width: 140 }}
|
||||
onChange={(value) => setDensity(value)}
|
||||
options={[
|
||||
{ value: 'compact', label: '紧凑' },
|
||||
{ value: 'comfortable', label: '标准' },
|
||||
{ value: 'spacious', label: '宽松' },
|
||||
{ value: 'standard', label: '紧凑' },
|
||||
{ value: 'compact', label: '极紧凑' },
|
||||
]}
|
||||
/>
|
||||
<Button
|
||||
@@ -128,7 +333,7 @@ const PerfDataGridHarness: React.FC = () => {
|
||||
type="info"
|
||||
showIcon
|
||||
message="这个页面只用于开发态滚动性能采样"
|
||||
description={`当前 ${data.length} 行 / ${columnNames.length} 列。直接在表格区域做纵向、横向、Shift+滚轮滚动采样。`}
|
||||
description={`当前 ${uiVersion === 'v2' ? '新版' : '旧版'} UI,${data.length} 行 / ${columnNames.length} 列。直接在表格区域做纵向、横向、Shift+滚轮滚动采样。`}
|
||||
/>
|
||||
<div style={{ flex: '1 1 auto', minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
<DataGrid
|
||||
|
||||
Reference in New Issue
Block a user