添加链接管理功能,包括路由配置、权限设置和在管理布局中显示链接管理菜单项

This commit is contained in:
zyh
2025-08-26 10:47:31 +08:00
parent da1ac4ddcf
commit 7a75fbe887
8 changed files with 876 additions and 1 deletions

182
README_LINKS.md Normal file
View File

@@ -0,0 +1,182 @@
# 链接管理功能
## 功能概述
链接管理页面提供了批量生成链接的功能支持设置生成次数和每次生成的链接数量。根据您提供的API接口系统会调用 `POST /api/link/generate` 接口来生成链接。
## 主要功能
### 1. 批量生成链接
- **生成次数**: 1-100次
- **每次链接数量**: 1-50个
- 支持表单验证
- 生成成功后自动刷新列表
### 2. 链接列表管理
- 分页显示已生成的链接批次
- 显示批次ID、机器编号、扣除积分、过期时间
- 支持查看、删除操作
- 根据过期时间自动判断链接状态
- 支持导出CSV数据
### 3. 二维码功能
- 根据机器编号生成对应的链接二维码
- 支持下载二维码图片
- 使用在线二维码生成服务
- 自动生成游戏链接地址
### 4. 权限控制
- 管理员:拥有所有权限(生成、查看、删除、导出)
- 代理商:只有查看权限
## API接口
### 生成链接
```http
POST /api/link/generate
Authorization: Bearer {token}
Content-Type: application/json
{
"times": 10, // 生成次数
"linkCount": 5 // 每次链接数量
}
```
**返回数据示例:**
```json
{
"batchId": 6,
"deductPoints": 50,
"expireAt": "2025-08-26T12:29:13.63955",
"codeNos": [
"X3T9ND84"
]
}
```
### 获取链接列表
```http
GET /api/link/list?page=1&pageSize=20
Authorization: Bearer {token}
```
### 删除链接
```http
DELETE /api/link/{id}
Authorization: Bearer {token}
```
## 页面路由
- **路径**: `/links`
- **名称**: `Links`
- **权限**: `LINK_VIEW`
## 文件结构
```
src/
├── api/
│ └── links.js # 链接相关API接口
├── views/links/
│ └── LinkGenerate.vue # 链接生成页面
├── utils/
│ └── links.js # 链接管理工具函数
├── config/
│ └── links.js # 链接管理配置文件
├── router/
│ └── index.js # 路由配置(已更新)
├── layouts/
│ └── AdminLayout.vue # 导航菜单(已更新)
└── utils/
└── permission.js # 权限配置(已更新)
```
## 使用说明
### 1. 生成链接
1. 在"生成次数"输入框中输入要生成的次数1-100
2. 在"每次链接数量"输入框中输入每次生成的链接数量1-50
3. 点击"开始生成"按钮
4. 系统会调用API生成链接成功后显示提示信息
### 2. 管理链接
- 查看已生成的链接列表
- 点击"查看二维码"查看链接对应的二维码
- 点击"删除"删除不需要的链接
- 使用"导出CSV"功能导出链接数据
### 3. 权限说明
- 管理员可以执行所有操作
- 代理商只能查看链接列表,无法生成或删除链接
## 注意事项
1. **API地址**: 确保后端API地址配置正确当前配置为 `http://localhost:18080`
2. **认证**: 需要有效的Bearer Token才能访问API
3. **二维码**: 使用在线二维码生成服务,确保网络连接正常
4. **批量限制**: 单次最多生成50个链接避免API压力过大
5. **链接地址**: 系统会自动根据机器编号生成游戏链接地址,格式为 `https://yourdomain.com/play?code={机器编号}`
6. **状态判断**: 链接状态根据过期时间自动判断(正常/即将过期/已过期)
7. **配置自定义**: 可以在 `src/config/links.js` 中自定义链接地址生成规则和状态配置
## 配置说明
### 链接地址配置 (`src/config/links.js`)
```javascript
export const LINK_CONFIG = {
// 基础域名
BASE_URL: 'https://yourdomain.com',
// 游戏页面路径
GAME_PATH: '/play',
// 机器编号参数名
CODE_PARAM: 'code',
// 链接地址模板
getLinkUrl: (codeNo) => {
return `${LINK_CONFIG.BASE_URL}${LINK_CONFIG.GAME_PATH}?${LINK_CONFIG.CODE_PARAM}=${codeNo}`
}
}
```
### 状态配置
```javascript
export const STATUS_CONFIG = {
// 状态标签类型
LABEL_TYPES: {
NORMAL: 'success', // 正常
EXPIRING: 'warning', // 即将过期
EXPIRED: 'danger', // 已过期
UNKNOWN: 'info' // 未知
},
// 过期时间阈值(毫秒)
EXPIRING_THRESHOLD: 24 * 60 * 60 * 1000, // 24小时
}
```
## 扩展功能
可以根据需要添加以下功能:
- 链接状态切换(启用/禁用)
- 链接过期时间设置
- 链接使用统计
- 自定义链接模板
- 批量操作(批量删除、批量导出等)
- 链接访问统计和监控
## 技术特点
- 使用Vue 3 Composition API
- Element Plus UI组件库
- 响应式设计
- 权限控制集成
- 错误处理和用户提示
- 支持CSV导出

