添加链接管理功能,包括路由配置、权限设置和在管理布局中显示链接管理菜单项
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>
|
<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>
|
<span>报表分析</span>
|
||||||
</el-menu-item>
|
</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' }">
|
<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>
|
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="m12 8l-2 4h4l-2 4"/></svg></i>
|
||||||
<span>系统设置</span>
|
<span>系统设置</span>
|
||||||
@@ -91,6 +95,7 @@ const canAccessUsers = computed(() => canAccessRoute('Users'))
|
|||||||
const canAccessGames = computed(() => canAccessRoute('Games'))
|
const canAccessGames = computed(() => canAccessRoute('Games'))
|
||||||
const canAccessOrders = computed(() => canAccessRoute('Orders'))
|
const canAccessOrders = computed(() => canAccessRoute('Orders'))
|
||||||
const canAccessReports = computed(() => canAccessRoute('Reports'))
|
const canAccessReports = computed(() => canAccessRoute('Reports'))
|
||||||
|
const canAccessLinks = computed(() => canAccessRoute('Links'))
|
||||||
const canAccessSettings = computed(() => canAccessRoute('Settings'))
|
const canAccessSettings = computed(() => canAccessRoute('Settings'))
|
||||||
|
|
||||||
function onProfile() {
|
function onProfile() {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const GameList = () => import('@/views/games/GameList.vue')
|
|||||||
const OrderList = () => import('@/views/orders/OrderList.vue')
|
const OrderList = () => import('@/views/orders/OrderList.vue')
|
||||||
const ReportAnalysis = () => import('@/views/reports/ReportAnalysis.vue')
|
const ReportAnalysis = () => import('@/views/reports/ReportAnalysis.vue')
|
||||||
const Settings = () => import('@/views/settings/Settings.vue')
|
const Settings = () => import('@/views/settings/Settings.vue')
|
||||||
|
const LinkGenerate = () => import('@/views/links/LinkGenerate.vue')
|
||||||
const ErrorTest = () => import('@/views/ErrorTest.vue')
|
const ErrorTest = () => import('@/views/ErrorTest.vue')
|
||||||
const PermissionTest = () => import('@/views/PermissionTest.vue')
|
const PermissionTest = () => import('@/views/PermissionTest.vue')
|
||||||
const NotFound = () => import('@/views/NotFound.vue')
|
const NotFound = () => import('@/views/NotFound.vue')
|
||||||
@@ -27,6 +28,7 @@ export const routes = [
|
|||||||
{ path: 'orders', name: 'Orders', component: OrderList, meta: { title: '订单管理' } },
|
{ path: 'orders', name: 'Orders', component: OrderList, meta: { title: '订单管理' } },
|
||||||
{ path: 'reports', name: 'Reports', component: ReportAnalysis, meta: { title: '报表分析' } },
|
{ path: 'reports', name: 'Reports', component: ReportAnalysis, meta: { title: '报表分析' } },
|
||||||
{ path: 'settings', name: 'Settings', component: Settings, 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: 'error-test', name: 'ErrorTest', component: ErrorTest, meta: { title: '错误处理测试' } },
|
||||||
{ path: 'permission-test', name: 'PermissionTest', component: PermissionTest, 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',
|
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.ORDER_VIEW,
|
||||||
PERMISSIONS.REPORT_VIEW,
|
PERMISSIONS.REPORT_VIEW,
|
||||||
PERMISSIONS.SETTING_MANAGE,
|
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: [
|
AGENT: [
|
||||||
// 代理商只有查看权限,没有管理权限
|
// 代理商只有查看权限,没有管理权限
|
||||||
PERMISSIONS.GAME_VIEW,
|
PERMISSIONS.GAME_VIEW,
|
||||||
PERMISSIONS.ORDER_VIEW,
|
PERMISSIONS.ORDER_VIEW,
|
||||||
PERMISSIONS.REPORT_VIEW,
|
PERMISSIONS.REPORT_VIEW,
|
||||||
|
PERMISSIONS.LINK_VIEW,
|
||||||
|
PERMISSIONS.QR_VIEW,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +82,7 @@ export const ROUTE_PERMISSIONS = {
|
|||||||
'Orders': [PERMISSIONS.ORDER_VIEW],
|
'Orders': [PERMISSIONS.ORDER_VIEW],
|
||||||
'Reports': [PERMISSIONS.REPORT_VIEW],
|
'Reports': [PERMISSIONS.REPORT_VIEW],
|
||||||
'Settings': [PERMISSIONS.SETTING_MANAGE],
|
'Settings': [PERMISSIONS.SETTING_MANAGE],
|
||||||
|
'Links': [PERMISSIONS.LINK_VIEW],
|
||||||
'ErrorTest': [], // 错误测试页面所有用户都可以访问
|
'ErrorTest': [], // 错误测试页面所有用户都可以访问
|
||||||
'PermissionTest': [], // 权限测试页面所有用户都可以访问
|
'PermissionTest': [], // 权限测试页面所有用户都可以访问
|
||||||
}
|
}
|
||||||
@@ -132,7 +153,7 @@ export function getAccessibleRoutes() {
|
|||||||
|
|
||||||
// 管理员可以访问所有路由
|
// 管理员可以访问所有路由
|
||||||
if (isAdmin()) {
|
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()] || []
|
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