diff --git a/frontend/src/components/DataGrid.layout.test.tsx b/frontend/src/components/DataGrid.layout.test.tsx
index bb1edbf..d2795c4 100644
--- a/frontend/src/components/DataGrid.layout.test.tsx
+++ b/frontend/src/components/DataGrid.layout.test.tsx
@@ -77,4 +77,26 @@ describe('DataGrid layout', () => {
expect(markup).toContain('data-grid-secondary-actions="true"');
expect(markup).toContain('data-grid-view-switcher="true"');
});
+
+ it('renders row copy and paste actions in editable table toolbar', () => {
+ const markup = renderToStaticMarkup(
+ ,
+ );
+
+ expect(markup).toContain('data-grid-copy-row-action="true"');
+ expect(markup).toContain('data-grid-paste-row-action="true"');
+ expect(markup).toContain('复制行');
+ expect(markup).toContain('粘贴行');
+ });
});
diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx
index 42740e9..0c05295 100644
--- a/frontend/src/components/DataGrid.tsx
+++ b/frontend/src/components/DataGrid.tsx
@@ -50,6 +50,7 @@ import {
} from './dataGridCopyInsert';
import { calculateAutoFitColumnWidth } from './dataGridAutoWidth';
import { buildSelectedCellClipboardText } from './dataGridSelectionCopy';
+import { buildCopiedRowsForPaste, buildPastedRowsFromCopiedRows } from './dataGridRowClipboard';
import { applyNoAutoCapAttributesWithin, noAutoCapInputProps } from '../utils/inputAutoCap';
import {
TEMPORAL_FORMATS,
@@ -1221,6 +1222,7 @@ const DataGrid: React.FC = ({
const lastTableScrollLeftRef = useRef(0);
const lastExternalScrollLeftRef = useRef(0);
const pendingScrollToBottomRef = useRef(false);
+ const pastedRowSequenceRef = useRef(0);
const lastReportedScrollRef = useRef<{ top: number; left: number }>({ top: 0, left: 0 });
const didRestoreScrollRef = useRef(false);
@@ -1228,6 +1230,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 [copiedRowsForPaste, setCopiedRowsForPaste] = useState>>([]);
const [batchEditModalOpen, setBatchEditModalOpen] = useState(false);
const [batchEditValue, setBatchEditValue] = useState('');
const [batchEditSetNull, setBatchEditSetNull] = useState(false);
@@ -2251,6 +2254,7 @@ const DataGrid: React.FC = ({
setDeletedRowKeys(new Set());
setSelectedRowKeys([]);
setCopiedCellPatch(null);
+ setCopiedRowsForPaste([]);
setRowEditorOpen(false);
setRowEditorRowKey('');
rowEditorBaseRawRef.current = {};
@@ -3622,6 +3626,55 @@ const DataGrid: React.FC = ({
pendingScrollToBottomRef.current = true;
setAddedRows(prev => [...prev, newRow]);
};
+
+ const handleCopySelectedRowsForPaste = useCallback(() => {
+ if (selectedRowKeys.length === 0) {
+ void message.info('请先选择要复制的行');
+ return;
+ }
+
+ const copiedRows = buildCopiedRowsForPaste({
+ rows: mergedDisplayData as Array>,
+ selectedRowKeys,
+ columnNames,
+ rowKeyField: GONAVI_ROW_KEY,
+ rowKeyToString: rowKeyStr,
+ });
+ if (copiedRows.length === 0) {
+ void message.info('未识别到可复制的行');
+ return;
+ }
+
+ setCopiedRowsForPaste(copiedRows);
+ void message.success(`已复制 ${copiedRows.length} 行,可粘贴为新增行`);
+ }, [selectedRowKeys, mergedDisplayData, columnNames, rowKeyStr]);
+
+ const handlePasteCopiedRowsAsNew = useCallback(() => {
+ if (copiedRowsForPaste.length === 0) {
+ void message.info('请先复制行');
+ return;
+ }
+
+ const nextRows = buildPastedRowsFromCopiedRows({
+ rows: copiedRowsForPaste,
+ columnNames,
+ rowKeyField: GONAVI_ROW_KEY,
+ createRowKey: (index) => {
+ pastedRowSequenceRef.current += 1;
+ return `paste-${Date.now()}-${pastedRowSequenceRef.current}-${index}`;
+ },
+ });
+ if (nextRows.length === 0) {
+ void message.info('没有可粘贴的行');
+ return;
+ }
+
+ pendingScrollToBottomRef.current = true;
+ setAddedRows(prev => [...prev, ...nextRows]);
+ setSelectedRowKeys(nextRows.map(row => row[GONAVI_ROW_KEY]));
+ void message.success(`已粘贴 ${nextRows.length} 行为新增行,请检查后提交事务`);
+ }, [copiedRowsForPaste, columnNames]);
+
const handleDeleteSelected = () => {
setDeletedRowKeys(prev => {
const newDeleted = new Set(prev);
@@ -4921,6 +4974,22 @@ const DataGrid: React.FC = ({
<>
} onClick={handleAddRow}>添加行
+ }
+ disabled={selectedRowKeys.length === 0}
+ onClick={handleCopySelectedRowsForPaste}
+ >
+ 复制行
+
+ }
+ disabled={copiedRowsForPaste.length === 0}
+ onClick={handlePasteCopiedRowsAsNew}
+ >
+ {copiedRowsForPaste.length > 0 ? `粘贴行 (${copiedRowsForPaste.length})` : '粘贴行'}
+
} danger disabled={selectedRowKeys.length === 0} onClick={handleDeleteSelected}>删除选中
{selectedRowKeys.length > 0 && 已选 {selectedRowKeys.length}}