21
src/api/links.js Normal file
View File

@@ -0,0 +1,21 @@
import http from '@/plugins/http'
export function generateLinks(payload) {
// payload: { times: number, linkCount: number }
return http.post('/api/link/generate', payload)
}
export function fetchLinks(params) {
// params: { page, pageSize, keyword, status, batchId }
return http.get('/api/link/list', { params })
}
export function deleteLink(id) {
return http.delete(`/api/link/${id}`)
}
export function updateLink(id, payload) {
return http.patch(`/api/link/${id}`, payload)
}

56
src/config/links.js Normal file
View File

@@ -0,0 +1,56 @@
// 链接管理配置文件
// 链接地址生成规则
export const LINK_CONFIG = {
// 基础域名
BASE_URL: 'https://yourdomain.com',
// 游戏页面路径
GAME_PATH: '/play',
// 机器编号参数名
CODE_PARAM: 'code',
// 链接地址模板
getLinkUrl: (codeNo) => {
return `${LINK_CONFIG.BASE_URL}${LINK_CONFIG.GAME_PATH}?${LINK_CONFIG.CODE_PARAM}=${codeNo}`
}
}
// 状态配置
export const STATUS_CONFIG = {
// 状态标签类型
LABEL_TYPES: {
NORMAL: 'success', // 正常
EXPIRING: 'warning', // 即将过期
EXPIRED: 'danger', // 已过期
UNKNOWN: 'info' // 未知
},
// 状态文本
LABEL_TEXTS: {
NORMAL: '正常',
EXPIRING: '即将过期',
EXPIRED: '已过期',
UNKNOWN: '未知'
},
// 过期时间阈值(毫秒)
EXPIRING_THRESHOLD: 24 * 60 * 60 * 1000, // 24小时
}
// 导出配置
export const EXPORT_CONFIG = {
// CSV文件前缀
FILE_PREFIX: 'links',
// 默认列配置
DEFAULT_COLUMNS: [
{ key: 'batchId', label: '批次ID' },
{ key: 'codeNos', label: '机器编号' },
{ key: 'deductPoints', label: '扣除积分' },
{ key: 'expireAt', label: '过期时间' },
{ key: 'status', label: '状态' },
{ key: 'createdAt', label: '创建时间' }
]
}

View File

