添加批量删除、批量复制和导出选中链接到Excel的功能,同时更新链接状态和导出CSV的逻辑,优化了链接生成和显示的相关代码
This commit is contained in:
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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: '创建时间' }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
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 formatRemainingTime = (seconds) => {
|
||||
if (seconds <= 0) return '已过期'
|
||||
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user