From da5e879409873e0f7bc65281ea9364cb8473f344 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 20 Mar 2026 14:10:23 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(data-grid):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=A4=8D=E5=88=B6=E4=B8=BAINSERT/CSV/Markdown?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E4=B9=B1=E5=BA=8F=E5=8F=8A=E7=89=B9=E6=AE=8A?= =?UTF-8?q?=E5=AD=97=E7=AC=A6=E6=9C=AA=E8=BD=AC=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - INSERT:使用 columnNames 保持 DDL 字段顺序,值中单引号转义为 '' - CSV:使用 columnNames 保持字段顺序,值中双引号转义为 "",增加表头行 - Markdown:使用 columnNames 保持字段顺序,转义管道符和换行,增加表头行和分隔行 - refs #277 --- frontend/src/components/DataGrid.tsx | 48 ++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index bc85e60..80710df 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -673,11 +673,20 @@ const ContextMenuRow = React.memo(({ children, record, ...props }: any) => { { key: 'csv', label: '复制为 CSV', icon: , onClick: () => handleCopyCsv(record) }, { key: 'copy', label: '复制为 Markdown', icon: , onClick: () => { const records = getTargets(); - const lines = records.map((r: any) => { - const { [GONAVI_ROW_KEY]: _rowKey, ...vals } = r; - return `| ${Object.values(vals).join(' | ')} |`; + const orderedCols = displayDataRef.current.length > 0 + ? Object.keys(displayDataRef.current[0]).filter(c => c !== GONAVI_ROW_KEY) + : []; + const header = `| ${orderedCols.join(' | ')} |`; + const separator = `| ${orderedCols.map(() => '---').join(' | ')} |`; + const rows = records.map((r: any) => { + const values = orderedCols.map(c => { + const v = r[c]; + if (v === null || v === undefined) return 'NULL'; + return String(v).replace(/\|/g, '\\|').replace(/\n/g, ' '); + }); + return `| ${values.join(' | ')} |`; }); - copyToClipboard(lines.join('\n')); + copyToClipboard([header, separator, ...rows].join('\n')); } }, { type: 'divider' }, { @@ -3324,14 +3333,19 @@ const DataGrid: React.FC = ({ return; } const records = getTargets(record); + // 使用 columnNames 保持表定义的字段顺序,而非 Object.keys() 的不确定顺序 + const orderedCols = columnNames.filter(c => c !== GONAVI_ROW_KEY); const sqlList = records.map((r: any) => { - const { [GONAVI_ROW_KEY]: _rowKey, ...vals } = r; - const cols = Object.keys(vals); - const values = Object.values(vals).map(v => v === null ? 'NULL' : `'${v}'`); + const values = orderedCols.map(c => { + const v = r[c]; + if (v === null || v === undefined) return 'NULL'; + const escaped = String(v).replace(/'/g, "''"); + return `'${escaped}'`; + }); const targetTable = tableName || 'table'; - return `INSERT INTO \`${targetTable}\` (${cols.map(c => `\`${c}\``).join(', ')}) VALUES (${values.join(', ')});`; + return `INSERT INTO \`${targetTable}\` (${orderedCols.map(c => `\`${c}\``).join(', ')}) VALUES (${values.join(', ')});`; }); - copyToClipboard(sqlList.join('\n')); }, [supportsCopyInsert, tableName, getTargets, copyToClipboard]); + copyToClipboard(sqlList.join('\n')); }, [supportsCopyInsert, tableName, columnNames, getTargets, copyToClipboard]); const handleCopyJson = useCallback((record: any) => { const records = getTargets(record); @@ -3344,13 +3358,21 @@ const DataGrid: React.FC = ({ const handleCopyCsv = useCallback((record: any) => { const records = getTargets(record); + // 使用 columnNames 保持表定义的字段顺序 + const orderedCols = columnNames.filter(c => c !== GONAVI_ROW_KEY); + const header = orderedCols.map(c => `"${c}"`).join(','); const lines = records.map((r: any) => { - const { [GONAVI_ROW_KEY]: _rowKey, ...vals } = r; - const values = Object.values(vals).map(v => v === null ? 'NULL' : `"${v}"`); + const values = orderedCols.map(c => { + const v = r[c]; + if (v === null || v === undefined) return 'NULL'; + // CSV 标准:值中的双引号转义为两个双引号 + const escaped = String(v).replace(/"/g, '""'); + return `"${escaped}"`; + }); return values.join(','); }); - copyToClipboard(lines.join('\n')); - }, [getTargets, copyToClipboard]); + copyToClipboard([header, ...lines].join('\n')); + }, [getTargets, columnNames, copyToClipboard]); const buildConnConfig = useCallback(() => { if (!connectionId) return null;