mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-07 05:02:44 +08:00
@@ -23,11 +23,12 @@
|
||||
| #318 | mysql,bit 列,修改成 1 失败 | Fixed | `89d79ff` |
|
||||
| #319 | 关于运行外部 sql 文件的一些建议 | Deferred | - |
|
||||
| #320 | 无法连接达梦数据库 | Fixed | `1c2377b` |
|
||||
| #322 | 【拖选复制】希望添加 查询结果表格可以拖选复制,效果就如操作excel表格的选择复制一样 | Fixed | Pending |
|
||||
| #325 | 有没有考虑对数据库的驱动版本进行选择或者自定义? | Fixed | `af5e842` |
|
||||
| #327 | SHOW DATABASES 报错 | Fixed | `fb500ee` |
|
||||
| #328 | [Bug] 安装更新失败 | Fixed | `426ef3b` |
|
||||
| #329 | 如果调整了左侧导航栏的宽度后,建议左侧导航栏内增加横向滚动查看 | Fixed | `fcade0f` |
|
||||
| #330 | 建议在查询结果表格中增加自适应内容列宽的功能 | Fixed | Pending |
|
||||
| #330 | 建议在查询结果表格中增加自适应内容列宽的功能 | Fixed | `632e57e` |
|
||||
| #331 | 重复连接 DB,一分钟重试了 60 多次 | Fixed | `ca76440` |
|
||||
|
||||
## Notes
|
||||
@@ -92,6 +93,12 @@
|
||||
- 处理:为 `DataGrid` 的列宽拖拽手柄增加双击入口,按当前表头与已加载结果集内容估算目标宽度,并直接复用现有 `columnWidths` 状态更新布局。
|
||||
- 验证:新增 `frontend/src/components/dataGridAutoWidth.test.ts` 覆盖列宽估算规则,并执行 `frontend` 下 `npm run build` 确认 TS 与打包通过。
|
||||
|
||||
### #322
|
||||
|
||||
- 根因:`DataGrid` 已经具备拖选单元格和选区状态维护能力,但当前复制能力只支持把同一行选中的列值暂存为内部 patch,用于“粘贴到选中行”,没有把矩形选区真正导出到系统剪贴板。
|
||||
- 处理:新增选区复制 helper,将矩形选区按当前可见行列顺序导出为制表符文本;同时补上工具栏“复制选区”按钮和 `Ctrl/Cmd+C` 快捷键,让拖选后的复制行为更接近 Excel。
|
||||
- 验证:新增 `frontend/src/components/dataGridSelectionCopy.test.ts` 覆盖选区排序与剪贴板文本规整规则,并执行 `frontend` 下 `npm run build` 确认功能接线通过。
|
||||
|
||||
## Next
|
||||
|
||||
- 继续处理下一个最早且可直接落地的开放 issue。
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
resolveUniqueKeyGroupsFromIndexes,
|
||||
} from './dataGridCopyInsert';
|
||||
import { calculateAutoFitColumnWidth } from './dataGridAutoWidth';
|
||||
import { buildSelectedCellClipboardText } from './dataGridSelectionCopy';
|
||||
|
||||
// --- Error Boundary ---
|
||||
interface DataGridErrorBoundaryState {
|
||||
@@ -3734,6 +3735,59 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
navigator.clipboard.writeText(text).catch(console.error);
|
||||
void message.success("Copied to clipboard");
|
||||
}, []);
|
||||
|
||||
const handleCopySelectedCellsToClipboard = 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 text = buildSelectedCellClipboardText({
|
||||
selectedCells: parsed,
|
||||
rows: mergedDisplayData as Array<Record<string, any>>,
|
||||
columnOrder: displayColumnNames,
|
||||
rowKeyField: GONAVI_ROW_KEY,
|
||||
});
|
||||
if (!text) {
|
||||
void message.info('当前选区没有可复制内容');
|
||||
return;
|
||||
}
|
||||
|
||||
copyToClipboard(text);
|
||||
}, [selectedCells, mergedDisplayData, displayColumnNames, copyToClipboard]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cellEditMode) return;
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const isCopy = (event.ctrlKey || event.metaKey) && !event.altKey && String(event.key || '').toLowerCase() === 'c';
|
||||
if (!isCopy) return;
|
||||
|
||||
const activeElement = document.activeElement as HTMLElement | null;
|
||||
const tagName = String(activeElement?.tagName || '').toLowerCase();
|
||||
if (tagName === 'input' || tagName === 'textarea' || activeElement?.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeSelection = currentSelectionRef.current.size > 0 ? currentSelectionRef.current : selectedCells;
|
||||
if (activeSelection.size === 0) return;
|
||||
|
||||
event.preventDefault();
|
||||
handleCopySelectedCellsToClipboard();
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, [cellEditMode, selectedCells, handleCopySelectedCellsToClipboard]);
|
||||
|
||||
const getTargets = useCallback((clickedRecord: any) => {
|
||||
const selKeys = selectedRowKeysRef.current;
|
||||
@@ -4892,6 +4946,12 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
</Button>
|
||||
{cellEditMode && selectedCells.size > 0 && (
|
||||
<>
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
onClick={handleCopySelectedCellsToClipboard}
|
||||
>
|
||||
复制选区 ({selectedCells.size})
|
||||
</Button>
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
onClick={handleCopySelectedColumnsFromRow}
|
||||
|
||||
43
frontend/src/components/dataGridSelectionCopy.test.ts
Normal file
43
frontend/src/components/dataGridSelectionCopy.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildSelectedCellClipboardText } from './dataGridSelectionCopy';
|
||||
|
||||
describe('dataGridSelectionCopy helpers', () => {
|
||||
it('builds clipboard text in visible row and column order', () => {
|
||||
const text = buildSelectedCellClipboardText({
|
||||
selectedCells: [
|
||||
{ rowKey: 'row-2', colName: 'name' },
|
||||
{ rowKey: 'row-1', colName: 'id' },
|
||||
{ rowKey: 'row-1', colName: 'name' },
|
||||
{ rowKey: 'row-2', colName: 'id' },
|
||||
],
|
||||
rows: [
|
||||
{ __rowKey: 'row-1', id: 1, name: 'Alice' },
|
||||
{ __rowKey: 'row-2', id: 2, name: 'Bob' },
|
||||
],
|
||||
columnOrder: ['id', 'name', 'email'],
|
||||
rowKeyField: '__rowKey',
|
||||
});
|
||||
|
||||
expect(text).toBe('1\tAlice\n2\tBob');
|
||||
});
|
||||
|
||||
it('normalizes null, objects and multiline text for clipboard safety', () => {
|
||||
const text = buildSelectedCellClipboardText({
|
||||
selectedCells: [
|
||||
{ rowKey: 'row-1', colName: 'notes' },
|
||||
{ rowKey: 'row-1', colName: 'meta' },
|
||||
{ rowKey: 'row-2', colName: 'notes' },
|
||||
{ rowKey: 'row-2', colName: 'meta' },
|
||||
],
|
||||
rows: [
|
||||
{ __rowKey: 'row-1', notes: null, meta: { a: 1 } },
|
||||
{ __rowKey: 'row-2', notes: 'line1\nline2\tvalue', meta: [1, 2] },
|
||||
],
|
||||
columnOrder: ['notes', 'meta'],
|
||||
rowKeyField: '__rowKey',
|
||||
});
|
||||
|
||||
expect(text).toBe('NULL\t{"a":1}\nline1 line2 value\t[1,2]');
|
||||
});
|
||||
});
|
||||
65
frontend/src/components/dataGridSelectionCopy.ts
Normal file
65
frontend/src/components/dataGridSelectionCopy.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
export interface SelectedGridCell {
|
||||
rowKey: string;
|
||||
colName: string;
|
||||
}
|
||||
|
||||
const normalizeClipboardCellValue = (value: unknown): string => {
|
||||
if (value === null || value === undefined) {
|
||||
return 'NULL';
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value.replace(/\r\n/g, '\n').replace(/[\t\n\r]+/g, ' ').trim();
|
||||
}
|
||||
|
||||
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(value).replace(/[\t\n\r]+/g, ' ').trim();
|
||||
} catch {
|
||||
return String(value).replace(/[\t\n\r]+/g, ' ').trim();
|
||||
}
|
||||
};
|
||||
|
||||
export const buildSelectedCellClipboardText = ({
|
||||
selectedCells,
|
||||
rows,
|
||||
columnOrder,
|
||||
rowKeyField,
|
||||
}: {
|
||||
selectedCells: SelectedGridCell[];
|
||||
rows: Array<Record<string, any>>;
|
||||
columnOrder: string[];
|
||||
rowKeyField: string;
|
||||
}): string => {
|
||||
if (!selectedCells.length || !rows.length || !columnOrder.length || !rowKeyField) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const selectedRowKeys = new Set(selectedCells.map((cell) => cell.rowKey));
|
||||
const selectedColumnKeys = new Set(selectedCells.map((cell) => cell.colName));
|
||||
const orderedRows = rows.filter((row) => selectedRowKeys.has(String(row?.[rowKeyField] ?? '')));
|
||||
const orderedColumns = columnOrder.filter((columnName) => selectedColumnKeys.has(columnName));
|
||||
|
||||
if (!orderedRows.length || !orderedColumns.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const selectedCellKeySet = new Set(selectedCells.map((cell) => `${cell.rowKey}::${cell.colName}`));
|
||||
|
||||
return orderedRows
|
||||
.map((row) => {
|
||||
const rowKey = String(row?.[rowKeyField] ?? '');
|
||||
return orderedColumns
|
||||
.map((columnName) => {
|
||||
if (!selectedCellKeySet.has(`${rowKey}::${columnName}`)) {
|
||||
return '';
|
||||
}
|
||||
return normalizeClipboardCellValue(row?.[columnName]);
|
||||
})
|
||||
.join('\t');
|
||||
})
|
||||
.join('\n');
|
||||
};
|
||||
Reference in New Issue
Block a user