diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index 47604c9..04fde1b 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -1,12 +1,13 @@ import React, { useState, useEffect, useRef, useContext, useMemo, useCallback } from 'react'; import { createPortal } from 'react-dom'; -import { Table, message, Input, Button, Dropdown, MenuProps, Form, Pagination, Select, Modal, Checkbox, Segmented } from 'antd'; +import { Table, message, Input, Button, Dropdown, MenuProps, Form, Pagination, Select, Modal, Checkbox, Segmented, Tooltip, Popover } 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, EditOutlined, VerticalAlignBottomOutlined } from '@ant-design/icons'; import Editor from '@monaco-editor/react'; -import { ImportData, ExportTable, ExportData, ExportQuery, ApplyChanges } from '../../wailsjs/go/app/App'; +import { ImportData, ExportTable, ExportData, ExportQuery, ApplyChanges, DBGetColumns } from '../../wailsjs/go/app/App'; import ImportPreviewModal from './ImportPreviewModal'; import { useStore } from '../store'; +import type { ColumnDefinition } from '../types'; import { v4 as uuidv4 } from 'uuid'; import 'react-resizable/css/styles.css'; import { buildOrderBySQL, buildWhereSQL, escapeLiteral, quoteIdentPart, quoteQualifiedIdent, withSortBufferTuningSQL, type FilterCondition } from '../utils/sql'; @@ -292,6 +293,7 @@ const DataContext = React.createContext<{ handleExportSelected: (format: string, r: any) => void; copyToClipboard: (t: string) => void; tableName?: string; + enableRowContextMenu: boolean; } | null>(null); interface Item { @@ -434,7 +436,11 @@ const ContextMenuRow = React.memo(({ children, record, ...props }: any) => { if (!record || !context) return {children}; - const { selectedRowKeysRef, displayDataRef, handleCopyInsert, handleCopyJson, handleCopyCsv, handleExportSelected, copyToClipboard } = context; + const { selectedRowKeysRef, displayDataRef, handleCopyInsert, handleCopyJson, handleCopyCsv, handleExportSelected, copyToClipboard, enableRowContextMenu } = context; + + if (!enableRowContextMenu) { + return {children}; + } const getTargets = () => { const keys = selectedRowKeysRef.current; @@ -513,6 +519,11 @@ type GridFilterCondition = FilterCondition & { type GridViewMode = 'table' | 'json' | 'text'; +type ColumnMeta = { + type: string; + comment: string; +}; + const DataGrid: React.FC = ({ data, columnNames, loading, tableName, dbName, connectionId, pkColumns = [], readOnly = false, onReload, onSort, onPageChange, pagination, sortInfoExternal, showFilter, onToggleFilter, onApplyFilter @@ -521,10 +532,14 @@ const DataGrid: React.FC = ({ const addSqlLog = useStore(state => state.addSqlLog); const theme = useStore(state => state.theme); const appearance = useStore(state => state.appearance); + const queryOptions = useStore(state => state.queryOptions); + const setQueryOptions = useStore(state => state.setQueryOptions); const isMacLike = useMemo(() => isMacLikePlatform(), []); const darkMode = theme === 'dark'; const opacity = normalizeOpacityForPlatform(appearance.opacity); const canModifyData = !readOnly && !!tableName; + const showColumnComment = queryOptions?.showColumnComment !== false; + const showColumnType = queryOptions?.showColumnType !== false; const selectionColumnWidth = 46; // Background Helper @@ -538,7 +553,7 @@ const DataGrid: React.FC = ({ }; const bgContent = getBg('#1d1d1d'); const bgFilter = getBg('#262626'); - const bgContextMenu = getBg('#1f1f1f'); + const bgContextMenu = darkMode ? '#1f1f1f' : '#ffffff'; // Row Colors with Opacity const getRowBg = (r: number, g: number, b: number) => `rgba(${r}, ${g}, ${b}, ${opacity})`; @@ -661,6 +676,9 @@ const DataGrid: React.FC = ({ const [sortInfo, setSortInfo] = useState<{ columnKey: string, order: string } | null>(null); const [columnWidths, setColumnWidths] = useState>({}); + const [columnMetaMap, setColumnMetaMap] = useState>({}); + const columnMetaCacheRef = useRef>>({}); + const columnMetaSeqRef = useRef(0); useEffect(() => { const nextOrder = sortInfoExternal?.order === 'ascend' || sortInfoExternal?.order === 'descend' @@ -677,6 +695,129 @@ const DataGrid: React.FC = ({ } }, [sortInfoExternal, sortInfo]); + useEffect(() => { + const normalizedTableName = String(tableName || '').trim(); + const normalizedDbName = String(dbName || '').trim(); + if (!connectionId || !normalizedTableName) { + setColumnMetaMap({}); + return; + } + const cacheKey = `${connectionId}|${normalizedDbName}|${normalizedTableName}`; + setColumnMetaMap(columnMetaCacheRef.current[cacheKey] || {}); + }, [connectionId, dbName, tableName]); + + useEffect(() => { + const normalizedTableName = String(tableName || '').trim(); + const normalizedDbName = String(dbName || '').trim(); + if (!connectionId || !normalizedTableName) return; + + const cacheKey = `${connectionId}|${normalizedDbName}|${normalizedTableName}`; + if (columnMetaCacheRef.current[cacheKey]) return; + + const conn = connections.find(c => c.id === connectionId); + if (!conn) { + setColumnMetaMap({}); + return; + } + + const config = { + ...conn.config, + port: Number(conn.config.port), + password: conn.config.password || "", + database: conn.config.database || "", + useSSH: conn.config.useSSH || false, + ssh: conn.config.ssh || { host: "", port: 22, user: "", password: "", keyPath: "" } + }; + + const seq = ++columnMetaSeqRef.current; + DBGetColumns(config as any, normalizedDbName, normalizedTableName) + .then((res) => { + if (seq !== columnMetaSeqRef.current) return; + if (!res.success || !Array.isArray(res.data)) { + setColumnMetaMap({}); + return; + } + const nextMap: Record = {}; + (res.data as ColumnDefinition[]).forEach((column: any) => { + const name = String(column?.name ?? column?.Name ?? '').trim(); + if (!name) return; + const type = String(column?.type ?? column?.Type ?? '').trim(); + const comment = String(column?.comment ?? column?.Comment ?? '').trim(); + nextMap[name] = { type, comment }; + }); + columnMetaCacheRef.current[cacheKey] = nextMap; + setColumnMetaMap(nextMap); + }) + .catch(() => { + if (seq !== columnMetaSeqRef.current) return; + setColumnMetaMap({}); + }); + }, [connections, connectionId, dbName, tableName]); + + const columnMetaMapByLowerName = useMemo(() => { + const next: Record = {}; + Object.entries(columnMetaMap).forEach(([name, meta]) => { + const lowerName = String(name || '').toLowerCase(); + if (!lowerName || next[lowerName]) return; + next[lowerName] = meta; + }); + return next; + }, [columnMetaMap]); + + const renderColumnTitle = useCallback((name: string): React.ReactNode => { + const normalizedName = String(name || ''); + const meta = columnMetaMap[normalizedName] || columnMetaMapByLowerName[normalizedName.toLowerCase()]; + const hoverLines: string[] = []; + if (meta?.type) hoverLines.push(`类型:${meta.type}`); + if (meta?.comment) hoverLines.push(`备注:${meta.comment}`); + + const titleNode = ( +
+ {normalizedName} + {showColumnType && meta?.type && ( + + {meta.type} + + )} + {showColumnComment && meta?.comment && ( + + {meta.comment} + + )} +
+ ); + + if (hoverLines.length === 0) return titleNode; + return ( + {hoverLines.join('\n')}} + styles={{ root: { maxWidth: 640 } }} + > + {titleNode} + + ); + }, [columnMetaMap, columnMetaMapByLowerName, showColumnComment, showColumnType]); + const closeCellEditor = useCallback(() => { setCellEditorOpen(false); setCellEditorMeta(null); @@ -1592,7 +1733,7 @@ const DataGrid: React.FC = ({ const columns = useMemo(() => { return columnNames.map(key => ({ - title: key, + title: renderColumnTitle(key), dataIndex: key, key: key, // 不使用 ellipsis,避免 Ant Design 的 Tooltip 展开行为 @@ -1608,9 +1749,29 @@ const DataGrid: React.FC = ({ onHeaderCell: (column: any) => ({ width: column.width, onResizeStart: handleResizeStart(key), // Only need start + onClickCapture: (event: React.MouseEvent) => { + if (!onSort) return; + const headerCell = event.currentTarget as HTMLElement; + const upArrow = headerCell.querySelector('.ant-table-column-sorter-up') as HTMLElement | null; + const downArrow = headerCell.querySelector('.ant-table-column-sorter-down') as HTMLElement | null; + const isInArrow = [upArrow, downArrow].some((el) => { + if (!el) return false; + const rect = el.getBoundingClientRect(); + return ( + event.clientX >= rect.left && + event.clientX <= rect.right && + event.clientY >= rect.top && + event.clientY <= rect.bottom + ); + }); + if (isInArrow) return; + // 仅允许点击上下箭头触发排序,点击字段名或表头其它区域不触发排序。 + event.preventDefault(); + event.stopPropagation(); + }, }), })); - }, [columnNames, columnWidths, sortInfo, handleResizeStart, canModifyData, onSort]); + }, [columnNames, columnWidths, sortInfo, handleResizeStart, canModifyData, onSort, renderColumnTitle]); const mergedColumns = useMemo(() => columns.map(col => { if (!col.editable) return col; @@ -1620,7 +1781,7 @@ const DataGrid: React.FC = ({ record, editable: col.editable, dataIndex: col.dataIndex, - title: col.title, + title: String(col.dataIndex), handleSave: handleCellSave, focusCell: openCellEditor, }), @@ -2037,6 +2198,23 @@ const DataGrid: React.FC = ({ { key: 'md', label: 'Markdown', onClick: () => handleExport('md') }, ]; + const columnInfoSettingContent = ( +
+ setQueryOptions({ showColumnComment: e.target.checked })} + > + 下方显示备注 + + setQueryOptions({ showColumnType: e.target.checked })} + > + 下方显示类型 + +
+ ); + const tableComponents = useMemo(() => ({ body: { cell: EditableCell, row: ContextMenuRow }, header: { cell: ResizableTitle } @@ -2149,6 +2327,15 @@ const DataGrid: React.FC = ({ )}
+
+ + + +
= ({ {viewMode === 'table' ? (
- + = ({ .${gridId} .ant-table-tbody > tr > td { background: transparent !important; border-bottom: 1px solid ${darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'} !important; border-inline-end: 1px solid transparent !important; } .${gridId} .ant-table-thead > tr > th { background: transparent !important; border-bottom: 1px solid ${darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'} !important; border-inline-end: 1px solid transparent !important; } .${gridId} .ant-table-thead > tr > th::before { display: none !important; } + .${gridId} .ant-table-thead > tr > th .ant-table-column-sorters { cursor: default !important; } + .${gridId} .ant-table-thead > tr > th .ant-table-column-sorter, + .${gridId} .ant-table-thead > tr > th .ant-table-column-sorter * { cursor: pointer !important; } .${gridId} .ant-table-tbody > tr:hover > td { background-color: ${darkMode ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.02)'} !important; } .${gridId} .ant-table-tbody > tr.ant-table-row-selected > td { background-color: ${darkMode ? 'rgba(24, 144, 255, 0.15)' : 'rgba(24, 144, 255, 0.08)'} !important; } .${gridId} .ant-table-tbody > tr.ant-table-row-selected:hover > td { background-color: ${darkMode ? 'rgba(24, 144, 255, 0.25)' : 'rgba(24, 144, 255, 0.12)'} !important; } diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 109f9d2..45b9f1f 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -252,6 +252,12 @@ export interface SqlLog { affectedRows?: number; } +export interface QueryOptions { + maxRows: number; + showColumnComment: boolean; + showColumnType: boolean; +} + interface AppState { connections: SavedConnection[]; tabs: TabData[]; @@ -261,7 +267,7 @@ interface AppState { theme: 'light' | 'dark'; appearance: { opacity: number; blur: number }; sqlFormatOptions: { keywordCase: 'upper' | 'lower' }; - queryOptions: { maxRows: number }; + queryOptions: QueryOptions; sqlLogs: SqlLog[]; tableAccessCount: Record; tableSortPreference: Record; @@ -287,7 +293,7 @@ interface AppState { setTheme: (theme: 'light' | 'dark') => void; setAppearance: (appearance: Partial<{ opacity: number; blur: number }>) => void; setSqlFormatOptions: (options: { keywordCase: 'upper' | 'lower' }) => void; - setQueryOptions: (options: Partial<{ maxRows: number }>) => void; + setQueryOptions: (options: Partial) => void; addSqlLog: (log: SqlLog) => void; clearSqlLogs: () => void; @@ -326,13 +332,15 @@ const sanitizeSqlFormatOptions = (value: unknown): { keywordCase: 'upper' | 'low return { keywordCase: raw.keywordCase === 'lower' ? 'lower' : 'upper' }; }; -const sanitizeQueryOptions = (value: unknown): { maxRows: number } => { +const sanitizeQueryOptions = (value: unknown): QueryOptions => { const raw = (value && typeof value === 'object') ? value as Record : {}; const maxRows = Number(raw.maxRows); + const showColumnComment = typeof raw.showColumnComment === 'boolean' ? raw.showColumnComment : true; + const showColumnType = typeof raw.showColumnType === 'boolean' ? raw.showColumnType : true; if (!Number.isFinite(maxRows) || maxRows <= 0) { - return { maxRows: 5000 }; + return { maxRows: 5000, showColumnComment, showColumnType }; } - return { maxRows: Math.min(50000, Math.trunc(maxRows)) }; + return { maxRows: Math.min(50000, Math.trunc(maxRows)), showColumnComment, showColumnType }; }; const sanitizeTableAccessCount = (value: unknown): Record => { @@ -383,7 +391,7 @@ export const useStore = create()( theme: 'light', appearance: { ...DEFAULT_APPEARANCE }, sqlFormatOptions: { keywordCase: 'upper' }, - queryOptions: { maxRows: 5000 }, + queryOptions: { maxRows: 5000, showColumnComment: true, showColumnType: true }, sqlLogs: [], tableAccessCount: {}, tableSortPreference: {},