添加 vue-router 依赖并配置路由,重构登录组件,完善 HTTP 请求拦截器以支持 token 刷新机制
This commit is contained in:
6
.env
Normal file
6
.env
Normal file
@@ -0,0 +1,6 @@
|
||||
# 默认环境变量(如未提供具体 .env.* 时使用)
|
||||
# 开发环境已通过 Vite 代理将 /api 转发到 http://localhost:18080
|
||||
# 生产环境请根据部署地址覆盖为真实后端地址
|
||||
|
||||
VITE_API_BASE=/api
|
||||
|
||||
1
.env.development
Normal file
1
.env.development
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API_BASE=/api
|
||||
468
docs/接口文档.md
Normal file
468
docs/接口文档.md
Normal file
@@ -0,0 +1,468 @@
|
||||
# API 文档
|
||||
|
||||
## 用户管理接口
|
||||
|
||||
### 创建用户账户
|
||||
|
||||
#### 管理员创建用户账户
|
||||
|
||||
**接口地址:** `POST /api/admin/accounts`
|
||||
|
||||
**请求头:**
|
||||
```
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
```json
|
||||
{
|
||||
"userType": "ADMIN", // 必填,用户类型:ADMIN 或 AGENT
|
||||
"username": "newuser", // 必填,用户名,3-64字符,只能包含字母、数字、下划线
|
||||
"password": "123456", // 必填,密码,6-128字符
|
||||
"status": "ENABLED", // 可选,状态:ENABLED 或 DISABLED,默认ENABLED
|
||||
"pointsBalance": 0 // 可选,积分余额(仅AGENT类型),默认0
|
||||
}
|
||||
```
|
||||
|
||||
**成功响应(200):**
|
||||
```json
|
||||
{
|
||||
"id": 2,
|
||||
"userType": "ADMIN",
|
||||
"username": "newuser",
|
||||
"status": "ENABLED",
|
||||
"pointsBalance": 0,
|
||||
"createdAt": "2025-08-24T18:30:00.000",
|
||||
"updatedAt": "2025-08-24T18:30:00.000"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应:**
|
||||
|
||||
**400 Bad Request - 参数验证失败:**
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-08-24T18:30:00.000",
|
||||
"status": 400,
|
||||
"error": "Bad Request",
|
||||
"message": "Validation failed",
|
||||
"details": [
|
||||
{
|
||||
"field": "username",
|
||||
"message": "用户名长度必须在3-64字符之间"
|
||||
},
|
||||
{
|
||||
"field": "password",
|
||||
"message": "密码长度必须在6-128字符之间"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**409 Conflict - 用户名已存在:**
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-08-24T18:30:00.000",
|
||||
"status": 409,
|
||||
"error": "Conflict",
|
||||
"message": "用户名已存在"
|
||||
}
|
||||
```
|
||||
|
||||
**401 Unauthorized - 未授权:**
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-08-24T18:30:00.000",
|
||||
"status": 401,
|
||||
"error": "Unauthorized",
|
||||
"message": "访问被拒绝"
|
||||
}
|
||||
```
|
||||
|
||||
**403 Forbidden - 权限不足:**
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-08-24T18:30:00.000",
|
||||
"status": 403,
|
||||
"error": "Forbidden",
|
||||
"message": "权限不足,无法创建用户"
|
||||
}
|
||||
```
|
||||
|
||||
#### 用户自注册接口
|
||||
|
||||
**接口地址:** `POST /api/users`
|
||||
|
||||
**请求头:**
|
||||
```
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
```json
|
||||
{
|
||||
"userType": "AGENT", // 必填,用户类型:只能为 AGENT
|
||||
"username": "newagent", // 必填,用户名,3-64字符
|
||||
"password": "123456", // 必填,密码,6-128字符
|
||||
"pointsBalance": 0 // 可选,积分余额,默认0
|
||||
}
|
||||
```
|
||||
|
||||
**成功响应(201):**
|
||||
```json
|
||||
{
|
||||
"id": 3,
|
||||
"userType": "AGENT",
|
||||
"username": "newagent",
|
||||
"status": "ENABLED",
|
||||
"pointsBalance": 0,
|
||||
"createdAt": "2025-08-24T18:30:00.000",
|
||||
"updatedAt": "2025-08-24T18:30:00.000"
|
||||
}
|
||||
```
|
||||
|
||||
### 接口说明
|
||||
|
||||
#### 权限要求
|
||||
- **管理员接口** (`/api/admin/accounts`):需要管理员权限,可以创建ADMIN和AGENT类型用户
|
||||
- **用户接口** (`/api/users`):公开接口,只能创建AGENT类型用户
|
||||
|
||||
#### 参数说明
|
||||
|
||||
**userType(用户类型):**
|
||||
- `ADMIN`:管理员用户
|
||||
- `AGENT`:代理用户
|
||||
|
||||
**username(用户名):**
|
||||
- 长度:3-64字符
|
||||
- 格式:只能包含字母、数字、下划线
|
||||
- 唯一性:系统内必须唯一
|
||||
|
||||
**password(密码):**
|
||||
- 长度:6-128字符
|
||||
- 存储:使用BCrypt加密存储
|
||||
- 安全:建议包含大小写字母、数字和特殊字符
|
||||
|
||||
**status(状态):**
|
||||
- 可选值:`ENABLED`(启用)、`DISABLED`(禁用)
|
||||
- 默认值:`ENABLED`
|
||||
|
||||
**pointsBalance(积分余额):**
|
||||
- 仅AGENT类型用户可以设置
|
||||
- 类型:整数,不能为负数
|
||||
- 默认值:0
|
||||
|
||||
#### 业务规则
|
||||
1. 用户名在系统内必须唯一
|
||||
2. 密码使用BCrypt加密存储,无法解密
|
||||
3. 创建成功后账户默认状态为ENABLED
|
||||
4. 只有管理员可以创建ADMIN类型用户
|
||||
5. 简化的用户模型,去除了角色和显示名称等复杂字段
|
||||
|
||||
#### 使用示例
|
||||
|
||||
**创建管理员用户:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/admin/accounts \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer your-token" \
|
||||
-d '{
|
||||
"userType": "ADMIN",
|
||||
"username": "admin001",
|
||||
"password": "Admin123!"
|
||||
}'
|
||||
```
|
||||
|
||||
**创建代理用户:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/admin/accounts \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer your-token" \
|
||||
-d '{
|
||||
"userType": "AGENT",
|
||||
"username": "agent001",
|
||||
"password": "Agent123!",
|
||||
"pointsBalance": 1000
|
||||
}'
|
||||
```
|
||||
|
||||
**用户自注册:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/users \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"userType": "AGENT",
|
||||
"username": "newuser",
|
||||
"password": "User123!"
|
||||
}'
|
||||
```
|
||||
|
||||
### 重置密码接口
|
||||
|
||||
**接口地址:** `POST /api/admin/accounts/{id}/reset-password`
|
||||
|
||||
**请求头:**
|
||||
```
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**路径参数:**
|
||||
- `id`: 账户ID
|
||||
|
||||
**请求参数:**
|
||||
```json
|
||||
{
|
||||
"newPassword": "NewPassword123!", // 必填,新密码,6-128字符
|
||||
"forceLogout": true // 可选,是否强制登出,默认true
|
||||
}
|
||||
```
|
||||
|
||||
**成功响应(204):** 无响应体
|
||||
|
||||
**错误响应:**
|
||||
|
||||
**400 Bad Request - 密码格式错误:**
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-08-24T18:30:00.000",
|
||||
"status": 400,
|
||||
"error": "Bad Request",
|
||||
"message": "新密码长度必须在6-128字符之间"
|
||||
}
|
||||
```
|
||||
|
||||
**404 Not Found - 用户不存在:**
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-08-24T18:30:00.000",
|
||||
"status": 404,
|
||||
"error": "Not Found",
|
||||
"message": "用户不存在"
|
||||
}
|
||||
```
|
||||
|
||||
### 启用/禁用用户接口
|
||||
|
||||
**启用用户:** `POST /api/admin/accounts/{id}/enable`
|
||||
**禁用用户:** `POST /api/admin/accounts/{id}/disable`
|
||||
|
||||
**请求头:**
|
||||
```
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**成功响应(204):** 无响应体
|
||||
|
||||
## 前端接口调用说明
|
||||
|
||||
### 用户管理接口 (src/api/users.js)
|
||||
|
||||
#### 获取用户列表
|
||||
```javascript
|
||||
import { fetchUsers } from '@/api/users'
|
||||
|
||||
// 获取用户列表
|
||||
const params = {
|
||||
page: 1,
|
||||
size: 20, // 每页大小,默认20,最大200
|
||||
keyword: '搜索关键词', // 可选,搜索关键词
|
||||
userType: 'AGENT', // 可选,用户类型:ADMIN 或 AGENT
|
||||
status: 'ENABLED' // 可选,账户状态:ENABLED 或 DISABLED
|
||||
}
|
||||
const response = await fetchUsers(params)
|
||||
```
|
||||
|
||||
#### 创建用户
|
||||
```javascript
|
||||
import { createUser } from '@/api/users'
|
||||
|
||||
// 创建管理员用户
|
||||
const adminPayload = {
|
||||
userType: 'ADMIN',
|
||||
username: 'admin001',
|
||||
password: 'Admin123!',
|
||||
status: 'ENABLED'
|
||||
}
|
||||
|
||||
// 创建代理用户
|
||||
const agentPayload = {
|
||||
userType: 'AGENT',
|
||||
username: 'agent001',
|
||||
password: 'Agent123!',
|
||||
pointsBalance: 1000,
|
||||
status: 'ENABLED'
|
||||
}
|
||||
|
||||
const response = await createUser(adminPayload)
|
||||
```
|
||||
|
||||
#### 用户自注册(仅限AGENT)
|
||||
```javascript
|
||||
import { registerUser } from '@/api/users'
|
||||
|
||||
// 用户自注册(公开接口,无需权限)
|
||||
const agentPayload = {
|
||||
userType: 'AGENT', // 必填,只能为 AGENT
|
||||
username: 'newagent', // 必填,用户名,3-64字符
|
||||
password: 'Agent123!', // 必填,密码,6-128字符
|
||||
pointsBalance: 0 // 可选,积分余额,默认0
|
||||
}
|
||||
|
||||
const response = await registerUser(agentPayload)
|
||||
```
|
||||
|
||||
#### 更新用户
|
||||
```javascript
|
||||
import { updateUser } from '@/api/users'
|
||||
|
||||
const payload = {
|
||||
userType: 'AGENT',
|
||||
username: 'agent001',
|
||||
pointsBalance: 2000,
|
||||
status: 'ENABLED'
|
||||
}
|
||||
|
||||
const response = await updateUser(userId, payload)
|
||||
```
|
||||
|
||||
#### 删除用户
|
||||
```javascript
|
||||
import { deleteUser } from '@/api/users'
|
||||
|
||||
const response = await deleteUser(userId)
|
||||
```
|
||||
|
||||
#### 设置用户状态
|
||||
```javascript
|
||||
import { setUserStatus } from '@/api/users'
|
||||
|
||||
// 启用用户
|
||||
await setUserStatus(userId, true)
|
||||
|
||||
// 禁用用户
|
||||
await setUserStatus(userId, false)
|
||||
```
|
||||
|
||||
#### 重置用户密码
|
||||
```javascript
|
||||
import { resetUserPassword } from '@/api/users'
|
||||
|
||||
// 重置密码,需要指定新密码
|
||||
const newPassword = 'NewPassword123!'
|
||||
const forceLogout = true // 是否强制登出,默认为true
|
||||
const response = await resetUserPassword(userId, newPassword, forceLogout)
|
||||
```
|
||||
|
||||
### 认证接口 (src/api/auth.js)
|
||||
|
||||
#### 用户登录
|
||||
```javascript
|
||||
import { login } from '@/api/auth'
|
||||
|
||||
const payload = {
|
||||
username: 'admin',
|
||||
password: 'admin123'
|
||||
}
|
||||
|
||||
const response = await login(payload)
|
||||
// 返回包含 accessToken 和 refreshToken 的数据
|
||||
```
|
||||
|
||||
#### 刷新令牌
|
||||
```javascript
|
||||
import { refresh } from '@/api/auth'
|
||||
|
||||
const payload = {
|
||||
refreshToken: 'your-refresh-token'
|
||||
}
|
||||
|
||||
const response = await refresh(payload)
|
||||
```
|
||||
|
||||
#### 用户登出
|
||||
```javascript
|
||||
import { logout } from '@/api/auth'
|
||||
|
||||
const response = await logout()
|
||||
```
|
||||
|
||||
### 前端表单字段映射
|
||||
|
||||
#### 用户列表查询参数
|
||||
- `page`: 页码,默认1
|
||||
- `size`: 每页大小,默认20,最大200(前端使用pageSize,传参时转换为size)
|
||||
- `keyword`: 搜索关键词(用户名)
|
||||
- `userType`: 用户类型筛选 (ADMIN/AGENT)
|
||||
- `status`: 状态筛选 (ENABLED/DISABLED)
|
||||
|
||||
#### 用户创建/编辑表单字段
|
||||
- `userType`: 用户类型 (ADMIN/AGENT)
|
||||
- `username`: 用户名
|
||||
- `password`: 密码 (仅创建时)
|
||||
- `enabled`: 状态 (true/false,转换为ENABLED/DISABLED)
|
||||
- `pointsBalance`: 积分余额 (仅AGENT类型)
|
||||
|
||||
#### 后端返回数据字段
|
||||
- `status`: 用户状态 (ENABLED/DISABLED)
|
||||
- `enabled`: 兼容字段,部分接口可能返回 (true/false)
|
||||
|
||||
#### 表单验证规则
|
||||
- `userType`: 必填
|
||||
- `username`: 必填,3-64字符
|
||||
- `password`: 创建时必填,最少6位
|
||||
- `pointsBalance`: 仅AGENT类型必填,非负整数
|
||||
|
||||
### 注意事项
|
||||
|
||||
1. **接口路径**: 所有用户管理接口都使用 `/api/admin/accounts` 前缀
|
||||
2. **权限控制**:
|
||||
- 需要有效的 Bearer Token
|
||||
- 只有 ADMIN 类型用户可以访问用户管理功能
|
||||
- AGENT 类型用户无法访问用户管理页面和接口
|
||||
3. **状态字段**:
|
||||
- 前端表单使用 `enabled` (boolean),提交时转换为 `status` (ENABLED/DISABLED)
|
||||
- 后端返回使用 `status` (ENABLED/DISABLED),前端显示时需要转换
|
||||
4. **积分余额**: 仅AGENT类型用户可以设置积分余额
|
||||
5. **简化模型**: 去除了角色和显示名称字段,简化了用户管理
|
||||
6. **错误处理**: 统一使用HTTP状态码和错误消息处理
|
||||
7. **错误显示**: 前端自动显示后端返回的错误消息,支持字段级错误提示
|
||||
|
||||
### 错误处理机制
|
||||
|
||||
#### 后端错误响应格式
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-08-24T18:30:00.000",
|
||||
"status": 400,
|
||||
"error": "Bad Request",
|
||||
"message": "用户名已存在"
|
||||
}
|
||||
```
|
||||
|
||||
#### 字段验证错误格式
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-08-24T18:30:00.000",
|
||||
"status": 400,
|
||||
"error": "Bad Request",
|
||||
"message": "Validation failed",
|
||||
"details": [
|
||||
{
|
||||
"field": "username",
|
||||
"message": "用户名长度必须在3-64字符之间"
|
||||
},
|
||||
{
|
||||
"field": "password",
|
||||
"message": "密码长度必须在6-128字符之间"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 前端错误处理
|
||||
- **全局拦截**: HTTP拦截器自动显示错误消息
|
||||
- **字段级错误**: 支持显示具体的字段验证错误
|
||||
- **状态码映射**: 根据HTTP状态码提供友好的错误提示
|
||||
- **降级处理**: 当无法获取具体错误时,显示默认错误消息
|
||||
160
docs/权限管理.md
Normal file
160
docs/权限管理.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# 权限管理文档
|
||||
|
||||
## 概述
|
||||
|
||||
本系统采用基于角色的权限控制(RBAC)机制,通过用户类型(ADMIN/AGENT)来控制用户的功能访问权限。
|
||||
|
||||
## 用户类型
|
||||
|
||||
### ADMIN(管理员)
|
||||
- 拥有系统所有功能的访问权限
|
||||
- 可以管理用户账户(创建、编辑、删除、启用/禁用)
|
||||
- 可以管理游戏、订单、报表等所有模块
|
||||
- 可以访问系统设置
|
||||
|
||||
### AGENT(代理商)
|
||||
- 只能查看游戏、订单、报表等基础信息
|
||||
- 无法访问用户管理功能
|
||||
- 无法访问系统设置
|
||||
- 主要用于业务操作,不具备管理权限
|
||||
|
||||
## 权限定义
|
||||
|
||||
### 用户管理权限
|
||||
- `user:view` - 查看用户列表
|
||||
- `user:create` - 创建用户
|
||||
- `user:update` - 编辑用户
|
||||
- `user:delete` - 删除用户
|
||||
- `user:manage` - 用户管理(包含所有用户相关权限)
|
||||
|
||||
### 游戏管理权限
|
||||
- `game:view` - 查看游戏列表
|
||||
- `game:create` - 创建游戏
|
||||
- `game:update` - 编辑游戏
|
||||
- `game:delete` - 删除游戏
|
||||
- `game:manage` - 游戏管理(包含所有游戏相关权限)
|
||||
|
||||
### 订单管理权限
|
||||
- `order:view` - 查看订单列表
|
||||
- `order:manage` - 订单管理
|
||||
|
||||
### 报表分析权限
|
||||
- `report:view` - 查看报表
|
||||
|
||||
### 系统设置权限
|
||||
- `setting:manage` - 系统设置管理
|
||||
|
||||
## 角色权限映射
|
||||
|
||||
```javascript
|
||||
const ROLE_PERMISSIONS = {
|
||||
ADMIN: [
|
||||
// 管理员拥有所有权限
|
||||
'user:manage', 'user:create', 'user:update', 'user:delete', 'user:view',
|
||||
'game:manage', 'game:create', 'game:update', 'game:delete', 'game:view',
|
||||
'order:manage', 'order:view',
|
||||
'report:view',
|
||||
'setting:manage',
|
||||
],
|
||||
AGENT: [
|
||||
// 代理商只有查看权限
|
||||
'game:view',
|
||||
'order:view',
|
||||
'report:view',
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 前端权限控制
|
||||
|
||||
### 1. 路由权限控制
|
||||
- 在路由守卫中检查用户是否有访问权限
|
||||
- 无权限时自动跳转到仪表盘页面
|
||||
|
||||
### 2. 菜单权限控制
|
||||
- 根据用户权限动态显示/隐藏菜单项
|
||||
- 代理商用户看不到"用户管理"和"系统设置"菜单
|
||||
|
||||
### 3. 页面权限控制
|
||||
- 在页面组件中检查用户权限
|
||||
- 无权限时显示权限不足提示
|
||||
|
||||
### 4. 操作权限控制
|
||||
- 根据用户权限显示/隐藏操作按钮
|
||||
- 代理商用户无法进行用户管理操作
|
||||
|
||||
## 权限检查方法
|
||||
|
||||
### 1. 权限工具函数
|
||||
```javascript
|
||||
import { hasPermission, hasAnyPermission, hasAllPermissions } from '@/utils/permission'
|
||||
|
||||
// 检查单个权限
|
||||
const canCreateUser = hasPermission('user:create')
|
||||
|
||||
// 检查多个权限(任一)
|
||||
const canManageUsers = hasAnyPermission(['user:create', 'user:update', 'user:delete'])
|
||||
|
||||
// 检查多个权限(全部)
|
||||
const canFullManage = hasAllPermissions(['user:create', 'user:update', 'user:delete'])
|
||||
```
|
||||
|
||||
### 2. 权限指令
|
||||
```vue
|
||||
<!-- 单个权限 -->
|
||||
<el-button v-permission="'user:create'">新增用户</el-button>
|
||||
|
||||
<!-- 多个权限(OR逻辑) -->
|
||||
<el-button v-permission="['user:create', 'user:update']">操作</el-button>
|
||||
|
||||
<!-- 多个权限(AND逻辑) -->
|
||||
<el-button v-permission="['user:create', 'user:update', 'AND']">操作</el-button>
|
||||
|
||||
<!-- 对象格式 -->
|
||||
<el-button v-permission="{ permission: ['user:create', 'user:update'], logic: 'OR' }">操作</el-button>
|
||||
```
|
||||
|
||||
### 3. 计算属性
|
||||
```javascript
|
||||
const canViewUsers = computed(() => hasPermission('user:view'))
|
||||
const canCreateUser = computed(() => hasPermission('user:create'))
|
||||
const canEditUser = computed(() => hasPermission('user:update'))
|
||||
const canDeleteUser = computed(() => hasPermission('user:delete'))
|
||||
```
|
||||
|
||||
## 后端权限控制
|
||||
|
||||
### 1. 接口权限
|
||||
- 用户管理接口(`/api/admin/accounts`)只允许 ADMIN 用户访问
|
||||
- 代理商用户访问时返回 403 Forbidden 错误
|
||||
|
||||
### 2. 数据权限
|
||||
- 代理商用户只能查看自己的相关数据
|
||||
- 管理员可以查看和管理所有数据
|
||||
|
||||
## 权限配置
|
||||
|
||||
### 添加新权限
|
||||
1. 在 `src/utils/permission.js` 中的 `PERMISSIONS` 对象中添加新权限
|
||||
2. 在 `ROLE_PERMISSIONS` 中为不同角色分配权限
|
||||
3. 在 `ROUTE_PERMISSIONS` 中配置路由权限要求
|
||||
|
||||
### 修改角色权限
|
||||
1. 修改 `ROLE_PERMISSIONS` 中对应角色的权限数组
|
||||
2. 更新相关的前端权限检查逻辑
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
1. **前端权限控制仅用于用户体验**,不能作为安全依据
|
||||
2. **后端必须进行权限验证**,确保数据安全
|
||||
3. **敏感操作需要二次确认**,如删除用户
|
||||
4. **权限变更需要重新登录**,确保权限生效
|
||||
5. **定期审查权限配置**,避免权限泄露
|
||||
|
||||
## 测试建议
|
||||
|
||||
1. 使用不同用户类型登录,验证权限控制
|
||||
2. 测试无权限访问时的跳转逻辑
|
||||
3. 验证菜单和按钮的显示/隐藏
|
||||
4. 测试后端接口的权限验证
|
||||
5. 检查权限变更后的效果
|
||||
193
docs/权限系统配置总结.md
Normal file
193
docs/权限系统配置总结.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# 权限系统配置总结
|
||||
|
||||
## 概述
|
||||
|
||||
本系统采用基于角色的权限控制(RBAC)机制,通过用户类型(ADMIN/AGENT)来控制用户的功能访问权限。
|
||||
|
||||
## 用户类型权限配置
|
||||
|
||||
### ADMIN(管理员)
|
||||
- **权限范围**:拥有系统所有功能的访问权限
|
||||
- **路由访问**:可以访问所有页面和路由
|
||||
- **功能权限**:
|
||||
- 用户管理(创建、编辑、删除、启用/禁用)
|
||||
- 游戏管理(查看、创建、编辑、删除)
|
||||
- 订单管理(查看、管理)
|
||||
- 报表分析(查看)
|
||||
- 系统设置(管理)
|
||||
|
||||
### AGENT(代理商)
|
||||
- **权限范围**:只能查看基础信息,无管理权限
|
||||
- **路由访问**:只能访问部分页面
|
||||
- **功能权限**:
|
||||
- 游戏查看
|
||||
- 订单查看
|
||||
- 报表查看
|
||||
|
||||
## 权限检查机制
|
||||
|
||||
### 1. 路由权限检查
|
||||
```javascript
|
||||
// 管理员可以访问所有路由
|
||||
export function canAccessRoute(routeName) {
|
||||
if (isAdmin()) return true
|
||||
|
||||
const requiredPermissions = ROUTE_PERMISSIONS[routeName] || []
|
||||
if (requiredPermissions.length === 0) return true
|
||||
|
||||
return hasAnyPermission(requiredPermissions)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 功能权限检查
|
||||
```javascript
|
||||
// 检查单个权限
|
||||
const canCreateUser = hasPermission('user:create')
|
||||
|
||||
// 检查多个权限(任一)
|
||||
const canManageUsers = hasAnyPermission(['user:create', 'user:update', 'user:delete'])
|
||||
|
||||
// 检查多个权限(全部)
|
||||
const canFullManage = hasAllPermissions(['user:create', 'user:update', 'user:delete'])
|
||||
```
|
||||
|
||||
### 3. 权限指令使用
|
||||
```vue
|
||||
<!-- 单个权限 -->
|
||||
<el-button v-permission="'user:create'">新增用户</el-button>
|
||||
|
||||
<!-- 多个权限(OR逻辑) -->
|
||||
<el-button v-permission="['user:create', 'user:update']">操作</el-button>
|
||||
|
||||
<!-- 多个权限(AND逻辑) -->
|
||||
<el-button v-permission="['user:create', 'user:update', 'AND']">操作</el-button>
|
||||
```
|
||||
|
||||
## 权限配置详情
|
||||
|
||||
### 权限定义
|
||||
```javascript
|
||||
export const PERMISSIONS = {
|
||||
// 用户管理权限
|
||||
USER_MANAGE: 'user:manage',
|
||||
USER_CREATE: 'user:create',
|
||||
USER_UPDATE: 'user:update',
|
||||
USER_DELETE: 'user:delete',
|
||||
USER_VIEW: 'user:view',
|
||||
|
||||
// 游戏管理权限
|
||||
GAME_MANAGE: 'game:manage',
|
||||
GAME_CREATE: 'game:create',
|
||||
GAME_UPDATE: 'game:update',
|
||||
GAME_DELETE: 'game:delete',
|
||||
GAME_VIEW: 'game:view',
|
||||
|
||||
// 订单管理权限
|
||||
ORDER_MANAGE: 'order:manage',
|
||||
ORDER_VIEW: 'order:view',
|
||||
|
||||
// 报表分析权限
|
||||
REPORT_VIEW: 'report:view',
|
||||
|
||||
// 系统设置权限
|
||||
SETTING_MANAGE: 'setting:manage',
|
||||
}
|
||||
```
|
||||
|
||||
### 角色权限映射
|
||||
```javascript
|
||||
export const ROLE_PERMISSIONS = {
|
||||
ADMIN: [
|
||||
// 管理员拥有所有权限
|
||||
PERMISSIONS.USER_MANAGE, PERMISSIONS.USER_CREATE, PERMISSIONS.USER_UPDATE,
|
||||
PERMISSIONS.USER_DELETE, PERMISSIONS.USER_VIEW,
|
||||
PERMISSIONS.GAME_MANAGE, PERMISSIONS.GAME_CREATE, PERMISSIONS.GAME_UPDATE,
|
||||
PERMISSIONS.GAME_DELETE, PERMISSIONS.GAME_VIEW,
|
||||
PERMISSIONS.ORDER_MANAGE, PERMISSIONS.ORDER_VIEW,
|
||||
PERMISSIONS.REPORT_VIEW, PERMISSIONS.SETTING_MANAGE,
|
||||
],
|
||||
AGENT: [
|
||||
// 代理商只有查看权限
|
||||
PERMISSIONS.GAME_VIEW, PERMISSIONS.ORDER_VIEW, PERMISSIONS.REPORT_VIEW,
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 路由权限映射
|
||||
```javascript
|
||||
export const ROUTE_PERMISSIONS = {
|
||||
'Users': [PERMISSIONS.USER_VIEW],
|
||||
'Games': [PERMISSIONS.GAME_VIEW],
|
||||
'Orders': [PERMISSIONS.ORDER_VIEW],
|
||||
'Reports': [PERMISSIONS.REPORT_VIEW],
|
||||
'Settings': [PERMISSIONS.SETTING_MANAGE],
|
||||
}
|
||||
```
|
||||
|
||||
## 权限控制层级
|
||||
|
||||
### 1. 路由级权限控制
|
||||
- 在路由守卫中检查用户是否有访问权限
|
||||
- 管理员可以访问所有路由
|
||||
- 代理商只能访问有权限的路由
|
||||
- 无权限时自动跳转到仪表盘页面
|
||||
|
||||
### 2. 菜单级权限控制
|
||||
- 根据用户权限动态显示/隐藏菜单项
|
||||
- 代理商用户看不到"用户管理"和"系统设置"菜单
|
||||
- 管理员可以看到所有菜单项
|
||||
|
||||
### 3. 页面级权限控制
|
||||
- 在页面组件中检查用户权限
|
||||
- 无权限时显示权限不足提示
|
||||
- 提供友好的用户体验
|
||||
|
||||
### 4. 操作级权限控制
|
||||
- 根据用户权限显示/隐藏操作按钮
|
||||
- 代理商用户无法进行用户管理操作
|
||||
- 使用权限指令控制按钮显示
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
1. **前端权限控制仅用于用户体验**,不能作为安全依据
|
||||
2. **后端必须进行权限验证**,确保数据安全
|
||||
3. **敏感操作需要二次确认**,如删除用户
|
||||
4. **权限变更需要重新登录**,确保权限生效
|
||||
5. **定期审查权限配置**,避免权限泄露
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 权限测试页面
|
||||
- 访问 `/permission-test` 页面
|
||||
- 查看当前用户的权限状态
|
||||
- 测试各种权限检查功能
|
||||
- 验证路由访问权限
|
||||
|
||||
### 测试建议
|
||||
1. 使用不同用户类型登录,验证权限控制
|
||||
2. 测试无权限访问时的跳转逻辑
|
||||
3. 验证菜单和按钮的显示/隐藏
|
||||
4. 测试后端接口的权限验证
|
||||
5. 检查权限变更后的效果
|
||||
|
||||
## 扩展说明
|
||||
|
||||
### 添加新权限
|
||||
1. 在 `PERMISSIONS` 对象中添加新权限
|
||||
2. 在 `ROLE_PERMISSIONS` 中为不同角色分配权限
|
||||
3. 在 `ROUTE_PERMISSIONS` 中配置路由权限要求
|
||||
4. 更新相关的前端权限检查逻辑
|
||||
|
||||
### 修改角色权限
|
||||
1. 修改 `ROLE_PERMISSIONS` 中对应角色的权限数组
|
||||
2. 更新相关的前端权限检查逻辑
|
||||
3. 测试权限变更后的效果
|
||||
|
||||
## 总结
|
||||
|
||||
当前的权限系统配置确保了:
|
||||
- **管理员拥有所有权限**,可以访问所有页面和功能
|
||||
- **代理商只有查看权限**,无法进行管理操作
|
||||
- **权限检查机制完善**,包括路由、菜单、页面、操作四个层级
|
||||
- **用户体验友好**,无权限时提供清晰的提示和跳转
|
||||
- **安全性保障**,前后端都有权限验证机制
|
||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -10,7 +10,8 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.7.7",
|
||||
"element-plus": "^2.8.8",
|
||||
"vue": "^3.4.38"
|
||||
"vue": "^3.4.38",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
@@ -886,6 +887,12 @@
|
||||
"@vue/shared": "3.5.19"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
"version": "6.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
|
||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.19.tgz",
|
||||
@@ -1650,6 +1657,21 @@
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
||||
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.6.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.7.7",
|
||||
"element-plus": "^2.8.8",
|
||||
"vue": "^3.4.38"
|
||||
"vue": "^3.4.38",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"vite": "^5.4.6"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<Login />
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Login from './views/Login.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 页面样式由 Login 组件内维护 */
|
||||
/* 根组件仅承载路由视图 */
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import http from '../plugins/http'
|
||||
|
||||
// 刷新与登出端点(供需要时直接调用)
|
||||
export function refresh(payload) {
|
||||
return http.post('/api/auth/refresh', payload)
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return http.post('/api/auth/logout')
|
||||
}
|
||||
|
||||
// 登录 API 封装
|
||||
export function login(payload) {
|
||||
// 约定 payload: { username: string, password: string }
|
||||
// 根据实际接口调整路径与字段
|
||||
return http.post('/auth/login', payload)
|
||||
return http.post('/api/auth/login', payload)
|
||||
}
|
||||
|
||||
|
||||
39
src/api/users.js
Normal file
39
src/api/users.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import http from '@/plugins/http'
|
||||
|
||||
export function fetchUsers(params) {
|
||||
// params: { page, pageSize, keyword, status }
|
||||
return http.get('/api/admin/accounts', { params })
|
||||
}
|
||||
|
||||
export function createUser(payload) {
|
||||
// 管理员创建用户账户
|
||||
return http.post('/api/admin/accounts', payload)
|
||||
}
|
||||
|
||||
export function registerUser(payload) {
|
||||
// 用户自注册(仅限AGENT类型)
|
||||
return http.post('/api/users', payload)
|
||||
}
|
||||
|
||||
export function updateUser(id, payload) {
|
||||
return http.patch(`/api/admin/accounts/${id}`, payload)
|
||||
}
|
||||
|
||||
export function deleteUser(id) {
|
||||
return http.delete(`/api/admin/accounts/${id}`)
|
||||
}
|
||||
|
||||
export function setUserStatus(id, enabled) {
|
||||
const endpoint = enabled
|
||||
? `/api/admin/accounts/${id}/enable`
|
||||
: `/api/admin/accounts/${id}/disable`
|
||||
return http.post(endpoint)
|
||||
}
|
||||
|
||||
export function resetUserPassword(id, newPassword, forceLogout = true) {
|
||||
return http.post(`/api/admin/accounts/${id}/reset-password`, {
|
||||
newPassword,
|
||||
forceLogout
|
||||
})
|
||||
}
|
||||
|
||||
37
src/components/PermissionDenied.vue
Normal file
37
src/components/PermissionDenied.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="permission-denied">
|
||||
<el-result
|
||||
icon="warning"
|
||||
title="权限不足"
|
||||
sub-title="您没有访问此页面的权限,请联系管理员"
|
||||
>
|
||||
<template #extra>
|
||||
<el-button type="primary" @click="goBack">返回上一页</el-button>
|
||||
<el-button @click="goHome">返回首页</el-button>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
function goBack() {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
function goHome() {
|
||||
router.push({ name: 'Dashboard' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.permission-denied {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
</style>
|
||||
57
src/directives/permission.js
Normal file
57
src/directives/permission.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { hasPermission, hasAnyPermission, hasAllPermissions } from '@/utils/permission'
|
||||
|
||||
// 权限指令
|
||||
export const permission = {
|
||||
mounted(el, binding) {
|
||||
const { value } = binding
|
||||
|
||||
if (!value) return
|
||||
|
||||
let hasAuth = false
|
||||
|
||||
if (typeof value === 'string') {
|
||||
// 单个权限检查
|
||||
hasAuth = hasPermission(value)
|
||||
} else if (Array.isArray(value)) {
|
||||
// 权限数组检查
|
||||
if (value.length === 0) {
|
||||
hasAuth = true
|
||||
} else if (value.length === 1) {
|
||||
hasAuth = hasPermission(value[0])
|
||||
} else {
|
||||
// 检查是否包含逻辑操作符
|
||||
const logicIndex = value.findIndex(item => item === 'AND' || item === 'OR')
|
||||
if (logicIndex > 0) {
|
||||
const logic = value[logicIndex]
|
||||
const permissions = value.filter(item => item !== 'AND' && item !== 'OR')
|
||||
|
||||
if (logic === 'AND') {
|
||||
hasAuth = hasAllPermissions(permissions)
|
||||
} else if (logic === 'OR') {
|
||||
hasAuth = hasAnyPermission(permissions)
|
||||
}
|
||||
} else {
|
||||
// 默认使用 OR 逻辑
|
||||
hasAuth = hasAnyPermission(value)
|
||||
}
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
// 对象格式:{ permission: string|array, logic: 'AND'|'OR' }
|
||||
const { permission, logic = 'OR' } = value
|
||||
if (Array.isArray(permission)) {
|
||||
hasAuth = logic === 'AND' ? hasAllPermissions(permission) : hasAnyPermission(permission)
|
||||
} else {
|
||||
hasAuth = hasPermission(permission)
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAuth) {
|
||||
el.style.display = 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 注册指令
|
||||
export function setupPermissionDirective(app) {
|
||||
app.directive('permission', permission)
|
||||
}
|
||||
138
src/layouts/AdminLayout.vue
Normal file
138
src/layouts/AdminLayout.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div class="admin-layout">
|
||||
<aside class="sider">
|
||||
<div class="brand">
|
||||
<img class="logo" src="https://vuejs.org/images/logo.png" alt="logo" />
|
||||
<span class="name">管理后台</span>
|
||||
</div>
|
||||
<el-menu
|
||||
class="menu"
|
||||
router
|
||||
:default-active="$route.name"
|
||||
:collapse="collapsed"
|
||||
background-color="#001529"
|
||||
text-color="#bfcbd9"
|
||||
active-text-color="#fff"
|
||||
>
|
||||
<el-menu-item index="Dashboard" :route="{ name: 'Dashboard' }">
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg></i>
|
||||
<span>仪表盘</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="canAccessUsers" index="Users" :route="{ name: 'Users' }">
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 12c2.7 0 5-2.3 5-5s-2.3-5-5-5s-5 2.3-5 5s2.3 5 5 5m0 2c-3.3 0-10 1.7-10 5v3h20v-3c0-3.3-6.7-5-10-5Z"/></svg></i>
|
||||
<span>用户管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="canAccessGames" index="Games" :route="{ name: 'Games' }">
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M6 8h12v8H6z"/></svg></i>
|
||||
<span>游戏管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="canAccessOrders" index="Orders" :route="{ name: 'Orders' }">
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M3 6h18v2H3V6m0 5h18v2H3v-2m0 5h18v2H3v-2Z"/></svg></i>
|
||||
<span>订单管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="canAccessReports" index="Reports" :route="{ name: 'Reports' }">
|
||||
<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="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>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="ErrorTest" :route="{ name: 'ErrorTest' }">
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg></i>
|
||||
<span>错误测试</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="PermissionTest" :route="{ name: 'PermissionTest' }">
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm-2 16l-4-4 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg></i>
|
||||
<span>权限测试</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</aside>
|
||||
<section class="main">
|
||||
<header class="header">
|
||||
<el-button text @click="collapsed = !collapsed" class="collapse-btn">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24"><path fill="currentColor" d="M3 6h18v2H3V6m0 5h12v2H3v-2m0 5h18v2H3v-2Z"/></svg>
|
||||
</el-button>
|
||||
<div class="spacer" />
|
||||
<el-dropdown>
|
||||
<span class="el-dropdown-link">
|
||||
管理员<i class="el-icon el-icon--right"><svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M7 10l5 5 5-5z"/></svg></i>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="onProfile">个人中心</el-dropdown-item>
|
||||
<el-dropdown-item divided @click="onLogout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</header>
|
||||
<main class="content">
|
||||
<router-view />
|
||||
</main>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { clearTokens } from '@/utils/auth'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { canAccessRoute } from '@/utils/permission'
|
||||
|
||||
const collapsed = ref(false)
|
||||
const router = useRouter()
|
||||
|
||||
// 权限检查
|
||||
const canAccessUsers = computed(() => canAccessRoute('Users'))
|
||||
const canAccessGames = computed(() => canAccessRoute('Games'))
|
||||
const canAccessOrders = computed(() => canAccessRoute('Orders'))
|
||||
const canAccessReports = computed(() => canAccessRoute('Reports'))
|
||||
const canAccessSettings = computed(() => canAccessRoute('Settings'))
|
||||
|
||||
function onProfile() {
|
||||
// 可跳转到个人中心占位页
|
||||
}
|
||||
|
||||
function onLogout() {
|
||||
clearTokens()
|
||||
router.replace({ name: 'Login' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
}
|
||||
.sider {
|
||||
width: 220px;
|
||||
background: #001529;
|
||||
color: #bfcbd9;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.brand {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 16px;
|
||||
color: #fff;
|
||||
}
|
||||
.logo { width: 24px; height: 24px; }
|
||||
.menu { border-right: none; flex: 1; }
|
||||
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||||
.header {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.collapse-btn { margin-right: 8px; }
|
||||
.spacer { flex: 1; }
|
||||
.content { padding: 16px; overflow: auto; background: #f5f7fa; height: calc(100vh - 56px); }
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
|
||||
import router from '@/router'
|
||||
import App from './App.vue'
|
||||
import { setupPermissionDirective } from './directives/permission'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(ElementPlus)
|
||||
app.use(router)
|
||||
setupPermissionDirective(app)
|
||||
app.mount('#app')
|
||||
|
||||
|
||||
@@ -1,29 +1,97 @@
|
||||
import axios from 'axios'
|
||||
import { getAccessToken, getRefreshToken, getTokenType, setTokens, clearTokens } from '../utils/auth'
|
||||
import { showErrorMessage } from '@/utils/error'
|
||||
|
||||
const baseURL = import.meta.env?.VITE_API_BASE || '/'
|
||||
|
||||
const http = axios.create({
|
||||
baseURL: '/',
|
||||
baseURL,
|
||||
timeout: 15000,
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
// 专用刷新客户端,避免拦截器递归
|
||||
const refreshClient = axios.create({ baseURL, timeout: 15000 })
|
||||
|
||||
let isRefreshing = false
|
||||
let pendingQueue = []
|
||||
|
||||
function subscribeTokenRefresh(cb) {
|
||||
pendingQueue.push(cb)
|
||||
}
|
||||
|
||||
function onRefreshed(newToken, tokenType) {
|
||||
pendingQueue.forEach((cb) => cb(newToken, tokenType))
|
||||
pendingQueue = []
|
||||
}
|
||||
|
||||
// 请求拦截:附加 Authorization
|
||||
http.interceptors.request.use(
|
||||
(config) => {
|
||||
// 例如:在此添加认证 token
|
||||
// const token = localStorage.getItem('token')
|
||||
// if (token) config.headers.Authorization = `Bearer ${token}`
|
||||
const token = getAccessToken()
|
||||
const type = getTokenType()
|
||||
if (token) {
|
||||
config.headers = config.headers || {}
|
||||
config.headers.Authorization = `${type} ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
// 刷新 token 请求
|
||||
async function refreshTokenRequest() {
|
||||
const refreshToken = getRefreshToken()
|
||||
if (!refreshToken) throw new Error('NO_REFRESH_TOKEN')
|
||||
const { data } = await refreshClient.post('/auth/refresh', { refreshToken })
|
||||
// 兼容 { code, data }
|
||||
const payload = data?.code === 0 && data?.data ? data.data : data
|
||||
if (!payload?.accessToken) throw new Error('INVALID_REFRESH_RESPONSE')
|
||||
setTokens(payload)
|
||||
return payload
|
||||
}
|
||||
|
||||
// 响应拦截:处理 401 自动刷新
|
||||
http.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
// 统一错误处理(可以按需要自定义)
|
||||
async (error) => {
|
||||
const { config, response } = error || {}
|
||||
const status = response?.status
|
||||
const url = config?.url || ''
|
||||
|
||||
// 避免对登录/刷新自身进行重复刷新
|
||||
const isAuthPath = /\/auth\/(login|refresh)/.test(url || '')
|
||||
|
||||
if (status === 401 && !isAuthPath) {
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true
|
||||
try {
|
||||
const payload = await refreshTokenRequest()
|
||||
isRefreshing = false
|
||||
onRefreshed(payload.accessToken, payload.tokenType || 'Bearer')
|
||||
} catch (e) {
|
||||
isRefreshing = false
|
||||
clearTokens()
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 返回一个新的 Promise,等待刷新完成后重试原请求
|
||||
return new Promise((resolve) => {
|
||||
subscribeTokenRefresh((newToken, tokenType) => {
|
||||
const retryConfig = { ...config }
|
||||
retryConfig.headers = retryConfig.headers || {}
|
||||
retryConfig.headers.Authorization = `${tokenType || 'Bearer'} ${newToken}`
|
||||
resolve(http.request(retryConfig))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 对于非401错误,显示错误消息
|
||||
if (status !== 401) {
|
||||
showErrorMessage(error)
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default http
|
||||
|
||||
|
||||
57
src/router/index.js
Normal file
57
src/router/index.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { isLoggedIn } from '@/utils/auth'
|
||||
import { canAccessRoute } from '@/utils/permission'
|
||||
|
||||
const AdminLayout = () => import('@/layouts/AdminLayout.vue')
|
||||
const Login = () => import('@/views/Login.vue')
|
||||
|
||||
const Dashboard = () => import('@/views/Dashboard.vue')
|
||||
const UserList = () => import('@/views/users/UserList.vue')
|
||||
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 ErrorTest = () => import('@/views/ErrorTest.vue')
|
||||
const PermissionTest = () => import('@/views/PermissionTest.vue')
|
||||
const NotFound = () => import('@/views/NotFound.vue')
|
||||
|
||||
export const routes = [
|
||||
{ path: '/login', name: 'Login', component: Login, meta: { public: true, title: '登录' } },
|
||||
{
|
||||
path: '/',
|
||||
component: AdminLayout,
|
||||
children: [
|
||||
{ path: '', name: 'Dashboard', component: Dashboard, meta: { title: '仪表盘' } },
|
||||
{ path: 'users', name: 'Users', component: UserList, meta: { title: '用户管理' } },
|
||||
{ path: 'games', name: 'Games', component: GameList, meta: { title: '游戏管理' } },
|
||||
{ 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: 'error-test', name: 'ErrorTest', component: ErrorTest, meta: { title: '错误处理测试' } },
|
||||
{ path: 'permission-test', name: 'PermissionTest', component: PermissionTest, meta: { title: '权限测试' } },
|
||||
],
|
||||
},
|
||||
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound, meta: { public: true, title: '未找到' } },
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta?.public) return next()
|
||||
if (!isLoggedIn()) {
|
||||
return next({ name: 'Login', query: { redirect: to.fullPath } })
|
||||
}
|
||||
|
||||
// 检查路由权限
|
||||
if (to.name && !canAccessRoute(to.name)) {
|
||||
return next({ name: 'Dashboard' }) // 无权限时跳转到仪表盘
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
67
src/utils/auth.js
Normal file
67
src/utils/auth.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const STORAGE_KEY = 'app.auth'
|
||||
|
||||
export function getAuth() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) return null
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function setTokens(payload) {
|
||||
// payload: { accessToken, refreshToken, tokenType, expiresIn, userType, userId, username, ... }
|
||||
const now = Date.now()
|
||||
const expiresAt = payload?.expiresIn ? now + payload.expiresIn * 1000 : undefined
|
||||
|
||||
// 构建用户信息对象,兼容两种格式:
|
||||
// 1. 嵌套格式: { user: { userType, userId, username } }
|
||||
// 2. 平铺格式: { userType, userId, username }
|
||||
const user = payload?.user || {
|
||||
userType: payload?.userType,
|
||||
userId: payload?.userId,
|
||||
username: payload?.username,
|
||||
}
|
||||
|
||||
const data = {
|
||||
accessToken: payload?.accessToken || '',
|
||||
refreshToken: payload?.refreshToken || '',
|
||||
tokenType: payload?.tokenType || 'Bearer',
|
||||
user,
|
||||
expiresAt,
|
||||
}
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data))
|
||||
return data
|
||||
}
|
||||
|
||||
export function clearTokens() {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
}
|
||||
|
||||
export function getAccessToken() {
|
||||
const a = getAuth()
|
||||
return a?.accessToken || ''
|
||||
}
|
||||
|
||||
export function getRefreshToken() {
|
||||
const a = getAuth()
|
||||
return a?.refreshToken || ''
|
||||
}
|
||||
|
||||
export function getTokenType() {
|
||||
const a = getAuth()
|
||||
return a?.tokenType || 'Bearer'
|
||||
}
|
||||
|
||||
export function isTokenExpired() {
|
||||
const a = getAuth()
|
||||
if (!a?.expiresAt) return false
|
||||
return Date.now() >= a.expiresAt
|
||||
}
|
||||
|
||||
export function isLoggedIn() {
|
||||
const a = getAuth()
|
||||
return Boolean(a?.accessToken)
|
||||
}
|
||||
|
||||
83
src/utils/error.js
Normal file
83
src/utils/error.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
/**
|
||||
* 从错误对象中提取错误消息
|
||||
* @param {Error} error - 错误对象
|
||||
* @param {string} defaultMessage - 默认错误消息
|
||||
* @returns {string} 错误消息
|
||||
*/
|
||||
export function extractErrorMessage(error, defaultMessage = '操作失败') {
|
||||
if (!error) return defaultMessage
|
||||
|
||||
// 优先使用后端返回的错误消息
|
||||
if (error.response?.data?.message) {
|
||||
return error.response.data.message
|
||||
}
|
||||
|
||||
// 处理字段验证错误
|
||||
if (error.response?.data?.details && Array.isArray(error.response.data.details)) {
|
||||
const fieldErrors = error.response.data.details
|
||||
.map(detail => `${detail.field}: ${detail.message}`)
|
||||
.join('; ')
|
||||
return fieldErrors || defaultMessage
|
||||
}
|
||||
|
||||
// 使用错误对象的消息
|
||||
if (error.message) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
// 根据HTTP状态码提供默认消息
|
||||
const status = error.response?.status
|
||||
if (status) {
|
||||
const statusMessages = {
|
||||
400: '请求参数错误',
|
||||
401: '未授权,请重新登录',
|
||||
403: '权限不足',
|
||||
404: '资源不存在',
|
||||
409: '数据冲突',
|
||||
422: '数据验证失败',
|
||||
500: '服务器内部错误',
|
||||
502: '网关错误',
|
||||
503: '服务不可用',
|
||||
504: '网关超时'
|
||||
}
|
||||
return statusMessages[status] || defaultMessage
|
||||
}
|
||||
|
||||
return defaultMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误消息
|
||||
* @param {Error} error - 错误对象
|
||||
* @param {string} defaultMessage - 默认错误消息
|
||||
*/
|
||||
export function showErrorMessage(error, defaultMessage = '操作失败') {
|
||||
const message = extractErrorMessage(error, defaultMessage)
|
||||
ElMessage.error(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示成功消息
|
||||
* @param {string} message - 成功消息
|
||||
*/
|
||||
export function showSuccessMessage(message = '操作成功') {
|
||||
ElMessage.success(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示警告消息
|
||||
* @param {string} message - 警告消息
|
||||
*/
|
||||
export function showWarningMessage(message = '警告') {
|
||||
ElMessage.warning(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示信息消息
|
||||
* @param {string} message - 信息消息
|
||||
*/
|
||||
export function showInfoMessage(message = '提示') {
|
||||
ElMessage.info(message)
|
||||
}
|
||||
148
src/utils/permission.js
Normal file
148
src/utils/permission.js
Normal file
@@ -0,0 +1,148 @@
|
||||
import { getAuth } from './auth'
|
||||
|
||||
// 权限定义
|
||||
export const PERMISSIONS = {
|
||||
// 用户管理权限
|
||||
USER_MANAGE: 'user:manage',
|
||||
USER_CREATE: 'user:create',
|
||||
USER_UPDATE: 'user:update',
|
||||
USER_DELETE: 'user:delete',
|
||||
USER_VIEW: 'user:view',
|
||||
|
||||
// 游戏管理权限
|
||||
GAME_MANAGE: 'game:manage',
|
||||
GAME_CREATE: 'game:create',
|
||||
GAME_UPDATE: 'game:update',
|
||||
GAME_DELETE: 'game:delete',
|
||||
GAME_VIEW: 'game:view',
|
||||
|
||||
// 订单管理权限
|
||||
ORDER_MANAGE: 'order:manage',
|
||||
ORDER_VIEW: 'order:view',
|
||||
|
||||
// 报表分析权限
|
||||
REPORT_VIEW: 'report:view',
|
||||
|
||||
// 系统设置权限
|
||||
SETTING_MANAGE: 'setting:manage',
|
||||
}
|
||||
|
||||
// 角色权限映射
|
||||
export const ROLE_PERMISSIONS = {
|
||||
ADMIN: [
|
||||
// 管理员拥有所有权限
|
||||
PERMISSIONS.USER_MANAGE,
|
||||
PERMISSIONS.USER_CREATE,
|
||||
PERMISSIONS.USER_UPDATE,
|
||||
PERMISSIONS.USER_DELETE,
|
||||
PERMISSIONS.USER_VIEW,
|
||||
PERMISSIONS.GAME_MANAGE,
|
||||
PERMISSIONS.GAME_CREATE,
|
||||
PERMISSIONS.GAME_UPDATE,
|
||||
PERMISSIONS.GAME_DELETE,
|
||||
PERMISSIONS.GAME_VIEW,
|
||||
PERMISSIONS.ORDER_MANAGE,
|
||||
PERMISSIONS.ORDER_VIEW,
|
||||
PERMISSIONS.REPORT_VIEW,
|
||||
PERMISSIONS.SETTING_MANAGE,
|
||||
],
|
||||
AGENT: [
|
||||
// 代理商只有查看权限,没有管理权限
|
||||
PERMISSIONS.GAME_VIEW,
|
||||
PERMISSIONS.ORDER_VIEW,
|
||||
PERMISSIONS.REPORT_VIEW,
|
||||
]
|
||||
}
|
||||
|
||||
// 路由权限映射
|
||||
export const ROUTE_PERMISSIONS = {
|
||||
'Dashboard': [], // 仪表盘所有用户都可以访问
|
||||
'Users': [PERMISSIONS.USER_VIEW],
|
||||
'Games': [PERMISSIONS.GAME_VIEW],
|
||||
'Orders': [PERMISSIONS.ORDER_VIEW],
|
||||
'Reports': [PERMISSIONS.REPORT_VIEW],
|
||||
'Settings': [PERMISSIONS.SETTING_MANAGE],
|
||||
'ErrorTest': [], // 错误测试页面所有用户都可以访问
|
||||
'PermissionTest': [], // 权限测试页面所有用户都可以访问
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
export function getCurrentUser() {
|
||||
const auth = getAuth()
|
||||
const user = auth?.user || null
|
||||
return user
|
||||
}
|
||||
|
||||
// 获取当前用户类型
|
||||
export function getCurrentUserType() {
|
||||
const user = getCurrentUser()
|
||||
const userType = user?.userType || null
|
||||
return userType
|
||||
}
|
||||
|
||||
// 检查用户是否有指定权限
|
||||
export function hasPermission(permission) {
|
||||
const userType = getCurrentUserType()
|
||||
if (!userType) return false
|
||||
|
||||
const userPermissions = ROLE_PERMISSIONS[userType?.toUpperCase()] || []
|
||||
return userPermissions.includes(permission)
|
||||
}
|
||||
|
||||
// 检查用户是否有指定权限列表中的任意一个
|
||||
export function hasAnyPermission(permissions) {
|
||||
return permissions.some(permission => hasPermission(permission))
|
||||
}
|
||||
|
||||
// 检查用户是否有指定权限列表中的所有权限
|
||||
export function hasAllPermissions(permissions) {
|
||||
return permissions.every(permission => hasPermission(permission))
|
||||
}
|
||||
|
||||
// 检查用户是否可以访问指定路由
|
||||
export function canAccessRoute(routeName) {
|
||||
// 管理员可以访问所有路由
|
||||
if (isAdmin()) return true
|
||||
|
||||
const requiredPermissions = ROUTE_PERMISSIONS[routeName] || []
|
||||
if (requiredPermissions.length === 0) return true // 没有权限要求,默认允许访问
|
||||
|
||||
return hasAnyPermission(requiredPermissions)
|
||||
}
|
||||
|
||||
// 检查用户是否为管理员
|
||||
export function isAdmin() {
|
||||
const userType = getCurrentUserType()
|
||||
const isAdminUser = userType?.toLowerCase() === 'admin'
|
||||
return isAdminUser
|
||||
}
|
||||
|
||||
// 检查用户是否为代理商
|
||||
export function isAgent() {
|
||||
return getCurrentUserType()?.toLowerCase() === 'agent'
|
||||
}
|
||||
|
||||
// 获取用户可访问的路由列表
|
||||
export function getAccessibleRoutes() {
|
||||
const userType = getCurrentUserType()
|
||||
|
||||
if (!userType) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 管理员可以访问所有路由
|
||||
if (isAdmin()) {
|
||||
return ['Dashboard', 'Users', 'Games', 'Orders', 'Reports', 'Settings', 'ErrorTest', 'PermissionTest']
|
||||
}
|
||||
|
||||
const userPermissions = ROLE_PERMISSIONS[userType?.toUpperCase()] || []
|
||||
const accessibleRoutes = []
|
||||
|
||||
Object.entries(ROUTE_PERMISSIONS).forEach(([routeName, requiredPermissions]) => {
|
||||
if (requiredPermissions.length === 0 || hasAnyPermission(requiredPermissions)) {
|
||||
accessibleRoutes.push(routeName)
|
||||
}
|
||||
})
|
||||
|
||||
return accessibleRoutes
|
||||
}
|
||||
10
src/views/Dashboard.vue
Normal file
10
src/views/Dashboard.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<el-card shadow="hover">
|
||||
<template #header>仪表盘</template>
|
||||
<p>这里是仪表盘空页面,你可以放置卡片、统计与图表。</p>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
180
src/views/ErrorTest.vue
Normal file
180
src/views/ErrorTest.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div class="error-test-page">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<h3>错误处理测试</h3>
|
||||
</template>
|
||||
|
||||
<el-space direction="vertical" size="large" style="width: 100%">
|
||||
<el-alert
|
||||
title="错误处理功能说明"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<p>1. 全局HTTP拦截器会自动显示错误消息</p>
|
||||
<p>2. 支持显示后端返回的具体错误信息</p>
|
||||
<p>3. 支持字段级验证错误显示</p>
|
||||
<p>4. 根据HTTP状态码提供友好提示</p>
|
||||
</el-alert>
|
||||
|
||||
<el-divider>测试不同类型的错误</el-divider>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-button type="danger" @click="test400Error">测试 400 错误</el-button>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-button type="danger" @click="test401Error">测试 401 错误</el-button>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-button type="danger" @click="test403Error">测试 403 错误</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-button type="danger" @click="test404Error">测试 404 错误</el-button>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-button type="danger" @click="test409Error">测试 409 错误</el-button>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-button type="danger" @click="test500Error">测试 500 错误</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-button type="warning" @click="testValidationError">测试字段验证错误</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-button type="warning" @click="testNetworkError">测试网络错误</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider>手动显示消息</el-divider>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-button type="success" @click="showSuccess">成功消息</el-button>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-button type="warning" @click="showWarning">警告消息</el-button>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-button type="info" @click="showInfo">信息消息</el-button>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-button type="danger" @click="showError">错误消息</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-space>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { showErrorMessage, showSuccessMessage, showWarningMessage, showInfoMessage } from '@/utils/error'
|
||||
import http from '@/plugins/http'
|
||||
|
||||
// 测试不同类型的HTTP错误
|
||||
async function test400Error() {
|
||||
try {
|
||||
await http.get('/api/test/400')
|
||||
} catch (error) {
|
||||
// 错误会被全局拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
async function test401Error() {
|
||||
try {
|
||||
await http.get('/api/test/401')
|
||||
} catch (error) {
|
||||
// 401错误不会被全局拦截器显示,需要手动处理
|
||||
showErrorMessage(error, '认证失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function test403Error() {
|
||||
try {
|
||||
await http.get('/api/test/403')
|
||||
} catch (error) {
|
||||
// 错误会被全局拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
async function test404Error() {
|
||||
try {
|
||||
await http.get('/api/test/404')
|
||||
} catch (error) {
|
||||
// 错误会被全局拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
async function test409Error() {
|
||||
try {
|
||||
await http.get('/api/test/409')
|
||||
} catch (error) {
|
||||
// 错误会被全局拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
async function test500Error() {
|
||||
try {
|
||||
await http.get('/api/test/500')
|
||||
} catch (error) {
|
||||
// 错误会被全局拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
// 测试字段验证错误
|
||||
async function testValidationError() {
|
||||
try {
|
||||
await http.post('/api/test/validation', {
|
||||
username: 'a', // 太短
|
||||
password: '123' // 太短
|
||||
})
|
||||
} catch (error) {
|
||||
// 错误会被全局拦截器处理,显示字段级错误
|
||||
}
|
||||
}
|
||||
|
||||
// 测试网络错误
|
||||
async function testNetworkError() {
|
||||
try {
|
||||
await http.get('http://invalid-url-that-does-not-exist.com/api/test')
|
||||
} catch (error) {
|
||||
// 错误会被全局拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
// 手动显示消息
|
||||
function showSuccess() {
|
||||
showSuccessMessage('这是一个成功消息')
|
||||
}
|
||||
|
||||
function showWarning() {
|
||||
showWarningMessage('这是一个警告消息')
|
||||
}
|
||||
|
||||
function showInfo() {
|
||||
showInfoMessage('这是一个信息消息')
|
||||
}
|
||||
|
||||
function showError() {
|
||||
showErrorMessage(new Error('这是一个手动触发的错误消息'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.error-test-page {
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.el-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -42,6 +42,9 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { login } from '../api/auth'
|
||||
import { setTokens } from '../utils/auth'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { showErrorMessage, showSuccessMessage } from '@/utils/error'
|
||||
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
@@ -49,8 +52,8 @@ const remember = ref(false)
|
||||
const notice = ref('')
|
||||
|
||||
const form = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
username: 'admin',
|
||||
password: 'admin1',
|
||||
})
|
||||
|
||||
const rules = {
|
||||
@@ -88,6 +91,9 @@ function persistRemember() {
|
||||
}
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
async function onSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
@@ -96,14 +102,23 @@ async function onSubmit() {
|
||||
try {
|
||||
const payload = { username: form.value.username, password: form.value.password }
|
||||
const res = await login(payload)
|
||||
// 依据后端返回结构处理,这里仅做示例提示
|
||||
ElMessage.success('登录成功')
|
||||
|
||||
// 兼容两种返回结构:纯数据 或 { code, data }
|
||||
let data = res?.data
|
||||
if (data && typeof data === 'object' && 'code' in data) {
|
||||
data = data.code === 0 ? data.data : null
|
||||
}
|
||||
|
||||
if (data?.accessToken) {
|
||||
setTokens(data)
|
||||
}
|
||||
showSuccessMessage('登录成功')
|
||||
persistRemember()
|
||||
console.debug('login response:', res.data)
|
||||
// TODO: 登录成功后的跳转(如接入 Router)
|
||||
const redirect = route.query.redirect || '/'
|
||||
router.replace(String(redirect))
|
||||
} catch (e) {
|
||||
const msg = e?.response?.data?.message || e.message || '登录失败'
|
||||
ElMessage.error(msg)
|
||||
showErrorMessage(e, '登录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -138,4 +153,3 @@ async function onSubmit() {
|
||||
.submit { width: 100%; }
|
||||
.notice { margin-top: 8px; }
|
||||
</style>
|
||||
|
||||
|
||||
9
src/views/NotFound.vue
Normal file
9
src/views/NotFound.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div style="padding: 40px; text-align: center;">
|
||||
<el-empty description="页面不存在" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
167
src/views/PermissionTest.vue
Normal file
167
src/views/PermissionTest.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div class="permission-test">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<h3>权限测试页面</h3>
|
||||
</template>
|
||||
|
||||
<el-descriptions title="当前用户信息" :column="2" border>
|
||||
<el-descriptions-item label="用户类型">
|
||||
<el-tag :type="userType === 'ADMIN' ? 'danger' : 'success'">
|
||||
{{ getUserTypeDisplayName(userType) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="用户名">
|
||||
{{ currentUser?.username || '未知' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4>权限检查结果</h4>
|
||||
<el-table :data="permissionResults" border>
|
||||
<el-table-column prop="permission" label="权限" width="200" />
|
||||
<el-table-column prop="description" label="描述" />
|
||||
<el-table-column prop="result" label="结果" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.result ? 'success' : 'danger'">
|
||||
{{ row.result ? '有权限' : '无权限' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4>功能测试</h4>
|
||||
<el-space wrap>
|
||||
<el-button v-permission="'user:view'" type="primary">
|
||||
查看用户列表
|
||||
</el-button>
|
||||
<el-button v-permission="'user:create'" type="success">
|
||||
创建用户
|
||||
</el-button>
|
||||
<el-button v-permission="'user:update'" type="warning">
|
||||
编辑用户
|
||||
</el-button>
|
||||
<el-button v-permission="'user:delete'" type="danger">
|
||||
删除用户
|
||||
</el-button>
|
||||
<el-button v-permission="'game:view'" type="info">
|
||||
查看游戏
|
||||
</el-button>
|
||||
<el-button v-permission="'setting:manage'" type="primary">
|
||||
系统设置
|
||||
</el-button>
|
||||
</el-space>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4>路由访问测试</h4>
|
||||
<el-space wrap>
|
||||
<el-button
|
||||
v-for="route in testRoutes"
|
||||
:key="route.name"
|
||||
:type="route.canAccess ? 'primary' : 'info'"
|
||||
:disabled="!route.canAccess"
|
||||
@click="testRouteAccess(route)"
|
||||
>
|
||||
{{ route.title }}
|
||||
</el-button>
|
||||
</el-space>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4>权限指令测试</h4>
|
||||
<el-space wrap>
|
||||
<el-button v-permission="'user:create'" type="success">
|
||||
单个权限测试
|
||||
</el-button>
|
||||
<el-button v-permission="['user:create', 'user:update']" type="warning">
|
||||
多权限OR测试
|
||||
</el-button>
|
||||
<el-button v-permission="['user:create', 'user:update', 'AND']" type="danger">
|
||||
多权限AND测试
|
||||
</el-button>
|
||||
<el-button v-permission="{ permission: ['user:create', 'user:update'], logic: 'OR' }" type="info">
|
||||
对象格式测试
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
getCurrentUser,
|
||||
getCurrentUserType,
|
||||
hasPermission,
|
||||
canAccessRoute,
|
||||
PERMISSIONS
|
||||
} from '@/utils/permission'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const currentUser = ref(null)
|
||||
const userType = ref(null)
|
||||
|
||||
// 用户类型显示名称映射
|
||||
const userTypeDisplayMap = {
|
||||
'ADMIN': '管理员',
|
||||
'AGENT': '代理商'
|
||||
}
|
||||
|
||||
function getUserTypeDisplayName(userType) {
|
||||
return userTypeDisplayMap[userType] || userType
|
||||
}
|
||||
|
||||
// 权限检查结果
|
||||
const permissionResults = computed(() => [
|
||||
{ permission: 'user:view', description: '查看用户列表', result: hasPermission(PERMISSIONS.USER_VIEW) },
|
||||
{ permission: 'user:create', description: '创建用户', result: hasPermission(PERMISSIONS.USER_CREATE) },
|
||||
{ permission: 'user:update', description: '编辑用户', result: hasPermission(PERMISSIONS.USER_UPDATE) },
|
||||
{ permission: 'user:delete', description: '删除用户', result: hasPermission(PERMISSIONS.USER_DELETE) },
|
||||
{ permission: 'game:view', description: '查看游戏', result: hasPermission(PERMISSIONS.GAME_VIEW) },
|
||||
{ permission: 'game:create', description: '创建游戏', result: hasPermission(PERMISSIONS.GAME_CREATE) },
|
||||
{ permission: 'order:view', description: '查看订单', result: hasPermission(PERMISSIONS.ORDER_VIEW) },
|
||||
{ permission: 'report:view', description: '查看报表', result: hasPermission(PERMISSIONS.REPORT_VIEW) },
|
||||
{ permission: 'setting:manage', description: '系统设置', result: hasPermission(PERMISSIONS.SETTING_MANAGE) },
|
||||
])
|
||||
|
||||
// 路由访问测试
|
||||
const testRoutes = computed(() => [
|
||||
{ name: 'Users', title: '用户管理', canAccess: canAccessRoute('Users') },
|
||||
{ name: 'Games', title: '游戏管理', canAccess: canAccessRoute('Games') },
|
||||
{ name: 'Orders', title: '订单管理', canAccess: canAccessRoute('Orders') },
|
||||
{ name: 'Reports', title: '报表分析', canAccess: canAccessRoute('Reports') },
|
||||
{ name: 'Settings', title: '系统设置', canAccess: canAccessRoute('Settings') },
|
||||
])
|
||||
|
||||
function testRouteAccess(route) {
|
||||
if (route.canAccess) {
|
||||
router.push({ name: route.name })
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
currentUser.value = getCurrentUser()
|
||||
userType.value = getCurrentUserType()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.permission-test {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.el-divider {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 16px 0 12px 0;
|
||||
color: #303133;
|
||||
}
|
||||
</style>
|
||||
10
src/views/games/GameList.vue
Normal file
10
src/views/games/GameList.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<el-card shadow="hover">
|
||||
<template #header>游戏管理</template>
|
||||
<p>游戏列表空页面(表格、搜索、上下架)。</p>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
10
src/views/orders/OrderList.vue
Normal file
10
src/views/orders/OrderList.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<el-card shadow="hover">
|
||||
<template #header>订单管理</template>
|
||||
<p>订单管理空页面(订单列表、状态筛选、导出)。</p>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
10
src/views/reports/ReportAnalysis.vue
Normal file
10
src/views/reports/ReportAnalysis.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<el-card shadow="hover">
|
||||
<template #header>报表分析</template>
|
||||
<p>报表分析空页面(趋势图、维度筛选、下载)。</p>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
10
src/views/settings/Settings.vue
Normal file
10
src/views/settings/Settings.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<el-card shadow="hover">
|
||||
<template #header>系统设置</template>
|
||||
<p>系统设置空页面(参数配置、权限、主题)。</p>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
355
src/views/users/UserList.vue
Normal file
355
src/views/users/UserList.vue
Normal file
@@ -0,0 +1,355 @@
|
||||
<template>
|
||||
<div class="users-page">
|
||||
<!-- 权限检查 -->
|
||||
<div v-if="!canViewUsers" class="permission-denied">
|
||||
<el-result
|
||||
icon="warning"
|
||||
title="权限不足"
|
||||
sub-title="您没有访问用户管理的权限,请联系管理员"
|
||||
>
|
||||
<template #extra>
|
||||
<el-button type="primary" @click="$router.push({ name: 'Dashboard' })">返回首页</el-button>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
|
||||
<!-- 用户管理内容 -->
|
||||
<div v-else>
|
||||
<el-card class="mb16" shadow="never">
|
||||
<el-form :inline="true" :model="query" @submit.prevent>
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model.trim="query.keyword" placeholder="用户名" clearable style="width: 220px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户类型">
|
||||
<el-select v-model="query.userType" clearable placeholder="全部" style="width: 140px">
|
||||
<el-option value="ADMIN" label="管理员" />
|
||||
<el-option value="AGENT" label="代理" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="query.status" clearable placeholder="全部" style="width: 140px">
|
||||
<el-option value="ENABLED" label="启用" />
|
||||
<el-option value="DISABLED" label="禁用" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" @click="onSearch">查询</el-button>
|
||||
<el-button @click="onReset">重置</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="canCreateUser">
|
||||
<el-button type="success" @click="openCreate">新增用户</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<el-table :data="list" v-loading="loading" border stripe>
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
<el-table-column prop="username" label="用户名" min-width="140" />
|
||||
<el-table-column label="用户类型" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.userType === 'ADMIN' ? 'danger' : 'success'">
|
||||
{{ getUserTypeDisplayName(row.userType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="pointsBalance" label="积分余额" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.userType === 'AGENT'">{{ row.pointsBalance || 0 }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch :model-value="row.status === 'ENABLED'" @change="(v)=>onToggle(row, v)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" min-width="180" />
|
||||
<el-table-column label="操作" width="260" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="canEditUser" size="small" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button v-if="canResetPassword" size="small" type="warning" @click="onResetPwd(row)">重置密码</el-button>
|
||||
<el-button v-if="canDeleteUser" size="small" type="danger" @click="onRemove(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pager">
|
||||
<el-pagination
|
||||
background
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
:page-size="query.pageSize"
|
||||
:current-page="query.page"
|
||||
:page-sizes="[10,20,50,100]"
|
||||
@size-change="(s)=>{ query.pageSize=s; query.page=1; load(); }"
|
||||
@current-change="(p)=>{ query.page=p; load(); }"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="visible" :title="isEdit ? '编辑用户' : '新增用户'" width="520px">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="88px">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model.trim="form.username" :disabled="isEdit" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户类型" prop="userType">
|
||||
<el-select v-model="form.userType" placeholder="选择用户类型">
|
||||
<el-option v-for="ut in userTypeOptions" :key="ut.value" :label="ut.label" :value="ut.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="积分余额" prop="pointsBalance" v-if="form.userType === 'AGENT'">
|
||||
<el-input-number v-model="form.pointsBalance" :min="0" :max="999999" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!isEdit" label="初始密码" prop="password">
|
||||
<el-input v-model.trim="form.password" type="password" show-password placeholder="至少6位" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="enabled">
|
||||
<el-switch v-model="form.enabled" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible=false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="onSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, watch, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { fetchUsers, createUser, updateUser, deleteUser, setUserStatus, resetUserPassword } from '@/api/users'
|
||||
import { showErrorMessage, showSuccessMessage } from '@/utils/error'
|
||||
import { hasPermission, PERMISSIONS } from '@/utils/permission'
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
|
||||
const query = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
keyword: '',
|
||||
userType: undefined,
|
||||
status: undefined,
|
||||
})
|
||||
|
||||
// 用户类型选项
|
||||
const userTypeOptions = ref([
|
||||
{ value: 'ADMIN', label: '管理员' },
|
||||
{ value: 'AGENT', label: '代理商' }
|
||||
])
|
||||
|
||||
// 用户类型显示名称映射
|
||||
const userTypeDisplayMap = {
|
||||
'ADMIN': '管理员',
|
||||
'AGENT': '代理商'
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
const canViewUsers = computed(() => hasPermission(PERMISSIONS.USER_VIEW))
|
||||
const canCreateUser = computed(() => hasPermission(PERMISSIONS.USER_CREATE))
|
||||
const canEditUser = computed(() => hasPermission(PERMISSIONS.USER_UPDATE))
|
||||
const canDeleteUser = computed(() => hasPermission(PERMISSIONS.USER_DELETE))
|
||||
const canResetPassword = computed(() => hasPermission(PERMISSIONS.USER_UPDATE))
|
||||
|
||||
function getUserTypeDisplayName(userType) {
|
||||
return userTypeDisplayMap[userType] || userType
|
||||
}
|
||||
|
||||
function unwrap(res) {
|
||||
const d = res?.data
|
||||
if (d && typeof d === 'object' && 'code' in d) return d.code === 0 ? d.data : d
|
||||
return d
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
// 根据OpenAPI文档,参数映射:pageSize -> size
|
||||
const params = {
|
||||
...query,
|
||||
size: query.pageSize
|
||||
}
|
||||
delete params.pageSize
|
||||
|
||||
const res = await fetchUsers(params)
|
||||
const data = unwrap(res) || {}
|
||||
// 兼容后端返回两种分页结构
|
||||
list.value = data.list || data.items || []
|
||||
total.value = data.total || data.totalCount || list.value.length
|
||||
} catch (e) {
|
||||
showErrorMessage(e, '加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
query.page = 1
|
||||
load()
|
||||
}
|
||||
|
||||
function onReset() {
|
||||
query.keyword = ''
|
||||
query.userType = undefined
|
||||
query.status = undefined
|
||||
query.page = 1
|
||||
query.pageSize = 10
|
||||
load()
|
||||
}
|
||||
|
||||
async function onToggle(row, val) {
|
||||
try {
|
||||
await setUserStatus(row.id, val)
|
||||
// 更新 status 字段,保持与后端一致
|
||||
row.status = val ? 'ENABLED' : 'DISABLED'
|
||||
showSuccessMessage('状态已更新')
|
||||
} catch (e) {
|
||||
showErrorMessage(e, '更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function onRemove(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除用户「${row.username}」吗?`, '提示', { type: 'warning' })
|
||||
await deleteUser(row.id)
|
||||
showSuccessMessage('删除成功')
|
||||
load()
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') showErrorMessage(e, '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function onResetPwd(row) {
|
||||
try {
|
||||
const { value: newPassword } = await ElMessageBox.prompt(
|
||||
`请输入用户「${row.username}」的新密码`,
|
||||
'重置密码',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputType: 'password',
|
||||
inputValidator: (value) => {
|
||||
if (!value || value.length < 6) {
|
||||
return '密码长度至少6位'
|
||||
}
|
||||
return true
|
||||
},
|
||||
inputErrorMessage: '请输入有效的密码'
|
||||
}
|
||||
)
|
||||
|
||||
await resetUserPassword(row.id, newPassword)
|
||||
showSuccessMessage('密码重置成功')
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') showErrorMessage(e, '重置失败')
|
||||
}
|
||||
}
|
||||
|
||||
const visible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const saving = ref(false)
|
||||
const formRef = ref()
|
||||
const form = reactive({
|
||||
id: undefined,
|
||||
userType: 'AGENT',
|
||||
username: '',
|
||||
enabled: true,
|
||||
password: '',
|
||||
pointsBalance: 0
|
||||
})
|
||||
|
||||
const rules = {
|
||||
userType: [{ required: true, message: '请选择用户类型', trigger: 'change' }],
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
password: [
|
||||
{ required: () => !isEdit.value, message: '请输入初始密码', trigger: 'blur' },
|
||||
{ min: 6, message: '至少 6 位', trigger: 'blur' },
|
||||
],
|
||||
pointsBalance: [{
|
||||
required: () => form.userType === 'AGENT',
|
||||
message: '请输入积分余额',
|
||||
trigger: 'blur'
|
||||
}],
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
isEdit.value = false
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
userType: 'AGENT',
|
||||
username: '',
|
||||
enabled: true,
|
||||
password: '',
|
||||
pointsBalance: 0
|
||||
})
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
isEdit.value = true
|
||||
Object.assign(form, {
|
||||
...row,
|
||||
password: '',
|
||||
pointsBalance: row.pointsBalance || 0,
|
||||
// 将后端的 status 字段映射为前端的 enabled 字段
|
||||
enabled: row.status === 'ENABLED'
|
||||
})
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
function doSubmit() {
|
||||
// 根据接口文档构建请求参数
|
||||
const payload = {
|
||||
userType: form.userType,
|
||||
username: form.username,
|
||||
status: form.enabled ? 'ENABLED' : 'DISABLED',
|
||||
pointsBalance: form.pointsBalance || 0
|
||||
}
|
||||
|
||||
// 如果是新增用户,需要设置密码
|
||||
if (!isEdit.value) {
|
||||
payload.password = form.password
|
||||
}
|
||||
|
||||
return isEdit.value
|
||||
? updateUser(form.id, payload)
|
||||
: createUser(payload)
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
saving.value = true
|
||||
try {
|
||||
await doSubmit()
|
||||
showSuccessMessage('保存成功')
|
||||
visible.value = false
|
||||
load()
|
||||
} catch (e) {
|
||||
showErrorMessage(e, '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 监听用户类型变化,重置积分余额字段
|
||||
watch(() => form.userType, (newType) => {
|
||||
if (newType === 'ADMIN') {
|
||||
form.pointsBalance = 0
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb16 { margin-bottom: 16px; }
|
||||
.mr4 { margin-right: 4px; }
|
||||
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
</style>
|
||||
@@ -1,8 +1,20 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'node:path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: { '@': path.resolve(__dirname, 'src') },
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:18080',
|
||||
changeOrigin: true,
|
||||
rewrite: (p) => p.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user