From e3e38800aa059fc07dfb8063a4a1bed63319b149 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Thu, 30 Apr 2026 22:53:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20rbac=E5=88=9D=E6=AD=A5=E5=AF=B9?= =?UTF-8?q?=E6=8E=A5=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- API_DOCS.md | 1050 ++++++++++++++++++++++++ src/App.tsx | 8 + src/api/system.ts | 91 +- src/api/system/auth.ts | 29 + src/api/system/client.ts | 37 + src/api/system/dict.ts | 37 + src/api/system/file.ts | 49 ++ src/api/system/log.ts | 24 +- src/api/system/menu.ts | 31 +- src/api/system/role.ts | 37 +- src/api/system/user.ts | 34 +- src/api/systemCrud.ts | 120 --- src/components/AIBanner.tsx | 2 +- src/components/CommandPalette.tsx | 4 +- src/components/ParticleBackground.tsx | 2 +- src/components/TabBar.tsx | 2 +- src/components/ui/popover.tsx | 29 + src/layouts/AdminLayout.tsx | 26 +- src/lib/request.ts | 9 +- src/main.tsx | 4 +- src/mock/system/clients.ts | 7 +- src/mock/system/logs.ts | 80 +- src/mock/system/menus.ts | 65 +- src/mock/system/roles.ts | 10 +- src/mock/system/users.ts | 12 +- src/pages/Dashboard.tsx | 11 +- src/pages/LoginPage.tsx | 8 +- src/pages/plant/achievement/Badge.tsx | 2 +- src/pages/plant/achievement/Level.tsx | 2 +- src/pages/plant/banner/index.tsx | 8 +- src/pages/plant/exchange/Order.tsx | 2 +- src/pages/plant/exchange/index.tsx | 6 +- src/pages/plant/wiki/Class.tsx | 2 +- src/pages/radio/channel/index.tsx | 2 +- src/pages/radio/program/index.tsx | 2 +- src/pages/radio/subscription/index.tsx | 5 +- src/pages/system/Clients.tsx | 6 +- src/pages/system/Files.tsx | 4 +- src/pages/system/Logs.tsx | 61 +- src/pages/system/Menus.tsx | 110 ++- src/pages/system/Roles.tsx | 23 +- src/pages/system/Users.tsx | 41 +- src/store/auth.ts | 7 +- tsconfig.app.json | 1 + 45 files changed, 1637 insertions(+), 467 deletions(-) create mode 100644 API_DOCS.md create mode 100644 src/api/system/auth.ts create mode 100644 src/api/system/client.ts create mode 100644 src/api/system/dict.ts create mode 100644 src/api/system/file.ts delete mode 100644 src/api/systemCrud.ts create mode 100644 src/components/ui/popover.tsx diff --git a/.env b/.env index 6727fa1..a340d7f 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -VITE_USE_MOCK=true +VITE_USE_MOCK=false diff --git a/API_DOCS.md b/API_DOCS.md new file mode 100644 index 0000000..06fc153 --- /dev/null +++ b/API_DOCS.md @@ -0,0 +1,1050 @@ +# Sundynix 前端接入文档 + +> 网关地址:`http://{GATEWAY_HOST}:8888` +> 所有接口统一经过网关转发,前端只需对接网关地址 + +--- + +## 一、全局约定 + +### 1.1 统一响应格式 + +所有接口返回 **HTTP 200**,通过 `code` 区分业务状态: + +```json +{ + "code": 200, + "msg": "success", + "data": { ... } +} +``` + +| code | 含义 | +|:----:|------| +| 200 | 成功 | +| 400 | 业务错误(参数错误、逻辑异常等) | +| 401 | 未授权(Token 缺失/过期/无效) | + +### 1.2 鉴权机制 + +需要鉴权的接口必须携带以下请求头: + +``` +Authorization: Bearer +X-Client-Id: <客户端标识> +``` + +> [!IMPORTANT] +> - `Authorization` 从登录接口获取 +> - `X-Client-Id` 由后台管理端配置的客户端标识(如 `sundynix-admin`、`mini-app`) + +### 1.3 Token 自动续期 + +网关内置**滑动窗口续期**机制。当 Token 剩余有效时间 < 1 小时时,响应头会携带新 Token: + +``` +X-Refresh-Token: +``` + +前端需要在响应拦截器中处理: + +```ts +// axios 响应拦截器 +axios.interceptors.response.use((response) => { + const newToken = response.headers['x-refresh-token'] + if (newToken) { + // 静默替换本地存储的 Token + useUserStore().setToken(newToken) + } + return response +}) +``` + +### 1.4 CORS 说明 + +网关已配置跨域,前端无需额外处理。已暴露自定义响应头 `X-Refresh-Token`。 + +--- + +## 二、Auth 服务(`/api/auth`) + +### 2.1 获取图形验证码 + +> 🔓 无需鉴权 + +``` +GET /api/auth/captcha +``` + +**响应**: +```json +{ + "code": 200, + "msg": "success", + "data": { + "captchaId": "abc123", + "captchaImg": "data:image/png;base64,iVBOR..." + } +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| captchaId | string | 验证码 ID,登录时回传 | +| captchaImg | string | Base64 图片,直接赋给 `` | + +--- + +### 2.2 账号密码登录 + +> 🔓 无需鉴权 + +``` +POST /api/auth/login +``` + +**请求头**: +``` +X-Client-Id: sundynix-admin +``` + +**请求体**: +```json +{ + "account": "admin", + "password": "123456", + "captchaId": "abc123", + "captcha": "5xmn" +} +``` + +| 字段 | 必填 | 说明 | +|------|:----:|------| +| account | ✅ | 账号 | +| password | ✅ | 密码 | +| captchaId | ✅ | 验证码 ID(从 captcha 接口获取) | +| captcha | ✅ | 用户输入的验证码 | + +**响应**: +```json +{ + "code": 200, + "msg": "success", + "data": { + "token": "eyJhbGciOiJIUzI1NiIs...", + "userInfo": { + "id": "1234567890", + "name": "张三", + "account": "admin", + "nickName": "管理员", + "phone": "13800138000", + "avatarId": "file-id-xxx", + "gender": 1 + } + } +} +``` + +--- + +### 2.3 微信小程序登录 + +> 🔓 无需鉴权 + +``` +POST /api/auth/miniLogin +``` + +**请求体**: +```json +{ + "code": "wx_login_code_from_wx.login()", + "clientId": "mini-app" +} +``` + +**响应**:同 2.2 的响应格式 + +--- + +### 2.4 手机号登录 + +> 🔓 无需鉴权 + +``` +POST /api/auth/loginByPhone +``` + +**请求体**: +```json +{ + "code": "phone_code_from_getPhoneNumber", + "openId": "用户openId", + "clientId": "mini-app" +} +``` + +**响应**:同 2.2 的响应格式 + +--- + +### 2.5 获取当前用户信息(含角色、菜单) + +> 🔒 需要鉴权 + +``` +GET /api/auth/info +``` + +**响应**: +```json +{ + "code": 200, + "msg": "success", + "data": { + "id": "1234567890", + "name": "张三", + "account": "admin", + "nickName": "管理员", + "phone": "13800138000", + "avatarId": "file-id-xxx", + "gender": 1, + "roles": ["admin", "editor"], + "menus": [ + { + "id": "m1", + "parentId": "0", + "category": 1, + "name": "Dashboard", + "title": "仪表盘", + "code": "dashboard", + "path": "/dashboard", + "permission": "", + "locale": "menu.dashboard", + "icon": "icon-dashboard", + "sort": 1, + "children": [ + { + "id": "m2", + "parentId": "m1", + "name": "Workplace", + "title": "工作台", + "path": "/dashboard/workplace", + "sort": 1, + "children": [] + } + ] + } + ], + "createdAt": 1714000000 + } +} +``` + +> [!TIP] +> - `roles` 是角色 code 数组,用于前端权限判断 +> - `menus` 是树形结构,已按 `sort` 排序,前端直接渲染侧边栏 +> - `createdAt` 是 Unix 时间戳(秒) + +--- + +### 2.6 更新个人信息 + +> 🔒 需要鉴权 + +``` +POST /api/auth/update +``` + +**请求体**(所有字段可选,只传需要修改的): +```json +{ + "name": "李四", + "nickName": "小李", + "phone": "13900139000", + "avatarId": "new-file-id" +} +``` + +**响应**: +```json +{ "code": 200, "msg": "success" } +``` + +--- + +## 三、System 服务(`/api/sys`) + +> [!IMPORTANT] +> 以下所有接口均需要鉴权(`Authorization: Bearer `) + +--- + +### 3.1 客户端管理 + +#### 创建客户端 + +``` +POST /api/sys/client/create +``` + +```json +{ + "clientId": "sundynix-admin", + "name": "Web管理端", + "grantType": "password", + "additionalInfo": "", + "activeTimeout": 7200 +} +``` + +| 字段 | 必填 | 说明 | +|------|:----:|------| +| clientId | ✅ | 客户端唯一标识 | +| name | ✅ | 客户端名称 | +| grantType | ❌ | 授权类型 | +| additionalInfo | ❌ | 附加信息 | +| activeTimeout | ❌ | Token 有效期(秒),默认 7200 | + +#### 更新客户端 + +``` +POST /api/sys/client/update +``` + +```json +{ + "id": "客户端记录ID", + "name": "新名称", + "activeTimeout": 3600 +} +``` + +#### 删除客户端 + +``` +POST /api/sys/client/delete +``` + +```json +{ "ids": ["id1", "id2"] } +``` + +#### 客户端列表 + +``` +POST /api/sys/client/list +``` + +```json +{ + "current": 1, + "pageSize": 10, + "name": "" +} +``` + +**响应**: +```json +{ + "code": 200, + "msg": "success", + "data": { + "list": [ + { + "id": "xxx", + "clientId": "sundynix-admin", + "name": "Web管理端", + "grantType": "password", + "additionalInfo": "", + "activeTimeout": 7200 + } + ], + "total": 1, + "current": 1, + "size": 10 + } +} +``` + +--- + +### 3.2 角色管理 + +#### 创建角色 + +``` +POST /api/sys/role/create +``` + +```json +{ + "name": "管理员", + "code": "admin", + "sort": 1, + "menuIds": ["menu-id-1", "menu-id-2"] +} +``` + +#### 更新角色 + +``` +POST /api/sys/role/update +``` + +```json +{ + "id": "角色ID", + "name": "超级管理员", + "code": "super-admin", + "sort": 0, + "menuIds": ["menu-id-1", "menu-id-2", "menu-id-3"] +} +``` + +> [!NOTE] +> `menuIds` 是**全量替换**,传什么就关联什么。不传或空数组则清除所有菜单关联。 + +#### 删除角色 + +``` +POST /api/sys/role/delete +``` + +```json +{ "ids": ["role-id-1"] } +``` + +#### 角色列表 + +``` +POST /api/sys/role/list +``` + +```json +{ + "current": 1, + "pageSize": 10, + "name": "" +} +``` + +**响应**: +```json +{ + "code": 200, + "msg": "success", + "data": { + "list": [ + { + "id": "xxx", + "name": "管理员", + "code": "admin", + "sort": 1, + "menuIds": ["m1", "m2"] + } + ], + "total": 1 + } +} +``` + +--- + +### 3.3 菜单管理 + +#### 创建菜单 + +``` +POST /api/sys/menu/create +``` + +```json +{ + "parentId": "0", + "category": 1, + "name": "Dashboard", + "title": "仪表盘", + "code": "dashboard", + "path": "/dashboard", + "permission": "", + "locale": "menu.dashboard", + "icon": "icon-dashboard", + "sort": 1 +} +``` + +| 字段 | 说明 | +|------|------| +| parentId | 父级ID,顶级菜单传 `"0"` 或空字符串 | +| category | 类型:1=菜单 2=按钮/权限 | +| name | 路由 name(英文,如 `Dashboard`) | +| title | 显示标题(中文,如 `仪表盘`) | +| code | 权限标识(如 `dashboard`) | +| path | 路由路径(如 `/dashboard`) | +| permission | 操作权限标识 | +| locale | 国际化 key | +| icon | 图标名称 | +| sort | 排序值,越小越靠前 | + +#### 更新菜单 + +``` +POST /api/sys/menu/update +``` + +```json +{ + "id": "菜单ID", + "title": "新标题", + "sort": 2 +} +``` + +#### 删除菜单 + +``` +POST /api/sys/menu/delete +``` + +```json +{ "ids": ["menu-id-1"] } +``` + +> [!WARNING] +> 删除菜单会同步清除该菜单与所有角色的关联关系 + +#### 菜单列表(树形) + +``` +GET /api/sys/menu/list +``` + +无参数。返回完整的树形菜单结构。 + +**响应**: +```json +{ + "code": 200, + "msg": "success", + "data": [ + { + "id": "m1", + "parentId": "0", + "category": 1, + "name": "System", + "title": "系统管理", + "code": "system", + "path": "/system", + "icon": "icon-settings", + "sort": 99, + "children": [ + { + "id": "m2", + "parentId": "m1", + "name": "User", + "title": "用户管理", + "path": "/system/user", + "sort": 1, + "children": [] + } + ] + } + ] +} +``` + +#### 根据角色获取菜单 + +``` +POST /api/sys/menu/byRole +``` + +```json +{ "id": "角色ID" } +``` + +**响应**:同菜单列表格式,但只包含该角色关联的菜单 + +--- + +### 3.4 用户管理(后台管理) + +#### 用户列表 + +``` +POST /api/sys/user/list +``` + +```json +{ + "current": 1, + "pageSize": 10, + "name": "", + "account": "" +} +``` + +**响应**: +```json +{ + "code": 200, + "msg": "success", + "data": { + "list": [ + { + "id": "xxx", + "name": "张三", + "account": "admin", + "nickName": "管理员", + "phone": "13800138000", + "gender": 1, + "createdAt": 1714000000 + } + ], + "total": 1 + } +} +``` + +#### 创建用户 + +``` +POST /api/sys/user/create +``` + +```json +{ + "name": "张三", + "account": "zhangsan", + "password": "123456", + "phone": "13800138000", + "nickName": "小张", + "roleIds": ["role-id-1", "role-id-2"] +} +``` + +| 字段 | 必填 | 说明 | +|------|:----:|------| +| name | ✅ | 姓名 | +| account | ✅ | 账号(唯一) | +| password | ✅ | 密码 | +| phone | ❌ | 手机号 | +| nickName | ❌ | 昵称 | +| roleIds | ❌ | 关联角色ID数组 | + +#### 更新用户 + +``` +POST /api/sys/user/update +``` + +```json +{ + "id": "用户ID", + "name": "李四", + "phone": "13900139000", + "roleIds": ["role-id-1"] +} +``` + +#### 删除用户 + +``` +POST /api/sys/user/delete +``` + +```json +{ "ids": ["user-id-1", "user-id-2"] } +``` + +#### 重置密码 + +``` +POST /api/sys/user/resetPassword +``` + +```json +{ + "id": "用户ID", + "password": "newpassword123" +} +``` + +--- + +### 3.5 字典管理 + +#### 创建字典 + +``` +POST /api/sys/dict/create +``` + +```json +{ + "type": "gender", + "label": "男", + "value": "1", + "sort": 1, + "desc": "性别-男" +} +``` + +#### 更新字典 + +``` +POST /api/sys/dict/update +``` + +```json +{ + "id": "字典ID", + "label": "女", + "value": "2" +} +``` + +#### 删除字典 + +``` +POST /api/sys/dict/delete +``` + +```json +{ "ids": ["dict-id-1"] } +``` + +#### 字典列表 + +``` +POST /api/sys/dict/list +``` + +```json +{ + "current": 1, + "pageSize": 10, + "type": "gender" +} +``` + +**响应**: +```json +{ + "code": 200, + "msg": "success", + "data": { + "list": [ + { + "id": "xxx", + "type": "gender", + "label": "男", + "value": "1", + "sort": 1, + "desc": "性别-男" + } + ], + "total": 2 + } +} +``` + +--- + +### 3.6 操作日志 + +#### 日志列表 + +``` +POST /api/sys/log/list +``` + +```json +{ + "current": 1, + "pageSize": 20, + "method": "POST", + "path": "/api/auth/login", + "status": 200 +} +``` + +**响应**: +```json +{ + "code": 200, + "msg": "success", + "data": { + "list": [ + { + "id": "xxx", + "clientId": "sundynix-admin", + "ip": "192.168.1.100", + "method": "POST", + "path": "/api/auth/login", + "status": 200, + "latency": 52000000, + "agent": "Mozilla/5.0...", + "errorMessage": "", + "body": "{\"account\":\"admin\"...}", + "resp": "{\"code\":200...}", + "userId": "user-id-xxx", + "createdAt": 1714000000 + } + ], + "total": 100 + } +} +``` + +> [!NOTE] +> `latency` 单位是纳秒,前端显示时需转换:`(latency / 1000000).toFixed(0) + 'ms'` + +#### 删除日志 + +``` +POST /api/sys/log/delete +``` + +```json +{ "ids": ["log-id-1", "log-id-2"] } +``` + +--- + +## 四、前端接入参考 + +### 4.1 Axios 封装建议 + +```ts +import axios from 'axios' + +const request = axios.create({ + baseURL: 'http://192.168.100.4:8888', // 网关地址 + timeout: 10000, +}) + +// 请求拦截器 +request.interceptors.request.use((config) => { + const token = localStorage.getItem('token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + config.headers['X-Client-Id'] = 'sundynix-admin' // 你的客户端标识 + return config +}) + +// 响应拦截器 +request.interceptors.response.use((response) => { + // Token 自动续期 + const newToken = response.headers['x-refresh-token'] + if (newToken) { + localStorage.setItem('token', newToken) + } + + const { code, msg, data } = response.data + if (code === 401) { + // 跳转登录 + window.location.href = '/login' + return Promise.reject(new Error(msg)) + } + if (code !== 200) { + return Promise.reject(new Error(msg)) + } + return data +}) + +export default request +``` + +### 4.2 API 分组 + +```ts +// api/auth.ts +export const authApi = { + captcha: () => request.get('/api/auth/captcha'), + login: (data: LoginReq) => request.post('/api/auth/login', data), + getUserInfo: () => request.get('/api/auth/info'), + updateUser: (data: UpdateUserReq) => request.post('/api/auth/update', data), +} + +// api/system.ts +export const clientApi = { + list: (data: PageReq) => request.post('/api/sys/client/list', data), + create: (data: ClientReq) => request.post('/api/sys/client/create', data), + update: (data: ClientUpdateReq) => request.post('/api/sys/client/update', data), + delete: (ids: string[]) => request.post('/api/sys/client/delete', { ids }), +} + +export const roleApi = { + list: (data: PageReq) => request.post('/api/sys/role/list', data), + create: (data: RoleReq) => request.post('/api/sys/role/create', data), + update: (data: RoleUpdateReq) => request.post('/api/sys/role/update', data), + delete: (ids: string[]) => request.post('/api/sys/role/delete', { ids }), +} + +export const menuApi = { + list: () => request.get('/api/sys/menu/list'), + byRole: (id: string) => request.post('/api/sys/menu/byRole', { id }), + create: (data: MenuReq) => request.post('/api/sys/menu/create', data), + update: (data: MenuUpdateReq) => request.post('/api/sys/menu/update', data), + delete: (ids: string[]) => request.post('/api/sys/menu/delete', { ids }), +} + +export const userApi = { + list: (data: UserListReq) => request.post('/api/sys/user/list', data), + create: (data: UserCreateReq) => request.post('/api/sys/user/create', data), + update: (data: UserUpdateReq) => request.post('/api/sys/user/update', data), + delete: (ids: string[]) => request.post('/api/sys/user/delete', { ids }), + resetPassword: (data: ResetPasswordReq) => request.post('/api/sys/user/resetPassword', data), +} + +export const dictApi = { + list: (data: DictListReq) => request.post('/api/sys/dict/list', data), + create: (data: DictReq) => request.post('/api/sys/dict/create', data), + update: (data: DictUpdateReq) => request.post('/api/sys/dict/update', data), + delete: (ids: string[]) => request.post('/api/sys/dict/delete', { ids }), +} + +export const logApi = { + list: (data: LogListReq) => request.post('/api/sys/log/list', data), + delete: (ids: string[]) => request.post('/api/sys/log/delete', { ids }), +} +``` + +### 4.3 TypeScript 类型定义 + +```ts +// ========== Auth ========== +interface LoginReq { + account: string + password: string + captchaId: string + captcha: string +} + +interface LoginResp { + token: string + userInfo: UserInfo +} + +interface UserInfo { + id: string + name: string + account: string + nickName: string + phone: string + avatarId: string + gender: number // 0=未知 1=男 2=女 + roles?: string[] // 角色 code 列表(仅 /info 接口返回) + menus?: MenuItem[] // 菜单树(仅 /info 接口返回) + createdAt?: number // Unix 时间戳(秒) +} + +interface MenuItem { + id: string + parentId: string + category: number // 1=菜单 2=按钮 + name: string // 路由名(英文) + title: string // 显示标题(中文) + code: string + path: string + permission: string + locale: string + icon: string + sort: number + children: MenuItem[] +} + +// ========== System ========== +interface ClientInfo { + id: string + clientId: string + name: string + grantType: string + additionalInfo: string + activeTimeout: number // Token有效期(秒) +} + +interface RoleInfo { + id: string + name: string + code: string + sort: number + menuIds: string[] +} + +interface DictInfo { + id: string + type: string + label: string + value: string + sort: number + desc: string +} + +interface OperationRecordInfo { + id: string + clientId: string + ip: string + method: string + path: string + status: number + latency: number // 纳秒 + agent: string + errorMessage: string + body: string + resp: string + userId: string + createdAt: number // Unix 时间戳(秒) +} + +// ========== 通用 ========== +interface PageReq { + current?: number + pageSize?: number +} + +interface PageResp { + list: T[] + total: number +} +``` + +--- + +## 五、接口速查表 + +| 模块 | 方法 | 路径 | 鉴权 | 说明 | +|------|:----:|------|:----:|------| +| **Auth** | GET | `/api/auth/captcha` | ❌ | 获取验证码 | +| | POST | `/api/auth/login` | ❌ | 账号密码登录 | +| | POST | `/api/auth/miniLogin` | ❌ | 小程序登录 | +| | POST | `/api/auth/loginByPhone` | ❌ | 手机号登录 | +| | GET | `/api/auth/info` | ✅ | 获取用户信息+角色+菜单 | +| | POST | `/api/auth/update` | ✅ | 更新个人信息 | +| **客户端** | POST | `/api/sys/client/create` | ✅ | 创建客户端 | +| | POST | `/api/sys/client/update` | ✅ | 更新客户端 | +| | POST | `/api/sys/client/delete` | ✅ | 删除客户端 | +| | POST | `/api/sys/client/list` | ✅ | 客户端列表 | +| **角色** | POST | `/api/sys/role/create` | ✅ | 创建角色 | +| | POST | `/api/sys/role/update` | ✅ | 更新角色 | +| | POST | `/api/sys/role/delete` | ✅ | 删除角色 | +| | POST | `/api/sys/role/list` | ✅ | 角色列表 | +| **菜单** | POST | `/api/sys/menu/create` | ✅ | 创建菜单 | +| | POST | `/api/sys/menu/update` | ✅ | 更新菜单 | +| | POST | `/api/sys/menu/delete` | ✅ | 删除菜单 | +| | GET | `/api/sys/menu/list` | ✅ | 菜单列表(树形) | +| | POST | `/api/sys/menu/byRole` | ✅ | 按角色查菜单 | +| **用户** | POST | `/api/sys/user/list` | ✅ | 用户列表 | +| | POST | `/api/sys/user/create` | ✅ | 创建用户 | +| | POST | `/api/sys/user/update` | ✅ | 更新用户 | +| | POST | `/api/sys/user/delete` | ✅ | 删除用户 | +| | POST | `/api/sys/user/resetPassword` | ✅ | 重置密码 | +| **字典** | POST | `/api/sys/dict/create` | ✅ | 创建字典 | +| | POST | `/api/sys/dict/update` | ✅ | 更新字典 | +| | POST | `/api/sys/dict/delete` | ✅ | 删除字典 | +| | POST | `/api/sys/dict/list` | ✅ | 字典列表 | +| **日志** | POST | `/api/sys/log/list` | ✅ | 操作日志列表 | +| | POST | `/api/sys/log/delete` | ✅ | 删除日志 | diff --git a/src/App.tsx b/src/App.tsx index 365cfc0..2c84b8d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -85,6 +85,14 @@ function AppRoutes() { {hasFetchedMenus && dynamicRoutes.length === 0 && ( } /> )} + {hasFetchedMenus && dynamicRoutes.length > 0 && ( + +

