feat(data-grid): 增加复制行和粘贴行操作

- 表数据工具栏新增复制行、粘贴行按钮
- 支持将选中行复制为新增行草稿,提交前可继续检查编辑
- 抽离行复制粘贴数据构造逻辑并补充回归测试
Refs #332
This commit is contained in:
Syngnat
2026-04-26 19:09:10 +08:00
parent 01dd62f4e2
commit 9eb06f6f96
2 changed files with 91 additions and 0 deletions

View File

@@ -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(
<DataGrid
data={[
{
__gonavi_row_key__: 'row-1',
id: 1,
name: 'alpha',
},
]}
columnNames={['id', 'name']}
loading={false}
tableName="users"
/>,
);
expect(markup).toContain('data-grid-copy-row-action="true"');
expect(markup).toContain('data-grid-paste-row-action="true"');
expect(markup).toContain('复制行');
expect(markup).toContain('粘贴行');
});
});

View File

@@ -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<DataGridProps> = ({
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<DataGridProps> = ({
const [cellEditMode, setCellEditMode] = useState(false);
const [selectedCells, setSelectedCells] = useState<Set<string>>(new Set());
const [copiedCellPatch, setCopiedCellPatch] = useState<{ sourceRowKey: string; values: Record<string, any> } | null>(null);
const [copiedRowsForPaste, setCopiedRowsForPaste] = useState<Array<Record<string, any>>>([]);
const [batchEditModalOpen, setBatchEditModalOpen] = useState(false);
const [batchEditValue, setBatchEditValue] = useState('');
const [batchEditSetNull, setBatchEditSetNull] = useState(false);
@@ -2251,6 +2254,7 @@ const DataGrid: React.FC<DataGridProps> = ({
setDeletedRowKeys(new Set());
setSelectedRowKeys([]);
setCopiedCellPatch(null);
setCopiedRowsForPaste([]);
setRowEditorOpen(false);
setRowEditorRowKey('');
rowEditorBaseRawRef.current = {};
@@ -3622,6 +3626,55 @@ const DataGrid: React.FC<DataGridProps> = ({
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<Record<string, any>>,
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<DataGridProps> = ({
<>
<div style={{ width: 1, background: toolbarDividerColor, height: 20, margin: '0 8px' }} />
<Button icon={<PlusOutlined />} onClick={handleAddRow}></Button>
<Button
data-grid-copy-row-action="true"
icon={<CopyOutlined />}
disabled={selectedRowKeys.length === 0}
onClick={handleCopySelectedRowsForPaste}
>
</Button>
<Button
data-grid-paste-row-action="true"
icon={<VerticalAlignBottomOutlined />}
disabled={copiedRowsForPaste.length === 0}
onClick={handlePasteCopiedRowsAsNew}
>
{copiedRowsForPaste.length > 0 ? `粘贴行 (${copiedRowsForPaste.length})` : '粘贴行'}
</Button>
<Button icon={<DeleteOutlined />} danger disabled={selectedRowKeys.length === 0} onClick={handleDeleteSelected}></Button>
{selectedRowKeys.length > 0 && <span style={{ fontSize: '12px', color: '#888' }}> {selectedRowKeys.length}</span>}
<div style={{ width: 1, background: toolbarDividerColor, height: 20, margin: '0 8px' }} />