mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-07 07:22:58 +08:00
⚡️ perf(frontend): 大数据表格拖拽与打开加载性能
- 列宽拖拽改为 rAF + transform 更新幽灵线,降低 mousemove 负载 - 大结果集自动启用 antd Table virtual 渲染,减少 DOM 压力 - 打开表改为先查数据,COUNT(*) 后台统计并回填分页总数,避免长时间 loading - 统一内部 rowKey 字段 __gonavi_row_key__,避免与业务字段 key 冲突
This commit is contained in:
@@ -1 +1 @@
|
||||
5b8157374dae5f9340e31b2d0bd2c00e
|
||||
d0f9366af59a6367ad3c7e2d4185ead4
|
||||
@@ -2,12 +2,14 @@ import React, { useState, useEffect, useRef, useContext, useMemo, useCallback }
|
||||
import { Table, message, Input, Button, Dropdown, MenuProps, Form, Pagination, Select, Modal } from 'antd';
|
||||
import type { SortOrder } from 'antd/es/table/interface';
|
||||
import { ReloadOutlined, ImportOutlined, ExportOutlined, DownOutlined, PlusOutlined, DeleteOutlined, SaveOutlined, UndoOutlined, FilterOutlined, CloseOutlined, ConsoleSqlOutlined, FileTextOutlined, CopyOutlined, ClearOutlined } from '@ant-design/icons';
|
||||
import { Resizable } from 'react-resizable';
|
||||
import { ImportData, ExportTable, ExportData, ApplyChanges } from '../../wailsjs/go/app/App';
|
||||
import { useStore } from '../store';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import 'react-resizable/css/styles.css';
|
||||
|
||||
// 内部行标识字段:避免与真实业务字段(如 `key` 列)冲突。
|
||||
export const GONAVI_ROW_KEY = '__gonavi_row_key__';
|
||||
|
||||
// Normalize RFC3339-like datetime strings to `YYYY-MM-DD HH:mm:ss` for display/editing.
|
||||
const normalizeDateTimeString = (val: string) => {
|
||||
const match = val.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/);
|
||||
@@ -73,7 +75,6 @@ const DataContext = React.createContext<{
|
||||
} | null>(null);
|
||||
|
||||
interface Item {
|
||||
key: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@@ -150,8 +151,9 @@ const ContextMenuRow = React.memo(({ children, record, ...props }: any) => {
|
||||
|
||||
const getTargets = () => {
|
||||
const keys = selectedRowKeysRef.current;
|
||||
if (keys.includes(record.key)) {
|
||||
return displayDataRef.current.filter(d => keys.includes(d.key));
|
||||
const recordKey = record?.[GONAVI_ROW_KEY];
|
||||
if (recordKey !== undefined && keys.includes(recordKey)) {
|
||||
return displayDataRef.current.filter(d => keys.includes(d?.[GONAVI_ROW_KEY]));
|
||||
}
|
||||
return [record];
|
||||
};
|
||||
@@ -168,7 +170,7 @@ const ContextMenuRow = React.memo(({ children, record, ...props }: any) => {
|
||||
{ key: 'copy', label: '复制为 Markdown', icon: <CopyOutlined />, onClick: () => {
|
||||
const records = getTargets();
|
||||
const lines = records.map((r: any) => {
|
||||
const { key, ...vals } = r;
|
||||
const { [GONAVI_ROW_KEY]: _rowKey, ...vals } = r;
|
||||
return `| ${Object.values(vals).join(' | ')} |`;
|
||||
});
|
||||
copyToClipboard(lines.join('\n'));
|
||||
@@ -206,7 +208,7 @@ interface DataGridProps {
|
||||
onReload?: () => void;
|
||||
onSort?: (field: string, order: string) => void;
|
||||
onPageChange?: (page: number, size: number) => void;
|
||||
pagination?: { current: number, pageSize: number, total: number };
|
||||
pagination?: { current: number, pageSize: number, total: number, totalKnown?: boolean };
|
||||
// Filtering
|
||||
showFilter?: boolean;
|
||||
onToggleFilter?: () => void;
|
||||
@@ -226,7 +228,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
// Helper to export specific data
|
||||
const exportData = async (rows: any[], format: string) => {
|
||||
const hide = message.loading(`正在导出 ${rows.length} 条数据...`, 0);
|
||||
const cleanRows = rows.map(({ key, ...rest }) => rest);
|
||||
const cleanRows = rows.map(({ [GONAVI_ROW_KEY]: _rowKey, ...rest }) => rest);
|
||||
// Pass tableName (or 'export') as default filename
|
||||
const res = await ExportData(cleanRows, columnNames, tableName || 'export', format);
|
||||
hide();
|
||||
@@ -267,7 +269,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [addedRows, setAddedRows] = useState<any[]>([]);
|
||||
const [modifiedRows, setModifiedRows] = useState<Record<string, any>>({});
|
||||
const [deletedRowKeys, setDeletedRowKeys] = useState<Set<React.Key>>(new Set());
|
||||
const [deletedRowKeys, setDeletedRowKeys] = useState<Set<string>>(new Set());
|
||||
|
||||
// Filter State
|
||||
const [filterConditions, setFilterConditions] = useState<{ id: number, column: string, op: string, value: string }[]>([]);
|
||||
@@ -287,8 +289,13 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
form.resetFields();
|
||||
}, [tableName, dbName, connectionId]); // Reset on context change
|
||||
|
||||
const rowKeyStr = useCallback((k: React.Key) => String(k), []);
|
||||
|
||||
const displayData = useMemo(() => {
|
||||
return [...data, ...addedRows].filter(item => !deletedRowKeys.has(item.key));
|
||||
return [...data, ...addedRows].filter(item => {
|
||||
const k = item?.[GONAVI_ROW_KEY];
|
||||
return k === undefined ? true : !deletedRowKeys.has(rowKeyStr(k));
|
||||
});
|
||||
}, [data, addedRows, deletedRowKeys]);
|
||||
|
||||
useEffect(() => { displayDataRef.current = displayData; }, [displayData]);
|
||||
@@ -311,10 +318,21 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const draggingRef = useRef<{
|
||||
startX: number,
|
||||
startWidth: number,
|
||||
key: string
|
||||
key: string,
|
||||
containerLeft: number
|
||||
} | null>(null);
|
||||
const ghostRef = useRef<HTMLDivElement>(null);
|
||||
const resizeRafRef = useRef<number | null>(null);
|
||||
const latestClientXRef = useRef<number | null>(null);
|
||||
const isResizingRef = useRef(false); // Lock for sorting
|
||||
|
||||
const flushGhostPosition = useCallback(() => {
|
||||
resizeRafRef.current = null;
|
||||
if (!draggingRef.current || !ghostRef.current) return;
|
||||
if (latestClientXRef.current === null) return;
|
||||
const relativeLeft = latestClientXRef.current - draggingRef.current.containerLeft;
|
||||
ghostRef.current.style.transform = `translateX(${relativeLeft}px)`;
|
||||
}, []);
|
||||
|
||||
// 1. Drag Start
|
||||
|
||||
@@ -334,21 +352,18 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
|
||||
const currentWidth = columnWidths[key] || 200;
|
||||
|
||||
|
||||
const containerLeft = containerRef.current?.getBoundingClientRect().left ?? 0;
|
||||
|
||||
draggingRef.current = { startX, startWidth: currentWidth, key };
|
||||
draggingRef.current = { startX, startWidth: currentWidth, key, containerLeft };
|
||||
latestClientXRef.current = startX;
|
||||
|
||||
|
||||
|
||||
// Show Ghost Line at initial position
|
||||
|
||||
if (ghostRef.current && containerRef.current) {
|
||||
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
|
||||
const relativeLeft = startX - containerRect.left;
|
||||
|
||||
ghostRef.current.style.left = `${relativeLeft}px`;
|
||||
const relativeLeft = startX - containerLeft;
|
||||
ghostRef.current.style.transform = `translateX(${relativeLeft}px)`;
|
||||
|
||||
ghostRef.current.style.display = 'block';
|
||||
|
||||
@@ -370,13 +385,11 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
|
||||
// 2. Drag Move (Global)
|
||||
const handleResizeMove = useCallback((e: MouseEvent) => {
|
||||
if (!draggingRef.current || !ghostRef.current || !containerRef.current) return;
|
||||
|
||||
// Update Ghost Line Position directly
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const relativeLeft = e.clientX - containerRect.left;
|
||||
ghostRef.current.style.left = `${relativeLeft}px`;
|
||||
}, []);
|
||||
if (!draggingRef.current) return;
|
||||
latestClientXRef.current = e.clientX;
|
||||
if (resizeRafRef.current !== null) return;
|
||||
resizeRafRef.current = requestAnimationFrame(flushGhostPosition);
|
||||
}, [flushGhostPosition]);
|
||||
|
||||
// 3. Drag Stop (Global)
|
||||
const handleResizeStop = useCallback((e: MouseEvent) => {
|
||||
@@ -390,6 +403,11 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
setColumnWidths(prev => ({ ...prev, [key]: newWidth }));
|
||||
|
||||
// Cleanup
|
||||
if (resizeRafRef.current !== null) {
|
||||
cancelAnimationFrame(resizeRafRef.current);
|
||||
resizeRafRef.current = null;
|
||||
}
|
||||
latestClientXRef.current = null;
|
||||
if (ghostRef.current) ghostRef.current.style.display = 'none';
|
||||
document.removeEventListener('mousemove', handleResizeMove);
|
||||
document.removeEventListener('mouseup', handleResizeStop);
|
||||
@@ -412,11 +430,13 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
// So we update 'modifiedRows'.
|
||||
|
||||
// Check if it's an added row
|
||||
const isAdded = addedRows.some(r => r.key === row.key);
|
||||
const rowKey = row?.[GONAVI_ROW_KEY];
|
||||
if (rowKey === undefined) return;
|
||||
const isAdded = addedRows.some(r => r?.[GONAVI_ROW_KEY] === rowKey);
|
||||
if (isAdded) {
|
||||
setAddedRows(prev => prev.map(r => r.key === row.key ? { ...r, ...row } : r));
|
||||
setAddedRows(prev => prev.map(r => r?.[GONAVI_ROW_KEY] === rowKey ? { ...r, ...row } : r));
|
||||
} else {
|
||||
setModifiedRows(prev => ({ ...prev, [row.key]: row }));
|
||||
setModifiedRows(prev => ({ ...prev, [rowKeyStr(rowKey)]: row }));
|
||||
}
|
||||
}, [addedRows]);
|
||||
|
||||
@@ -425,8 +445,9 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
// We need to merge modifiedRows into it for rendering.
|
||||
const mergedDisplayData = useMemo(() => {
|
||||
return displayData.map(row => {
|
||||
if (modifiedRows[row.key]) {
|
||||
return { ...row, ...modifiedRows[row.key] };
|
||||
const k = row?.[GONAVI_ROW_KEY];
|
||||
if (k !== undefined && modifiedRows[rowKeyStr(k)]) {
|
||||
return { ...row, ...modifiedRows[rowKeyStr(k)] };
|
||||
}
|
||||
return row;
|
||||
});
|
||||
@@ -466,7 +487,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
|
||||
const handleAddRow = () => {
|
||||
const newKey = `new-${Date.now()}`;
|
||||
const newRow: any = { key: newKey };
|
||||
const newRow: any = { [GONAVI_ROW_KEY]: newKey };
|
||||
columnNames.forEach(col => newRow[col] = '');
|
||||
setAddedRows(prev => [...prev, newRow]);
|
||||
};
|
||||
@@ -474,7 +495,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const handleDeleteSelected = () => {
|
||||
setDeletedRowKeys(prev => {
|
||||
const newDeleted = new Set(prev);
|
||||
selectedRowKeys.forEach(key => newDeleted.add(key));
|
||||
selectedRowKeys.forEach(key => newDeleted.add(rowKeyStr(key)));
|
||||
return newDeleted;
|
||||
});
|
||||
setSelectedRowKeys([]);
|
||||
@@ -489,27 +510,27 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const updates: any[] = [];
|
||||
const deletes: any[] = [];
|
||||
|
||||
addedRows.forEach(row => { const { key, ...vals } = row; inserts.push(vals); });
|
||||
deletedRowKeys.forEach(key => {
|
||||
addedRows.forEach(row => { const { [GONAVI_ROW_KEY]: _rowKey, ...vals } = row; inserts.push(vals); });
|
||||
deletedRowKeys.forEach(keyStr => {
|
||||
// Find original data
|
||||
const originalRow = data.find(d => d.key === key) || addedRows.find(d => d.key === key);
|
||||
const originalRow = data.find(d => rowKeyStr(d?.[GONAVI_ROW_KEY]) === keyStr) || addedRows.find(d => rowKeyStr(d?.[GONAVI_ROW_KEY]) === keyStr);
|
||||
if (originalRow) {
|
||||
const pkData: any = {};
|
||||
if (pkColumns.length > 0) pkColumns.forEach(k => pkData[k] = originalRow[k]);
|
||||
else { const { key: _, ...rest } = originalRow; Object.assign(pkData, rest); }
|
||||
else { const { [GONAVI_ROW_KEY]: _rowKey, ...rest } = originalRow; Object.assign(pkData, rest); }
|
||||
deletes.push(pkData);
|
||||
}
|
||||
});
|
||||
Object.entries(modifiedRows).forEach(([key, newRow]) => {
|
||||
if (deletedRowKeys.has(key)) return;
|
||||
const originalRow = data.find(d => d.key === key);
|
||||
Object.entries(modifiedRows).forEach(([keyStr, newRow]) => {
|
||||
if (deletedRowKeys.has(keyStr)) return;
|
||||
const originalRow = data.find(d => rowKeyStr(d?.[GONAVI_ROW_KEY]) === keyStr);
|
||||
if (!originalRow) return; // Should not happen for modified rows unless deleted
|
||||
|
||||
const pkData: any = {};
|
||||
if (pkColumns.length > 0) pkColumns.forEach(k => pkData[k] = originalRow[k]);
|
||||
else { const { key: _, ...rest } = originalRow; Object.assign(pkData, rest); }
|
||||
else { const { [GONAVI_ROW_KEY]: _rowKey, ...rest } = originalRow; Object.assign(pkData, rest); }
|
||||
|
||||
const { key: _, ...vals } = newRow;
|
||||
const { [GONAVI_ROW_KEY]: _rowKey, ...vals } = newRow;
|
||||
updates.push({ keys: pkData, values: vals });
|
||||
});
|
||||
|
||||
@@ -574,8 +595,9 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const getTargets = useCallback((clickedRecord: any) => {
|
||||
const selKeys = selectedRowKeysRef.current;
|
||||
const currentData = displayDataRef.current;
|
||||
if (selKeys.includes(clickedRecord.key)) {
|
||||
return currentData.filter(d => selKeys.includes(d.key));
|
||||
const clickedKey = clickedRecord?.[GONAVI_ROW_KEY];
|
||||
if (clickedKey !== undefined && selKeys.includes(clickedKey)) {
|
||||
return currentData.filter(d => selKeys.includes(d?.[GONAVI_ROW_KEY]));
|
||||
}
|
||||
return [clickedRecord];
|
||||
}, []);
|
||||
@@ -583,7 +605,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const handleCopyInsert = useCallback((record: any) => {
|
||||
const records = getTargets(record);
|
||||
const sqls = records.map((r: any) => {
|
||||
const { key, ...vals } = r;
|
||||
const { [GONAVI_ROW_KEY]: _rowKey, ...vals } = r;
|
||||
const cols = Object.keys(vals);
|
||||
const values = Object.values(vals).map(v => v === null ? 'NULL' : `'${v}'`);
|
||||
const targetTable = tableName || 'table';
|
||||
@@ -595,7 +617,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const handleCopyJson = useCallback((record: any) => {
|
||||
const records = getTargets(record);
|
||||
const cleanRecords = records.map((r: any) => {
|
||||
const { key, ...rest } = r;
|
||||
const { [GONAVI_ROW_KEY]: _rowKey, ...rest } = r;
|
||||
return rest;
|
||||
});
|
||||
copyToClipboard(JSON.stringify(cleanRecords, null, 2));
|
||||
@@ -604,7 +626,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const handleCopyCsv = useCallback((record: any) => {
|
||||
const records = getTargets(record);
|
||||
const lines = records.map((r: any) => {
|
||||
const { key, ...vals } = r;
|
||||
const { [GONAVI_ROW_KEY]: _rowKey, ...vals } = r;
|
||||
const values = Object.values(vals).map(v => v === null ? 'NULL' : `"${v}"`);
|
||||
return values.join(',');
|
||||
});
|
||||
@@ -623,7 +645,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
|
||||
// 1. Export Selected
|
||||
if (selectedRowKeys.length > 0) {
|
||||
const selectedRows = displayData.filter(d => selectedRowKeys.includes(d.key));
|
||||
const selectedRows = displayData.filter(d => selectedRowKeys.includes(d?.[GONAVI_ROW_KEY]));
|
||||
await exportData(selectedRows, format);
|
||||
return;
|
||||
}
|
||||
@@ -702,6 +724,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
}), []);
|
||||
|
||||
const totalWidth = columns.reduce((sum, col) => sum + (col.width as number || 200), 0);
|
||||
const enableVirtual = mergedDisplayData.length >= 200;
|
||||
|
||||
return (
|
||||
<div className={gridId} style={{ height: '100%', overflow: 'hidden', padding: 0, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
||||
@@ -777,7 +800,9 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
columns={mergedColumns}
|
||||
size="small"
|
||||
scroll={{ x: Math.max(totalWidth, 1000), y: tableHeight }}
|
||||
virtual={enableVirtual}
|
||||
loading={loading}
|
||||
rowKey={GONAVI_ROW_KEY}
|
||||
pagination={false}
|
||||
onChange={handleTableChange}
|
||||
bordered
|
||||
@@ -786,8 +811,9 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
onChange: setSelectedRowKeys,
|
||||
}}
|
||||
rowClassName={(record) => {
|
||||
if (addedRows.some(r => r.key === record.key)) return 'row-added';
|
||||
if (modifiedRows[record.key] || deletedRowKeys.has(record.key)) return 'row-modified'; // deleted won't show
|
||||
const k = record?.[GONAVI_ROW_KEY];
|
||||
if (k !== undefined && addedRows.some(r => r?.[GONAVI_ROW_KEY] === k)) return 'row-added';
|
||||
if (k !== undefined && (modifiedRows[rowKeyStr(k)] || deletedRowKeys.has(rowKeyStr(k)))) return 'row-modified'; // deleted won't show
|
||||
return '';
|
||||
}}
|
||||
onRow={(record) => ({ record } as any)}
|
||||
@@ -799,11 +825,15 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
|
||||
{pagination && (
|
||||
<div style={{ padding: '8px', borderTop: '1px solid #eee', display: 'flex', justifyContent: 'flex-end', background: '#fff' }}>
|
||||
<Pagination
|
||||
<Pagination
|
||||
current={pagination.current}
|
||||
pageSize={pagination.pageSize}
|
||||
total={pagination.total}
|
||||
showTotal={(total, range) => `当前 ${range[1] - range[0] + 1} 条 / 共 ${total} 条`}
|
||||
showTotal={(total, range) => {
|
||||
const currentCount = Math.max(0, range[1] - range[0] + 1);
|
||||
if (pagination.totalKnown === false) return `当前 ${currentCount} 条 / 正在统计总数...`;
|
||||
return `当前 ${currentCount} 条 / 共 ${total} 条`;
|
||||
}}
|
||||
showSizeChanger
|
||||
pageSizeOptions={['100', '200', '500', '1000']}
|
||||
onChange={onPageChange}
|
||||
@@ -828,11 +858,13 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0, // Fits container height
|
||||
left: 0,
|
||||
width: '2px',
|
||||
background: '#1890ff',
|
||||
zIndex: 9999,
|
||||
display: 'none',
|
||||
pointerEvents: 'none'
|
||||
pointerEvents: 'none',
|
||||
willChange: 'transform'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { message } from 'antd';
|
||||
import { TabData, ColumnDefinition } from '../types';
|
||||
import { useStore } from '../store';
|
||||
import { DBQuery, DBGetColumns } from '../../wailsjs/go/app/App';
|
||||
import DataGrid from './DataGrid';
|
||||
import DataGrid, { GONAVI_ROW_KEY } from './DataGrid';
|
||||
|
||||
const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
@@ -12,11 +12,14 @@ const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { connections, addSqlLog } = useStore();
|
||||
const fetchSeqRef = useRef(0);
|
||||
const countSeqRef = useRef(0);
|
||||
const countKeyRef = useRef<string>('');
|
||||
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 100,
|
||||
total: 0
|
||||
total: 0,
|
||||
totalKnown: false
|
||||
});
|
||||
|
||||
const [sortInfo, setSortInfo] = useState<{ columnKey: string, order: string } | null>(null);
|
||||
@@ -79,32 +82,22 @@ const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
sql += ` ORDER BY ${quoteIdentPart(sortInfo.columnKey)} ${sortInfo.order === 'ascend' ? 'ASC' : 'DESC'}`;
|
||||
}
|
||||
const offset = (page - 1) * size;
|
||||
sql += ` LIMIT ${size} OFFSET ${offset}`;
|
||||
// 大表性能:打开表不阻塞在 COUNT(*),先通过多取 1 条判断是否还有下一页;总数在后台统计并异步回填。
|
||||
sql += ` LIMIT ${size + 1} OFFSET ${offset}`;
|
||||
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const pCount = DBQuery(config as any, dbName, countSql);
|
||||
const pData = DBQuery(config as any, dbName, sql);
|
||||
|
||||
let pCols = null;
|
||||
|
||||
let pCols: Promise<any> | null = null;
|
||||
if (pkColumns.length === 0) {
|
||||
pCols = DBGetColumns(config as any, dbName, tableName);
|
||||
}
|
||||
|
||||
const [resCount, resData] = await Promise.all([pCount, pData]);
|
||||
const resData = await pData;
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// Log Execution
|
||||
addSqlLog({
|
||||
id: `log-${Date.now()}-count`,
|
||||
timestamp: Date.now(),
|
||||
sql: countSql,
|
||||
status: resCount.success ? 'success' : 'error',
|
||||
duration: duration / 2, // Estimate
|
||||
message: resCount.success ? '' : resCount.message,
|
||||
dbName
|
||||
});
|
||||
|
||||
addSqlLog({
|
||||
id: `log-${Date.now()}-data`,
|
||||
timestamp: Date.now(),
|
||||
@@ -124,23 +117,76 @@ const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
}
|
||||
}
|
||||
|
||||
let totalRecords = 0;
|
||||
if (resCount.success && Array.isArray(resCount.data) && resCount.data.length > 0) {
|
||||
totalRecords = Number(resCount.data[0]['total']);
|
||||
}
|
||||
|
||||
if (resData.success) {
|
||||
let resultData = resData.data as any[];
|
||||
if (!Array.isArray(resultData)) resultData = [];
|
||||
|
||||
const hasMore = resultData.length > size;
|
||||
if (hasMore) resultData = resultData.slice(0, size);
|
||||
|
||||
let fieldNames = resData.fields || [];
|
||||
if (fieldNames.length === 0 && resultData.length > 0) {
|
||||
fieldNames = Object.keys(resultData[0]);
|
||||
}
|
||||
if (fetchSeqRef.current !== seq) return;
|
||||
setColumnNames(fieldNames);
|
||||
setData(resultData.map((row: any, i: number) => ({ ...row, key: `row-${i}` })));
|
||||
setPagination(prev => ({ ...prev, current: page, pageSize: size, total: totalRecords }));
|
||||
resultData.forEach((row: any, i: number) => {
|
||||
if (row && typeof row === 'object') row[GONAVI_ROW_KEY] = `row-${offset + i}`;
|
||||
});
|
||||
setData(resultData);
|
||||
const countKey = `${tab.connectionId}|${dbName}|${tableName}|${whereSQL}`;
|
||||
const derivedTotalKnown = !hasMore;
|
||||
const derivedTotal = derivedTotalKnown ? offset + resultData.length : page * size + 1;
|
||||
if (derivedTotalKnown) countKeyRef.current = countKey;
|
||||
|
||||
setPagination(prev => {
|
||||
if (derivedTotalKnown) {
|
||||
return { ...prev, current: page, pageSize: size, total: derivedTotal, totalKnown: true };
|
||||
}
|
||||
if (prev.totalKnown && countKeyRef.current === countKey) {
|
||||
return { ...prev, current: page, pageSize: size };
|
||||
}
|
||||
return { ...prev, current: page, pageSize: size, total: derivedTotal, totalKnown: false };
|
||||
});
|
||||
|
||||
if (!derivedTotalKnown) {
|
||||
if (countKeyRef.current !== countKey) {
|
||||
countKeyRef.current = countKey;
|
||||
const countSeq = ++countSeqRef.current;
|
||||
const countStart = Date.now();
|
||||
|
||||
DBQuery(config as any, dbName, countSql)
|
||||
.then((resCount: any) => {
|
||||
const countDuration = Date.now() - countStart;
|
||||
|
||||
addSqlLog({
|
||||
id: `log-${Date.now()}-count`,
|
||||
timestamp: Date.now(),
|
||||
sql: countSql,
|
||||
status: resCount.success ? 'success' : 'error',
|
||||
duration: countDuration,
|
||||
message: resCount.success ? '' : resCount.message,
|
||||
dbName
|
||||
});
|
||||
|
||||
if (countSeqRef.current !== countSeq) return;
|
||||
if (countKeyRef.current !== countKey) return;
|
||||
|
||||
if (!resCount.success) return;
|
||||
if (!Array.isArray(resCount.data) || resCount.data.length === 0) return;
|
||||
|
||||
const total = Number(resCount.data[0]?.['total']);
|
||||
if (!Number.isFinite(total) || total < 0) return;
|
||||
|
||||
setPagination(prev => ({ ...prev, total, totalKnown: true }));
|
||||
})
|
||||
.catch(() => {
|
||||
if (countSeqRef.current !== countSeq) return;
|
||||
if (countKeyRef.current !== countKey) return;
|
||||
// 统计失败不影响主流程,不弹窗;可在日志里查看。
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message.error(resData.message);
|
||||
}
|
||||
@@ -167,7 +213,10 @@ const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
// So it's fine.
|
||||
|
||||
// Handlers memoized
|
||||
const handleReload = useCallback(() => fetchData(), [fetchData]);
|
||||
const handleReload = useCallback(() => {
|
||||
countKeyRef.current = '';
|
||||
fetchData(pagination.current, pagination.pageSize);
|
||||
}, [fetchData, pagination.current, pagination.pageSize]);
|
||||
const handleSort = useCallback((field: string, order: string) => setSortInfo({ columnKey: field, order }), []);
|
||||
const handlePageChange = useCallback((page: number, size: number) => fetchData(page, size), [fetchData]);
|
||||
const handleToggleFilter = useCallback(() => setShowFilter(prev => !prev), []);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { format } from 'sql-formatter';
|
||||
import { TabData, ColumnDefinition } from '../types';
|
||||
import { useStore } from '../store';
|
||||
import { DBQuery, DBGetTables, DBGetAllColumns, DBGetDatabases, DBGetColumns } from '../../wailsjs/go/app/App';
|
||||
import DataGrid from './DataGrid';
|
||||
import DataGrid, { GONAVI_ROW_KEY } from './DataGrid';
|
||||
|
||||
const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
const [query, setQuery] = useState(tab.query || 'SELECT * FROM ');
|
||||
@@ -271,7 +271,11 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
if (res.data.length > 0) {
|
||||
const cols = Object.keys(res.data[0]);
|
||||
setColumnNames(cols);
|
||||
setResults(res.data.map((row: any, i: number) => ({ ...row, key: i })));
|
||||
const rows = res.data as any[];
|
||||
rows.forEach((row: any, i: number) => {
|
||||
if (row && typeof row === 'object') row[GONAVI_ROW_KEY] = i;
|
||||
});
|
||||
setResults(rows);
|
||||
} else {
|
||||
message.info('查询执行成功,但没有返回结果。');
|
||||
setResults([]);
|
||||
|
||||
Reference in New Issue
Block a user