@@ -34,6 +34,10 @@
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M3 3h2v18H3V3m4 8h2v10H7V11m4-6h2v16h-2V5m4 10h2v6h-2v-6m4-3h2v9h-2v-9Z"/></svg></i>
<span>报表分析</span>
</el-menu-item>
<el-menu-item v-if="canAccessLinks" index="Links" :route="{ name: 'Links' }">
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M10.59 13.41c.41.39.41 1.03 0 1.42c-.39.39-1.03.39-1.42 0a5.003 5.003 0 0 1 0-7.07l3.54-3.54a5.003 5.003 0 0 1 7.07 0a5.003 5.003 0 0 1 0 7.07l-1.49 1.49c.01-.82-.12-1.64-.4-2.42l.47-.48a2.982 2.982 0 0 0 0-4.24a2.982 2.982 0 0 0-4.24 0l-3.53 3.53a2.982 2.982 0 0 0 0 4.24zm2.82-4.24c.39-.39 1.03-.39 1.42 0a5.003 5.003 0 0 1 0 7.07l-3.54 3.54a5.003 5.003 0 0 1-7.07 0a5.003 5.003 0 0 1 0-7.07l1.49-1.49c-.01.82.12 1.64.4 2.43l-.47.47a2.982 2.982 0 0 0 0 4.24a2.982 2.982 0 0 0 4.24 0l3.53-3.53a2.982 2.982 0 0 0 0-4.24a.973.973 0 0 1 0-1.42z"/></svg></i>
<span>链接管理</span>
</el-menu-item>
<el-menu-item v-if="canAccessSettings" index="Settings" :route="{ name: 'Settings' }">
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="m12 8l-2 4h4l-2 4"/></svg></i>
<span>系统设置</span>
@@ -91,6 +95,7 @@ const canAccessUsers = computed(() => canAccessRoute('Users'))
const canAccessGames = computed(() => canAccessRoute('Games'))
const canAccessOrders = computed(() => canAccessRoute('Orders'))
const canAccessReports = computed(() => canAccessRoute('Reports'))
const canAccessLinks = computed(() => canAccessRoute('Links'))
const canAccessSettings = computed(() => canAccessRoute('Settings'))
function onProfile() {

View File

@@ -11,6 +11,7 @@ const GameList = () => import('@/views/games/GameList.vue')
const OrderList = () => import('@/views/orders/OrderList.vue')
const ReportAnalysis = () => import('@/views/reports/ReportAnalysis.vue')
const Settings = () => import('@/views/settings/Settings.vue')
const LinkGenerate = () => import('@/views/links/LinkGenerate.vue')
const ErrorTest = () => import('@/views/ErrorTest.vue')
const PermissionTest = () => import('@/views/PermissionTest.vue')
const NotFound = () => import('@/views/NotFound.vue')
@@ -27,6 +28,7 @@ export const routes = [
{ path: 'orders', name: 'Orders', component: OrderList, meta: { title: '订单管理' } },
{ path: 'reports', name: 'Reports', component: ReportAnalysis, meta: { title: '报表分析' } },
{ path: 'settings', name: 'Settings', component: Settings, meta: { title: '系统设置' } },
{ path: 'links', name: 'Links', component: LinkGenerate, meta: { title: '链接管理' } },
{ path: 'error-test', name: 'ErrorTest', component: ErrorTest, meta: { title: '错误处理测试' } },
{ path: 'permission-test', name: 'PermissionTest', component: PermissionTest, meta: { title: '权限测试' } },
],

94
src/utils/links.js Normal file
View File

@@ -0,0 +1,94 @@
// 链接管理工具函数
// 格式化链接状态
export function formatLinkStatus(status) {
const statusMap = {
'ACTIVE': '启用',
'INACTIVE': '禁用',
'EXPIRED': '已过期'
}
return statusMap[status] || status
}
// 获取链接状态标签类型
export function getLinkStatusType(status) {
const typeMap = {
'ACTIVE': 'success',
'INACTIVE': 'danger',
'EXPIRED': 'warning'
}
return typeMap[status] || 'info'
}
// 检查链接是否过期
export function isLinkExpired(expiredAt) {
if (!expiredAt) return false
return new Date(expiredAt) < new Date()
}
// 计算剩余时间
export function getRemainingTime(expiredAt) {
if (!expiredAt) return null
const now = new Date()
const expired = new Date(expiredAt)
const diff = expired - now
if (diff <= 0) return '已过期'
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
if (days > 0) return `${days}${hours}小时`
if (hours > 0) return `${hours}小时${minutes}分钟`
return `${minutes}分钟`
}
// 生成二维码URL使用在线服务
export function generateQRCodeUrl(data, size = 200) {
return `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&data=${encodeURIComponent(data)}`
}
// 下载图片
export function downloadImage(url, filename) {
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
// 复制到剪贴板
export async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text)
return true
} catch (error) {
console.error('复制失败:', error)
return false
}
}
// 导出CSV数据
export function exportToCSV(data, headers, filename) {
const csvContent = [
headers.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()
}

View File

@@ -25,6 +25,17 @@ export const PERMISSIONS = {
// 系统设置权限
SETTING_MANAGE: 'setting:manage',
// 链接管理权限
LINK_MANAGE: 'link:manage',
LINK_CREATE: 'link:create',
LINK_UPDATE: 'link:update',
LINK_DELETE: 'link:delete',
LINK_VIEW: 'link:view',
// 二维码权限
QR_GENERATE: 'qr:generate',
QR_VIEW: 'qr:view',
}
// 角色权限映射
@@ -45,12 +56,21 @@ export const ROLE_PERMISSIONS = {
PERMISSIONS.ORDER_VIEW,
PERMISSIONS.REPORT_VIEW,
PERMISSIONS.SETTING_MANAGE,
PERMISSIONS.LINK_MANAGE,
PERMISSIONS.LINK_CREATE,
PERMISSIONS.LINK_UPDATE,
PERMISSIONS.LINK_DELETE,
PERMISSIONS.LINK_VIEW,
PERMISSIONS.QR_GENERATE,
PERMISSIONS.QR_VIEW,
],
AGENT: [
// 代理商只有查看权限,没有管理权限
PERMISSIONS.GAME_VIEW,
PERMISSIONS.ORDER_VIEW,
PERMISSIONS.REPORT_VIEW,
PERMISSIONS.LINK_VIEW,
PERMISSIONS.QR_VIEW,
]
}
@@ -62,6 +82,7 @@ export const ROUTE_PERMISSIONS = {
'Orders': [PERMISSIONS.ORDER_VIEW],
'Reports': [PERMISSIONS.REPORT_VIEW],
'Settings': [PERMISSIONS.SETTING_MANAGE],
'Links': [PERMISSIONS.LINK_VIEW],
'ErrorTest': [], // 错误测试页面所有用户都可以访问
'PermissionTest': [], // 权限测试页面所有用户都可以访问
}
@@ -132,7 +153,7 @@ export function getAccessibleRoutes() {
// 管理员可以访问所有路由
if (isAdmin()) {
return ['Dashboard', 'Users', 'Games', 'Orders', 'Reports', 'Settings', 'ErrorTest', 'PermissionTest']
return ['Dashboard', 'Users', 'Games', 'Orders', 'Reports', 'Settings', 'Links', 'ErrorTest', 'PermissionTest']
}
const userPermissions = ROLE_PERMISSIONS[userType?.toUpperCase()] || []

View File

@@ -0,0 +1,494 @@
<template>
<div class="link-generate">
<el-card class="generate-form-card">
<template #header>
<div class="card-header">
<span>批量生成链接</span>
</div>
</template>
<el-form
ref="generateFormRef"
:model="generateForm"
:rules="generateRules"
label-width="120px"
class="generate-form"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="生成次数" prop="times">
<el-input-number
v-model="generateForm.times"
:min="1"
:max="100"
placeholder="请输入生成次数"
style="width: 100%"
/>
<div class="form-tip">每次生成将创建指定数量的链接</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="每次链接数量" prop="linkCount">
<el-input-number
v-model="generateForm.linkCount"
:min="1"
:max="50"
placeholder="请输入每次生成的链接数量"
style="width: 100%"
/>
<div class="form-tip">单次最多生成50个链接</div>
</el-form-item>
</el-col>
</el-row>
<el-form-item>
<el-button
type="primary"
:loading="generating"
@click="handleGenerate"
>
{{ generating ? '生成中...' : '开始生成' }}
</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="link-list-card" style="margin-top: 20px;">
<template #header>
<div class="card-header">
<span>已生成的链接</span>
<div class="header-actions">
<el-button
type="primary"
size="small"
@click="refreshList"
:loading="loading"
>
刷新列表
</el-button>
<el-button
type="success"
size="small"
@click="exportToCSV"
:disabled="!linkList.length"
>
导出CSV
</el-button>
</div>
</div>
</template>
<el-table
v-loading="loading"
:data="linkList"
style="width: 100%"
border
>
<el-table-column prop="batchId" label="批次ID" width="100" />
<el-table-column prop="codeNos" label="机器编号" width="200">
<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>
</template>
</el-table-column>
<el-table-column prop="deductPoints" label="扣除积分" width="100">
<template #default="{ row }">
<el-tag type="warning" size="small">
{{ row.deductPoints || 0 }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="expireAt" label="过期时间" width="180">
<template #default="{ row }">
{{ formatDateTime(row.expireAt) }}
</template>
</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>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag
:type="getLinkStatusByExpire(row.expireAt)"
size="small"
>
{{ getLinkStatusByExpire(row.expireAt, true) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" width="180">
<template #default="{ row }">
{{ formatDateTime(new Date()) }}
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="viewQRCode(row)"
>
查看二维码
</el-button>
<el-button
type="danger"
size="small"
@click="deleteLinkItem(row.batchId)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 二维码查看对话框 -->
<el-dialog
v-model="qrCodeDialogVisible"
title="二维码"
width="400px"
center
>
<div class="qr-code-content">
<div v-if="currentQRCode" class="qr-code-image">
<img :src="currentQRCode" alt="二维码" style="max-width: 100%;" />
</div>
<div v-else class="qr-code-placeholder">
<el-empty description="暂无二维码" />
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="qrCodeDialogVisible = false">关闭</el-button>
<el-button
v-if="currentQRCode"
type="primary"
@click="downloadQRCode"
>
下载二维码
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } 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 { LINK_CONFIG, STATUS_CONFIG, EXPORT_CONFIG } from '@/config/links'
// 响应式数据
const generateFormRef = ref()
const generating = ref(false)
const loading = ref(false)
const linkList = ref([])
const qrCodeDialogVisible = ref(false)
const currentQRCode = ref('')
// 生成表单
const generateForm = reactive({
times: 1,
linkCount: 5
})
// 分页
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0
})
// 表单验证规则
const generateRules = {
times: [
{ required: true, message: '请输入生成次数', trigger: 'blur' },
{ type: 'number', min: 1, max: 100, message: '生成次数必须在1-100之间', trigger: 'blur' }
],
linkCount: [
{ required: true, message: '请输入每次链接数量', trigger: 'blur' },
{ type: 'number', min: 1, max: 50, message: '每次链接数量必须在1-50之间', trigger: 'blur' }
]
}
// 生成链接
const handleGenerate = async () => {
try {
await generateFormRef.value.validate()
generating.value = true
const response = await generateLinks(generateForm)
// 显示生成结果
const result = response.data
if (result && result.codeNos && result.codeNos.length > 0) {
ElMessage.success(`成功生成批次 ${result.batchId},包含 ${result.codeNos.length} 个机器编号,扣除积分 ${result.deductPoints}`)
} else {
ElMessage.success('链接生成成功')
}
// 刷新列表
await refreshList()
// 重置表单
resetForm()
} catch (error) {
console.error('生成链接失败:', error)
ElMessage.error(error.response?.data?.message || '生成链接失败')
} finally {
generating.value = false
}
}
// 重置表单
const resetForm = () => {
generateFormRef.value?.resetFields()
generateForm.times = 1
generateForm.linkCount = 5
}
// 获取链接列表
const getLinkList = async () => {
try {
loading.value = true
const params = {
page: pagination.page,
pageSize: pagination.pageSize
}
const response = await fetchLinks(params)
linkList.value = response.data.items || []
pagination.total = response.data.total || 0
} catch (error) {
console.error('获取链接列表失败:', error)
ElMessage.error('获取链接列表失败')
} finally {
loading.value = false
}
}
// 刷新列表
const refreshList = () => {
pagination.page = 1
return getLinkList()
}
// 分页处理
const handleSizeChange = (size) => {
pagination.pageSize = size
pagination.page = 1
getLinkList()
}
const handleCurrentChange = (page) => {
pagination.page = page
getLinkList()
}
// 删除链接
const deleteLinkItem = async (id) => {
try {
await ElMessageBox.confirm('确定要删除这个链接吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await deleteLink(id)
ElMessage.success('删除成功')
await getLinkList()
} catch (error) {
if (error !== 'cancel') {
console.error('删除链接失败:', error)
ElMessage.error('删除链接失败')
}
}
}
// 查看二维码
const viewQRCode = (row) => {
if (row.codeNos && row.codeNos.length > 0) {
// 使用第一个机器编号生成二维码
const linkUrl = generateLinkUrl(row.codeNos[0])
currentQRCode.value = generateQRCodeUrl(linkUrl, 200)
qrCodeDialogVisible.value = true
} else {
ElMessage.warning('没有可用的机器编号')
}
}
// 下载二维码
const downloadQRCode = () => {
if (currentQRCode.value) {
downloadImage(currentQRCode.value, 'qrcode.png')
}
}
// 复制到剪贴板
const copyToClipboard = async (text) => {
const success = await copyText(text)
if (success) {
ElMessage.success('复制成功')
} else {
ElMessage.error('复制失败')
}
}
// 导出CSV
const exportToCSV = () => {
const headers = EXPORT_CONFIG.DEFAULT_COLUMNS
const data = linkList.value.map(item => ({
...item,
codeNos: (item.codeNos || []).join(', '),
status: getLinkStatusByExpire(item.expireAt, true),
createdAt: formatDateTime(new Date())
}))
exportCSV(data, headers, `${EXPORT_CONFIG.FILE_PREFIX}_${new Date().toISOString().split('T')[0]}.csv`)
}
// 生成链接URL
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 now = new Date()
const expire = new Date(expireAt)
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
} else {
return returnText ? STATUS_CONFIG.LABEL_TEXTS.NORMAL : STATUS_CONFIG.LABEL_TYPES.NORMAL
}
}
// 格式化日期时间
const formatDateTime = (dateString) => {
if (!dateString) return '-'
const date = new Date(dateString)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
// 页面加载时获取数据
onMounted(() => {
getLinkList()
})
</script>
<style scoped>
.link-generate {
padding: 20px;
}
.generate-form-card,
.link-list-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-actions {
display: flex;
gap: 10px;
}
.generate-form {
max-width: 800px;
}
.form-tip {
font-size: 12px;
color: #999;
margin-top: 5px;
}
.pagination-wrapper {
margin-top: 20px;
text-align: right;
}
.qr-code-content {
text-align: center;
padding: 20px;
}
.qr-code-image img {
border: 1px solid #eee;
border-radius: 4px;
}
.qr-code-placeholder {
padding: 40px 0;
}
</style>