diff --git a/README_LINKS.md b/README_LINKS.md new file mode 100644 index 0000000..85c6447 --- /dev/null +++ b/README_LINKS.md @@ -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导出 + + diff --git a/src/api/links.js b/src/api/links.js new file mode 100644 index 0000000..80397b8 --- /dev/null +++ b/src/api/links.js @@ -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) +} + + diff --git a/src/config/links.js b/src/config/links.js new file mode 100644 index 0000000..897201c --- /dev/null +++ b/src/config/links.js @@ -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: '创建时间' } + ] +} diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue index fc21fc6..6f7844e 100644 --- a/src/layouts/AdminLayout.vue +++ b/src/layouts/AdminLayout.vue @@ -34,6 +34,10 @@ 报表分析 + + + 链接管理 + 系统设置 @@ -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() { diff --git a/src/router/index.js b/src/router/index.js index fe336eb..5023d92 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -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: '权限测试' } }, ], diff --git a/src/utils/links.js b/src/utils/links.js new file mode 100644 index 0000000..4cc6ae5 --- /dev/null +++ b/src/utils/links.js @@ -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() +} + + diff --git a/src/utils/permission.js b/src/utils/permission.js index 397421e..697ce65 100644 --- a/src/utils/permission.js +++ b/src/utils/permission.js @@ -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()] || [] diff --git a/src/views/links/LinkGenerate.vue b/src/views/links/LinkGenerate.vue new file mode 100644 index 0000000..95125fb --- /dev/null +++ b/src/views/links/LinkGenerate.vue @@ -0,0 +1,494 @@ + + + + +