From 4d32dd2cb57b092174f256549f4dc03397c7b694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E5=9B=BD=E9=94=8B?= Date: Tue, 10 Feb 2026 18:41:25 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20fix(data-viewer):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E7=AD=9B=E9=80=89=E5=90=8E=E6=8F=90=E4=BA=A4=E4=BA=8B?= =?UTF-8?q?=E5=8A=A1=E5=AF=BC=E8=87=B4=E8=AE=B0=E5=BD=95=E9=A1=BA=E5=BA=8F?= =?UTF-8?q?=E6=BC=82=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 抽取统一 ORDER BY 生成逻辑,避免无序重载 - 无显式排序时回退按主键升序,保证结果稳定 - 同步更新 DataGrid 当前页查询导出排序规则 --- frontend/package.json.md5 | 2 +- frontend/src/components/DataGrid.tsx | 8 +++--- frontend/src/components/DataViewer.tsx | 16 ++++-------- frontend/src/utils/sql.ts | 35 ++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 0f8f4fe..a7661c0 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -5b8157374dae5f9340e31b2d0bd2c00e \ No newline at end of file +d0f9366af59a6367ad3c7e2d4185ead4 \ No newline at end of file diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index 7a1a604..6bf7aaa 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -9,7 +9,7 @@ import ImportPreviewModal from './ImportPreviewModal'; import { useStore } from '../store'; import { v4 as uuidv4 } from 'uuid'; import 'react-resizable/css/styles.css'; -import { buildWhereSQL, escapeLiteral, quoteIdentPart, quoteQualifiedIdent, type FilterCondition } from '../utils/sql'; +import { buildOrderBySQL, buildWhereSQL, escapeLiteral, quoteIdentPart, quoteQualifiedIdent, type FilterCondition } from '../utils/sql'; import { isMacLikePlatform, normalizeOpacityForPlatform } from '../utils/appearance'; // --- Error Boundary --- @@ -1819,13 +1819,11 @@ const DataGrid: React.FC = ({ if (!tableName || !pagination) return ''; const whereSQL = buildWhereSQL(dbType, filterConditions); let sql = `SELECT * FROM ${quoteQualifiedIdent(dbType, tableName)} ${whereSQL}`; - if (sortInfo && sortInfo.order) { - sql += ` ORDER BY ${quoteIdentPart(dbType, sortInfo.columnKey)} ${sortInfo.order === 'ascend' ? 'ASC' : 'DESC'}`; - } + sql += buildOrderBySQL(dbType, sortInfo, pkColumns); const offset = (pagination.current - 1) * pagination.pageSize; sql += ` LIMIT ${pagination.pageSize} OFFSET ${offset}`; return sql; - }, [tableName, pagination, filterConditions, sortInfo]); + }, [tableName, pagination, filterConditions, sortInfo, pkColumns]); // Context Menu Export const handleExportSelected = useCallback(async (format: string, record: any) => { diff --git a/frontend/src/components/DataViewer.tsx b/frontend/src/components/DataViewer.tsx index 1ca4b8c..cdc132d 100644 --- a/frontend/src/components/DataViewer.tsx +++ b/frontend/src/components/DataViewer.tsx @@ -4,7 +4,7 @@ import { TabData, ColumnDefinition } from '../types'; import { useStore } from '../store'; import { DBQuery, DBGetColumns } from '../../wailsjs/go/app/App'; import DataGrid, { GONAVI_ROW_KEY } from './DataGrid'; -import { buildWhereSQL, quoteIdentPart, quoteQualifiedIdent, type FilterCondition } from '../utils/sql'; +import { buildOrderBySQL, buildWhereSQL, quoteQualifiedIdent, type FilterCondition } from '../utils/sql'; const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => { const [data, setData] = useState([]); @@ -69,9 +69,7 @@ const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => { const countSql = `SELECT COUNT(*) as total FROM ${quoteQualifiedIdent(dbType, tableName)} ${whereSQL}`; let sql = `SELECT * FROM ${quoteQualifiedIdent(dbType, tableName)} ${whereSQL}`; - if (sortInfo && sortInfo.order) { - sql += ` ORDER BY ${quoteIdentPart(dbType, sortInfo.columnKey)} ${sortInfo.order === 'ascend' ? 'ASC' : 'DESC'}`; - } + sql += buildOrderBySQL(dbType, sortInfo, pkColumns); const offset = (page - 1) * size; // 大表性能:打开表不阻塞在 COUNT(*),先通过多取 1 条判断是否还有下一页;总数在后台统计并异步回填。 sql += ` LIMIT ${size + 1} OFFSET ${offset}`; @@ -205,13 +203,9 @@ const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => { }); } if (fetchSeqRef.current === seq) setLoading(false); - }, [connections, tab, sortInfo, filterConditions, pkColumns.length]); - // Depend on pkColumns.length to avoid loop? No, pkColumns is updated inside. - // Actually, 'pkColumns' state shouldn't trigger re-fetch. - // The 'if (pkColumns.length === 0)' check is inside. - // So adding pkColumns to dependency is safer but might trigger double fetch if not careful? - // Only if pkColumns changes. It changes once from [] to [...]. - // So it's fine. + }, [connections, tab, sortInfo, filterConditions, pkColumns]); + // 依赖 pkColumns:在无手动排序时可回退到主键稳定排序。 + // 主键信息只会在首次加载后更新一次,避免循环查询。 // Handlers memoized const handleReload = useCallback(() => { diff --git a/frontend/src/utils/sql.ts b/frontend/src/utils/sql.ts index 6aca539..0571c99 100644 --- a/frontend/src/utils/sql.ts +++ b/frontend/src/utils/sql.ts @@ -63,6 +63,41 @@ export const quoteQualifiedIdent = (dbType: string, ident: string) => { export const escapeLiteral = (val: string) => (val || '').replace(/'/g, "''"); +type SortInfo = { + columnKey?: string; + order?: string; +} | null | undefined; + +export const buildOrderBySQL = ( + dbType: string, + sortInfo: SortInfo, + fallbackColumns: string[] = [], +) => { + const sortColumn = normalizeIdentPart(String(sortInfo?.columnKey || '')); + const sortOrder = String(sortInfo?.order || ''); + const direction = sortOrder === 'ascend' ? 'ASC' : sortOrder === 'descend' ? 'DESC' : ''; + if (sortColumn && direction) { + return ` ORDER BY ${quoteIdentPart(dbType, sortColumn)} ${direction}`; + } + + const seen = new Set(); + const stableColumns = (fallbackColumns || []) + .map((col) => normalizeIdentPart(String(col || ''))) + .filter((col) => { + if (!col) return false; + const key = col.toLowerCase(); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + if (stableColumns.length > 0) { + const parts = stableColumns.map((col) => `${quoteIdentPart(dbType, col)} ASC`); + return ` ORDER BY ${parts.join(', ')}`; + } + + return ''; +}; + export const parseListValues = (val: string) => { const raw = (val || '').trim(); if (!raw) return [];