页面不存在或开发中

+

该菜单没有对应的页面组件,或者路径未匹配。

+ + } /> + )}
} /> diff --git a/src/api/system.ts b/src/api/system.ts index 8813d3f..5b99dd1 100644 --- a/src/api/system.ts +++ b/src/api/system.ts @@ -1,25 +1,42 @@ -import { get, post } from '@/lib/request' + // ==================== Types ==================== export interface SystemUser { - id: string; account: string; name: string; nickName?: string; phone?: string - avatar?: SystemOss; avatarId?: string; clientId?: string; tenantId?: string - createdAt: string; updatedAt: string; roles?: string[] // Adjusted based on backend info - menus?: any[] - gender?: number + id: string + name: string + account: string + nickName?: string + phone?: string + avatarId?: string + gender?: number // 0=未知 1=男 2=女 + roles?: string[] // 角色 code 列表(仅 /auth/info 返回) + menus?: SystemMenu[] // 菜单树(仅 /auth/info 返回) + createdAt?: number // Unix 时间戳(秒) + roleIds?: string[] // 关联角色 ID(用户管理接口) } export interface SystemRole { - id: string; name: string; code: string; sort?: number - menus?: SystemMenu[]; createdAt: string; updatedAt: string + id: string + name: string + code: string + sort?: number + menuIds?: string[] // 关联菜单 ID 列表 } export interface SystemMenu { - id: string; name: string; title?: string; code?: string; path?: string - icon?: string; locale?: string; parentId?: string; permission?: string - sort?: number; category?: number; children?: SystemMenu[] - createdAt: string; updatedAt: string + id: string + parentId?: string + category?: number // 1=菜单 2=按钮/权限 + name: string // 路由名(英文) + title?: string // 显示标题(中文) + code?: string // 权限标识 + path?: string // 路由路径 + permission?: string // 操作权限标识 + locale?: string // 国际化 key + icon?: string // 图标名称 + sort?: number // 排序 + children?: SystemMenu[] } export interface SystemOss { @@ -29,35 +46,37 @@ export interface SystemOss { } export interface SystemClient { - id: string; clientId: string; name: string; grantType?: string - activeTimeout?: number; additionalInfo?: string - createdAt: string; updatedAt: string + id: string + clientId: string + name: string + grantType?: string + additionalInfo?: string + activeTimeout?: number // Token有效期(秒) } export interface OperationLog { - id: string; operatorId: string; operatorName: string - clientId: string; clientName: string - method: string; path: string; title: string - statusCode: number; duration: number // ms - ip: string; userAgent?: string - requestBody?: string; responseBody?: string - createdAt: string + id: string + clientId: string + ip: string + method: string + path: string + status: number + latency: number // 纳秒 + agent: string + errorMessage: string + body: string // 请求体 JSON 字符串 + resp: string // 响应体 JSON 字符串 + userId: string + createdAt: number // Unix 时间戳(秒) } -export interface CaptchaRes { captchaImg: string; captchaId: string } -export interface LoginParams { account: string; password: string; captcha: string; captchaId: string } -export interface LoginResponse { token: string; userInfo: SystemUser } - -// ==================== Auth ==================== - -export async function getCaptcha() { - return get<{ code: number; data: CaptchaRes; msg: string }>('/auth/captcha') +export interface DictInfo { + id: string + type: string + label: string + value: string + sort: number + desc: string } -export async function login(data: LoginParams) { - return post<{ code: number; data: LoginResponse; msg: string }>('/auth/login', data) -} -export async function logout() { - return get<{ code: number; data: null; msg: string }>('/auth/logout') // If backend doesn't have it, we just clear local token -} diff --git a/src/api/system/auth.ts b/src/api/system/auth.ts new file mode 100644 index 0000000..f5ab7fe --- /dev/null +++ b/src/api/system/auth.ts @@ -0,0 +1,29 @@ +import { get, post } from '@/lib/request' +import type { SystemUser } from '../system' + +export interface CaptchaRes { captchaImg: string; captchaId: string } +export interface LoginParams { account: string; password: string; captcha: string; captchaId: string } +export interface LoginResponse { token: string; userInfo: SystemUser } + +const BASE_URL = '/auth' + +export async function getCaptcha() { + return get(`${BASE_URL}/captcha`) +} + +export async function login(data: LoginParams) { + return post(`${BASE_URL}/login`, data) +} + +export async function getUserInfo() { + return get(`${BASE_URL}/info`) +} + +export async function updateProfile(data: Partial>) { + return post(`${BASE_URL}/update`, data) +} + +export async function logout() { + // 后端无 logout 接口,仅清理本地状态 + return Promise.resolve() +} diff --git a/src/api/system/client.ts b/src/api/system/client.ts new file mode 100644 index 0000000..a1b7bb7 --- /dev/null +++ b/src/api/system/client.ts @@ -0,0 +1,37 @@ +import { post } from '@/lib/request' +import { USE_MOCK, delay, paginate } from '@/mock' +import type { SystemClient } from '../system' +import type { PageResult } from '@/lib/request' + +const BASE_URL = '/sys/client' + +const mockClients: SystemClient[] = [ + { id: '1', clientId: 'sundynix-admin', name: 'Web管理端', grantType: 'password', activeTimeout: 7200 }, + { id: '2', clientId: 'mini-app', name: '微信小程序', grantType: 'wechat', activeTimeout: 3600 }, + { id: '3', clientId: 'plant', name: 'Plant 花园服务', grantType: 'password', activeTimeout: 3600 }, +] + +export async function getClientList(params: { current: number; pageSize: number; name?: string }) { + if (USE_MOCK) { + await delay() + let list = [...mockClients] + if (params.name) list = list.filter(c => c.name.includes(params.name!)) + return paginate(list, params.current, params.pageSize) + } + return post>(`${BASE_URL}/list`, params) +} + +export async function createClient(data: Partial) { + if (USE_MOCK) { await delay(); return null } + return post(`${BASE_URL}/create`, data) +} + +export async function updateClient(data: Partial & { id: string }) { + if (USE_MOCK) { await delay(); return null } + return post(`${BASE_URL}/update`, data) +} + +export async function deleteClient(ids: string[]) { + if (USE_MOCK) { await delay(); return null } + return post(`${BASE_URL}/delete`, { ids }) +} diff --git a/src/api/system/dict.ts b/src/api/system/dict.ts new file mode 100644 index 0000000..6634bee --- /dev/null +++ b/src/api/system/dict.ts @@ -0,0 +1,37 @@ +import { post } from '@/lib/request' +import { USE_MOCK, delay, paginate } from '@/mock' +import type { DictInfo } from '../system' +import type { PageResult } from '@/lib/request' + +const BASE_URL = '/sys/dict' + +const mockDicts: DictInfo[] = [ + { id: '1', type: 'gender', label: '男', value: '1', sort: 1, desc: '性别-男' }, + { id: '2', type: 'gender', label: '女', value: '2', sort: 2, desc: '性别-女' }, + { id: '3', type: 'gender', label: '未知', value: '0', sort: 3, desc: '性别-未知' }, +] + +export async function getDictList(params: { current: number; pageSize: number; type?: string }) { + if (USE_MOCK) { + await delay() + let list = [...mockDicts] + if (params.type) list = list.filter(d => d.type === params.type) + return paginate(list, params.current, params.pageSize) + } + return post>(`${BASE_URL}/list`, params) +} + +export async function createDict(data: Partial) { + if (USE_MOCK) { await delay(); return null } + return post(`${BASE_URL}/create`, data) +} + +export async function updateDict(data: Partial & { id: string }) { + if (USE_MOCK) { await delay(); return null } + return post(`${BASE_URL}/update`, data) +} + +export async function deleteDict(ids: string[]) { + if (USE_MOCK) { await delay(); return null } + return post(`${BASE_URL}/delete`, { ids }) +} diff --git a/src/api/system/file.ts b/src/api/system/file.ts new file mode 100644 index 0000000..9c80ab9 --- /dev/null +++ b/src/api/system/file.ts @@ -0,0 +1,49 @@ +import { post } from '@/lib/request' +import { USE_MOCK, delay, paginate, mockId } from '@/mock' +import type { SystemOss } from '../system' +import type { PageResult } from '@/lib/request' + +const BASE_URL = '/file/oss' + +const mockFiles: SystemOss[] = [ + { id: '1', name: 'avatar.jpg', key: 'avatars/1.jpg', url: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=800&q=80', suffix: 'jpg', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }, + { id: '2', name: 'bg.png', key: 'bg/1.png', url: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=800&q=80', suffix: 'png', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }, +] + +export async function getFileList(params: { current: number; pageSize: number; keyword?: string }) { + if (USE_MOCK) { + await delay() + let list = [...mockFiles] + if (params.keyword) list = list.filter(f => f.name.includes(params.keyword!)) + return paginate(list, params.current, params.pageSize) + } + return post>(`${BASE_URL}/getFileList`, params) +} + +export async function uploadFile(_file: File) { + if (USE_MOCK) { + await delay(1000) + const newFile: SystemOss = { + id: mockId(), name: _file.name, key: `upload/${_file.name}`, + url: URL.createObjectURL(_file), suffix: _file.name.split('.').pop() || '', + createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() + } + mockFiles.unshift(newFile) + return { file: newFile } + } + const formData = new FormData() + formData.append('file', _file) + return post<{ file: SystemOss }>(`${BASE_URL}/upload`, formData) +} + +export async function deleteFile(ids: string[]) { + if (USE_MOCK) { + await delay() + const idSet = new Set(ids) + for (let i = mockFiles.length - 1; i >= 0; i--) { + if (idSet.has(mockFiles[i].id)) mockFiles.splice(i, 1) + } + return null + } + return post(`${BASE_URL}/delete`, { ids }) +} diff --git a/src/api/system/log.ts b/src/api/system/log.ts index 14d4487..9896386 100644 --- a/src/api/system/log.ts +++ b/src/api/system/log.ts @@ -1,25 +1,31 @@ -import { get } from '@/lib/request' -import { USE_MOCK, delay, paginate, mockResponse } from '@/mock' +import { post } from '@/lib/request' +import { USE_MOCK, delay, paginate } from '@/mock' import { mockLogs } from '@/mock/system/logs' import type { OperationLog } from '../system' +import type { PageResult } from '@/lib/request' + +const BASE_URL = '/sys/log' export async function getLogList(params: { current: number; pageSize: number; - operatorName?: string; - clientId?: string; + method?: string; path?: string; status?: number; }) { if (USE_MOCK) { await delay() let list = [...mockLogs] - if (params.operatorName) list = list.filter(l => l.operatorName?.includes(params.operatorName!)) - if (params.clientId && params.clientId !== 'all') list = list.filter(l => l.clientId === params.clientId) + if (params.method) list = list.filter(l => l.method === params.method) if (params.path) list = list.filter(l => l.path.includes(params.path!)) - if (params.status) list = list.filter(l => l.statusCode === params.status) + if (params.status) list = list.filter(l => l.status === params.status) - return mockResponse(paginate(list, params.current, params.pageSize)) + return paginate(list, params.current, params.pageSize) } - return get<{ code: number; data: { list: OperationLog[]; total: number }; msg: string }>('/system/logs', params) + return post>(`${BASE_URL}/list`, params) +} + +export async function deleteLog(ids: string[]) { + if (USE_MOCK) { await delay(); return null } + return post(`${BASE_URL}/delete`, { ids }) } diff --git a/src/api/system/menu.ts b/src/api/system/menu.ts index 3f5c6d5..167b878 100644 --- a/src/api/system/menu.ts +++ b/src/api/system/menu.ts @@ -1,24 +1,31 @@ -import { get, post, put, del } from '@/lib/request' -import { USE_MOCK, delay, mockResponse } from '@/mock' +import { get, post } from '@/lib/request' +import { USE_MOCK, delay } from '@/mock' import { mockMenuTree } from '@/mock/system/menus' import type { SystemMenu } from '../system' +const BASE_URL = '/sys/menu' + export async function getMenuTree() { - if (USE_MOCK) { await delay(); return mockResponse(mockMenuTree) } - return get<{ code: number; data: SystemMenu[]; msg: string }>('/system/menus/tree') + if (USE_MOCK) { await delay(); return mockMenuTree } + return get(`${BASE_URL}/list`) +} + +export async function getMenuByRole(id: string) { + if (USE_MOCK) { await delay(); return mockMenuTree } + return post(`${BASE_URL}/byRole`, { id }) } export async function createMenu(data: Partial) { - if (USE_MOCK) { await delay(); return mockResponse(null, '创建成功') } - return post<{ code: number; data: null; msg: string }>('/system/menus', data) + if (USE_MOCK) { await delay(); return null } + return post(`${BASE_URL}/create`, data) } -export async function updateMenu(id: string, data: Partial) { - if (USE_MOCK) { await delay(); return mockResponse(null, '更新成功') } - return put<{ code: number; data: null; msg: string }>(`/system/menus/${id}`, data) +export async function updateMenu(data: Partial & { id: string }) { + if (USE_MOCK) { await delay(); return null } + return post(`${BASE_URL}/update`, data) } -export async function deleteMenu(id: string) { - if (USE_MOCK) { await delay(); return mockResponse(null, '删除成功') } - return del<{ code: number; data: null; msg: string }>(`/system/menus/${id}`) +export async function deleteMenu(ids: string[]) { + if (USE_MOCK) { await delay(); return null } + return post(`${BASE_URL}/delete`, { ids }) } diff --git a/src/api/system/role.ts b/src/api/system/role.ts index 31ef069..8e66ffc 100644 --- a/src/api/system/role.ts +++ b/src/api/system/role.ts @@ -1,13 +1,16 @@ -import { get, post, put, del } from '@/lib/request' -import { USE_MOCK, delay, paginate, mockDate, mockResponse } from '@/mock' +import { post } from '@/lib/request' +import { USE_MOCK, delay, paginate } from '@/mock' import type { SystemRole } from '../system' +import type { PageResult } from '@/lib/request' + +const BASE_URL = '/sys/role' const mockRoles: SystemRole[] = [ - { id: '1', name: '超级管理员', code: 'super_admin', sort: 1, createdAt: mockDate(120), updatedAt: mockDate(1) }, - { id: '2', name: '系统管理员', code: 'admin', sort: 2, createdAt: mockDate(120), updatedAt: mockDate(1) }, - { id: '3', name: '运营专员', code: 'operator', sort: 3, createdAt: mockDate(90), updatedAt: mockDate(1) }, - { id: '4', name: '内容审核', code: 'auditor', sort: 4, createdAt: mockDate(90), updatedAt: mockDate(1) }, - { id: '5', name: '客服', code: 'customer_service', sort: 5, createdAt: mockDate(30), updatedAt: mockDate(1) }, + { id: '1', name: '超级管理员', code: 'super_admin', sort: 1 }, + { id: '2', name: '系统管理员', code: 'admin', sort: 2 }, + { id: '3', name: '运营专员', code: 'operator', sort: 3 }, + { id: '4', name: '内容审核', code: 'auditor', sort: 4 }, + { id: '5', name: '客服', code: 'customer_service', sort: 5 }, ] export async function getRoleList(params: { current: number; pageSize: number; name?: string }) { @@ -15,22 +18,22 @@ export async function getRoleList(params: { current: number; pageSize: number; n await delay() let list = [...mockRoles] if (params.name) list = list.filter(r => r.name.includes(params.name!)) - return mockResponse(paginate(list, params.current, params.pageSize)) + return paginate(list, params.current, params.pageSize) } - return get<{ code: number; data: { list: SystemRole[]; total: number }; msg: string }>('/system/roles', params) + return post>(`${BASE_URL}/list`, params) } export async function createRole(data: Partial) { - if (USE_MOCK) { await delay(); return mockResponse(null, '创建成功') } - return post<{ code: number; data: null; msg: string }>('/system/roles', data) + if (USE_MOCK) { await delay(); return null } + return post(`${BASE_URL}/create`, data) } -export async function updateRole(id: string, data: Partial) { - if (USE_MOCK) { await delay(); return mockResponse(null, '更新成功') } - return put<{ code: number; data: null; msg: string }>(`/system/roles/${id}`, data) +export async function updateRole(data: Partial & { id: string }) { + if (USE_MOCK) { await delay(); return null } + return post(`${BASE_URL}/update`, data) } -export async function deleteRole(id: string) { - if (USE_MOCK) { await delay(); return mockResponse(null, '删除成功') } - return del<{ code: number; data: null; msg: string }>(`/system/roles/${id}`) +export async function deleteRole(ids: string[]) { + if (USE_MOCK) { await delay(); return null } + return post(`${BASE_URL}/delete`, { ids }) } diff --git a/src/api/system/user.ts b/src/api/system/user.ts index ce9a97b..55f6ecc 100644 --- a/src/api/system/user.ts +++ b/src/api/system/user.ts @@ -1,7 +1,10 @@ -import { get, post, put, del } from '@/lib/request' -import { USE_MOCK, delay, paginate, mockResponse } from '@/mock' +import { post } from '@/lib/request' +import { USE_MOCK, delay, paginate } from '@/mock' import { mockUsers } from '@/mock/system/users' import type { SystemUser } from '../system' +import type { PageResult } from '@/lib/request' + +const BASE_URL = '/sys/user' export async function getUserList(params: { current: number; pageSize: number; account?: string; name?: string }) { if (USE_MOCK) { @@ -9,22 +12,27 @@ export async function getUserList(params: { current: number; pageSize: number; a let list = [...mockUsers] if (params.account) list = list.filter(u => u.account.includes(params.account!)) if (params.name) list = list.filter(u => u.name.includes(params.name!)) - return mockResponse(paginate(list, params.current, params.pageSize)) + return paginate(list, params.current, params.pageSize) } - return get<{ code: number; data: { list: SystemUser[]; total: number }; msg: string }>('/system/users', params) + return post>(`${BASE_URL}/list`, params) } -export async function createUser(data: Partial) { - if (USE_MOCK) { await delay(); return mockResponse(null, '创建成功') } - return post<{ code: number; data: null; msg: string }>('/system/users', data) +export async function createUser(data: Partial & { password?: string }) { + if (USE_MOCK) { await delay(); return null } + return post(`${BASE_URL}/create`, data) } -export async function updateUser(id: string, data: Partial) { - if (USE_MOCK) { await delay(); return mockResponse(null, '更新成功') } - return put<{ code: number; data: null; msg: string }>(`/system/users/${id}`, data) +export async function updateUser(data: Partial & { id: string }) { + if (USE_MOCK) { await delay(); return null } + return post(`${BASE_URL}/update`, data) } -export async function deleteUser(id: string) { - if (USE_MOCK) { await delay(); return mockResponse(null, '删除成功') } - return del<{ code: number; data: null; msg: string }>(`/system/users/${id}`) +export async function deleteUser(ids: string[]) { + if (USE_MOCK) { await delay(); return null } + return post(`${BASE_URL}/delete`, { ids }) +} + +export async function resetPassword(data: { id: string; password: string }) { + if (USE_MOCK) { await delay(); return null } + return post(`${BASE_URL}/resetPassword`, data) } diff --git a/src/api/systemCrud.ts b/src/api/systemCrud.ts deleted file mode 100644 index df9851d..0000000 --- a/src/api/systemCrud.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { get, post, type PageResult, type PageParams } from '@/lib/request' -import type { SystemUser, SystemRole, SystemMenu, SystemClient, SystemOss, OperationLog } from './system' - -// ==================== User ==================== - -export async function getUserList(data: PageParams & { account?: string; phone?: string; name?: string }) { - // map keyword to name or account if needed, or backend can handle it - const reqData = { ...data, name: data.keyword, account: data.keyword } - return post<{ data: PageResult }>('/sys/user/list', reqData) -} - -export async function saveUser(data: Partial) { - return post<{ msg: string }>('/sys/user/create', data) -} - -export async function updateUser(data: Partial) { - return post<{ msg: string }>('/sys/user/update', data) -} - -export async function deleteUser(ids: string[]) { - return post<{ msg: string }>('/sys/user/delete', { ids }) -} - -export async function changePassword(data: { id: string; newPwd: string }) { - // admin reset password - return post<{ msg: string }>('/sys/user/resetPassword', { id: data.id, password: data.newPwd }) -} - -export async function grantRole(data: { userId: string; roleIds: string[] }) { - // backend UserUpdateReq has RoleIds - return post<{ msg: string }>('/sys/user/update', { id: data.userId, roleIds: data.roleIds }) -} - -// ==================== Role ==================== - -export async function getRoleList(data: PageParams & { name?: string }) { - return post<{ data: PageResult }>('/sys/role/list', { ...data, name: data.keyword }) -} - -export async function getAllRoles() { - return post<{ data: { list: SystemRole[] } }>('/sys/role/list', { current: 1, pageSize: 1000 }) -} - -export async function saveRole(data: Partial) { - return post<{ msg: string }>('/sys/role/create', data) -} - -export async function updateRole(data: Partial) { - return post<{ msg: string }>('/sys/role/update', data) -} - -export async function deleteRole(ids: string[]) { - return post<{ msg: string }>('/sys/role/delete', { ids }) -} - -export async function grantMenu(data: { roleId: string; menuIds: string[] }) { - return post<{ msg: string }>('/sys/role/update', { id: data.roleId, menuIds: data.menuIds }) -} - -// ==================== Menu ==================== - -export async function getAllMenuTree() { - return get<{ data: { list: SystemMenu[] } }>('/sys/menu/list') -} - -export async function getUserMenuTree() { - // Get current user info (including menus) - return get<{ data: SystemUser }>('/auth/info') -} - -export async function saveMenu(data: Partial) { - return post<{ msg: string }>('/sys/menu/create', data) -} - -export async function updateMenu(data: Partial) { - return post<{ msg: string }>('/sys/menu/update', data) -} - -export async function deleteMenu(id: string) { - return post<{ msg: string }>('/sys/menu/delete', { ids: [id] }) -} - -// ==================== Client ==================== - -export async function getClientList(data: PageParams & { clientId?: string; name?: string }) { - return post<{ data: PageResult }>('/sys/client/list', { ...data, name: data.keyword }) -} - -export async function saveClient(data: Partial) { - return post<{ msg: string }>('/sys/client/create', data) -} - -export async function updateClient(data: Partial) { - return post<{ msg: string }>('/sys/client/update', data) -} - -export async function deleteClient(ids: string[]) { - return post<{ msg: string }>('/sys/client/delete', { ids }) -} - -// ==================== File ==================== - -export async function getFileList(data: PageParams & { name?: string }) { - return post<{ data: PageResult }>('/file/oss/getFileList', data) -} - -export async function uploadFile(_file: File) { - const formData = new FormData(); formData.append('file', _file) - return post<{ data: { file: SystemOss }; msg: string }>('/file/oss/upload', formData) -} - -export async function deleteFile(ids: string[]) { - return post<{ msg: string }>('/file/oss/delete', { ids }) -} - -// ==================== Operation Log ==================== - -export async function getOperationLogList(data: PageParams & { clientId?: string; method?: string; statusCode?: number }) { - return post<{ data: PageResult }>('/sys/log/list', data) -} diff --git a/src/components/AIBanner.tsx b/src/components/AIBanner.tsx index fd80b46..0225bb2 100644 --- a/src/components/AIBanner.tsx +++ b/src/components/AIBanner.tsx @@ -1,4 +1,4 @@ -import React from 'react' + import { motion } from 'framer-motion' export default function AIBanner() { diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index 37c6002..f1de810 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import { Command } from 'cmdk' import { useNavigate } from 'react-router-dom' -import { Search, Monitor, Moon, Sun, Laptop, Palette } from 'lucide-react' +import { Search, Monitor, Moon, Sun, Laptop } from 'lucide-react' import { useAppStore } from '@/store/app' import { useAuthStore } from '@/store/auth' import './cmdk.css' // We'll add some styles for cmdk @@ -10,7 +10,7 @@ export default function CommandPalette() { const { cmdKOpen, setCmdKOpen, setThemeHue } = useAppStore() const menus = useAuthStore(s => s.menus) const navigate = useNavigate() - const [theme, setTheme] = useState(document.documentElement.classList.contains('dark') ? 'dark' : 'light') + const [, setTheme] = useState(document.documentElement.classList.contains('dark') ? 'dark' : 'light') useEffect(() => { const down = (e: KeyboardEvent) => { diff --git a/src/components/ParticleBackground.tsx b/src/components/ParticleBackground.tsx index ee5252e..71b1d69 100644 --- a/src/components/ParticleBackground.tsx +++ b/src/components/ParticleBackground.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import { useEffect, useRef } from 'react'; interface Particle { x: number; diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx index ceb4df4..f42af27 100644 --- a/src/components/TabBar.tsx +++ b/src/components/TabBar.tsx @@ -32,7 +32,7 @@ export default function TabBar() { // Auto-register tab on route change useEffect(() => { const path = location.pathname - if (path === '/login') return + if (path === '/login' || path === '/') return const title = resolveTitle(menus || [], path) || path.split('/').filter(Boolean).pop()?.replace(/^\w/, c => c.toUpperCase()) || 'Page' addTab({ path, title, closable: path !== '/dashboard' }) diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..bbba7e0 --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent } diff --git a/src/layouts/AdminLayout.tsx b/src/layouts/AdminLayout.tsx index d8e2c26..187050c 100644 --- a/src/layouts/AdminLayout.tsx +++ b/src/layouts/AdminLayout.tsx @@ -1,5 +1,5 @@ -import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom' -import { AnimatePresence, motion } from 'framer-motion' +import { NavLink, useNavigate, useLocation, useOutlet } from 'react-router-dom' +import { motion } from 'framer-motion' import CommandPalette from '@/components/CommandPalette' import { LayoutDashboard, Users, Shield, MessageSquare, FolderTree, Leaf, @@ -210,6 +210,7 @@ function LayoutShell({ sidebarOpen, mobileMenuOpen, toggleSidebar, setMobileMenu }) { const { setCmdKOpen } = useAppStore() const location = useLocation() + const outlet = useOutlet() const spotlightRef = useRef(null) useEffect(() => { @@ -272,7 +273,7 @@ function LayoutShell({ sidebarOpen, mobileMenuOpen, toggleSidebar, setMobileMenu + + +
+ {commonIcons.map(name => { + const Cmp = (Icons as any)[name] + return ( + + ) + })} +
+
+ + ) +} + export default function Menus() { const [menus, setMenus] = useState([]) const [loading, setLoading] = useState(false) @@ -89,13 +127,13 @@ export default function Menus() { // Dialog State const [dialogOpen, setDialogOpen] = useState(false) const [editingMenu, setEditingMenu] = useState(null) - const [formData, setFormData] = useState({ title: '', name: '', path: '', icon: '', sort: 0, category: 1, parentId: '' }) + const [formData, setFormData] = useState({ title: '', name: '', path: '', code: '', permission: '', icon: '', sort: 0, category: 1, parentId: '' }) const fetchMenus = async () => { setLoading(true) try { const res = await getMenuTree() - if (res.data) setMenus(res.data) + if (res) setMenus(res) } finally { setLoading(false) } } @@ -116,18 +154,23 @@ export default function Menus() { const openCreateDialog = (parentId: string = '') => { setEditingMenu(null) - setFormData({ title: '', name: '', path: '', icon: '', sort: 1, category: 1, parentId }) + setFormData({ title: '', name: '', path: '', code: '', permission: '', icon: '', sort: 1, category: 1, parentId }) setDialogOpen(true) } const openEditDialog = (menu: SystemMenu) => { setEditingMenu(menu) - setFormData({ title: menu.title || '', name: menu.name || '', path: menu.path || '', icon: menu.icon || '', sort: menu.sort || 1, category: menu.category || 1, parentId: menu.parentId || '' }) + setFormData({ + title: menu.title || '', name: menu.name || '', path: menu.path || '', + code: menu.code || '', permission: menu.permission || '', + icon: menu.icon || '', sort: menu.sort || 1, + category: menu.category || 1, parentId: menu.parentId || '' + }) setDialogOpen(true) } const handleSave = async () => { - if (editingMenu) await updateMenu(editingMenu.id, formData) + if (editingMenu) await updateMenu({ id: editingMenu.id, ...formData }) else await createMenu(formData) setDialogOpen(false) fetchMenus() @@ -135,7 +178,7 @@ export default function Menus() { const handleDelete = async (id: string) => { if (confirm('确定要删除这个菜单节点吗?其子节点也会受到影响。')) { - await deleteMenu(id) + await deleteMenu([id]) fetchMenus() } } @@ -191,11 +234,24 @@ export default function Menus() { - + {editingMenu ? '编辑菜单' : '新增菜单'}
+
+ +
+ + +
+
@@ -213,21 +269,37 @@ export default function Menus() {
- - setFormData({ ...formData, title: e.target.value })} className="col-span-3" /> + + setFormData({ ...formData, title: e.target.value })} className="col-span-3" placeholder={formData.category === 1 ? '如: 用户管理' : '如: 新增用户'} />
- setFormData({ ...formData, name: e.target.value })} className="col-span-3" placeholder="如: system_users" /> + setFormData({ ...formData, name: e.target.value })} className="col-span-3" placeholder="如: UserList" />
- - setFormData({ ...formData, path: e.target.value })} className="col-span-3" placeholder="/system/users" /> -
-
- - setFormData({ ...formData, icon: e.target.value })} className="col-span-3" placeholder="lucide-react 图标名" /> + + setFormData({ ...formData, code: e.target.value })} className="col-span-3" placeholder="如: systemRole" />
+ {formData.category === 1 && ( + <> +
+ + setFormData({ ...formData, path: e.target.value })} className="col-span-3" placeholder="如: /system/users" /> +
+
+ +
+ setFormData({ ...formData, icon: v })} /> +
+
+ + )} + {formData.category === 2 && ( +
+ + setFormData({ ...formData, permission: e.target.value })} className="col-span-3" placeholder="如: sys:user:add" /> +
+ )}
setFormData({ ...formData, sort: parseInt(e.target.value) || 0 })} className="col-span-3" /> diff --git a/src/pages/system/Roles.tsx b/src/pages/system/Roles.tsx index fb7bcfc..8d1a650 100644 --- a/src/pages/system/Roles.tsx +++ b/src/pages/system/Roles.tsx @@ -34,9 +34,9 @@ export default function Roles() { setLoading(true) try { const res = await getRoleList({ current: 1, pageSize: 10, ...search }) - if (res.data) { - setRoles(res.data.list) - setTotal(res.data.total) + if (res) { + setRoles(res.list) + setTotal(res.total) } } finally { setLoading(false) @@ -45,7 +45,7 @@ export default function Roles() { const fetchMenus = async () => { const res = await getMenuTree() - if (res.data) setAllMenus(res.data) + if (res) setAllMenus(res) } useEffect(() => { @@ -69,7 +69,7 @@ export default function Roles() { } const handleSave = async () => { - if (editingRole) await updateRole(editingRole.id, formData) + if (editingRole) await updateRole({ id: editingRole.id, ...formData }) else await createRole(formData) setDialogOpen(false) fetchRoles() @@ -77,20 +77,23 @@ export default function Roles() { const handleDelete = async (id: string) => { if (confirm('确定要删除这个角色吗?')) { - await deleteRole(id) + await deleteRole([id]) fetchRoles() } } const openPermDialog = (role: SystemRole) => { setActiveRole(role) - // mock some selected ids - setSelectedMenuIds(['1', '10', '11', '12']) + // Actually get the role's menus + setSelectedMenuIds(role.menuIds || []) setPermDialogOpen(true) } const handleSavePerms = async () => { - // In real app, call assignMenusToRole API + if (activeRole) { + await updateRole({ id: activeRole.id, menuIds: selectedMenuIds }) + fetchRoles() + } setPermDialogOpen(false) } @@ -182,7 +185,7 @@ export default function Roles() { {role.code} {role.sort} - {new Date(role.createdAt).toLocaleDateString('zh-CN')} + -
diff --git a/src/pages/system/Users.tsx b/src/pages/system/Users.tsx index 823413e..c1c562d 100644 --- a/src/pages/system/Users.tsx +++ b/src/pages/system/Users.tsx @@ -32,9 +32,9 @@ export default function UserManage() { setLoading(true) try { const res = await getUserList({ current: 1, pageSize: 10, ...search }) - if (res.data) { - setUsers(res.data.list) - setTotal(res.data.total) + if (res) { + setUsers(res.list) + setTotal(res.total) } } finally { setLoading(false) @@ -43,7 +43,7 @@ export default function UserManage() { const fetchRoles = async () => { const res = await getRoleList({ current: 1, pageSize: 100 }) - if (res.data) setAllRoles(res.data.list) + if (res) setAllRoles(res.list) } useEffect(() => { @@ -67,17 +67,17 @@ export default function UserManage() { const openEditDialog = (user: SystemUser) => { setEditingUser(user) setFormData({ - account: user.account, name: user.name, phone: user.phone || '', clientId: user.clientId || '', - roleIds: user.roles?.map(r => r.id) || [] + account: user.account, name: user.name, phone: user.phone || '', clientId: '', + roleIds: user.roles || [] }) setDialogOpen(true) } const handleSave = async () => { if (editingUser) { - await updateUser(editingUser.id, formData) + await updateUser({ id: editingUser.id, ...formData }) } else { - await createUser(formData) + await createUser({ ...formData, password: '123' }) // 默认密码 } setDialogOpen(false) fetchUsers() @@ -85,7 +85,7 @@ export default function UserManage() { const handleDelete = async (id: string) => { if (confirm('确定要删除这个用户吗?')) { - await deleteUser(id) + await deleteUser([id]) fetchUsers() } } @@ -147,7 +147,6 @@ export default function UserManage() { 账号 姓名 手机号 - 客户端来源 角色 创建时间 操作 @@ -171,26 +170,20 @@ export default function UserManage() { {user.account} {user.name} {user.phone || '-'} - - {user.clientId ? ( - - {user.clientId} - - ) : ( - System - )} -
- {user.roles?.map(r => ( - - {r.name} + {user.roles?.map(code => { + const role = allRoles.find(r => r.code === code) + return ( + + {role ? role.name : code} - )) || 无角色} + ) + }) || 无角色}
- {new Date(user.createdAt).toLocaleDateString('zh-CN')} + {user.createdAt ? new Date(user.createdAt * 1000).toLocaleDateString('zh-CN') : '-'} diff --git a/src/store/auth.ts b/src/store/auth.ts index ee89448..63324a6 100644 --- a/src/store/auth.ts +++ b/src/store/auth.ts @@ -1,7 +1,6 @@ import { create } from 'zustand' import type { SystemUser, SystemMenu } from '@/api/system' -import { getUserMenuTree } from '@/api/systemCrud' -import { logout as apiLogout } from '@/api/system' +import { getUserInfo, logout as apiLogout } from '@/api/system/auth' const TOKEN_KEY = 'token' const USER_KEY = 'user' @@ -66,8 +65,8 @@ export const useAuthStore = create((set, get) => ({ refreshMenus: async () => { if (!get().isAuthenticated) return try { - const res = await getUserMenuTree() - const menus = (res.data as any).menus || [] + const userInfo = await getUserInfo() + const menus = userInfo.menus || [] set({ menus, permissions: extractPermissions(menus), hasFetchedMenus: true }) } catch (e) { console.error('获取菜单失败:', e) diff --git a/tsconfig.app.json b/tsconfig.app.json index 7abe077..61649e8 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -8,6 +8,7 @@ "paths": { "@/*": ["./src/*"] }, "types": ["vite/client"], "skipLibCheck": true, + "ignoreDeprecations": "6.0", /* Bundler mode */ "moduleResolution": "bundler",