添加批量删除、批量复制和导出选中链接到Excel的功能,同时更新链接状态和导出CSV的逻辑,优化了链接生成和显示的相关代码

This commit is contained in:
zyh
2025-08-26 16:41:11 +08:00
parent 7a75fbe887
commit 7854bd0288
4 changed files with 337 additions and 84 deletions

View File

@@ -18,4 +18,8 @@ export function updateLink(id, payload) {
return http.patch(`/api/link/${id}`, payload)
}
export function batchDeleteLinks(codeNos) {
return http.post('/api/link/batch-delete', { codeNos })
}

View File

@@ -19,38 +19,40 @@ export const LINK_CONFIG = {
// 状态配置
export const STATUS_CONFIG = {
// 状态标签类型
// 状态标签类型映射
LABEL_TYPES: {
NORMAL: 'success', // 正常
EXPIRING: 'warning', // 即将过期
EXPIRED: 'danger', // 已过期
UNKNOWN: 'info' // 未知
NEW: 'success', // 新建
USED: 'info', // 已使用
EXPIRED: 'danger', // 已过期
UNKNOWN: 'info' // 未知
},
// 状态文本
// 状态文本映射
LABEL_TEXTS: {
NORMAL: '正常',
EXPIRING: '即将过期',
NEW: '新建',
USED: '已使用',
EXPIRED: '已过期',
UNKNOWN: '未知'
},
// 过期时间阈值(秒)
EXPIRING_THRESHOLD: 24 * 60 * 60 * 1000, // 24小时
// 过期警告阈值(秒)
EXPIRING_THRESHOLD: 24 * 60 * 60, // 24小时
}
// 导出配置
export const EXPORT_CONFIG = {
// CSV文件前缀
FILE_PREFIX: 'links',
FILE_PREFIX: 'redemption_codes',
// 默认列配置
DEFAULT_COLUMNS: [
{ key: 'codeNo', label: '兑换码' },
{ key: 'batchId', label: '批次ID' },
{ key: 'codeNos', label: '机器编号' },
{ key: 'deductPoints', label: '扣除积分' },
{ key: 'quantity', label: '数量' },
{ key: 'times', label: '次数' },
{ key: 'totalPoints', label: '总积分' },
{ key: 'statusDesc', label: '状态' },
{ key: 'expireAt', label: '过期时间' },
{ key: 'status', label: '状态' },
{ key: 'createdAt', label: '创建时间' }
]
}

View File

@@ -74,7 +74,7 @@ export async function copyToClipboard(text) {
// 导出CSV数据
export function exportToCSV(data, headers, filename) {
const csvContent = [
headers.join(','),
headers.map(h => h.label).join(','),
...data.map(row =>
headers.map(header => {
const value = row[header.key] || ''
@@ -91,4 +91,46 @@ export function exportToCSV(data, headers, filename) {
link.click()
}
// 导出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;'
})
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename
link.click()
}

View File

@@ -75,36 +75,81 @@
>
导出CSV
</el-button>
<!-- 批量操作按钮 -->
<div v-if="showBatchActions" class="batch-actions">
<el-button
type="info"
size="small"
@click="selectAll"
>
全选
</el-button>
<el-button
type="info"
size="small"
@click="clearSelection"
>
取消选择
</el-button>
<el-button
type="primary"
size="small"
@click="batchCopyLinks"
:disabled="selectedRows.length === 0"
>
批量复制链接 ({{ selectedRows.length }})
</el-button>
<el-button
type="success"
size="small"
@click="exportSelectedToExcel"
:disabled="selectedRows.length === 0"
>
导出选中Excel
</el-button>
<el-button
type="danger"
size="small"
@click="handleBatchDelete"
:disabled="selectedRows.length === 0"
>
批量删除 ({{ selectedRows.length }})
</el-button>
</div>
</div>
</div>
</template>
<el-table
ref="tableRef"
v-loading="loading"
:data="linkList"
style="width: 100%"
border
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="codeNo" label="兑换码" width="120" />
<el-table-column prop="batchId" label="批次ID" width="100" />
<el-table-column prop="codeNos" label="机器编号" width="200">
<el-table-column prop="quantity" label="数量" width="80">
<template #default="{ row }">
<div v-if="row.codeNos && row.codeNos.length > 0">
<el-tag
v-for="codeNo in row.codeNos"
:key="codeNo"
size="small"
style="margin-right: 5px; margin-bottom: 5px;"
>
{{ codeNo }}
</el-tag>
</div>
<span v-else>-</span>
<el-tag type="info" size="small">
{{ row.quantity }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="deductPoints" label="扣除积分" width="100">
<el-table-column prop="times" label="次数" width="80">
<template #default="{ row }">
<el-tag type="success" size="small">
{{ row.times }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="totalPoints" label="总积分" width="100">
<template #default="{ row }">
<el-tag type="warning" size="small">
{{ row.deductPoints || 0 }}
{{ row.totalPoints }}
</el-tag>
</template>
</el-table-column>
@@ -115,46 +160,44 @@
</el-table-column>
<el-table-column label="链接地址" min-width="200">
<template #default="{ row }">
<div v-if="row.codeNos && row.codeNos.length > 0">
<div
v-for="codeNo in row.codeNos"
:key="codeNo"
style="margin-bottom: 8px;"
>
<el-link
:href="generateLinkUrl(codeNo)"
target="_blank"
type="primary"
:underline="false"
>
{{ generateLinkUrl(codeNo) }}
</el-link>
<el-button
type="text"
size="small"
@click="copyToClipboard(generateLinkUrl(codeNo))"
style="margin-left: 5px;"
>
复制
</el-button>
</div>
</div>
<span v-else>-</span>
<el-link
:href="generateLinkUrl(row.codeNo)"
target="_blank"
type="primary"
:underline="false"
>
{{ generateLinkUrl(row.codeNo) }}
</el-link>
<el-button
type="text"
size="small"
@click="copyToClipboard(generateLinkUrl(row.codeNo))"
style="margin-left: 5px;"
>
复制
</el-button>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag
:type="getLinkStatusByExpire(row.expireAt)"
:type="getStatusType(row)"
size="small"
>
{{ getLinkStatusByExpire(row.expireAt, true) }}
{{ row.statusDesc }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="剩余时间" width="120">
<template #default="{ row }">
<span v-if="row.isExpired" class="expired-text">已过期</span>
<span v-else-if="row.remainingSeconds > 0">{{ formatRemainingTime(row.remainingSeconds) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="创建时间" width="180">
<template #default="{ row }">
{{ formatDateTime(new Date()) }}
{{ formatDateTime(row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
@@ -169,7 +212,7 @@
<el-button
type="danger"
size="small"
@click="deleteLinkItem(row.batchId)"
@click="deleteLinkItem(row.codeNo)"
>
删除
</el-button>
@@ -222,10 +265,10 @@
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { generateLinks, fetchLinks, deleteLink } from '@/api/links'
import { formatLinkStatus, getLinkStatusType, generateQRCodeUrl, downloadImage, copyToClipboard as copyText, exportToCSV as exportCSV } from '@/utils/links'
import { generateLinks, fetchLinks, deleteLink, batchDeleteLinks } from '@/api/links'
import { formatLinkStatus, getLinkStatusType, generateQRCodeUrl, downloadImage, copyToClipboard as copyText, exportToCSV as exportCSV, exportToExcel } from '@/utils/links'
import { LINK_CONFIG, STATUS_CONFIG, EXPORT_CONFIG } from '@/config/links'
// 响应式数据
@@ -236,10 +279,15 @@ const linkList = ref([])
const qrCodeDialogVisible = ref(false)
const currentQRCode = ref('')
// 多选相关状态
const selectedRows = ref([])
const showBatchActions = computed(() => selectedRows.value.length > 0)
const tableRef = ref()
// 生成表单
const generateForm = reactive({
times: 1,
linkCount: 5
times: null,
linkCount: 1
})
// 分页
@@ -308,8 +356,12 @@ const getLinkList = async () => {
}
const response = await fetchLinks(params)
linkList.value = response.data.items || []
pagination.total = response.data.total || 0
const data = response.data
linkList.value = data.items || []
pagination.total = data.total || 0
pagination.page = data.page || 1
pagination.pageSize = data.pageSize || 20
} catch (error) {
console.error('获取链接列表失败:', error)
@@ -360,13 +412,12 @@ const deleteLinkItem = async (id) => {
// 查看二维码
const viewQRCode = (row) => {
if (row.codeNos && row.codeNos.length > 0) {
// 使用第一个机器编号生成二维码
const linkUrl = generateLinkUrl(row.codeNos[0])
if (row.codeNo) {
const linkUrl = generateLinkUrl(row.codeNo)
currentQRCode.value = generateQRCodeUrl(linkUrl, 200)
qrCodeDialogVisible.value = true
} else {
ElMessage.warning('没有可用的机器编号')
ElMessage.warning('没有可用的兑换码')
}
}
@@ -389,16 +440,24 @@ const copyToClipboard = async (text) => {
// 导出CSV
const exportToCSV = () => {
const headers = EXPORT_CONFIG.DEFAULT_COLUMNS
const headers = [
{ key: 'codeNo', label: '兑换码' },
{ key: 'batchId', label: '批次ID' },
{ key: 'quantity', label: '数量' },
{ key: 'times', label: '次数' },
{ key: 'totalPoints', label: '总积分' },
{ key: 'statusDesc', label: '状态' },
{ key: 'expireAt', label: '过期时间' },
{ key: 'createdAt', label: '创建时间' }
]
const data = linkList.value.map(item => ({
...item,
codeNos: (item.codeNos || []).join(', '),
status: getLinkStatusByExpire(item.expireAt, true),
createdAt: formatDateTime(new Date())
expireAt: formatDateTime(item.expireAt),
createdAt: formatDateTime(item.createdAt)
}))
exportCSV(data, headers, `${EXPORT_CONFIG.FILE_PREFIX}_${new Date().toISOString().split('T')[0]}.csv`)
exportCSV(data, headers, `兑换码列表_${new Date().toISOString().split('T')[0]}.csv`)
}
// 生成链接URL
@@ -406,19 +465,38 @@ const generateLinkUrl = (codeNo) => {
return LINK_CONFIG.getLinkUrl(codeNo)
}
// 根据过期时间判断链接状态
const getLinkStatusByExpire = (expireAt, returnText = false) => {
if (!expireAt) return returnText ? STATUS_CONFIG.LABEL_TEXTS.UNKNOWN : STATUS_CONFIG.LABEL_TYPES.UNKNOWN
// 根据状态获取标签类型
const getStatusType = (row) => {
if (row.isExpired) {
return 'danger'
}
const now = new Date()
const expire = new Date(expireAt)
switch (row.status) {
case 'NEW':
return 'success'
case 'USED':
return 'info'
case 'EXPIRED':
return 'danger'
default:
return 'info'
}
}
// 格式化剩余时间(秒)
const formatRemainingTime = (seconds) => {
if (seconds <= 0) return '已过期'
if (expire < now) {
return returnText ? STATUS_CONFIG.LABEL_TEXTS.EXPIRED : STATUS_CONFIG.LABEL_TYPES.EXPIRED
} else if (expire - now < STATUS_CONFIG.EXPIRING_THRESHOLD) {
return returnText ? STATUS_CONFIG.LABEL_TEXTS.EXPIRING : STATUS_CONFIG.LABEL_TYPES.EXPIRING
const days = Math.floor(seconds / (24 * 60 * 60))
const hours = Math.floor((seconds % (24 * 60 * 60)) / (60 * 60))
const minutes = Math.floor((seconds % (60 * 60)) / 60)
if (days > 0) {
return `${days}${hours}小时`
} else if (hours > 0) {
return `${hours}小时${minutes}分钟`
} else {
return returnText ? STATUS_CONFIG.LABEL_TEXTS.NORMAL : STATUS_CONFIG.LABEL_TYPES.NORMAL
return `${minutes}分钟`
}
}
@@ -436,6 +514,113 @@ const formatDateTime = (dateString) => {
})
}
// 多选处理函数
const handleSelectionChange = (selection) => {
selectedRows.value = selection
}
// 批量删除
const handleBatchDelete = async () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要删除的兑换码')
return
}
try {
await ElMessageBox.confirm(
`确定要删除选中的 ${selectedRows.value.length} 个兑换码吗?此操作不可恢复!`,
'批量删除确认',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning'
}
)
// 使用批量删除API
const codeNos = selectedRows.value.map(row => row.codeNo)
await batchDeleteLinks(codeNos)
ElMessage.success(`成功删除 ${selectedRows.value.length} 个兑换码`)
selectedRows.value = []
await getLinkList()
} catch (error) {
if (error !== 'cancel') {
console.error('批量删除失败:', error)
ElMessage.error('批量删除失败')
}
}
}
// 批量复制链接
const batchCopyLinks = async () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要复制的兑换码')
return
}
try {
const links = selectedRows.value.map(row => generateLinkUrl(row.codeNo))
const linkText = links.join('\n')
await navigator.clipboard.writeText(linkText)
ElMessage.success(`成功复制 ${selectedRows.value.length} 个链接到剪贴板`)
} catch (error) {
console.error('批量复制失败:', error)
ElMessage.error('复制失败,请手动复制')
}
}
// 导出选中的Excel
const exportSelectedToExcel = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要导出的兑换码')
return
}
const headers = [
{ key: 'codeNo', label: '兑换码' },
{ key: 'batchId', label: '批次ID' },
{ key: 'quantity', label: '数量' },
{ key: 'times', label: '次数' },
{ key: 'totalPoints', label: '总积分' },
{ key: 'statusDesc', label: '状态' },
{ key: 'remainingTime', label: '剩余时间' },
{ key: 'linkUrl', label: '链接地址' },
{ key: 'expireAt', label: '过期时间' },
{ key: 'createdAt', label: '创建时间' }
]
const data = selectedRows.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)
}))
const filename = `选中兑换码_${selectedRows.value.length}个_${new Date().toISOString().split('T')[0]}.xls`
exportToExcel(data, headers, filename)
ElMessage.success(`成功导出 ${selectedRows.value.length} 个兑换码到Excel`)
}
// 全选
const selectAll = () => {
if (tableRef.value) {
linkList.value.forEach(row => {
tableRef.value.toggleRowSelection(row, true)
})
}
}
// 取消选择
const clearSelection = () => {
if (tableRef.value) {
tableRef.value.clearSelection()
}
}
// 页面加载时获取数据
onMounted(() => {
getLinkList()
@@ -491,4 +676,24 @@ onMounted(() => {
.qr-code-placeholder {
padding: 40px 0;
}
.expired-text {
color: #f56c6c;
font-weight: 500;
}
.batch-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
padding: 8px;
background: #f5f7fa;
border-radius: 4px;
border: 1px solid #e4e7ed;
}
.batch-actions .el-button {
margin: 0;
}
</style>