Commit ef99984a authored by Data Governance Dev's avatar Data Governance Dev

feat(web3): 删导入/批量删除按钮 + 通用 Excel 导出(抽自 web2)

1. DataQualityView 按钮区只留「字段配置 + 导出 Excel」

   删导入(btn-success)/批量删除(btn-danger)+ 对应的 mock 函数

   导出按钮加 :disabled(rows 为空或导出中禁用)

2. 新增 src/utils/excel.js

   从 web2 IdCardTable.vue 抽 exportExcel + applyAllBorders + applyHeaderStyle

   封装成纯函数 exportToExcel({ rows, columns, filename, sheetName, confirm })

   三行表头:comment / cn / 数据;冻结前 2 行 + 黑色细边框 + 加粗灰背景

   columns 兼容 {key,cn,comment}[] 和 string[](fallback)

   大于 5000 行走 confirm(n) 二次确认(弹窗 UI 在调用方做)

3. onExport 列范围

   visibleFields 非空 → 用户勾选;为空(未保存过)→ 退化为 resultFields 全量

   过滤掉 __reason(ResultTable 内部追加的备注列)

   文件名 <任务名>_<时间戳>.xlsx 防多次导出覆盖

4. +xlsx-js-style 依赖(跟 web2 同库,导出样式一致)

   ⚠ bundle 多约 700KB(xlsx-js-style 是胖依赖,build 已超 500KB 阈值)

   后续可改 dynamic import(点导出按钮时再加载),本次没做
