mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-11 17:09:45 +08:00
feat: 表筛选结果一键导出功能 (#161)
* 🔧 chore(gitignore): 忽略 AI 上下文文档避免版本控制污染
添加 CLAUDE.md 及其子目录变体到 .gitignore,防止 AI 辅助开发过程中生成的临时上下文文件被意外提交到仓库。
- 忽略根目录 CLAUDE.md
- 忽略所有子目录下的 CLAUDE.md 文件
* feat: 表筛选结果一键导出功能
- 新增表浏览模式下筛选结果的导出功能
- DataViewer 生成包含筛选条件的完整 SQL
- DataGrid 动态显示分组导出菜单(筛选结果 + 全表)
- 支持 CSV、Excel、JSON、Markdown 四种格式
- 添加未提交修改的警告提示
- 复用现有 ExportQuery 后端方法,无需后端修改
实现细节:
- 使用 buildWhereSQL 和 buildOrderBySQL 构建 SQL
- 支持 MySQL/MariaDB 的 sort buffer 优化
- 分组菜单设计避免用户误操作
- 导出文件名包含 _filtered 后缀
关闭 #issue
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -19,3 +19,6 @@ GoNavi-Wails.exe
|
||||
.ace-tool/
|
||||
.claude/
|
||||
tmpclaude-*
|
||||
|
||||
CLAUDE.md
|
||||
**/CLAUDE.md
|
||||
|
||||
@@ -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<DataGridProps> = ({
|
||||
const DataGrid: React.FC<DataGridProps> = ({
|
||||
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<DataGridProps> = ({
|
||||
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<DataGridProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
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<DataGridProps> = ({
|
||||
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') },
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user