diff --git a/.env b/.env new file mode 100644 index 0000000..1f30c73 --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +# 默认环境变量(如未提供具体 .env.* 时使用) +# 开发环境已通过 Vite 代理将 /api 转发到 http://localhost:18080 +# 生产环境请根据部署地址覆盖为真实后端地址 + +VITE_API_BASE=/api + diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..0a0c303 --- /dev/null +++ b/.env.development @@ -0,0 +1 @@ +VITE_API_BASE=/api diff --git a/docs/接口文档.md b/docs/接口文档.md new file mode 100644 index 0000000..69b5b77 --- /dev/null +++ b/docs/接口文档.md @@ -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状态码提供友好的错误提示 +- **降级处理**: 当无法获取具体错误时,显示默认错误消息 diff --git a/docs/权限管理.md b/docs/权限管理.md new file mode 100644 index 0000000..91ff703 --- /dev/null +++ b/docs/权限管理.md @@ -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 + +新增用户 + + +操作 + + +操作 + + +操作 +``` + +### 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. 检查权限变更后的效果 diff --git a/docs/权限系统配置总结.md b/docs/权限系统配置总结.md new file mode 100644 index 0000000..72d555c --- /dev/null +++ b/docs/权限系统配置总结.md @@ -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 + +新增用户 + + +操作 + + +操作 +``` + +## 权限配置详情 + +### 权限定义 +```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. 测试权限变更后的效果 + +## 总结 + +当前的权限系统配置确保了: +- **管理员拥有所有权限**,可以访问所有页面和功能 +- **代理商只有查看权限**,无法进行管理操作 +- **权限检查机制完善**,包括路由、菜单、页面、操作四个层级 +- **用户体验友好**,无权限时提供清晰的提示和跳转 +- **安全性保障**,前后端都有权限验证机制 diff --git a/package-lock.json b/package-lock.json index b4b1b59..3dad8cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" + } } } } diff --git a/package.json b/package.json index ebcabd4..2fa24c4 100644 --- a/package.json +++ b/package.json @@ -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" } } - diff --git a/src/App.vue b/src/App.vue index 16fbbbf..82cde9c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,11 +1,10 @@ diff --git a/src/api/auth.js b/src/api/auth.js index 5d2d782..169cb69 100644 --- a/src/api/auth.js +++ b/src/api/auth.js @@ -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) } - diff --git a/src/api/users.js b/src/api/users.js new file mode 100644 index 0000000..23de8ad --- /dev/null +++ b/src/api/users.js @@ -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 + }) +} + diff --git a/src/components/PermissionDenied.vue b/src/components/PermissionDenied.vue new file mode 100644 index 0000000..f50427f --- /dev/null +++ b/src/components/PermissionDenied.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/src/directives/permission.js b/src/directives/permission.js new file mode 100644 index 0000000..ccc027d --- /dev/null +++ b/src/directives/permission.js @@ -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) +} diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue new file mode 100644 index 0000000..c03f388 --- /dev/null +++ b/src/layouts/AdminLayout.vue @@ -0,0 +1,138 @@ + + + + + + diff --git a/src/main.js b/src/main.js index 2c644ea..58010ac 100644 --- a/src/main.js +++ b/src/main.js @@ -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') - diff --git a/src/plugins/http.js b/src/plugins/http.js index 5d77d52..705d358 100644 --- a/src/plugins/http.js +++ b/src/plugins/http.js @@ -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 - diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..fe336eb --- /dev/null +++ b/src/router/index.js @@ -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 + diff --git a/src/utils/auth.js b/src/utils/auth.js new file mode 100644 index 0000000..17c69a7 --- /dev/null +++ b/src/utils/auth.js @@ -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) +} + diff --git a/src/utils/error.js b/src/utils/error.js new file mode 100644 index 0000000..ab265dd --- /dev/null +++ b/src/utils/error.js @@ -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) +} diff --git a/src/utils/permission.js b/src/utils/permission.js new file mode 100644 index 0000000..397421e --- /dev/null +++ b/src/utils/permission.js @@ -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 +} diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue new file mode 100644 index 0000000..f1c8bd6 --- /dev/null +++ b/src/views/Dashboard.vue @@ -0,0 +1,10 @@ + + + + diff --git a/src/views/ErrorTest.vue b/src/views/ErrorTest.vue new file mode 100644 index 0000000..2960fee --- /dev/null +++ b/src/views/ErrorTest.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/src/views/Login.vue b/src/views/Login.vue index e4a065a..bd3a9bb 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -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; } - diff --git a/src/views/NotFound.vue b/src/views/NotFound.vue new file mode 100644 index 0000000..77e8234 --- /dev/null +++ b/src/views/NotFound.vue @@ -0,0 +1,9 @@ + + + + diff --git a/src/views/PermissionTest.vue b/src/views/PermissionTest.vue new file mode 100644 index 0000000..9c8fa0c --- /dev/null +++ b/src/views/PermissionTest.vue @@ -0,0 +1,167 @@ + + + + + diff --git a/src/views/games/GameList.vue b/src/views/games/GameList.vue new file mode 100644 index 0000000..a16c253 --- /dev/null +++ b/src/views/games/GameList.vue @@ -0,0 +1,10 @@ + + + + diff --git a/src/views/orders/OrderList.vue b/src/views/orders/OrderList.vue new file mode 100644 index 0000000..f441045 --- /dev/null +++ b/src/views/orders/OrderList.vue @@ -0,0 +1,10 @@ + + + + diff --git a/src/views/reports/ReportAnalysis.vue b/src/views/reports/ReportAnalysis.vue new file mode 100644 index 0000000..3dcbf06 --- /dev/null +++ b/src/views/reports/ReportAnalysis.vue @@ -0,0 +1,10 @@ + + + + diff --git a/src/views/settings/Settings.vue b/src/views/settings/Settings.vue new file mode 100644 index 0000000..758a23a --- /dev/null +++ b/src/views/settings/Settings.vue @@ -0,0 +1,10 @@ + + + + diff --git a/src/views/users/UserList.vue b/src/views/users/UserList.vue new file mode 100644 index 0000000..54ec3bf --- /dev/null +++ b/src/views/users/UserList.vue @@ -0,0 +1,355 @@ + + + + + diff --git a/vite.config.js b/vite.config.js index 37e3700..d9bacfc 100644 --- a/vite.config.js +++ b/vite.config.js @@ -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/, ''), + }, + }, + }, }) -