From bb257c35bc646f4c54860c810f89faa8dff7699c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E5=9B=BD=E9=94=8B?= Date: Thu, 12 Mar 2026 23:14:21 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(data-grid):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=90=8C=E8=A1=A8=E5=A4=9A=E5=88=97=E8=B7=A8=E8=A1=8C?= =?UTF-8?q?=E5=A4=8D=E5=88=B6=E7=B2=98=E8=B4=B4=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在单元格编辑模式新增复制缓冲区,保存源行与多列值 - 新增“复制选区列值”操作,仅允许同一行多列选区复制 - 新增“粘贴到选中行”操作,按同名列批量写入并自动排除源行 - 复用 addedRows/modifiedRows 变更路径,保持提交事务与回滚逻辑一致 - 单元格右键菜单增加“粘贴已复制列(同名列)”入口 - 切换连接/库/表时自动清空复制缓冲区,避免跨上下文误粘贴 - refs #217 --- frontend/src/components/DataGrid.tsx | 209 ++++++++++++++++++++++++++- 1 file changed, 204 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index 0a35d9f..96e5661 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -1012,6 +1012,7 @@ const DataGrid: React.FC = ({ // 批量编辑模式状态 const [cellEditMode, setCellEditMode] = useState(false); const [selectedCells, setSelectedCells] = useState>(new Set()); + const [copiedCellPatch, setCopiedCellPatch] = useState<{ sourceRowKey: string; values: Record } | null>(null); const [batchEditModalOpen, setBatchEditModalOpen] = useState(false); const [batchEditValue, setBatchEditValue] = useState(''); const [batchEditSetNull, setBatchEditSetNull] = useState(false); @@ -1407,6 +1408,7 @@ const DataGrid: React.FC = ({ setModifiedRows({}); setDeletedRowKeys(new Set()); setSelectedRowKeys([]); + setCopiedCellPatch(null); setRowEditorOpen(false); setRowEditorRowKey(''); rowEditorBaseRawRef.current = {}; @@ -1775,6 +1777,163 @@ const DataGrid: React.FC = ({ }; }, [cellEditMode, displayColumnNames, columnIndexMap, updateCellSelection]); + const handleCopySelectedColumnsFromRow = useCallback(() => { + const activeSelection = currentSelectionRef.current.size > 0 ? currentSelectionRef.current : selectedCells; + if (activeSelection.size === 0) { + void message.info('请先在同一行选中要复制的单元格'); + return; + } + + const parsed = Array.from(activeSelection) + .map((cellKey) => splitCellKey(cellKey)) + .filter((item): item is { rowKey: string; colName: string } => !!item); + if (parsed.length === 0) { + void message.info('未识别到可复制的单元格'); + return; + } + + const sourceRowKeySet = new Set(parsed.map((item) => item.rowKey)); + if (sourceRowKeySet.size !== 1) { + void message.info('复制列值时请只选择同一行的单元格'); + return; + } + + const sourceRowKey = parsed[0].rowKey; + const selectedColumnNames = Array.from(new Set(parsed.map((item) => item.colName))); + if (selectedColumnNames.length === 0) { + void message.info('未识别到可复制的列'); + return; + } + + const sourceBaseRow = displayDataRef.current.find((row) => { + const key = row?.[GONAVI_ROW_KEY]; + return key !== undefined && key !== null && rowKeyStr(key) === sourceRowKey; + }); + const sourceAddedRow = addedRows.find((row) => { + const key = row?.[GONAVI_ROW_KEY]; + return key !== undefined && key !== null && rowKeyStr(key) === sourceRowKey; + }); + const sourceModified = modifiedRows[sourceRowKey]; + + const values: Record = {}; + selectedColumnNames.forEach((colName) => { + if (sourceAddedRow) { + values[colName] = sourceAddedRow[colName]; + return; + } + + if (sourceModified && Object.prototype.hasOwnProperty.call(sourceModified as any, colName)) { + values[colName] = (sourceModified as any)[colName]; + return; + } + + values[colName] = sourceBaseRow?.[colName]; + }); + + setCopiedCellPatch({ sourceRowKey, values }); + void message.success(`已复制 ${selectedColumnNames.length} 列,可粘贴到目标行`); + }, [selectedCells, rowKeyStr, addedRows, modifiedRows]); + + const handlePasteCopiedColumnsToSelectedRows = useCallback((fallbackRowKey?: React.Key) => { + if (!copiedCellPatch || Object.keys(copiedCellPatch.values).length === 0) { + void message.info('请先复制列值'); + return; + } + + const targetKeySet = new Set(); + const selectedKeys = selectedRowKeysRef.current; + if (selectedKeys.length > 0) { + selectedKeys.forEach((key) => targetKeySet.add(rowKeyStr(key))); + } else if (fallbackRowKey !== undefined && fallbackRowKey !== null) { + targetKeySet.add(rowKeyStr(fallbackRowKey)); + } else { + void message.info('请先选择目标行'); + return; + } + + targetKeySet.delete(copiedCellPatch.sourceRowKey); + if (targetKeySet.size === 0) { + void message.info('目标行不能仅为源行,请选择其他行'); + return; + } + + const addedRowMap = new Map(); + addedRows.forEach((row) => { + const key = row?.[GONAVI_ROW_KEY]; + if (key === undefined || key === null) return; + addedRowMap.set(rowKeyStr(key), row); + }); + + const baseRowMap = new Map(); + displayDataRef.current.forEach((row) => { + const key = row?.[GONAVI_ROW_KEY]; + if (key === undefined || key === null) return; + baseRowMap.set(rowKeyStr(key), row); + }); + + const patchesByRow = new Map>(); + let updatedCellCount = 0; + + targetKeySet.forEach((targetRowKey) => { + const patch: Record = {}; + const existing = modifiedRows[targetRowKey]; + const addedRow = addedRowMap.get(targetRowKey); + const baseRow = baseRowMap.get(targetRowKey); + + Object.entries(copiedCellPatch.values).forEach(([colName, nextValue]) => { + let currentValue: any; + + if (addedRow) { + currentValue = addedRow[colName]; + } else if (existing && Object.prototype.hasOwnProperty.call(existing as any, GONAVI_ROW_KEY)) { + currentValue = (existing as any)[colName]; + } else if (existing && Object.prototype.hasOwnProperty.call(existing as any, colName)) { + currentValue = (existing as any)[colName]; + } else { + currentValue = baseRow?.[colName]; + } + + if (isCellValueEqualForDiff(currentValue, nextValue)) return; + patch[colName] = nextValue; + updatedCellCount++; + }); + + if (Object.keys(patch).length > 0) { + patchesByRow.set(targetRowKey, patch); + } + }); + + if (patchesByRow.size === 0 || updatedCellCount === 0) { + void message.info('目标行无需更新'); + return; + } + + setAddedRows(prev => prev.map((row) => { + const key = row?.[GONAVI_ROW_KEY]; + if (key === undefined || key === null) return row; + const patch = patchesByRow.get(rowKeyStr(key)); + if (!patch) return row; + return { ...row, ...patch }; + })); + + setModifiedRows(prev => { + let next: Record | null = null; + + patchesByRow.forEach((patch, keyStr) => { + if (addedRowMap.has(keyStr)) return; + const existing = prev[keyStr]; + const merged = existing ? { ...(existing as any), ...patch } : patch; + if (!next) next = { ...prev }; + next[keyStr] = merged; + }); + + return next || prev; + }); + + void message.success(`已粘贴到 ${patchesByRow.size} 行,共 ${updatedCellCount} 个单元格`); + setCellContextMenu(prev => ({ ...prev, visible: false })); + }, [copiedCellPatch, addedRows, modifiedRows, rowKeyStr]); + // 批量填充到选中行 const handleBatchFillToSelected = useCallback((sourceRecord: Item, dataIndex: string) => { const sourceValue = sourceRecord[dataIndex]; @@ -3576,15 +3735,35 @@ const DataGrid: React.FC = ({ {cellEditMode && selectedCells.size > 0 && ( <> + + + )} + {cellEditMode && copiedCellPatch && ( + <> + + + 已复制 {Object.keys(copiedCellPatch.values).length} 列 + )}
@@ -4105,6 +4284,26 @@ const DataGrid: React.FC = ({ 填充到选中行 ({selectedRowKeys.length})
+
{ + if (copiedCellPatch) e.currentTarget.style.background = darkMode ? '#303030' : '#f5f5f5'; + }} + onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'} + onClick={() => { + if (!copiedCellPatch) return; + const fallbackKey = cellContextMenu.record?.[GONAVI_ROW_KEY]; + handlePasteCopiedColumnsToSelectedRows(fallbackKey); + }} + > + + 粘贴已复制列(同名列) +
)}