From d8d0f0b5588fe4d082087255a9efb3bf071ce46f Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 7 Jun 2026 22:10:42 +0800 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20=E6=95=B0=E6=8D=AE=E8=A1=A8?= =?UTF-8?q?=E8=99=9A=E6=8B=9F=E6=BB=9A=E5=8A=A8=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=A4=A7=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增可复用 VirtualTable(仅渲染可视区行 + 上下占位, 表头 sticky) - DataTableView 改用 VirtualTable, 去掉 500 行显示上限 - CSV/TSV 大文件流畅滚动, 图表仍使用全量数据 --- src/components/DataTableView.vue | 28 +++--------- src/components/VirtualTable.vue | 76 ++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 23 deletions(-) create mode 100644 src/components/VirtualTable.vue diff --git a/src/components/DataTableView.vue b/src/components/DataTableView.vue index b0ed49a..3ac6297 100644 --- a/src/components/DataTableView.vue +++ b/src/components/DataTableView.vue @@ -31,27 +31,11 @@
- -
-
运行后在此查看数据表(支持 CSV / TSV)
-
- - - - - - - - - - - - - -
#{{ c }}
{{ i + 1 }}{{ row[ci] }}
-
共 {{ parsed.rows.length }} 行,表格仅显示前 {{ displayLimit }} 行(图表使用全部数据)
-
+ +
+
运行后在此查看数据表(支持 CSV / TSV)
+
@@ -60,6 +44,7 @@ import {computed, ref, watch} from 'vue' import {debounce} from 'lodash-es' import {BarChart3, FileDown, Table2, Trash2} from 'lucide-vue-next' import ChartPanel from './charts/ChartPanel.vue' +import VirtualTable from './VirtualTable.vue' import {downloadCsv} from '../utils/csv' const props = defineProps<{ @@ -70,7 +55,6 @@ const props = defineProps<{ const emit = defineEmits<{ clear: [] }>() const viewMode = ref<'table' | 'chart'>('table') -const displayLimit = 500 const stable = ref(props.output) const applyOutput = debounce((v: string) => { stable.value = v }, 150) @@ -158,8 +142,6 @@ const parsed = computed<{ columns: string[]; rows: string[][] }>(() => { return {columns, rows} }) -const displayRows = computed(() => parsed.value.rows.slice(0, displayLimit)) - const exportCsv = () => downloadCsv(parsed.value.columns, parsed.value.rows, `data-${Date.now()}.csv`) // 数据为空时回退表格视图 diff --git a/src/components/VirtualTable.vue b/src/components/VirtualTable.vue new file mode 100644 index 0000000..dd4c921 --- /dev/null +++ b/src/components/VirtualTable.vue @@ -0,0 +1,76 @@ + + + From 611a49318920dd564233a083158e78259c403dc3 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 7 Jun 2026 22:19:11 +0800 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20SQL=20=E7=BB=93=E6=9E=9C=E8=A1=A8?= =?UTF-8?q?=E6=94=B9=E7=94=A8=E8=99=9A=E6=8B=9F=E6=BB=9A=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - VirtualTable 增加 maxHeight 选项, 限高内虚拟滚动(适配多结果集堆叠) - SqlResultTable 每个结果集改用 VirtualTable, 大结果集与执行历史详情同样受益 - 移除内联表格与重复的 fmt(统一到 VirtualTable) --- src/components/SqlResultTable.vue | 28 ++++------------------------ src/components/VirtualTable.vue | 3 ++- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/src/components/SqlResultTable.vue b/src/components/SqlResultTable.vue index ff4053e..8d1cd57 100644 --- a/src/components/SqlResultTable.vue +++ b/src/components/SqlResultTable.vue @@ -11,26 +11,11 @@
✓ {{ m }}
- +
结果集 {{ ri + 1 }} · {{ rs.rows.length }} 行
-
- - - - - - - - - - - - - - -
{{ c }}
{{ fmt(row[ci]) }}
(0 行)
+
+
@@ -44,6 +29,7 @@ diff --git a/src/components/VirtualTable.vue b/src/components/VirtualTable.vue index dd4c921..7a909c7 100644 --- a/src/components/VirtualTable.vue +++ b/src/components/VirtualTable.vue @@ -1,5 +1,5 @@ diff --git a/src/utils/delimited.ts b/src/utils/delimited.ts new file mode 100644 index 0000000..ae44a02 --- /dev/null +++ b/src/utils/delimited.ts @@ -0,0 +1,89 @@ +// CSV / TSV 等分隔文本解析(纯函数,供主线程与 Web Worker 复用) + +export interface DelimitedTable { + columns: string[] + rows: string[][] +} + +/** 根据首行推断分隔符(制表符优先支持 TSV,其次分号,默认逗号) */ +export function detectDelimiter(text: string): string { + const firstLine = text.split('\n', 1)[0] || '' + const tabs = (firstLine.match(/\t/g) || []).length + const commas = (firstLine.match(/,/g) || []).length + const semis = (firstLine.match(/;/g) || []).length + if (tabs > 0 && tabs >= commas) { + return '\t' + } + if (semis > commas) { + return ';' + } + return ',' +} + +/** 支持引号、转义引号("")、字段内换行的分隔解析,返回二维数组 */ +export function parseDelimited(text: string, delim: string): string[][] { + const rows: string[][] = [] + let field = '' + let row: string[] = [] + let inQuotes = false + for (let i = 0; i < text.length; i++) { + const c = text[i] + if (inQuotes) { + if (c === '"') { + if (text[i + 1] === '"') { + field += '"' + i++ + } + else { + inQuotes = false + } + } + else { + field += c + } + } + else if (c === '"') { + inQuotes = true + } + else if (c === delim) { + row.push(field) + field = '' + } + else if (c === '\n') { + row.push(field) + rows.push(row) + row = [] + field = '' + } + else if (c !== '\r') { + field += c + } + } + if (field.length > 0 || row.length > 0) { + row.push(field) + rows.push(row) + } + return rows +} + +/** 解析为表格:首行作列名,其余对齐为等长行 */ +export function parseTable(text: string): DelimitedTable { + const trimmed = text.trim() + if (!trimmed) { + return {columns: [], rows: []} + } + const delim = detectDelimiter(trimmed) + const all = parseDelimited(trimmed, delim).filter(r => r.length > 1 || (r.length === 1 && r[0] !== '')) + if (all.length === 0) { + return {columns: [], rows: []} + } + const columns = all[0].map((c, i) => c.trim() || `列${i + 1}`) + const rows = all.slice(1).map(r => { + const out: string[] = new Array(columns.length).fill('') + for (let i = 0; i < columns.length; i++) { + out[i] = r[i] ?? '' + } + return out + }) + return {columns, rows} +} diff --git a/src/workers/delimited.worker.ts b/src/workers/delimited.worker.ts new file mode 100644 index 0000000..a1c5194 --- /dev/null +++ b/src/workers/delimited.worker.ts @@ -0,0 +1,18 @@ +// CSV / TSV 解析 Worker:在后台线程解析,避免超大文件阻塞 UI +import {parseTable} from '../utils/delimited' + +interface ParseRequest { + id: number + text: string +} + +self.onmessage = (e: MessageEvent) => { + const {id, text} = e.data + try { + const table = parseTable(text) + ;(self as unknown as Worker).postMessage({id, ...table}) + } + catch { + ;(self as unknown as Worker).postMessage({id, columns: [], rows: []}) + } +} From 45f341f29ca638bfaf105019465d98c592e248ff Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 7 Jun 2026 22:25:28 +0800 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20=E6=95=B0=E6=8D=AE=E8=A1=A8?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=88=97=E5=AE=BD=E6=8B=96=E6=8B=BD=E4=B8=8E?= =?UTF-8?q?=E7=82=B9=E5=87=BB=E5=88=97=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - VirtualTable 改为固定布局 + 内容启发式初始列宽, 列头右缘可拖拽调宽 - 点击列头排序: 升→降→取消, 数值/字符串智能比较, 空值置底 - 列变化时重置排序与列宽; SQL 结果表/CSV 数据表/执行历史均受益 --- src/components/VirtualTable.vue | 137 +++++++++++++++++++++++++++++--- 1 file changed, 127 insertions(+), 10 deletions(-) diff --git a/src/components/VirtualTable.vue b/src/components/VirtualTable.vue index 7a909c7..a9e44e9 100644 --- a/src/components/VirtualTable.vue +++ b/src/components/VirtualTable.vue @@ -1,18 +1,27 @@ From 96b0e255ea0171459a7f4222aee3ba542dba99c6 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 7 Jun 2026 22:27:06 +0800 Subject: [PATCH 05/17] =?UTF-8?q?feat:=20CSV/TSV=20=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6=E7=99=BE=E5=88=86=E6=AF=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - delimited.ts 解析支持 onProgress(0~1) 回调 - Worker 改为 progress/done 消息, 仅在整数百分比变化时上报 - DataTableView 头部显示「解析中 N%…」 --- src/components/DataTableView.vue | 16 ++++++++++++---- src/utils/delimited.ts | 17 +++++++++++------ src/workers/delimited.worker.ts | 17 +++++++++++++---- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/components/DataTableView.vue b/src/components/DataTableView.vue index 93ac098..d195697 100644 --- a/src/components/DataTableView.vue +++ b/src/components/DataTableView.vue @@ -5,7 +5,7 @@
数据表 - 解析中… + 解析中{{ parsing && percent > 0 ? ` ${percent}%` : '' }}… {{ parsed.columns.length }} 列 · {{ parsed.rows.length }} 行
@@ -59,17 +59,24 @@ const viewMode = ref<'table' | 'chart'>('table') // shallowRef:大数组不做深度响应,避免开销 const parsed = shallowRef({columns: [], rows: []}) const parsing = ref(false) +const percent = ref(0) // 解析放到 Web Worker,超大文件不阻塞 UI;创建失败则主线程兜底 let worker: Worker | null = null let reqId = 0 +type WorkerMsg = { id: number; type: 'progress'; percent: number } | { id: number; type: 'done' } & DelimitedTable try { worker = new Worker(new URL('../workers/delimited.worker.ts', import.meta.url), {type: 'module'}) - worker.onmessage = (e: MessageEvent<{ id: number } & DelimitedTable>) => { - if (e.data.id !== reqId) { + worker.onmessage = (e: MessageEvent) => { + const msg = e.data + if (msg.id !== reqId) { return // 丢弃过期结果 } - parsed.value = {columns: e.data.columns, rows: e.data.rows} + if (msg.type === 'progress') { + percent.value = msg.percent + return + } + parsed.value = {columns: msg.columns, rows: msg.rows} parsing.value = false if (!parsed.value.columns.length && viewMode.value === 'chart') { viewMode.value = 'table' @@ -82,6 +89,7 @@ catch { const doParse = (text: string) => { reqId++ + percent.value = 0 if (!text.trim()) { parsed.value = {columns: [], rows: []} parsing.value = false diff --git a/src/utils/delimited.ts b/src/utils/delimited.ts index ae44a02..9cfac67 100644 --- a/src/utils/delimited.ts +++ b/src/utils/delimited.ts @@ -20,13 +20,18 @@ export function detectDelimiter(text: string): string { return ',' } -/** 支持引号、转义引号("")、字段内换行的分隔解析,返回二维数组 */ -export function parseDelimited(text: string, delim: string): string[][] { +/** 支持引号、转义引号("")、字段内换行的分隔解析,返回二维数组;onProgress 报告 0~1 进度 */ +export function parseDelimited(text: string, delim: string, onProgress?: (p: number) => void): string[][] { const rows: string[][] = [] let field = '' let row: string[] = [] let inQuotes = false - for (let i = 0; i < text.length; i++) { + const len = text.length + const step = onProgress ? Math.max(1, Math.floor(len / 100)) : 0 + for (let i = 0; i < len; i++) { + if (step && i % step === 0) { + onProgress!(i / len) + } const c = text[i] if (inQuotes) { if (c === '"') { @@ -66,14 +71,14 @@ export function parseDelimited(text: string, delim: string): string[][] { return rows } -/** 解析为表格:首行作列名,其余对齐为等长行 */ -export function parseTable(text: string): DelimitedTable { +/** 解析为表格:首行作列名,其余对齐为等长行;onProgress 报告 0~1 进度 */ +export function parseTable(text: string, onProgress?: (p: number) => void): DelimitedTable { const trimmed = text.trim() if (!trimmed) { return {columns: [], rows: []} } const delim = detectDelimiter(trimmed) - const all = parseDelimited(trimmed, delim).filter(r => r.length > 1 || (r.length === 1 && r[0] !== '')) + const all = parseDelimited(trimmed, delim, onProgress).filter(r => r.length > 1 || (r.length === 1 && r[0] !== '')) if (all.length === 0) { return {columns: [], rows: []} } diff --git a/src/workers/delimited.worker.ts b/src/workers/delimited.worker.ts index a1c5194..5a3409b 100644 --- a/src/workers/delimited.worker.ts +++ b/src/workers/delimited.worker.ts @@ -1,4 +1,4 @@ -// CSV / TSV 解析 Worker:在后台线程解析,避免超大文件阻塞 UI +// CSV / TSV 解析 Worker:后台解析并上报进度,避免超大文件阻塞 UI import {parseTable} from '../utils/delimited' interface ParseRequest { @@ -6,13 +6,22 @@ interface ParseRequest { text: string } +const post = (msg: any) => (self as unknown as Worker).postMessage(msg) + self.onmessage = (e: MessageEvent) => { const {id, text} = e.data try { - const table = parseTable(text) - ;(self as unknown as Worker).postMessage({id, ...table}) + let lastPercent = -1 + const table = parseTable(text, (p) => { + const percent = Math.floor(p * 100) + if (percent !== lastPercent) { + lastPercent = percent + post({id, type: 'progress', percent}) + } + }) + post({id, type: 'done', columns: table.columns, rows: table.rows}) } catch { - ;(self as unknown as Worker).postMessage({id, columns: [], rows: []}) + post({id, type: 'done', columns: [], rows: []}) } } From 16fbeaa80b9b87946d29fcdb7b9a65c6d9aef731 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 7 Jun 2026 22:29:29 +0800 Subject: [PATCH 06/17] =?UTF-8?q?fix:=20=E6=95=B0=E6=8D=AE=E8=A1=A8?= =?UTF-8?q?=E5=A1=AB=E6=BB=A1=E5=AE=B9=E5=99=A8=E5=AE=BD=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 表格 width:100% + min-width:总列宽, 末尾加自适应填充列吸收剩余宽度 - 真实列保持各自宽度, 不足时填充列补齐、超出时横向滚动 - 占位行补 colspan 单元格确保高度生效 --- src/components/VirtualTable.vue | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/VirtualTable.vue b/src/components/VirtualTable.vue index a9e44e9..0c2dce7 100644 --- a/src/components/VirtualTable.vue +++ b/src/components/VirtualTable.vue @@ -1,9 +1,10 @@