diff --git a/.gitignore b/.gitignore index 3bea14c..12d734c 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ GoNavi-Wails.exe .ace-tool/ .claude/ tmpclaude-* + +CLAUDE.md +**/CLAUDE.md diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index 3e4fb90..24f265c 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -577,6 +577,7 @@ interface DataGridProps { // Filtering showFilter?: boolean; onToggleFilter?: () => void; + exportSqlWithFilter?: string; onApplyFilter?: (conditions: GridFilterCondition[]) => void; } @@ -595,9 +596,9 @@ type ColumnMeta = { comment: string; }; -const DataGrid: React.FC = ({ +const DataGrid: React.FC = ({ data, columnNames, loading, tableName, exportScope = 'table', resultSql, dbName, connectionId, pkColumns = [], readOnly = false, - onReload, onSort, onPageChange, pagination, onRequestTotalCount, onCancelTotalCount, sortInfoExternal, showFilter, onToggleFilter, onApplyFilter + onReload, onSort, onPageChange, pagination, onRequestTotalCount, onCancelTotalCount, sortInfoExternal, showFilter, onToggleFilter, exportSqlWithFilter, onApplyFilter }) => { const connections = useStore(state => state.connections); const addSqlLog = useStore(state => state.addSqlLog); @@ -620,6 +621,8 @@ const DataGrid: React.FC = ({ const isQueryResultExport = exportScope === 'queryResult'; const canImport = exportScope === 'table' && !!tableName; const canExport = !!connectionId && (isQueryResultExport || !!tableName); + const filteredExportSql = useMemo(() => String(exportSqlWithFilter || '').trim(), [exportSqlWithFilter]); + const hasFilteredExportSql = exportScope === 'table' && filteredExportSql.length > 0; // Background Helper const getBg = (darkHex: string) => { @@ -2481,6 +2484,23 @@ const DataGrid: React.FC = ({ }); }; + const handleExportFilteredAll = async (format: string) => { + if (!connectionId || !tableName) return; + if (!filteredExportSql) { + message.warning('当前未应用筛选条件'); + return; + } + if (!supportsSqlQueryExport) { + message.error('当前数据源不支持按筛选结果导出'); + return; + } + if (hasChanges) { + message.warning("当前存在未提交修改,筛选结果导出基于数据库已提交数据。"); + } + + await exportByQuery(filteredExportSql, format, `${tableName || 'export'}_filtered`); + }; + const handleImport = async () => { if (!connectionId || !tableName) return; const config = buildConnConfig(); @@ -2562,7 +2582,21 @@ const DataGrid: React.FC = ({ if (onApplyFilter) onApplyFilter(filterConditions); }; - const exportMenu: MenuProps['items'] = [ + const exportMenu: MenuProps['items'] = hasFilteredExportSql ? [ + { type: 'group', label: '筛选结果', children: [ + { key: 'filtered-csv', label: 'CSV', onClick: () => handleExportFilteredAll('csv') }, + { key: 'filtered-xlsx', label: 'Excel (XLSX)', onClick: () => handleExportFilteredAll('xlsx') }, + { key: 'filtered-json', label: 'JSON', onClick: () => handleExportFilteredAll('json') }, + { key: 'filtered-md', label: 'Markdown', onClick: () => handleExportFilteredAll('md') }, + ]}, + { type: 'divider' }, + { type: 'group', label: '全表', children: [ + { key: 'table-csv', label: 'CSV', onClick: () => handleExport('csv') }, + { key: 'table-xlsx', label: 'Excel (XLSX)', onClick: () => handleExport('xlsx') }, + { key: 'table-json', label: 'JSON', onClick: () => handleExport('json') }, + { key: 'table-md', label: 'Markdown', onClick: () => handleExport('md') }, + ]}, + ] : [ { key: 'csv', label: 'CSV', onClick: () => handleExport('csv') }, { key: 'xlsx', label: 'Excel (XLSX)', onClick: () => handleExport('xlsx') }, { key: 'json', label: 'JSON', onClick: () => handleExport('json') }, diff --git a/frontend/src/components/DataViewer.tsx b/frontend/src/components/DataViewer.tsx index 8950629..9b81b5e 100644 --- a/frontend/src/components/DataViewer.tsx +++ b/frontend/src/components/DataViewer.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useCallback, useRef } from 'react'; +import React, { useEffect, useState, useCallback, useRef, useMemo } from 'react'; import { message } from 'antd'; import { TabData, ColumnDefinition } from '../types'; import { useStore } from '../store'; @@ -676,6 +676,24 @@ const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => { const handleToggleFilter = useCallback(() => setShowFilter(prev => !prev), []); const handleApplyFilter = useCallback((conditions: FilterCondition[]) => setFilterConditions(conditions), []); + const exportSqlWithFilter = useMemo(() => { + const tableName = String(tab.tableName || '').trim(); + const dbType = String(currentConnConfig?.type || '').trim(); + if (!tableName || !dbType) return ''; + + const whereSQL = buildWhereSQL(dbType, filterConditions); + if (!whereSQL) return ''; + + 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')) { + sql = withSortBufferTuningSQL(normalizedType, sql, 32 * 1024 * 1024); + } + return sql; + }, [tab.tableName, currentConnConfig?.type, filterConditions, sortInfo, pkColumns]); + useEffect(() => { fetchData(1, pagination.pageSize); }, [tab, sortInfo, filterConditions]); // Initial load and re-load on sort/filter @@ -702,6 +720,7 @@ const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => { onApplyFilter={handleApplyFilter} readOnly={forceReadOnly} sortInfoExternal={sortInfo} + exportSqlWithFilter={exportSqlWithFilter || undefined} /> );