parent 270ccea9
...@@ -11,7 +11,8 @@ ...@@ -11,7 +11,8 @@
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"element-plus": "^2.9.1", "element-plus": "^2.9.1",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0" "vue-router": "^4.5.0",
"xlsx-js-style": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
...@@ -1159,12 +1160,90 @@ ...@@ -1159,12 +1160,90 @@
"vue": "^3.5.0" "vue": "^3.5.0"
} }
}, },
"node_modules/adler-32": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz",
"integrity": "sha512-/vUqU/UY4MVeFsg+SsK6c+/05RZXIHZMGJA+PX5JyWI0ZRcBpupnRuPLU/NXXoFwMYCPCoxIfElM2eS+DUXCqQ==",
"license": "Apache-2.0",
"dependencies": {
"exit-on-epipe": "~1.0.1",
"printj": "~1.1.0"
},
"bin": {
"adler32": "bin/adler32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/async-validator": { "node_modules/async-validator": {
"version": "4.2.5", "version": "4.2.5",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/cfb/node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/codepage": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz",
"integrity": "sha512-iz3zJLhlrg37/gYRWgEPkaFTtzmnEv1h+r7NgZum2lFElYQPi0/5bnmuDfODHxfp0INEfnRqyfyeIJDbb7ahRw==",
"license": "Apache-2.0",
"dependencies": {
"commander": "~2.14.1",
"exit-on-epipe": "~1.0.1"
},
"bin": {
"codepage": "bin/codepage.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/codepage/node_modules/commander": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
"integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==",
"license": "MIT"
},
"node_modules/commander": {
"version": "2.17.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
"integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
"license": "MIT"
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
...@@ -1263,6 +1342,15 @@ ...@@ -1263,6 +1342,15 @@
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/exit-on-epipe": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz",
"integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fdir": { "node_modules/fdir": {
"version": "6.5.0", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
...@@ -1281,6 +1369,21 @@ ...@@ -1281,6 +1369,21 @@
} }
} }
}, },
"node_modules/fflate": {
"version": "0.3.11",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.3.11.tgz",
"integrity": "sha512-Rr5QlUeGN1mbOHlaqcSYMKVpPbgLy0AWT/W0EHxA6NGI12yO1jpoui2zBBvU2G824ltM6Ut8BFgfHSBGfkmS0A==",
"license": "MIT"
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
...@@ -1405,6 +1508,18 @@ ...@@ -1405,6 +1508,18 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/printj": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz",
"integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==",
"license": "Apache-2.0",
"bin": {
"printj": "bin/printj.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.62.4", "version": "4.62.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz",
...@@ -1460,6 +1575,18 @@ ...@@ -1460,6 +1575,18 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.17", "version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
...@@ -1593,6 +1720,48 @@ ...@@ -1593,6 +1720,48 @@
"peerDependencies": { "peerDependencies": {
"vue": "^3.5.0" "vue": "^3.5.0"
} }
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/xlsx-js-style": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/xlsx-js-style/-/xlsx-js-style-1.2.0.tgz",
"integrity": "sha512-DDT4FXFSWfT4DXMSok/m3TvmP1gvO3dn0Eu/c+eXHW5Kzmp7IczNkxg/iEPnImbG9X0Vb8QhROda5eatSR/97Q==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.2.0",
"cfb": "^1.1.4",
"codepage": "~1.14.0",
"commander": "~2.17.1",
"crc-32": "~1.2.0",
"exit-on-epipe": "~1.0.1",
"fflate": "^0.3.8",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
} }
} }
} }
...@@ -12,10 +12,11 @@ ...@@ -12,10 +12,11 @@
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"element-plus": "^2.9.1", "element-plus": "^2.9.1",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0" "vue-router": "^4.5.0",
"xlsx-js-style": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.0.5" "vite": "^6.0.5"
} }
} }
\ No newline at end of file
// ============================================================================
// 通用 Excel 导出工具(web2 IdCardTable 抽出来)
//
// 设计要点:
// - 复用 web2 IdCardTable.vue 里的「注释行 + 字段名行 + 数据」三行表头格式
// - 用 xlsx-js-style(支持 cell.s 边框 + 字体 + 背景)
// - 列定义兼容两种来源:
// ① 直接传 [{ key, cn, comment? }](resultFields 直接 map 即可)
// ② 传 string[](仅字段名,无注释行)
// - rows 里的字段值走 row[col.key] 抽取,null/undefined → ''(避免 Excel 显示 undefined)
//
// 字段命名(comment)约定:每个字段列头显示两行
// 第 1 行:字段注释(column_comment,缺则留空)
// 第 2 行:字段显示名(cn —— 中文表头;用户原始字段名为保留显示给业务人员)
// 这样业务人员打开 Excel 一眼能看懂字段含义,也能看到 DB 里的实际字段名
// ============================================================================
import * as XLSX from 'xlsx-js-style'
/**
* 把当前 rows 按勾选的列导出成 Excel
*
* @param {Object} opts
* @param {Array<Object>} opts.rows 数据行 [{ colKey: value, ... }]
* @param {Array<{ key: string, cn: string, comment?: string }>|string[]} opts.columns
* 字段定义;string[] 时退化为「无注释行」
* @param {string} [opts.filename='export.xlsx'] 输出文件名
* @param {string} [opts.sheetName='export'] sheet 名
* @param {number} [opts.largeThreshold=5000] 超过这个数走二次确认弹窗
* @param {(n:number)=>Promise<boolean>} [opts.confirm] 二次确认函数;返回 true 继续,false 取消
* —— 弹窗 UI 在调用方做,本工具保持纯净
* @returns {{ rows: number, cols: number, filename: string } | null}
* 导出成功返回摘要;用户取消返回 null
*/
export async function exportToExcel(opts) {
const {
rows,
columns,
filename = 'export.xlsx',
sheetName = 'export',
largeThreshold = 5000,
confirm,
} = opts
if (!rows || rows.length === 0) {
throw new Error('当前表无数据,无需导出')
}
if (!columns || columns.length === 0) {
throw new Error('未指定任何列,无法导出')
}
// 大数据量二次确认(弹窗放调用方,本工具只问 yes/no)
if (rows.length > largeThreshold && confirm) {
const ok = await confirm(rows.length)
if (!ok) return null
}
// 列归一:拿到 [{ key, cn, comment }]
const cols = normalizeColumns(columns)
// 第 1 行:注释(comment 缺省时留空字符串,不留 null —— Excel 不喜欢 null)
const commentRow = cols.map((c) => c.comment || '')
// 第 2 行:字段显示名(cn —— 中文表头)
const nameRow = cols.map((c) => c.cn || c.key)
// 数据行:按 col.key 抽取,null/undefined → ''(xlsx 把 null 显示成 #NULL!,不可读)
const dataRows = rows.map((row) =>
cols.map((c) => {
const v = row[c.key]
return v === null || v === undefined ? '' : v
}),
)
const aoa = [commentRow, nameRow, ...dataRows]
const ws = XLSX.utils.aoa_to_sheet(aoa)
// 样式:所有 cell 加细边框 + 前两行加粗 + 浅灰背景
applyAllBorders(ws)
applyHeaderStyle(ws, cols.length)
// 冻结前 2 行(滚动时表头固定)
ws['!freeze'] = { xSplit: 0, ySplit: 2 }
const wb = XLSX.utils.book_new()
const safeSheetName = sanitizeSheetName(sheetName)
XLSX.utils.book_append_sheet(wb, ws, safeSheetName)
const safeFilename = sanitizeFilename(filename)
XLSX.writeFile(wb, safeFilename)
return {
rows: rows.length,
cols: cols.length,
filename: safeFilename,
}
}
/**
* 列定义归一化:兼容两种输入
* - { key, cn, comment? }[]:直接用
* - string[]:视为只有 key,cn = key,comment 留空(fallback,给老调用方)
*/
function normalizeColumns(columns) {
return columns.map((c) => {
if (typeof c === 'string') {
return { key: c, cn: c, comment: '' }
}
return {
key: c.key,
cn: c.cn || c.key,
comment: c.comment || '',
}
})
}
/**
* Sheet 名规则(Excel 限制):
* - 最长 31 字符
* - 不能含 : \ / ? * [ ]
* - 空字符串兜底为 'export'
*/
function sanitizeSheetName(s) {
return (
String(s || 'export')
.replace(/[:\\/?*\[\]]/g, '_')
.slice(0, 31) || 'export'
)
}
/**
* 文件名规则(Windows 限制):
* - 不能含 < > : " / \ | ? *
* - 限长 100 字符
* - 空字符串兜底为 'export'
*/
function sanitizeFilename(s) {
return (
String(s || 'export').replace(/[<>:"/\\|?*]/g, '_').slice(0, 100) || 'export'
)
}
/* ── Excel 样式(xlsx-js-style 才支持 cell.s) ── */
const BORDER_THIN = { style: 'thin', color: { rgb: 'FF000000' } }
const BORDER_ALL = {
top: BORDER_THIN,
bottom: BORDER_THIN,
left: BORDER_THIN,
right: BORDER_THIN,
}
/**
* 给 sheet 内所有有数据的 cell 加黑色细边框
*/
function applyAllBorders(ws) {
if (!ws['!ref']) return
const range = XLSX.utils.decode_range(ws['!ref'])
for (let R = range.s.r; R <= range.e.r; R++) {
for (let C = range.s.c; C <= range.e.c; C++) {
const addr = XLSX.utils.encode_cell({ r: R, c: C })
const cell = ws[addr]
if (!cell) continue
cell.s = { ...(cell.s || {}), border: BORDER_ALL }
}
}
}
/**
* 给前两行(注释行 + 字段名行)加粗 + 浅灰背景 + 居中 + 自动换行
*/
function applyHeaderStyle(ws, cols) {
for (let R = 0; R < 2; R++) {
for (let C = 0; C < cols; C++) {
const addr = XLSX.utils.encode_cell({ r: R, c: C })
const cell = ws[addr]
if (!cell) continue
cell.s = {
...(cell.s || {}),
font: { bold: true, color: { rgb: 'FF303133' } },
fill: { patternType: 'solid', fgColor: { rgb: 'FFF2F2F2' } },
alignment: { horizontal: 'center', vertical: 'center', wrapText: true },
}
}
}
}
\ No newline at end of file
...@@ -66,9 +66,9 @@ ...@@ -66,9 +66,9 @@
</div> </div>
<div class="q-card__actions"> <div class="q-card__actions">
<button class="btn" @click="fieldDrawerOpen = true">字段配置</button> <button class="btn" @click="fieldDrawerOpen = true">字段配置</button>
<button class="btn">导出</button> <button class="btn" :disabled="!rows.length || exporting" @click="onExport">
<button class="btn btn-success" @click="onImport">导入</button> {{ exporting ? '导出中…' : '导出 Excel' }}
<button class="btn btn-danger" @click="batchDelete">批量删除</button> </button>
</div> </div>
</div> </div>
...@@ -104,6 +104,7 @@ import { ElMessage, ElMessageBox } from 'element-plus' ...@@ -104,6 +104,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
// mockData 已不引用:表头改为 resultFields(真任务字段),violationRows 同样由 /start 返回 // mockData 已不引用:表头改为 resultFields(真任务字段),violationRows 同样由 /start 返回
import { listTasks } from '@/api/tasks' import { listTasks } from '@/api/tasks'
import { startQuery, fetchPage, cancelQuery } from '@/api/queries' import { startQuery, fetchPage, cancelQuery } from '@/api/queries'
import { exportToExcel } from '@/utils/excel'
import FieldConfigDrawer from '@/components/FieldConfigDrawer.vue' import FieldConfigDrawer from '@/components/FieldConfigDrawer.vue'
import ResultTable from '@/components/ResultTable.vue' import ResultTable from '@/components/ResultTable.vue'
...@@ -353,33 +354,67 @@ function onPageChange({ page, pageSize }) { ...@@ -353,33 +354,67 @@ function onPageChange({ page, pageSize }) {
console.debug('[DataQualityView] page change', { page, pageSize }) console.debug('[DataQualityView] page change', { page, pageSize })
} }
// 占位操作(之前实现的 editRow / deleteRow / batchDelete / onImport 暂留,不影响主链路) // ── 导出 Excel ──
function editRow(row) { // 字段范围:优先用 FieldConfigDrawer 选定的 visibleFields;
ElMessageBox.alert(`打开「${row.user_id || row.id_card || '该行'}」的编辑表单(待实现)`, '修改') // 为空(用户没保存过 / 默认全勾)时退化为 resultFields 全量
} const exporting = ref(false)
function deleteRow(row) {
ElMessageBox.confirm('确定删除该条数据?', '提示', { type: 'warning' }) async function onExport() {
.then(() => { if (!rows.value.length) {
const i = rows.value.indexOf(row) ElMessage.warning('当前表无数据,无需导出')
if (i >= 0) rows.value.splice(i, 1)
ElMessage.success('已删除')
}).catch(() => {})
}
function batchDelete() {
const checked = rows.value.filter((r) => r.checked)
if (checked.length === 0) {
ElMessage.warning('请先勾选要删除的数据')
return return
} }
ElMessageBox.confirm(`确定删除选中的 ${checked.length} 条数据?`, '提示', { type: 'warning' }) // 决定导出哪些列 —— visibleFields 非空 → 用户勾选;空 → 全部 resultFields
.then(() => { // 过滤掉内部 __reason(不在 resultFields 里,但可见列表里可能含,防止误传)
rows.value = rows.value.filter((r) => !r.checked) const fieldSet = new Set(visibleFields.value || [])
ElMessage.success('已删除') const exportCols = resultFields.value.filter(
}).catch(() => {}) (c) => c.key !== '__reason' && (fieldSet.size === 0 || fieldSet.has(c.key)),
} )
function onImport() { if (!exportCols.length) {
ElMessage.info('触发导入(mock:实际将请求后端上传接口)') ElMessage.warning('未勾选任何字段,请先在「字段配置」里勾选要导出的列')
return
}
// 文件名:<任务名>_<时间戳>.xlsx(带时间戳避免多次导出覆盖)
const stamp = new Date()
.toISOString()
.replace(/[-:T]/g, '')
.slice(0, 14) // YYYYMMDDhhmmss
const filename = `${selectedTask.value || 'export'}_${stamp}.xlsx`
exporting.value = true
try {
const summary = await exportToExcel({
rows: rows.value,
columns: exportCols,
filename,
sheetName: selectedTask.value || 'export',
// 大于 5000 行走二次确认(避免误点 + 卡死)
confirm: async (n) => {
try {
await ElMessageBox.confirm(
`当前表有 ${n.toLocaleString()} 行,导出可能耗时数秒。是否继续?`,
'导出确认',
{ confirmButtonText: '导出', cancelButtonText: '取消', type: 'warning' },
)
return true
} catch {
return false
}
},
})
if (summary) {
ElMessage.success(
`已导出 ${summary.rows.toLocaleString()} 行 × ${summary.cols} 列 → ${summary.filename}`,
)
}
} catch (e) {
ElMessage.error(`导出失败:${e.message || e}`)
} finally {
exporting.value = false
}
} }
function onSaveFields(list) { function onSaveFields(list) {
visibleFields.value = list visibleFields.value = list
fieldDrawerOpen.value = false fieldDrawerOpen.value = false
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment