From f1296230002253603a3a15916db45e0e2440ba79 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Wed, 4 Feb 2026 11:43:47 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(table-edit):=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=95=B4=E8=A1=8C=E7=BC=96=E8=BE=91=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=EF=BC=8C=E6=8F=90=E5=8D=87=E5=A4=9A=E5=AD=97=E6=AE=B5/?= =?UTF-8?q?=E9=95=BF=E6=96=87=E6=9C=AC=E7=BC=96=E8=BE=91=E6=95=88=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 支持选中行后一键打开编辑面板 - 全字段可编辑,长文本/JSON 友好输入与弹窗编辑 - 应用后写入本地变更,提交事务后落库 --- frontend/src/components/DataGrid.tsx | 300 ++++++++++++++++++++++----- 1 file changed, 248 insertions(+), 52 deletions(-) diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index 36e6907..7fff8e7 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef, useContext, useMemo, useCallback } from 'react'; import { Table, message, Input, Button, Dropdown, MenuProps, Form, Pagination, Select, Modal } 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 } from '@ant-design/icons'; +import { ReloadOutlined, ImportOutlined, ExportOutlined, DownOutlined, PlusOutlined, DeleteOutlined, SaveOutlined, UndoOutlined, FilterOutlined, CloseOutlined, ConsoleSqlOutlined, FileTextOutlined, CopyOutlined, ClearOutlined, EditOutlined } from '@ant-design/icons'; import Editor from '@monaco-editor/react'; import { ImportData, ExportTable, ExportData, ApplyChanges } from '../../wailsjs/go/app/App'; import { useStore } from '../store'; @@ -38,6 +38,12 @@ const toEditableText = (val: any): string => { } }; +const toFormText = (val: any): string => { + if (val === null || val === undefined) return ''; + if (typeof val === 'string') return normalizeDateTimeString(val); + return toEditableText(val); +}; + const looksLikeJsonText = (text: string): boolean => { const raw = (text || '').trim(); if (!raw) return false; @@ -46,15 +52,6 @@ const looksLikeJsonText = (text: string): boolean => { return (first === '{' && last === '}') || (first === '[' && last === ']'); }; -const shouldUseModalEditorForValue = (val: any): boolean => { - if (val === null || val === undefined) return false; - if (typeof val === 'object') return true; - const s = toEditableText(val); - if (s.includes('\n') || s.includes('\r')) return true; - if (s.length >= 160) return true; - return looksLikeJsonText(s); -}; - // --- Resizable Header (Native Implementation) --- const ResizableTitle = (props: any) => { const { onResizeStart, width, ...restProps } = props; @@ -113,7 +110,7 @@ interface EditableCellProps { dataIndex: string; record: Item; handleSave: (record: Item) => void; - openEditor?: (record: Item, dataIndex: string, title: React.ReactNode) => void; + focusCell?: (record: Item, dataIndex: string, title: React.ReactNode) => void; [key: string]: any; } @@ -124,7 +121,7 @@ const EditableCell: React.FC = React.memo(({ dataIndex, record, handleSave, - openEditor, + focusCell, ...restProps }) => { const [editing, setEditing] = useState(false); @@ -171,14 +168,24 @@ const EditableCell: React.FC = React.memo(({ const handleDoubleClick = () => { if (!editable) return; - if (openEditor && shouldUseModalEditorForValue(record?.[dataIndex])) { - openEditor(record, dataIndex, title); - return; - } toggleEdit(); }; - return {childNode}; + const handleClick = (e: React.MouseEvent) => { + restProps?.onClick?.(e); + if (!editable) return; + if (typeof focusCell === 'function') focusCell(record, dataIndex, title); + }; + + return ( + + {childNode} + + ); }); const ContextMenuRow = React.memo(({ children, record, ...props }: any) => { @@ -269,6 +276,14 @@ const DataGrid: React.FC = ({ const [cellEditorValue, setCellEditorValue] = useState(''); const [cellEditorIsJson, setCellEditorIsJson] = useState(false); const [cellEditorMeta, setCellEditorMeta] = useState<{ record: Item; dataIndex: string; title: string } | null>(null); + const cellEditorApplyRef = useRef<((val: string) => void) | null>(null); + const [activeCell, setActiveCell] = useState<{ rowKey: string; dataIndex: string; title: string } | null>(null); + const [rowEditorOpen, setRowEditorOpen] = useState(false); + const [rowEditorRowKey, setRowEditorRowKey] = useState(''); + const rowEditorBaseRef = useRef>({}); + const rowEditorDisplayRef = useRef>({}); + const rowEditorNullColsRef = useRef>(new Set()); + const [rowEditorForm] = Form.useForm(); // Helper to export specific data const exportData = async (rows: any[], format: string) => { @@ -288,9 +303,10 @@ const DataGrid: React.FC = ({ setCellEditorMeta(null); setCellEditorValue(''); setCellEditorIsJson(false); + cellEditorApplyRef.current = null; }, []); - const openCellEditor = useCallback((record: Item, dataIndex: string, title: React.ReactNode) => { + const openCellEditor = useCallback((record: Item, dataIndex: string, title: React.ReactNode, onApplyValue?: (val: string) => void) => { if (!record || !dataIndex) return; const raw = record?.[dataIndex]; const text = toEditableText(raw); @@ -301,6 +317,7 @@ const DataGrid: React.FC = ({ setCellEditorValue(text); setCellEditorIsJson(isJson); setCellEditorOpen(true); + cellEditorApplyRef.current = typeof onApplyValue === 'function' ? onApplyValue : null; }, []); // Dynamic Height @@ -363,6 +380,14 @@ const DataGrid: React.FC = ({ setModifiedRows({}); setDeletedRowKeys(new Set()); setSelectedRowKeys([]); + setActiveCell(null); + setRowEditorOpen(false); + setRowEditorRowKey(''); + rowEditorBaseRef.current = {}; + rowEditorDisplayRef.current = {}; + rowEditorNullColsRef.current = new Set(); + rowEditorForm.resetFields(); + closeCellEditor(); form.resetFields(); }, [tableName, dbName, connectionId]); // Reset on context change @@ -519,6 +544,12 @@ const DataGrid: React.FC = ({ const handleCellEditorSave = useCallback(() => { if (!cellEditorMeta) return; + const apply = cellEditorApplyRef.current; + if (apply) { + apply(cellEditorValue); + closeCellEditor(); + return; + } const nextRow: any = { ...cellEditorMeta.record, [cellEditorMeta.dataIndex]: cellEditorValue }; handleCellSave(nextRow); closeCellEditor(); @@ -547,6 +578,110 @@ const DataGrid: React.FC = ({ }); }, [displayData, modifiedRows]); + const focusCell = useCallback((record: Item, dataIndex: string, title: React.ReactNode) => { + const k = record?.[GONAVI_ROW_KEY]; + if (k === undefined) return; + const titleText = typeof title === 'string' ? title : (typeof title === 'number' ? String(title) : String(dataIndex)); + setActiveCell({ rowKey: rowKeyStr(k), dataIndex, title: titleText }); + }, [rowKeyStr]); + + const closeRowEditor = useCallback(() => { + setRowEditorOpen(false); + setRowEditorRowKey(''); + rowEditorBaseRef.current = {}; + rowEditorDisplayRef.current = {}; + rowEditorNullColsRef.current = new Set(); + rowEditorForm.resetFields(); + }, [rowEditorForm]); + + const openRowEditor = useCallback(() => { + if (readOnly || !tableName) return; + if (selectedRowKeys.length > 1) { + message.info('一次只能编辑一行,请仅选择一行'); + return; + } + + const keyStr = + selectedRowKeys.length === 1 ? rowKeyStr(selectedRowKeys[0]) : activeCell?.rowKey; + if (!keyStr) { + message.info('请先选择一行(勾选一行或点击任意单元格)'); + return; + } + + const displayRow = mergedDisplayData.find(r => rowKeyStr(r?.[GONAVI_ROW_KEY]) === keyStr); + if (!displayRow) { + message.error('未找到目标行,请刷新后重试'); + return; + } + + const baseRow = + data.find(r => rowKeyStr(r?.[GONAVI_ROW_KEY]) === keyStr) || + addedRows.find(r => rowKeyStr(r?.[GONAVI_ROW_KEY]) === keyStr) || + displayRow; + + const baseMap: Record = {}; + const displayMap: Record = {}; + const nullCols = new Set(); + + columnNames.forEach((col) => { + const baseVal = (baseRow as any)?.[col]; + const displayVal = (displayRow as any)?.[col]; + baseMap[col] = toFormText(baseVal); + displayMap[col] = toFormText(displayVal); + if (baseVal === null || baseVal === undefined) nullCols.add(col); + }); + + rowEditorBaseRef.current = baseMap; + rowEditorDisplayRef.current = displayMap; + rowEditorNullColsRef.current = nullCols; + + rowEditorForm.setFieldsValue(displayMap); + setRowEditorRowKey(keyStr); + setRowEditorOpen(true); + }, [readOnly, tableName, selectedRowKeys, activeCell, mergedDisplayData, data, addedRows, columnNames, rowEditorForm, rowKeyStr]); + + const openRowEditorFieldEditor = useCallback((dataIndex: string) => { + if (!dataIndex) return; + const val = rowEditorForm.getFieldValue(dataIndex); + openCellEditor( + { [dataIndex]: val ?? '' }, + dataIndex, + dataIndex, + (nextVal) => rowEditorForm.setFieldsValue({ [dataIndex]: nextVal }), + ); + }, [rowEditorForm, openCellEditor]); + + const applyRowEditor = useCallback(() => { + const keyStr = rowEditorRowKey; + if (!keyStr) return; + const values = rowEditorForm.getFieldsValue(true) || {}; + + const isAdded = addedRows.some(r => rowKeyStr(r?.[GONAVI_ROW_KEY]) === keyStr); + if (isAdded) { + setAddedRows(prev => prev.map(r => rowKeyStr(r?.[GONAVI_ROW_KEY]) === keyStr ? { ...r, ...values } : r)); + closeRowEditor(); + return; + } + + const baseMap = rowEditorBaseRef.current || {}; + const patch: Record = {}; + columnNames.forEach((col) => { + const nextVal = values[col]; + const nextStr = toFormText(nextVal); + const baseStr = baseMap[col] ?? ''; + if (nextStr !== baseStr) patch[col] = nextStr; + }); + + setModifiedRows(prev => { + const next = { ...prev }; + if (Object.keys(patch).length === 0) delete next[keyStr]; + else next[keyStr] = patch; + return next; + }); + + closeRowEditor(); + }, [rowEditorRowKey, rowEditorForm, addedRows, columnNames, rowKeyStr, closeRowEditor]); + const columns = useMemo(() => { return columnNames.map(key => ({ title: key, @@ -575,10 +710,13 @@ const DataGrid: React.FC = ({ dataIndex: col.dataIndex, title: col.title, handleSave: handleCellSave, - openEditor: openCellEditor, + focusCell, + className: (activeCell && rowKeyStr(record?.[GONAVI_ROW_KEY]) === activeCell.rowKey && col.dataIndex === activeCell.dataIndex) + ? 'gonavi-active-cell' + : undefined, }), }; - }), [columns, handleCellSave, openCellEditor]); + }), [columns, handleCellSave, openCellEditor, focusCell, activeCell, rowKeyStr]); const handleAddRow = () => { const newKey = `new-${Date.now()}`; @@ -823,28 +961,36 @@ const DataGrid: React.FC = ({ return (
- {/* Toolbar */} -
- {onReload && } - {tableName && } - {tableName && } - - {!readOnly && tableName && ( - <> -
- - - {selectedRowKeys.length > 0 && 已选 {selectedRowKeys.length}} -
- - {hasChanges && (} + {tableName && } + {tableName && } + + {!readOnly && tableName && ( + <> +
+ + + + {selectedRowKeys.length > 0 && 已选 {selectedRowKeys.length}} +
+ + {hasChanges && ()} @@ -884,12 +1030,58 @@ const DataGrid: React.FC = ({
)} -
- {contextHolder} - + {contextHolder} + 取消, + , + ]} + > +
+ {tableName ? `${tableName}` : ''} + {rowEditorRowKey ? `rowKey: ${rowEditorRowKey}` : ''} +
+
+
+ {columnNames.map((col) => { + const sample = rowEditorDisplayRef.current?.[col] ?? ''; + const placeholder = rowEditorNullColsRef.current?.has(col) ? '(NULL)' : undefined; + const isJson = looksLikeJsonText(sample); + const useArea = isJson || sample.includes('\n') || sample.length >= 160; + + return ( + +
+ + {useArea ? ( + + ) : ( + + )} + + +
+
+ ); + })} +
+
+
+ = ({
)} - + {/* Ghost Resize Line for Columns */}