新增 xlsx 库支持,优化 Excel 导出功能,更新相关文档
This commit is contained in:
102
docs/excel导出.md
Normal file
102
docs/excel导出.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
要真·生成 Excel(.xlsx)并避免中文乱码,最简单稳妥的做法是用 **SheetJS (xlsx)**。xlsx 本身就是 Unicode,不需要再加 BOM,中文不会乱码。
|
||||||
|
|
||||||
|
### 1) 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i xlsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2) 在你的工具函数里引入并替换 `exportToExcel`
|
||||||
|
|
||||||
|
把你原来的 `exportToExcel`(写 `application/vnd.ms-excel` 的 HTML 方案)换成下面这个基于 xlsx 的实现——它会按 `headers` 的顺序与标题导出为真正的 `.xlsx` 文件,并自动设置列宽、表头冻结、筛选,同时做了“公式注入”防护。
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 顶部添加
|
||||||
|
import { utils as XLSXUtils, writeFile as XLSXWriteFile } from 'xlsx'
|
||||||
|
|
||||||
|
// 计算列宽(中文按双字节处理)
|
||||||
|
function calcWch(val) {
|
||||||
|
const s = (val ?? '').toString()
|
||||||
|
let w = 0
|
||||||
|
for (const ch of s) w += ch.charCodeAt(0) > 255 ? 2 : 1
|
||||||
|
return Math.min(Math.max(w, 8), 60) // 8~60 字符宽
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单日期格式化(如果字段是 Date 或 ISO 字符串)
|
||||||
|
function fmtDate(v) {
|
||||||
|
const d = v instanceof Date ? v : (typeof v === 'string' && !isNaN(Date.parse(v)) ? new Date(v) : null)
|
||||||
|
if (!d) return v
|
||||||
|
const pad = n => (n < 10 ? '0' + n : '' + n)
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 真实 .xlsx 导出(中文不会乱码)
|
||||||
|
export function exportToExcel(data, headers, filename = '导出.xlsx', sheetName = 'Sheet1') {
|
||||||
|
// 1) 规范化数据:按 headers 顺序映射到“显示列名 -> 值”
|
||||||
|
const headerLabels = headers.map(h => h.label)
|
||||||
|
const rows = (data || []).map(row => {
|
||||||
|
const obj = {}
|
||||||
|
headers.forEach(h => {
|
||||||
|
let v = row?.[h.key]
|
||||||
|
// 识别并格式化日期
|
||||||
|
v = fmtDate(v)
|
||||||
|
|
||||||
|
// 复杂对象转字符串
|
||||||
|
if (v && typeof v === 'object' && !(v instanceof Date)) {
|
||||||
|
try { v = JSON.stringify(v) } catch { v = String(v) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公式注入防护(以 = + - @ 开头的文本加前导单引号)
|
||||||
|
if (typeof v === 'string' && /^[=+\-@]/.test(v)) v = "'" + v
|
||||||
|
|
||||||
|
// null/undefined 转为空串,保留 0/false
|
||||||
|
if (v === null || v === undefined) v = ''
|
||||||
|
obj[h.label] = v
|
||||||
|
})
|
||||||
|
return obj
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2) 生成工作表
|
||||||
|
const ws = XLSXUtils.json_to_sheet(rows, { header: headerLabels })
|
||||||
|
|
||||||
|
// 3) 列宽:根据表头和数据计算
|
||||||
|
const cols = headers.map(h => {
|
||||||
|
const headW = calcWch(h.label)
|
||||||
|
const dataW = rows.reduce((mx, r) => Math.max(mx, calcWch(r[h.label])), 0)
|
||||||
|
return { wch: Math.max(headW, dataW) }
|
||||||
|
})
|
||||||
|
ws['!cols'] = cols
|
||||||
|
|
||||||
|
// 4) 冻结首行、开启筛选
|
||||||
|
if (ws['!ref']) {
|
||||||
|
ws['!freeze'] = { xSplit: 0, ySplit: 1 }
|
||||||
|
ws['!autofilter'] = { ref: ws['!ref'] }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) 生成工作簿并下载
|
||||||
|
const wb = XLSXUtils.book_new()
|
||||||
|
XLSXUtils.book_append_sheet(wb, ws, sheetName)
|
||||||
|
const safeName = filename.endsWith('.xlsx') ? filename : `${filename}.xlsx`
|
||||||
|
XLSXWriteFile(wb, safeName)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3) 用法保持不变
|
||||||
|
|
||||||
|
```js
|
||||||
|
const headers = [
|
||||||
|
{ label: '链接编号', key: 'codeNo' },
|
||||||
|
{ label: '状态', key: 'status' },
|
||||||
|
{ label: '到期时间', key: 'expiredAt' },
|
||||||
|
{ label: '备注', key: 'remark' },
|
||||||
|
]
|
||||||
|
|
||||||
|
exportToExcel(listData, headers, '链接列表.xlsx')
|
||||||
|
```
|
||||||
|
|
||||||
|
> 说明
|
||||||
|
>
|
||||||
|
> * 现在导出的是 **真正的 .xlsx 文件**,Excel/Numbers/WPS 打开都不会中文乱码。
|
||||||
|
> * 不再需要 BOM、小心 Excel 旧版 HTML 方案的各种兼容问题。
|
||||||
|
> * 已对以 `= + - @` 开头的字符串做了前缀 `'` 处理,避免“公式注入”。
|
||||||
|
> * 如需多表导出,可多次 `book_append_sheet` 追加不同工作表。
|
||||||
106
package-lock.json
generated
106
package-lock.json
generated
@@ -11,7 +11,8 @@
|
|||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"element-plus": "^2.8.8",
|
"element-plus": "^2.8.8",
|
||||||
"vue": "^3.4.38",
|
"vue": "^3.4.38",
|
||||||
"vue-router": "^4.4.5"
|
"vue-router": "^4.4.5",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.1.4",
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
@@ -1088,6 +1089,15 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/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",
|
||||||
@@ -1131,6 +1141,28 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/codepage": {
|
||||||
|
"version": "1.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||||
|
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/combined-stream": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
@@ -1150,6 +1182,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
@@ -1355,6 +1399,15 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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",
|
||||||
@@ -1669,6 +1722,18 @@
|
|||||||
"source-map": "^0.6.0"
|
"source-map": "^0.6.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/terser": {
|
"node_modules/terser": {
|
||||||
"version": "5.43.1",
|
"version": "5.43.1",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
|
||||||
@@ -1783,6 +1848,45 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": "^3.2.0"
|
"vue": "^3.2.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": {
|
||||||
|
"version": "0.18.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||||
|
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"cfb": "~1.2.1",
|
||||||
|
"codepage": "~1.15.0",
|
||||||
|
"crc-32": "~1.2.1",
|
||||||
|
"ssf": "~0.11.2",
|
||||||
|
"wmf": "~1.0.1",
|
||||||
|
"word": "~0.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"xlsx": "bin/xlsx.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"element-plus": "^2.8.8",
|
"element-plus": "^2.8.8",
|
||||||
"vue": "^3.4.38",
|
"vue": "^3.4.38",
|
||||||
"vue-router": "^4.4.5"
|
"vue-router": "^4.4.5",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.1.4",
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import http from '@/plugins/http'
|
|||||||
*/
|
*/
|
||||||
export async function getPointsBalance() {
|
export async function getPointsBalance() {
|
||||||
try {
|
try {
|
||||||
const response = await http.get('/admin/accounts/me/points-balance')
|
const response = await http.get('/api/admin/accounts/me/points-balance')
|
||||||
return response.data
|
return response.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取积分余额失败:', error)
|
console.error('获取积分余额失败:', error)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// 链接地址生成规则
|
// 链接地址生成规则
|
||||||
export const LINK_CONFIG = {
|
export const LINK_CONFIG = {
|
||||||
// 基础域名 - 生产环境需要修改为实际域名
|
// 基础域名 - 生产环境需要修改为实际域名
|
||||||
BASE_URL: import.meta.env.VITE_BASE_URL || window.location.origin,
|
BASE_URL: import.meta.env.VITE_BASE_URL || 'http://localhost:5173',
|
||||||
|
|
||||||
// 游戏页面路径
|
// 游戏页面路径
|
||||||
GAME_PATH: '/play',
|
GAME_PATH: '/play',
|
||||||
|
|||||||
@@ -71,66 +71,77 @@ export async function copyToClipboard(text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出CSV数据
|
// 计算列宽(中文按双字节处理)
|
||||||
export function exportToCSV(data, headers, filename) {
|
function calcWch(val) {
|
||||||
const csvContent = [
|
const s = (val ?? '').toString()
|
||||||
headers.map(h => h.label).join(','),
|
let w = 0
|
||||||
...data.map(row =>
|
for (const ch of s) w += ch.charCodeAt(0) > 255 ? 2 : 1
|
||||||
headers.map(header => {
|
return Math.min(Math.max(w, 8), 60) // 8~60 字符宽
|
||||||
const value = row[header.key] || ''
|
|
||||||
// 处理包含逗号的值
|
|
||||||
return value.toString().includes(',') ? `"${value}"` : value
|
|
||||||
}).join(',')
|
|
||||||
)
|
|
||||||
].join('\n')
|
|
||||||
|
|
||||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = URL.createObjectURL(blob)
|
|
||||||
link.download = filename
|
|
||||||
link.click()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出Excel数据
|
// 简单日期格式化(如果字段是 Date 或 ISO 字符串)
|
||||||
export function exportToExcel(data, headers, filename) {
|
function fmtDate(v) {
|
||||||
// 创建HTML表格格式
|
const d = v instanceof Date ? v : (typeof v === 'string' && !isNaN(Date.parse(v)) ? new Date(v) : null)
|
||||||
const tableHTML = `
|
if (!d) return v
|
||||||
<html>
|
const pad = n => (n < 10 ? '0' + n : '' + n)
|
||||||
<head>
|
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||||
<meta charset="UTF-8">
|
}
|
||||||
<style>
|
|
||||||
table { border-collapse: collapse; width: 100%; }
|
// 真实 .xlsx 导出(中文不会乱码)
|
||||||
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
export function exportToExcel(data, headers, filename = '导出.xlsx', sheetName = 'Sheet1') {
|
||||||
th { background-color: #f2f2f2; font-weight: bold; }
|
console.log('xlsx导出数据:', data)
|
||||||
</style>
|
// 动态导入 xlsx 库
|
||||||
</head>
|
import('xlsx').then(({ utils: XLSXUtils, writeFile: XLSXWriteFile }) => {
|
||||||
<body>
|
// 1) 规范化数据:按 headers 顺序映射到"显示列名 -> 值"
|
||||||
<table>
|
const headerLabels = headers.map(h => h.label)
|
||||||
<thead>
|
const rows = (data || []).map(row => {
|
||||||
<tr>
|
const obj = {}
|
||||||
${headers.map(header => `<th>${header.label}</th>`).join('')}
|
headers.forEach(h => {
|
||||||
</tr>
|
let v = row?.[h.key]
|
||||||
</thead>
|
// 识别并格式化日期
|
||||||
<tbody>
|
v = fmtDate(v)
|
||||||
${data.map(row =>
|
|
||||||
`<tr>
|
// 复杂对象转字符串
|
||||||
${headers.map(header => `<td>${row[header.key] || ''}</td>`).join('')}
|
if (v && typeof v === 'object' && !(v instanceof Date)) {
|
||||||
</tr>`
|
try { v = JSON.stringify(v) } catch { v = String(v) }
|
||||||
).join('')}
|
}
|
||||||
</tbody>
|
|
||||||
</table>
|
// 公式注入防护(以 = + - @ 开头的文本加前导单引号)
|
||||||
</body>
|
if (typeof v === 'string' && /^[=+\-@]/.test(v)) v = "'" + v
|
||||||
</html>
|
|
||||||
`
|
// null/undefined 转为空串,保留 0/false
|
||||||
|
if (v === null || v === undefined) v = ''
|
||||||
// 创建Blob并下载
|
obj[h.label] = v
|
||||||
const blob = new Blob([tableHTML], {
|
})
|
||||||
type: 'application/vnd.ms-excel;charset=utf-8;'
|
return obj
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2) 生成工作表
|
||||||
|
const ws = XLSXUtils.json_to_sheet(rows, { header: headerLabels })
|
||||||
|
|
||||||
|
// 3) 列宽:根据表头和数据计算
|
||||||
|
const cols = headers.map(h => {
|
||||||
|
const headW = calcWch(h.label)
|
||||||
|
const dataW = rows.reduce((mx, r) => Math.max(mx, calcWch(r[h.label])), 0)
|
||||||
|
return { wch: Math.max(headW, dataW) }
|
||||||
|
})
|
||||||
|
ws['!cols'] = cols
|
||||||
|
|
||||||
|
// 4) 冻结首行、开启筛选
|
||||||
|
if (ws['!ref']) {
|
||||||
|
ws['!freeze'] = { xSplit: 0, ySplit: 1 }
|
||||||
|
ws['!autofilter'] = { ref: ws['!ref'] }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) 生成工作簿并下载
|
||||||
|
const wb = XLSXUtils.book_new()
|
||||||
|
XLSXUtils.book_append_sheet(wb, ws, sheetName)
|
||||||
|
const safeName = filename.endsWith('.xlsx') ? filename : `${filename}.xlsx`
|
||||||
|
XLSXWriteFile(wb, safeName)
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Failed to load xlsx library:', error)
|
||||||
|
// 可以在这里添加错误提示
|
||||||
})
|
})
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = URL.createObjectURL(blob)
|
|
||||||
link.download = filename
|
|
||||||
link.click()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -149,11 +149,11 @@
|
|||||||
<el-button
|
<el-button
|
||||||
type="success"
|
type="success"
|
||||||
size="small"
|
size="small"
|
||||||
@click="exportToCSV"
|
@click="exportToExcel"
|
||||||
:disabled="!linkList.length"
|
:disabled="!linkList.length"
|
||||||
:icon="Download"
|
:icon="Download"
|
||||||
>
|
>
|
||||||
导出CSV
|
导出Excel
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="warning"
|
type="warning"
|
||||||
@@ -180,12 +180,12 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="success"
|
type="success"
|
||||||
@click="exportToCSV"
|
@click="exportToExcel"
|
||||||
:disabled="!linkList.length"
|
:disabled="!linkList.length"
|
||||||
class="mobile-action-btn"
|
class="mobile-action-btn"
|
||||||
>
|
>
|
||||||
<el-icon><Download /></el-icon>
|
<el-icon><Download /></el-icon>
|
||||||
导出CSV
|
导出Excel
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="warning"
|
type="warning"
|
||||||
@@ -492,7 +492,7 @@ import {
|
|||||||
DocumentCopy, Delete
|
DocumentCopy, Delete
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import { generateLinks, fetchLinks, deleteLink, batchDeleteLinks, batchDeleteByStatus } from '@/api/links'
|
import { generateLinks, fetchLinks, deleteLink, batchDeleteLinks, batchDeleteByStatus } from '@/api/links'
|
||||||
import { formatLinkStatus, getLinkStatusType, generateQRCodeUrl, downloadImage, copyToClipboard as copyText, exportToCSV as exportCSV, exportToExcel } from '@/utils/links'
|
import { copyToClipboard as copyText, exportToExcel as exportExcelUtil } from '@/utils/links'
|
||||||
import { LINK_CONFIG, STATUS_CONFIG, EXPORT_CONFIG } from '@/config/links'
|
import { LINK_CONFIG, STATUS_CONFIG, EXPORT_CONFIG } from '@/config/links'
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
@@ -691,8 +691,8 @@ const copyToClipboard = async (text) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出CSV
|
// 导出Excel
|
||||||
const exportToCSV = () => {
|
const exportToExcel = () => {
|
||||||
const headers = [
|
const headers = [
|
||||||
{ key: 'codeNo', label: '兑换码' },
|
{ key: 'codeNo', label: '兑换码' },
|
||||||
{ key: 'batchId', label: '批次ID' },
|
{ key: 'batchId', label: '批次ID' },
|
||||||
@@ -700,17 +700,24 @@ const exportToCSV = () => {
|
|||||||
{ key: 'times', label: '次数' },
|
{ key: 'times', label: '次数' },
|
||||||
{ key: 'totalPoints', label: '总积分' },
|
{ key: 'totalPoints', label: '总积分' },
|
||||||
{ key: 'statusDesc', label: '状态' },
|
{ key: 'statusDesc', label: '状态' },
|
||||||
|
{ key: 'remainingTime', label: '剩余时间' },
|
||||||
|
{ key: 'linkUrl', label: '链接地址' },
|
||||||
{ key: 'expireAt', label: '过期时间' },
|
{ key: 'expireAt', label: '过期时间' },
|
||||||
{ key: 'createdAt', label: '创建时间' }
|
{ key: 'createdAt', label: '创建时间' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const data = linkList.value.map(item => ({
|
const data = linkList.value.map(item => ({
|
||||||
...item,
|
...item,
|
||||||
|
remainingTime: item.isExpired ? '已过期' : (item.remainingSeconds > 0 ? formatRemainingTime(item.remainingSeconds) : '-'),
|
||||||
|
linkUrl: generateLinkUrl(item.codeNo),
|
||||||
expireAt: formatDateTime(item.expireAt),
|
expireAt: formatDateTime(item.expireAt),
|
||||||
createdAt: formatDateTime(item.createdAt)
|
createdAt: formatDateTime(item.createdAt)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
exportCSV(data, headers, `兑换码列表_${new Date().toISOString().split('T')[0]}.csv`)
|
const filename = `兑换码列表_${linkList.value.length}个_${new Date().toISOString().split('T')[0]}.xlsx`
|
||||||
|
exportExcelUtil(data, headers, filename)
|
||||||
|
|
||||||
|
ElMessage.success(`成功导出 ${linkList.value.length} 个兑换码到Excel`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成链接URL
|
// 生成链接URL
|
||||||
@@ -852,8 +859,8 @@ const exportSelectedToExcel = () => {
|
|||||||
createdAt: formatDateTime(item.createdAt)
|
createdAt: formatDateTime(item.createdAt)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const filename = `选中兑换码_${selectedRows.value.length}个_${new Date().toISOString().split('T')[0]}.xls`
|
const filename = `选中兑换码_${selectedRows.value.length}个_${new Date().toISOString().split('T')[0]}.xlsx`
|
||||||
exportToExcel(data, headers, filename)
|
exportExcelUtil(data, headers, filename)
|
||||||
|
|
||||||
ElMessage.success(`成功导出 ${selectedRows.value.length} 个兑换码到Excel`)
|
ElMessage.success(`成功导出 ${selectedRows.value.length} 个兑换码到Excel`)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user