mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-05 14:09:36 +08:00
✨ feat(data-grid): 筛选面板新增多字段排序功能并支持启用禁用
- 排序扩展:SortInfo 类型从单字段扩展为数组,SQL 和 MongoDB 均支持多字段 ORDER BY - 筛选面板:新增排序配置区域,支持动态添加/删除多个排序字段及启用/禁用 - 表头联动:启用 Ant Design 多列排序模式,表头排序图标与筛选面板双向同步 - 增量更新:表头点击排序时在现有排序数组中增量更新,不覆盖其他字段 - 循环优化:表头排序从"升序→降序→取消"改为"升序↔降序"切换 - 布局优化:操作按钮栏增加分隔符分组,排序区域与按钮间增加视觉分隔 - refs #279
This commit is contained in:
@@ -28,7 +28,7 @@ import { useStore } from '../store';
|
||||
import type { ColumnDefinition } from '../types';
|
||||
import { v4 as generateUuid } from 'uuid';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { buildOrderBySQL, buildPaginatedSelectSQL, buildWhereSQL, escapeLiteral, quoteIdentPart, quoteQualifiedIdent, withSortBufferTuningSQL, type FilterCondition } from '../utils/sql';
|
||||
import { buildOrderBySQL, buildPaginatedSelectSQL, buildWhereSQL, escapeLiteral, hasExplicitSort, quoteIdentPart, quoteQualifiedIdent, withSortBufferTuningSQL, type FilterCondition } from '../utils/sql';
|
||||
import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||||
import { getDataSourceCapabilities } from '../utils/dataSourceCapabilities';
|
||||
import { calculateTableBodyBottomPadding, calculateVirtualTableScrollX } from './dataGridLayout';
|
||||
@@ -726,7 +726,7 @@ interface DataGridProps {
|
||||
};
|
||||
onRequestTotalCount?: () => void;
|
||||
onCancelTotalCount?: () => void;
|
||||
sortInfoExternal?: { columnKey: string, order: string } | null;
|
||||
sortInfoExternal?: Array<{ columnKey: string, order: string, enabled?: boolean }>;
|
||||
// Filtering
|
||||
showFilter?: boolean;
|
||||
onToggleFilter?: () => void;
|
||||
@@ -1148,25 +1148,18 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const [sortInfo, setSortInfo] = useState<{ columnKey: string, order: string } | null>(null);
|
||||
const [sortInfo, setSortInfo] = useState<Array<{ columnKey: string, order: string, enabled?: boolean }>>([]);
|
||||
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
|
||||
const [columnMetaMap, setColumnMetaMap] = useState<Record<string, ColumnMeta>>({});
|
||||
const columnMetaCacheRef = useRef<Record<string, Record<string, ColumnMeta>>>({});
|
||||
const columnMetaSeqRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const nextOrder = sortInfoExternal?.order === 'ascend' || sortInfoExternal?.order === 'descend'
|
||||
? sortInfoExternal.order
|
||||
: '';
|
||||
const nextColumn = nextOrder ? String(sortInfoExternal?.columnKey || '') : '';
|
||||
const currColumn = String(sortInfo?.columnKey || '');
|
||||
const currOrder = sortInfo?.order === 'ascend' || sortInfo?.order === 'descend' ? sortInfo.order : '';
|
||||
if (nextColumn === currColumn && nextOrder === currOrder) return;
|
||||
if (!nextColumn || !nextOrder) {
|
||||
setSortInfo(null);
|
||||
} else {
|
||||
setSortInfo({ columnKey: nextColumn, order: nextOrder });
|
||||
}
|
||||
const ext = sortInfoExternal || [];
|
||||
const extKey = JSON.stringify(ext);
|
||||
const curKey = JSON.stringify(sortInfo);
|
||||
if (extKey === curKey) return;
|
||||
setSortInfo(ext);
|
||||
}, [sortInfoExternal, sortInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -2568,22 +2561,39 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
|
||||
const handleTableChange = useCallback((_pag: any, _filtersArg: any, sorter: any) => {
|
||||
if (isResizingRef.current) return; // Block sort if resizing
|
||||
if (sorter.field) {
|
||||
const field = String(sorter.field);
|
||||
const order = sorter.order as string;
|
||||
const normalizedOrder = order === 'ascend' || order === 'descend' ? order : '';
|
||||
if (!normalizedOrder) {
|
||||
setSortInfo(null);
|
||||
if (onSort) onSort('', '');
|
||||
return;
|
||||
}
|
||||
setSortInfo({ columnKey: field, order: normalizedOrder });
|
||||
if (onSort) onSort(field, normalizedOrder);
|
||||
} else {
|
||||
setSortInfo(null);
|
||||
if (onSort) onSort('', '');
|
||||
// Ant Design 多列排序模式下 sorter 可能是数组
|
||||
const sorters = Array.isArray(sorter) ? sorter : (sorter?.field ? [sorter] : []);
|
||||
if (sorters.length === 0) {
|
||||
setSortInfo([]);
|
||||
if (onSort) onSort(JSON.stringify([]), '');
|
||||
return;
|
||||
}
|
||||
}, [onSort]);
|
||||
// 在现有排序数组基础上增量更新
|
||||
const next = [...sortInfo];
|
||||
for (const s of sorters) {
|
||||
const field = String(s.field || '');
|
||||
if (!field) continue;
|
||||
const order = s.order as string;
|
||||
const normalizedOrder = order === 'ascend' || order === 'descend' ? order : '';
|
||||
const existIdx = next.findIndex(item => item.columnKey === field);
|
||||
if (!normalizedOrder) {
|
||||
// Ant Design 第三次点击想取消排序:
|
||||
// 如果该字段已在排序数组中,回转为升序而非移除
|
||||
if (existIdx >= 0) {
|
||||
next[existIdx] = { ...next[existIdx], order: 'ascend', enabled: true };
|
||||
}
|
||||
// 不在数组中则忽略
|
||||
} else if (existIdx >= 0) {
|
||||
// 已存在:更新排序方向
|
||||
next[existIdx] = { ...next[existIdx], order: normalizedOrder, enabled: true };
|
||||
} else {
|
||||
// 不存在:追加到末尾
|
||||
next.push({ columnKey: field, order: normalizedOrder, enabled: true });
|
||||
}
|
||||
}
|
||||
setSortInfo(next);
|
||||
if (onSort) onSort(JSON.stringify(next), '');
|
||||
}, [onSort, sortInfo]);
|
||||
|
||||
// Native Drag State
|
||||
const draggingRef = useRef<{
|
||||
@@ -3043,8 +3053,8 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
key: key,
|
||||
// 不使用 ellipsis,避免 Ant Design 的 Tooltip 展开行为
|
||||
width: columnWidths[key] || 200,
|
||||
sorter: !!onSort,
|
||||
sortOrder: (sortInfo?.columnKey === key ? sortInfo.order : null) as SortOrder | undefined,
|
||||
sorter: onSort ? { multiple: displayColumnNames.indexOf(key) + 1 } : false,
|
||||
sortOrder: (sortInfo.find(s => s.columnKey === key && s.enabled !== false)?.order || null) as SortOrder | undefined,
|
||||
editable: canModifyData, // Only editable if table name known and not readonly
|
||||
render: (text: any) => (
|
||||
<div style={CELL_ELLIPSIS_STYLE}>
|
||||
@@ -3402,10 +3412,10 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const baseSql = `SELECT * FROM ${quoteQualifiedIdent(dbType, tableName)} ${whereSQL}`;
|
||||
const orderBySQL = buildOrderBySQL(dbType, sortInfo, pkColumns);
|
||||
const normalizedType = String(dbType || '').trim().toLowerCase();
|
||||
const hasExplicitSort = !!sortInfo?.columnKey && (sortInfo?.order === 'ascend' || sortInfo?.order === 'descend');
|
||||
const hasSortForBuffer = hasExplicitSort(sortInfo);
|
||||
const offset = (pagination.current - 1) * pagination.pageSize;
|
||||
let sql = buildPaginatedSelectSQL(dbType, baseSql, orderBySQL, pagination.pageSize, offset);
|
||||
if (hasExplicitSort && (normalizedType === 'mysql' || normalizedType === 'mariadb')) {
|
||||
if (hasSortForBuffer && (normalizedType === 'mysql' || normalizedType === 'mariadb')) {
|
||||
sql = withSortBufferTuningSQL(normalizedType, sql, 32 * 1024 * 1024);
|
||||
}
|
||||
return sql;
|
||||
@@ -4523,7 +4533,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
</div>
|
||||
|
||||
{showFilter && (
|
||||
<div style={{
|
||||
<div style={{
|
||||
padding: `${filterTopPadding}px ${panelPaddingX}px ${panelPaddingY}px ${panelPaddingX}px`,
|
||||
background: 'transparent',
|
||||
boxSizing: 'border-box',
|
||||
@@ -4610,14 +4620,83 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
<Button icon={<CloseOutlined />} onClick={() => removeFilter(cond.id)} type="text" danger />
|
||||
</div>
|
||||
))}
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
{onSort && (
|
||||
<div style={{ paddingTop: filterConditions.length > 0 ? 4 : 0, borderTop: filterConditions.length > 0 ? `1px dashed ${panelFrameColor}` : 'none' }}>
|
||||
{sortInfo.map((s, idx) => (
|
||||
<div key={idx} style={{ display: 'flex', gap: 8, marginBottom: 8, alignItems: 'center', opacity: s.enabled === false ? 0.58 : 1 }}>
|
||||
<Checkbox
|
||||
checked={s.enabled !== false}
|
||||
onChange={e => {
|
||||
const next = [...sortInfo];
|
||||
next[idx] = { ...next[idx], enabled: e.target.checked };
|
||||
onSort(JSON.stringify(next), '');
|
||||
}}
|
||||
style={{ flex: '0 0 auto' }}
|
||||
/>
|
||||
<span style={{ fontSize: 12, color: 'inherit', opacity: 0.7, whiteSpace: 'nowrap', minWidth: 32 }}>{idx === 0 ? '排序' : '然后'}</span>
|
||||
<Select
|
||||
style={{ width: 180 }}
|
||||
value={s.columnKey || undefined}
|
||||
onChange={v => {
|
||||
const next = [...sortInfo];
|
||||
if (!v) { next.splice(idx, 1); } else { next[idx] = { ...next[idx], columnKey: v }; }
|
||||
const filtered = next.filter(si => si.columnKey);
|
||||
onSort(JSON.stringify(filtered), '');
|
||||
}}
|
||||
options={displayColumnNames
|
||||
.filter(c => c === s.columnKey || !sortInfo.some(si => si.columnKey === c))
|
||||
.map(c => ({ value: c, label: c }))}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
filterOption={(input, option) =>
|
||||
String(option?.label ?? '')
|
||||
.toLowerCase()
|
||||
.includes(String(input || '').trim().toLowerCase())
|
||||
}
|
||||
placeholder="选择排序字段"
|
||||
allowClear
|
||||
onClear={() => {
|
||||
const next = sortInfo.filter((_, i) => i !== idx);
|
||||
onSort(JSON.stringify(next), '');
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
style={{ width: 110 }}
|
||||
value={s.order || 'ascend'}
|
||||
onChange={v => {
|
||||
const next = [...sortInfo];
|
||||
next[idx] = { ...next[idx], order: v };
|
||||
onSort(JSON.stringify(next), '');
|
||||
}}
|
||||
options={[
|
||||
{ value: 'ascend', label: '升序 ↑' },
|
||||
{ value: 'descend', label: '降序 ↓' },
|
||||
]}
|
||||
disabled={!s.columnKey}
|
||||
/>
|
||||
<Button icon={<CloseOutlined />} type="text" danger size="small" onClick={() => {
|
||||
const next = sortInfo.filter((_, i) => i !== idx);
|
||||
onSort(JSON.stringify(next), '');
|
||||
}} />
|
||||
</div>
|
||||
))}
|
||||
<Button type="dashed" size="small" icon={<PlusOutlined />} onClick={() => {
|
||||
const next = [...sortInfo, { columnKey: displayColumnNames.find(c => !sortInfo.some(s => s.columnKey === c)) || displayColumnNames[0] || '', order: 'ascend', enabled: true }];
|
||||
onSort(JSON.stringify(next), '');
|
||||
}} disabled={sortInfo.length >= displayColumnNames.length} style={{ marginBottom: 4 }}>添加排序</Button>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center', marginTop: (onSort && sortInfo.length > 0) ? 4 : 0, paddingTop: (onSort && sortInfo.length > 0) ? 6 : 0, borderTop: (onSort && sortInfo.length > 0) ? `1px dashed ${panelFrameColor}` : 'none' }}>
|
||||
<Button type="dashed" onClick={addFilter} size="small" icon={<PlusOutlined />}>添加条件</Button>
|
||||
<div style={{ width: 1, height: 16, background: panelFrameColor, margin: '0 2px', flexShrink: 0 }} />
|
||||
<Button size="small" onClick={() => setFilterConditions(prev => prev.map(c => ({ ...c, enabled: true })))}>全启用</Button>
|
||||
<Button size="small" onClick={() => setFilterConditions(prev => prev.map(c => ({ ...c, enabled: false })))}>全停用</Button>
|
||||
<div style={{ width: 1, height: 16, background: panelFrameColor, margin: '0 2px', flexShrink: 0 }} />
|
||||
<Button type="primary" onClick={applyFilters} size="small">应用</Button>
|
||||
<Button size="small" icon={<ClearOutlined />} onClick={() => {
|
||||
setFilterConditions([]);
|
||||
if (onApplyFilter) onApplyFilter([]);
|
||||
if (onSort) onSort('', '');
|
||||
}}>清除</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 { buildOrderBySQL, buildPaginatedSelectSQL, buildWhereSQL, quoteIdentPart, quoteQualifiedIdent, withSortBufferTuningSQL, type FilterCondition } from '../utils/sql';
|
||||
import { buildOrderBySQL, buildPaginatedSelectSQL, buildWhereSQL, hasExplicitSort, quoteIdentPart, quoteQualifiedIdent, withSortBufferTuningSQL, type FilterCondition } from '../utils/sql';
|
||||
import { buildMongoCountCommand, buildMongoFilter, buildMongoFindCommand, buildMongoSort } from '../utils/mongodb';
|
||||
import { getDataSourceCapabilities } from '../utils/dataSourceCapabilities';
|
||||
|
||||
@@ -157,7 +157,7 @@ type ViewerFilterSnapshot = {
|
||||
conditions: FilterCondition[];
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
sortInfo: { columnKey: string, order: string } | null;
|
||||
sortInfo: Array<{ columnKey: string, order: string, enabled?: boolean }>;
|
||||
scrollTop: number;
|
||||
scrollLeft: number;
|
||||
};
|
||||
@@ -185,16 +185,17 @@ const normalizeViewerFilterConditions = (conditions: FilterCondition[] | undefin
|
||||
const getViewerFilterSnapshot = (tabId: string): ViewerFilterSnapshot => {
|
||||
const cached = viewerFilterSnapshotsByTab.get(String(tabId || '').trim());
|
||||
if (!cached) {
|
||||
return { showFilter: false, conditions: [], currentPage: 1, pageSize: 100, sortInfo: null, scrollTop: 0, scrollLeft: 0 };
|
||||
return { showFilter: false, conditions: [], currentPage: 1, pageSize: 100, sortInfo: [], scrollTop: 0, scrollLeft: 0 };
|
||||
}
|
||||
return {
|
||||
showFilter: cached.showFilter === true,
|
||||
conditions: normalizeViewerFilterConditions(cached.conditions),
|
||||
currentPage: Number.isFinite(Number(cached.currentPage)) && Number(cached.currentPage) > 0 ? Number(cached.currentPage) : 1,
|
||||
pageSize: Number.isFinite(Number(cached.pageSize)) && Number(cached.pageSize) > 0 ? Number(cached.pageSize) : 100,
|
||||
sortInfo: cached.sortInfo && cached.sortInfo.columnKey && (cached.sortInfo.order === 'ascend' || cached.sortInfo.order === 'descend')
|
||||
? { columnKey: String(cached.sortInfo.columnKey), order: cached.sortInfo.order }
|
||||
: null,
|
||||
sortInfo: Array.isArray(cached.sortInfo)
|
||||
? cached.sortInfo.filter(s => s && s.columnKey && (s.order === 'ascend' || s.order === 'descend'))
|
||||
.map(s => ({ columnKey: String(s.columnKey), order: s.order }))
|
||||
: (cached.sortInfo && (cached.sortInfo as any).columnKey ? [{ columnKey: String((cached.sortInfo as any).columnKey), order: (cached.sortInfo as any).order }] : []),
|
||||
scrollTop: Number.isFinite(Number(cached.scrollTop)) ? Number(cached.scrollTop) : 0,
|
||||
scrollLeft: Number.isFinite(Number(cached.scrollLeft)) ? Number(cached.scrollLeft) : 0,
|
||||
};
|
||||
@@ -238,7 +239,7 @@ const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
totalCountCancelled: false,
|
||||
});
|
||||
|
||||
const [sortInfo, setSortInfo] = useState<{ columnKey: string, order: string } | null>(initialViewerSnapshot.sortInfo);
|
||||
const [sortInfo, setSortInfo] = useState<Array<{ columnKey: string, order: string, enabled?: boolean }>>(initialViewerSnapshot.sortInfo);
|
||||
|
||||
const [showFilter, setShowFilter] = useState<boolean>(initialViewerSnapshot.showFilter);
|
||||
const [filterConditions, setFilterConditions] = useState<FilterCondition[]>(initialViewerSnapshot.conditions);
|
||||
@@ -511,7 +512,7 @@ const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const hasSort = !!sortInfo?.columnKey && (sortInfo?.order === 'ascend' || sortInfo?.order === 'descend');
|
||||
const hasSort = hasExplicitSort(sortInfo);
|
||||
const isSortMemoryErr = (msg: string) => /error\s*1038|out of sort memory/i.test(String(msg || ''));
|
||||
let resData = await executeDataQuery(sql, '主查询');
|
||||
|
||||
@@ -788,13 +789,21 @@ const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
fetchData(pagination.current, pagination.pageSize);
|
||||
}, [fetchData, pagination.current, pagination.pageSize]);
|
||||
const handleSort = useCallback((field: string, order: string) => {
|
||||
// 支持多字段排序:field 为 JSON 数组字符串时解析为多字段
|
||||
try {
|
||||
const parsed = JSON.parse(field);
|
||||
if (Array.isArray(parsed)) {
|
||||
setSortInfo(parsed.filter((s: any) => s && s.columnKey && (s.order === 'ascend' || s.order === 'descend')));
|
||||
return;
|
||||
}
|
||||
} catch { /* 单字段模式 */ }
|
||||
const normalizedOrder = order === 'ascend' || order === 'descend' ? order : '';
|
||||
const normalizedField = String(field || '').trim();
|
||||
if (!normalizedField || !normalizedOrder) {
|
||||
setSortInfo(null);
|
||||
setSortInfo([]);
|
||||
return;
|
||||
}
|
||||
setSortInfo({ columnKey: normalizedField, order: normalizedOrder });
|
||||
setSortInfo([{ columnKey: normalizedField, order: normalizedOrder, enabled: true }]);
|
||||
}, []);
|
||||
const handlePageChange = useCallback((page: number, size: number) => fetchData(page, size), [fetchData]);
|
||||
const handleToggleFilter = useCallback(() => setShowFilter(prev => !prev), []);
|
||||
@@ -811,8 +820,8 @@ const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
let sql = `SELECT * FROM ${quoteQualifiedIdent(dbType, tableName)} ${whereSQL}`;
|
||||
sql += buildOrderBySQL(dbType, sortInfo, pkColumns);
|
||||
const normalizedType = dbType.toLowerCase();
|
||||
const hasExplicitSort = !!sortInfo?.columnKey && (sortInfo?.order === 'ascend' || sortInfo?.order === 'descend');
|
||||
if (hasExplicitSort && (normalizedType === 'mysql' || normalizedType === 'mariadb')) {
|
||||
const hasSortForBuffer = hasExplicitSort(sortInfo);
|
||||
if (hasSortForBuffer && (normalizedType === 'mysql' || normalizedType === 'mariadb')) {
|
||||
sql = withSortBufferTuningSQL(normalizedType, sql, 32 * 1024 * 1024);
|
||||
}
|
||||
return sql;
|
||||
|
||||
Reference in New Issue
Block a user