添加链接管理功能,包括路由配置、权限设置和在管理布局中显示链接管理菜单项
This commit is contained in:
182
README_LINKS.md
Normal file
182
README_LINKS.md
Normal 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
21
src/api/links.js
Normal 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
56
src/config/links.js
Normal 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: '创建时间' }
|
||||
]
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
94
src/utils/links.js
Normal 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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()] || []
|
||||
|
||||
494
src/views/links/LinkGenerate.vue
Normal file
494
src/views/links/LinkGenerate.vue
Normal 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>
|
||||
Reference in New Issue
Block a user