新增 xlsx 库支持,优化 Excel 导出功能,更新相关文档

This commit is contained in:
zyh
2025-08-29 23:45:19 +08:00
parent 9ff0e7aa2d
commit ccbb1a3eb4
7 changed files with 296 additions and 71 deletions

View File

@@ -6,7 +6,7 @@ import http from '@/plugins/http'
*/
export async function getPointsBalance() {
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
} catch (error) {
console.error('获取积分余额失败:', error)

View File

@@ -3,7 +3,7 @@
// 链接地址生成规则
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',

View File

@@ -71,66 +71,77 @@ export async function copyToClipboard(text) {
}
}
// 导出CSV数据
export function exportToCSV(data, headers, filename) {
const csvContent = [
headers.map(h => h.label).join(','),
...data.map(row =>
headers.map(header => {
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()
// 计算列宽(中文按双字节处理)
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 字符宽
}
// 导出Excel数据
export function exportToExcel(data, headers, filename) {
// 创建HTML表格格式
const tableHTML = `
<html>
<head>
<meta charset="UTF-8">
<style>
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; font-weight: bold; }
</style>
</head>
<body>
<table>
<thead>
<tr>
${headers.map(header => `<th>${header.label}</th>`).join('')}
</tr>
</thead>
<tbody>
${data.map(row =>
`<tr>
${headers.map(header => `<td>${row[header.key] || ''}</td>`).join('')}
</tr>`
).join('')}
</tbody>
</table>
</body>
</html>
`
// 创建Blob并下载
const blob = new Blob([tableHTML], {
type: 'application/vnd.ms-excel;charset=utf-8;'
// 简单日期格式化(如果字段是 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') {
console.log('xlsx导出数据:', data)
// 动态导入 xlsx 库
import('xlsx').then(({ utils: XLSXUtils, writeFile: XLSXWriteFile }) => {
// 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)
}).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()
}

View File

@@ -149,11 +149,11 @@
<el-button
type="success"
size="small"
@click="exportToCSV"
@click="exportToExcel"
:disabled="!linkList.length"
:icon="Download"
>
导出CSV
导出Excel
</el-button>
<el-button
type="warning"
@@ -180,12 +180,12 @@
</el-button>
<el-button
type="success"
@click="exportToCSV"
@click="exportToExcel"
:disabled="!linkList.length"
class="mobile-action-btn"
>
<el-icon><Download /></el-icon>
导出CSV
导出Excel
</el-button>
<el-button
type="warning"
@@ -492,7 +492,7 @@ import {
DocumentCopy, Delete
} from '@element-plus/icons-vue'
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'
// 响应式数据
@@ -691,8 +691,8 @@ const copyToClipboard = async (text) => {
}
}
// 导出CSV
const exportToCSV = () => {
// 导出Excel
const exportToExcel = () => {
const headers = [
{ key: 'codeNo', label: '兑换码' },
{ key: 'batchId', label: '批次ID' },
@@ -700,17 +700,24 @@ const exportToCSV = () => {
{ key: 'times', label: '次数' },
{ key: 'totalPoints', label: '总积分' },
{ key: 'statusDesc', label: '状态' },
{ key: 'remainingTime', label: '剩余时间' },
{ key: 'linkUrl', label: '链接地址' },
{ key: 'expireAt', label: '过期时间' },
{ key: 'createdAt', label: '创建时间' }
]
const data = linkList.value.map(item => ({
...item,
remainingTime: item.isExpired ? '已过期' : (item.remainingSeconds > 0 ? formatRemainingTime(item.remainingSeconds) : '-'),
linkUrl: generateLinkUrl(item.codeNo),
expireAt: formatDateTime(item.expireAt),
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
@@ -852,8 +859,8 @@ const exportSelectedToExcel = () => {
createdAt: formatDateTime(item.createdAt)
}))
const filename = `选中兑换码_${selectedRows.value.length}个_${new Date().toISOString().split('T')[0]}.xls`
exportToExcel(data, headers, filename)
const filename = `选中兑换码_${selectedRows.value.length}个_${new Date().toISOString().split('T')[0]}.xlsx`
exportExcelUtil(data, headers, filename)
ElMessage.success(`成功导出 ${selectedRows.value.length} 个兑换码到Excel`)
}