refactor: 系统权限模块重构,迁移至PostgreSQL并对齐新设计
- 切换数据库为PostgreSQL,更新依赖与配置 - 重构权限体系,使用sys_user/sys_role/sys_menu等新实体替代旧模型 - 移除旧的用户角色/权限/系统设置相关代码与实体 - 新增字典、菜单管理模块与基础CRUD接口 - 优化鉴权逻辑,基于菜单权限标识进行接口校验 - 前端同步更新API、路由与权限控制逻辑 - 删除冗余的v-permission指令与相关代码
This commit is contained in:
1 parent
98c22f6afb
commit
0a8103f33d
67 files changed
+3218
-1879
No files matched your search
@@ -0,0 +1,47 @@
|
||||
# Checklist - 系统管理与认证授权模块对齐新设计
|
||||
|
||||
## 数据库与配置检查
|
||||
- [x] server 已安装 pg / pg-hstore 依赖
|
||||
- [x] config.default.ts 使用 PostgreSQL(dialect=postgres、port 5432、database=xintong)
|
||||
- [x] database/db_init.sql 在 PostgreSQL 中执行成功
|
||||
- [x] 初始数据齐全:4 角色、11 条菜单(6 顶级 + 5 系统子菜单)、20 条字典、admin 账户、角色菜单绑定
|
||||
- [x] admin 账户密码为 admin123456(bcrypt hash 正确)
|
||||
|
||||
## 后端实体与接口检查
|
||||
- [x] 7 个系统域实体(sysUser/sysRole/sysUserRole/sysMenu/sysRoleMenu/sysDict/sysOperationLog)创建成功且表名正确
|
||||
- [x] 旧实体(user/role/userRole/permission/rolePermission/operationLog)已删除
|
||||
- [x] 登录接口基于 sys_user 校验,返回 userInfo/roles/menus
|
||||
- [x] 刷新 Token 与当前用户接口适配新模型
|
||||
- [x] RbacMiddleware 基于菜单 permission 标识鉴权,ADMIN 放行
|
||||
- [x] 用户管理接口字段为 realName/phone/email,返回不包含密码哈希
|
||||
- [x] 角色管理接口字段为 roleCode/roleName/sortOrder
|
||||
- [x] 角色菜单配置接口(/roles/:id/menus)读写 sys_role_menu 正确
|
||||
- [x] 菜单管理接口:菜单树查询与 CRUD 正常
|
||||
- [x] 数据字典接口:列表/类型筛选/CRUD 正常(修复 types() 的 PostgreSQL GROUP BY/ORDER BY SQL 问题)
|
||||
- [x] 操作日志接口与中间件使用新字段(user_id/module/action/method/params/ip/created_at)
|
||||
- [x] system.controller.ts / system.service.ts 已删除,无残留引用
|
||||
|
||||
## 前端检查
|
||||
- [x] api/auth.ts 返回类型含 realName/roles/menus
|
||||
- [x] api/user.ts、api/role.ts 字段更新,权限接口改为菜单接口
|
||||
- [x] api/menu.ts、api/dict.ts 新增,api/system.ts 已删除
|
||||
- [x] stores/permission.ts 管理用户信息与菜单树(login 完整写入 roles/menus)
|
||||
- [x] 侧边栏菜单由用户菜单树动态渲染(含图标、两级结构)
|
||||
- [x] 路由改为静态全量注册,守卫按菜单权限标识控制;未开发模块显示占位页(替代原动态路由注册方案,修复深层刷新跳登录问题)
|
||||
- [x] 设置页路由与菜单已移除
|
||||
- [x] 用户管理页字段为姓名/手机/邮箱
|
||||
- [x] 角色管理页权限配置为菜单树勾选
|
||||
- [x] 菜单管理页(树形表格 CRUD)可用
|
||||
- [x] 数据字典页(列表+类型筛选 CRUD)可用
|
||||
- [x] 操作日志页字段与筛选更新
|
||||
- [x] v-permission 指令及引用已移除
|
||||
- [x] views/settings 目录已删除
|
||||
|
||||
## 联调与边界检查
|
||||
- [x] admin 登录 → 仪表盘 → 系统管理各子页面全流程走通(含深层链接整页刷新)
|
||||
- [x] 业务员角色仅显示授权菜单,访问未授权接口返回 403(SALESMAN/WORKER 实测 403)
|
||||
- [x] 写操作自动记录操作日志且可查询
|
||||
- [x] 无菜单角色登录不报错,侧边栏仅显示仪表盘(WORKER 实测 menus=0)
|
||||
- [x] 删除被占用角色被拦截
|
||||
- [x] 重复用户名/角色编码校验生效
|
||||
- [x] yarn build(server/client)无 TypeScript 编译错误
|
||||
@@ -0,0 +1,125 @@
|
||||
# 系统管理与认证授权模块对齐新设计 Spec
|
||||
|
||||
## Why
|
||||
系统设计已完成调整(鑫潼智电系统):数据库从 MySQL 6 表升级为 PostgreSQL 37 表;系统管理域改为 7 张 `sys_` 前缀表;RBAC 由"权限码"模型改为"菜单级"模型(`sys_menu`/`sys_role_menu`);新增数据字典(`sys_dict`);原"系统设置"模块在新设计中无对应表。现有系统管理与认证授权代码仍基于旧模型,需要整体对齐新设计。
|
||||
|
||||
## What Changes
|
||||
- **数据库引擎切换**:MySQL → PostgreSQL 15+(连接配置、安装 `pg`/`pg-hstore` 驱动;`database/db_init.sql` 已更新为 PostgreSQL 脚本)
|
||||
- **系统域数据模型重构**(7 张表,替代原 6 张表):
|
||||
- `sys_user`:username / password_hash / real_name / phone / email / status / last_login_at(移除 nickname、avatar、last_login_ip)
|
||||
- `sys_role`:role_code / role_name / description / sort_order(移除 status)
|
||||
- `sys_user_role`:user_id / role_id(不变)
|
||||
- `sys_menu`:菜单级 RBAC,menu_type 1目录/2菜单(本次不启用 3按钮),含 menu_path / component / icon / sort_order / permission / visible(替代 permissions 表)
|
||||
- `sys_role_menu`:role_id / menu_id(替代 role_permissions 表)
|
||||
- `sys_dict`:**新增**数据字典表
|
||||
- `sys_operation_log`:字段精简为 user_id / module / action / method / params / ip / created_at(移除 username、url、user_agent、request_params、response_data、status_code、duration、error_message)
|
||||
- **认证逻辑适配**:登录基于 `sys_user` + `password_hash` 比对,更新 `last_login_at`;登录返回 userInfo(含 realName)、roles(role_code)、menus(角色菜单树)
|
||||
- **RBAC 重构**:菜单级权限。前端由菜单树驱动侧边栏与路由;后端接口鉴权按"接口路径 → 菜单 permission 标识"校验,ADMIN 角色全量放行;**按钮级权限暂不保留**(v-permission 指令移除)
|
||||
- **系统设置模块移除**:删除前后端系统设置代码(settings 页面、system.controller/system.service、api/system.ts)
|
||||
- **新增菜单管理模块**:sys_menu 树形 CRUD(目录/菜单两级)
|
||||
- **新增数据字典模块**:sys_dict CRUD 与类型分组查询
|
||||
- **操作日志对齐**:实体、中间件、接口、前端页面字段更新
|
||||
- **初始数据补齐**:4 个角色(ADMIN/SALESMAN/WORKER/CUSTOMER)、6 个顶级业务菜单 + 5 个系统管理子菜单、19 条字典数据、管理员账户 admin/admin123456(ADMIN 角色)及角色-菜单绑定
|
||||
|
||||
## Impact
|
||||
- Affected specs: smart-grid-iot-admin(已完成项目,本次为增量调整)
|
||||
- Affected code:
|
||||
- server:config/config.default.ts、package.json、entity 全部系统域实体、controller(auth/user/role/log/menu/dict)、service(auth/user/role/log/menu/dict)、middleware(auth/rbac/log)、dto
|
||||
- client:api(auth/user/role/log/menu/dict)、stores/permission.ts、router/index.ts、layouts/index.vue、views(system/user、system/role、log/operation、新增 system/menu、system/dict)、删除 views/settings
|
||||
- database/db_init.sql:补充初始数据(管理员账户、角色菜单绑定、系统管理子菜单)
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: PostgreSQL 数据库连接
|
||||
系统 SHALL 使用 PostgreSQL 15+ 作为数据库:
|
||||
- server 依赖安装 `pg`、`pg-hstore`
|
||||
- `config.default.ts` 的 sequelize 数据源 dialect 为 `postgres`,连接 xintong 库
|
||||
- 系统域 7 张表通过 `database/db_init.sql` 创建(Sequelize `sync: false`)
|
||||
|
||||
#### Scenario: 服务连接 PostgreSQL 成功
|
||||
- **WHEN** 启动后端服务且本机已按 db_init.sql 建好库表
|
||||
- **THEN** 服务正常启动,登录、查询等接口可正常读写 sys_ 系列表
|
||||
|
||||
### Requirement: 菜单级 RBAC(sys_menu)
|
||||
系统 SHALL 以菜单为权限载体:
|
||||
- `sys_menu` 支持两级树:目录(menu_type=1)、菜单(menu_type=2);字段含 menu_path、component、icon、sort_order、permission、visible
|
||||
- 用户登录返回其角色(经 `sys_role_menu`)关联的菜单树;前端据此渲染侧边栏与动态路由
|
||||
- 后端接口鉴权:RbacMiddleware 将请求路径映射为权限标识,校验用户角色关联菜单的 permission 集合;ADMIN 角色(role_code=ADMIN)直接放行
|
||||
- 本次不做按钮级权限(menu_type=3、v-permission 指令移除)
|
||||
|
||||
#### Scenario: 登录用户仅看到被授权菜单
|
||||
- **WHEN** 业务员角色登录
|
||||
- **THEN** 侧边栏只渲染其角色已绑定的菜单,访问未授权接口返回 403
|
||||
|
||||
#### Scenario: ADMIN 全量访问
|
||||
- **WHEN** 运营管理员(ADMIN 角色)登录
|
||||
- **THEN** 可见全部菜单,所有受保护接口放行
|
||||
|
||||
### Requirement: 菜单管理模块
|
||||
系统 SHALL 提供菜单管理能力:
|
||||
- 菜单树查询(全部菜单,供角色配置使用)
|
||||
- 目录/菜单的创建、编辑、删除、排序、可见性切换
|
||||
- 权限标识(permission)维护
|
||||
|
||||
### Requirement: 数据字典模块
|
||||
系统 SHALL 提供数据字典管理能力:
|
||||
- sys_dict 的创建、编辑、删除
|
||||
- 按 dict_type 分组查询、状态筛选
|
||||
|
||||
### Requirement: 初始数据
|
||||
系统 SHALL 通过 db_init.sql 初始化:
|
||||
- 4 个角色:ADMIN(运营管理员)/ SALESMAN(业务员)/ WORKER(共享电工)/ CUSTOMER(客户)
|
||||
- 6 个顶级业务菜单(客户管理/项目管理/运营派单/设备监控/统计报表/系统管理)与系统管理下 5 个子菜单(用户/角色/菜单/数据字典/操作日志)
|
||||
- 管理员账户 admin/admin123456(bcrypt hash,角色 ADMIN)
|
||||
- 角色-菜单绑定:ADMIN 绑定全部菜单;SALESMAN 绑定客户/项目/统计报表等业务菜单;WORKER/CUSTOMER 无后台菜单
|
||||
- 19 条字典数据(customer_status/equip_type/device_type/dispatch_status/pay_cycle/dispatch_type)
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 用户管理模块
|
||||
The system SHALL 管理 sys_user:
|
||||
- 列表支持分页、关键词搜索(username/realName/phone)、状态筛选、角色筛选
|
||||
- 新增/编辑字段:username、password、realName、phone、email、status、角色分配(roleIds)
|
||||
- 删除、批量删除、状态切换(原能力保留)
|
||||
- 返回数据不包含 password_hash
|
||||
|
||||
#### Scenario: 管理员创建用户
|
||||
- **WHEN** 管理员填写 username/realName/phone/email/角色并提交
|
||||
- **THEN** 系统校验唯一性与必填项,bcrypt 加密密码写入 sys_user,分配 sys_user_role,列表刷新
|
||||
|
||||
### Requirement: 角色管理模块
|
||||
The system SHALL 管理 sys_role:
|
||||
- 列表支持分页、关键词搜索(roleCode/roleName)、关联用户数统计
|
||||
- 新增/编辑字段:roleCode、roleName、description、sortOrder
|
||||
- 权限配置由"权限码树"改为"菜单树勾选"(sys_role_menu)
|
||||
- 删除时若关联用户则禁止删除
|
||||
|
||||
### Requirement: 操作日志模块
|
||||
The system SHALL 记录操作日志:
|
||||
- 字段:user_id、module、action、method、params、ip、created_at
|
||||
- 日志中间件按新字段写入,仅记录写操作(POST/PUT/DELETE)
|
||||
- 查询接口支持按操作人/模块/时间范围筛选
|
||||
- 前端日志页字段与接口对齐
|
||||
|
||||
### Requirement: 认证授权服务
|
||||
The system SHALL 适配新用户模型:
|
||||
- 登录:username + password 校验(sys_user.password_hash)、禁用校验、更新 last_login_at
|
||||
- 登录返回:accessToken、refreshToken、userInfo(id/username/realName)、roles(role_code 数组)、menus(角色菜单树)
|
||||
- 刷新 Token、当前用户信息接口同步适配
|
||||
|
||||
### Requirement: 前端动态菜单与路由
|
||||
The system SHALL 由菜单树驱动前端:
|
||||
- 侧边栏菜单由用户 menus 渲染(目录→子菜单两级,含图标)
|
||||
- 动态路由按 sys_menu.menu_path/component 注册;component 缺失(未开发业务模块)时渲染"功能开发中"占位页
|
||||
- 登录跳转仪表盘(仪表盘保持固定路由,不纳入菜单表)
|
||||
- 移除设置页相关路由与菜单
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: 系统设置模块(站点名称/Logo/会话超时/密码策略/IP白名单)
|
||||
**Reason**: 新设计无对应表,模块整体移除。
|
||||
**Migration**: 删除 server 端 system.controller.ts/system.service.ts、client 端 views/settings 页面与 api/system.ts,以及相关路由、菜单、日志映射与中间件引用。
|
||||
|
||||
### Requirement: 权限码 RBAC 与按钮级权限(permissions/role_permissions + v-permission)
|
||||
**Reason**: 新设计采用菜单级 RBAC,按钮级权限暂不保留。
|
||||
**Migration**: 删除 permissions/role_permissions 相关实体、服务与 DTO,由 sys_menu/sys_role_menu 替代;移除前端 v-permission 指令及按钮级权限判断;路由 meta.permission 改为基于菜单 permission 标识。
|
||||
@@ -0,0 +1,107 @@
|
||||
# Tasks - 系统管理与认证授权模块对齐新设计
|
||||
|
||||
## 阶段一:数据库与基础配置
|
||||
- [x] Task 1: 切换数据库引擎为 PostgreSQL
|
||||
- [x] 在 server/package.json 安装 `pg`、`pg-hstore` 依赖
|
||||
- [x] 修改 server/src/config/config.default.ts:sequelize dialect 改为 `postgres`,host/port(5432)/database(xintong)/username/password 环境变量(默认 postgres)
|
||||
- [x] 检查 config.local.ts 的 sync 配置合理性(表由 SQL 脚本创建,保持 sync: false 或按需调整)
|
||||
- [x] 验证:yarn build 通过,服务可连接 PostgreSQL
|
||||
|
||||
- [x] Task 2: 补充 database/db_init.sql 初始数据
|
||||
- [x] 补充 sys_menu 系统管理目录下的 5 个子菜单(用户管理/角色管理/菜单管理/数据字典/操作日志,含 menu_path/component/permission/sort_order)
|
||||
- [x] 补充 sys_user:admin/admin123456(用 Node bcryptjs 生成 hash 填入 password_hash,real_name=运营管理员)
|
||||
- [x] 补充 sys_user_role:admin → ADMIN 角色
|
||||
- [x] 补充 sys_role_menu:ADMIN 绑定全部菜单;SALESMAN 绑定客户/项目/统计报表等业务菜单
|
||||
- [x] 验证:在 PostgreSQL 中执行脚本无报错,各表初始数据条数正确
|
||||
|
||||
## 阶段二:后端重构
|
||||
- [x] Task 3: 重构系统域实体层(7 个实体)
|
||||
- [x] 新建 sysUser.entity.ts(sys_user:username/password_hash/real_name/phone/email/status/last_login_at)
|
||||
- [x] 新建 sysRole.entity.ts(sys_role:role_code/role_name/description/sort_order)
|
||||
- [x] 新建 sysUserRole.entity.ts(sys_user_role)
|
||||
- [x] 新建 sysMenu.entity.ts(sys_menu:parent_id/menu_name/menu_path/component/icon/sort_order/menu_type/permission/visible)
|
||||
- [x] 新建 sysRoleMenu.entity.ts(sys_role_menu)
|
||||
- [x] 新建 sysDict.entity.ts(sys_dict:dict_type/dict_label/dict_value/sort_order/status)
|
||||
- [x] 新建 sysOperationLog.entity.ts(sys_operation_log:user_id/module/action/method/params/ip/created_at)
|
||||
- [x] 删除旧实体 user/role/userRole/permission/rolePermission/operationLog 的 .entity.ts 文件
|
||||
- [x] 验证:yarn build 通过
|
||||
|
||||
- [x] Task 4: 重构认证与 RBAC
|
||||
- [x] auth.service.ts:登录校验 sys_user.password_hash,更新 last_login_at;返回 accessToken/refreshToken/userInfo(id/username/realName)/roles(role_code 数组)/menus(菜单树)
|
||||
- [x] auth.controller.ts:登录/刷新/登出/当前用户接口适配
|
||||
- [x] 新增菜单查询能力:permission.service(或新 menu.service)提供 getUserMenus(userId)(角色经 sys_role_menu 关联 sys_menu 的树)与 checkUserPermission(userId, code)
|
||||
- [x] rbac.middleware.ts:路径→权限标识映射改为基于 sys_menu.permission;ADMIN 角色放行;未授权返回 403
|
||||
- [x] 验证:admin 登录返回 menus;无权限角色访问受保护接口返回 403
|
||||
|
||||
- [x] Task 5: 重构用户管理
|
||||
- [x] user.service.ts:字段适配 real_name/phone/email(删除 nickname/avatar/last_login_ip),搜索条件更新,返回数据剔除 password_hash
|
||||
- [x] user.controller.ts:接口保持不变,入参字段对齐
|
||||
- [x] dto/user.dto.ts:字段更新(realName/phone/email/roleIds)
|
||||
- [x] 验证:用户 CRUD 与角色分配联调通过
|
||||
|
||||
- [x] Task 6: 重构角色管理
|
||||
- [x] role.service.ts:字段适配 role_code/role_name/sort_order(删除 status);权限接口改为菜单(getRoleMenus 返回角色已勾选菜单树、updateRoleMenus 批量写 sys_role_menu)
|
||||
- [x] role.controller.ts:权限相关接口改为 `/roles/:id/menus`
|
||||
- [x] dto/role.dto.ts:字段更新
|
||||
- [x] 验证:角色列表、菜单勾选保存联调通过
|
||||
|
||||
- [x] Task 7: 新增菜单管理模块
|
||||
- [x] 新建 menu.controller.ts + menu.service.ts:菜单树查询(含全部菜单供角色配置)、创建/编辑/删除目录或菜单、排序与可见性维护
|
||||
- [x] 新建 dto/menu.dto.ts
|
||||
- [x] 验证:菜单树 CRUD 联调通过
|
||||
|
||||
- [x] Task 8: 新增数据字典模块
|
||||
- [x] 新建 dict.controller.ts + dict.service.ts:分页列表、按 dict_type 分组查询、创建/编辑/删除
|
||||
- [x] 新建 dto/dict.dto.ts
|
||||
- [x] 验证:字典 CRUD 与类型筛选联调通过
|
||||
|
||||
- [x] Task 9: 重构操作日志模块
|
||||
- [x] log.service.ts / log.controller.ts:字段对齐(user_id/module/action/method/params/ip),查询支持按 user/module/时间范围筛选
|
||||
- [x] log.middleware.ts:按新字段写入(params 存请求体),移除 url/user_agent/status_code/duration/error_message
|
||||
- [x] 验证:写操作自动记录日志,查询接口正常
|
||||
|
||||
- [x] Task 10: 移除系统设置模块
|
||||
- [x] 删除 system.controller.ts / system.service.ts
|
||||
- [x] 清理 rbac/log 中间件及配置中 settings 相关映射
|
||||
- [x] 验证:yarn build 通过,无残留引用
|
||||
|
||||
## 阶段三:前端重构
|
||||
- [x] Task 11: 前端 API 层适配
|
||||
- [x] api/auth.ts:登录/当前用户返回类型更新(userInfo.realName、roles、menus)
|
||||
- [x] api/user.ts:字段更新(realName/phone/email)
|
||||
- [x] api/role.ts:角色字段更新;权限接口改为菜单接口(getRoleMenus/updateRoleMenus)
|
||||
- [x] 新增 api/menu.ts、api/dict.ts
|
||||
- [x] 删除 api/system.ts
|
||||
- [x] api/log.ts:字段与筛选参数更新
|
||||
|
||||
- [x] Task 12: 前端权限 Store 与动态路由
|
||||
- [x] stores/permission.ts:UserInfo 更新(id/username/realName/roles/menus),提供 menus 状态
|
||||
- [x] router/index.ts:移除 settings 路由;基于用户 menus 注册动态路由(menu_path→component 映射,缺失组件渲染"功能开发中"占位页);登录跳转仪表盘
|
||||
- [x] layouts/index.vue:侧边栏菜单由 menus 动态渲染(目录→子菜单两级 + 图标),移除静态菜单与设置分组;用户名显示 realName
|
||||
- [x] 移除 v-permission 指令(directives/permission.ts)及引用
|
||||
- [x] 验证:admin 登录后侧边栏显示全部菜单,业务占位页可访问
|
||||
|
||||
- [x] Task 13: 前端页面适配
|
||||
- [x] views/system/user/index.vue:字段改为 realName/phone/email
|
||||
- [x] views/system/role/index.vue:权限配置弹窗改为菜单树勾选
|
||||
- [x] 新增 views/system/menu/index.vue(树形表格 CRUD)
|
||||
- [x] 新增 views/system/dict/index.vue(列表 + 类型筛选 CRUD)
|
||||
- [x] views/log/operation.vue:字段与筛选更新
|
||||
- [x] 删除 views/settings/basic.vue、views/settings/security.vue
|
||||
- [x] 验证:各页面构建通过、交互正常
|
||||
|
||||
## 阶段四:联调与验证
|
||||
- [ ] Task 14: 全流程联调
|
||||
- [ ] 前端 yarn dev + 后端服务 + PostgreSQL 环境启动
|
||||
- [ ] admin 登录 → 仪表盘 → 系统管理(用户/角色/菜单/字典/日志)全流程走通
|
||||
- [ ] 业务员角色仅见授权菜单,未授权接口 403
|
||||
- [ ] 日志自动记录写操作并可查询
|
||||
- [ ] 边界检查:无菜单角色登录、删除被占用角色、重复用户名/角色编码
|
||||
|
||||
## Task Dependencies
|
||||
- [Task 2] depends on [Task 1](PG 环境就绪后可执行 SQL)
|
||||
- [Task 3] 先行;[Task 4-10] depends on [Task 3](实体)
|
||||
- [Task 11] depends on [Task 5-9](接口就绪)
|
||||
- [Task 12] depends on [Task 11](API 类型)
|
||||
- [Task 13] depends on [Task 11]
|
||||
- [Task 14] depends on [Task 4-13]
|
||||
+38
-3
@@ -1,15 +1,50 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
// 菜单节点(内联定义,避免与 api/menu.ts 产生依赖)
|
||||
interface MenuNode {
|
||||
id: number;
|
||||
parentId: number | null;
|
||||
menuName: string;
|
||||
menuPath?: string | null;
|
||||
component?: string | null;
|
||||
icon?: string | null;
|
||||
sortOrder: number;
|
||||
menuType: number; // 1目录 2菜单
|
||||
permission?: string | null;
|
||||
visible: number;
|
||||
children?: MenuNode[];
|
||||
}
|
||||
|
||||
export interface LoginResult {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
userInfo: {
|
||||
id: number;
|
||||
username: string;
|
||||
realName: string;
|
||||
};
|
||||
roles: string[];
|
||||
menus: MenuNode[];
|
||||
}
|
||||
|
||||
export interface CurrentUserInfo {
|
||||
id: number;
|
||||
username: string;
|
||||
realName: string;
|
||||
roles: string[];
|
||||
menus: MenuNode[];
|
||||
}
|
||||
|
||||
export const authApi = {
|
||||
login: (data: { username: string; password: string }) =>
|
||||
request.post('/auth/login', data),
|
||||
request.post<LoginResult>('/auth/login', data),
|
||||
|
||||
logout: () =>
|
||||
request.post('/auth/logout'),
|
||||
|
||||
refreshToken: (refreshToken: string) =>
|
||||
request.post('/auth/refresh', { refreshToken }),
|
||||
request.post<{ accessToken: string }>('/auth/refresh', { refreshToken }),
|
||||
|
||||
getCurrentUser: () =>
|
||||
request.get('/auth/currentUser'),
|
||||
request.get<CurrentUserInfo>('/auth/currentUser'),
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
export interface DictItem {
|
||||
id: number;
|
||||
dictType: string;
|
||||
dictLabel: string;
|
||||
dictValue: string;
|
||||
sortOrder: number;
|
||||
status: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface DictQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
dictType?: string;
|
||||
keyword?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface DictListResult {
|
||||
list: DictItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface DictForm {
|
||||
dictType: string;
|
||||
dictLabel: string;
|
||||
dictValue: string;
|
||||
sortOrder?: number;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export const dictApi = {
|
||||
getDictList: (params?: DictQuery) =>
|
||||
request.get<DictListResult>('/dicts', { params }),
|
||||
|
||||
getDictTypes: () =>
|
||||
request.get<string[]>('/dicts/types'),
|
||||
|
||||
createDict: (data: DictForm) =>
|
||||
request.post('/dicts', data),
|
||||
|
||||
updateDict: (id: number, data: Partial<DictForm>) =>
|
||||
request.put(`/dicts/${id}`, data),
|
||||
|
||||
deleteDict: (id: number) =>
|
||||
request.delete(`/dicts/${id}`),
|
||||
};
|
||||
+28
-21
@@ -1,36 +1,43 @@
|
||||
import request from '@/utils/request'
|
||||
import request from '@/utils/request';
|
||||
|
||||
export interface LogItem {
|
||||
id: number
|
||||
userId?: number
|
||||
username?: string
|
||||
action: string
|
||||
module?: string
|
||||
method: string
|
||||
url: string
|
||||
ipAddress: string
|
||||
statusCode: number
|
||||
duration: number
|
||||
createdAt: string
|
||||
id: number;
|
||||
userId?: number;
|
||||
module?: string;
|
||||
action: string;
|
||||
method: string;
|
||||
params?: string;
|
||||
ip: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface LogQuery {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
operator?: string
|
||||
actionType?: string
|
||||
moduleName?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
userId?: number;
|
||||
module?: string;
|
||||
action?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
export interface LogListResult {
|
||||
list: LogItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export const logApi = {
|
||||
getLogList: (params: LogQuery) =>
|
||||
request.get('/logs', { params }),
|
||||
request.get<LogListResult>('/logs', { params }),
|
||||
|
||||
getLogDetail: (id: number) =>
|
||||
request.get<LogItem>(`/logs/${id}`),
|
||||
|
||||
exportLogs: (params: Omit<LogQuery, 'page' | 'pageSize'>) =>
|
||||
request.get('/logs/export', {
|
||||
params,
|
||||
responseType: 'blob',
|
||||
}),
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
// 菜单节点
|
||||
export interface MenuNode {
|
||||
id: number;
|
||||
parentId: number | null;
|
||||
menuName: string;
|
||||
menuPath?: string | null;
|
||||
component?: string | null;
|
||||
icon?: string | null;
|
||||
sortOrder: number;
|
||||
menuType: number; // 1目录 2菜单
|
||||
permission?: string | null;
|
||||
visible: number;
|
||||
children?: MenuNode[];
|
||||
}
|
||||
|
||||
export interface MenuForm {
|
||||
parentId?: number;
|
||||
menuName: string;
|
||||
menuPath?: string;
|
||||
component?: string;
|
||||
icon?: string;
|
||||
sortOrder?: number;
|
||||
menuType: number;
|
||||
permission?: string;
|
||||
visible?: number;
|
||||
}
|
||||
|
||||
export const menuApi = {
|
||||
getMenuTree: () =>
|
||||
request.get<MenuNode[]>('/menus/tree'),
|
||||
|
||||
createMenu: (data: MenuForm) =>
|
||||
request.post('/menus', data),
|
||||
|
||||
updateMenu: (id: number, data: Partial<MenuForm>) =>
|
||||
request.put(`/menus/${id}`, data),
|
||||
|
||||
deleteMenu: (id: number) =>
|
||||
request.delete(`/menus/${id}`),
|
||||
};
|
||||
+20
-24
@@ -1,31 +1,22 @@
|
||||
import request from '@/utils/request';
|
||||
import type { MenuNode } from './menu';
|
||||
|
||||
export interface RoleInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
roleCode: string;
|
||||
roleName: string;
|
||||
description?: string;
|
||||
status: number;
|
||||
sortOrder: number;
|
||||
userCount: number;
|
||||
permissions?: any[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface PermissionTreeNode {
|
||||
export interface SysRole {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
type: string;
|
||||
parent_id?: number | null;
|
||||
sortOrder: number;
|
||||
icon?: string;
|
||||
route?: string;
|
||||
component?: string;
|
||||
status: number;
|
||||
children?: PermissionTreeNode[];
|
||||
checked?: boolean;
|
||||
roleCode: string;
|
||||
roleName: string;
|
||||
description?: string;
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export interface RoleListResult {
|
||||
@@ -35,28 +26,33 @@ export interface RoleListResult {
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface RoleMenusResult {
|
||||
menuTree: MenuNode[];
|
||||
menuIds: number[];
|
||||
}
|
||||
|
||||
export const roleApi = {
|
||||
getRoleList: (params?: { page?: number; pageSize?: number; keyword?: string }) =>
|
||||
request.get<RoleListResult>('/roles', { params }),
|
||||
|
||||
getAllRoles: () =>
|
||||
request.get<RoleInfo[]>('/roles/all'),
|
||||
request.get<SysRole[]>('/roles/all'),
|
||||
|
||||
getRoleDetail: (id: number) =>
|
||||
request.get<RoleInfo>(`/roles/${id}`),
|
||||
|
||||
createRole: (data: { name: string; code: string; description?: string; status?: number; sortOrder?: number }) =>
|
||||
createRole: (data: { roleCode: string; roleName: string; description?: string; sortOrder?: number }) =>
|
||||
request.post('/roles', data),
|
||||
|
||||
updateRole: (id: number, data: { name?: string; description?: string; status?: number; sortOrder?: number }) =>
|
||||
updateRole: (id: number, data: { roleCode?: string; roleName?: string; description?: string; sortOrder?: number }) =>
|
||||
request.put(`/roles/${id}`, data),
|
||||
|
||||
deleteRole: (id: number) =>
|
||||
request.delete(`/roles/${id}`),
|
||||
|
||||
getRolePermissions: (id: number) =>
|
||||
request.get<PermissionTreeNode[]>(`/roles/${id}/permissions`),
|
||||
getRoleMenus: (id: number) =>
|
||||
request.get<RoleMenusResult>(`/roles/${id}/menus`),
|
||||
|
||||
updateRolePermissions: (id: number, permissionIds: number[]) =>
|
||||
request.put(`/roles/${id}/permissions`, { permissionIds }),
|
||||
updateRoleMenus: (id: number, menuIds: number[]) =>
|
||||
request.put(`/roles/${id}/menus`, { menuIds }),
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
export interface BasicSettings {
|
||||
siteName: string;
|
||||
logo: string;
|
||||
sessionTimeout: number;
|
||||
}
|
||||
|
||||
export interface SecuritySettings {
|
||||
passwordMinLength: number;
|
||||
maxLoginAttempts: number;
|
||||
lockoutDuration: number;
|
||||
ipWhitelist: string[];
|
||||
}
|
||||
|
||||
export interface NotificationSettings {
|
||||
enableEmail: boolean;
|
||||
emailSmtpConfig: any;
|
||||
}
|
||||
|
||||
export interface SystemSettings {
|
||||
basic: BasicSettings;
|
||||
security: SecuritySettings;
|
||||
notification: NotificationSettings;
|
||||
}
|
||||
|
||||
export const systemApi = {
|
||||
getSettings: () =>
|
||||
request.get<SystemSettings>('/settings'),
|
||||
|
||||
updateBasicSettings: (data: Partial<BasicSettings>) =>
|
||||
request.put('/settings/basic', data),
|
||||
|
||||
updateSecuritySettings: (data: Partial<SecuritySettings>) =>
|
||||
request.put('/settings/security', data),
|
||||
|
||||
updateNotificationSettings: (data: Partial<NotificationSettings>) =>
|
||||
request.put('/settings/notification', data),
|
||||
};
|
||||
+29
-21
@@ -3,30 +3,24 @@ import request from '@/utils/request';
|
||||
export interface UserInfo {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
realName: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
avatar?: string;
|
||||
status: number;
|
||||
lastLoginAt: string | null;
|
||||
userRoles?: UserRoleInfo[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface UserRoleInfo {
|
||||
id: number;
|
||||
userId: number;
|
||||
roleId: number;
|
||||
role?: RoleInfo;
|
||||
}
|
||||
|
||||
export interface RoleInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
status: number;
|
||||
sortOrder: number;
|
||||
roleCode: string;
|
||||
roleName: string;
|
||||
}
|
||||
|
||||
export interface UserQuery {
|
||||
@@ -37,10 +31,17 @@ export interface UserQuery {
|
||||
roleId?: number;
|
||||
}
|
||||
|
||||
export interface UserListResult {
|
||||
list: UserInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface CreateUserParams {
|
||||
username: string;
|
||||
password: string;
|
||||
nickname: string;
|
||||
realName: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
roleIds: number[];
|
||||
@@ -48,29 +49,36 @@ export interface CreateUserParams {
|
||||
}
|
||||
|
||||
export interface UpdateUserParams {
|
||||
nickname?: string;
|
||||
realName?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
avatar?: string;
|
||||
roleIds?: number[];
|
||||
status?: number;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export const userApi = {
|
||||
getUserList: (params: UserQuery) => request.get('/users', { params }),
|
||||
getUserList: (params: UserQuery) =>
|
||||
request.get<UserListResult>('/users', { params }),
|
||||
|
||||
getRoleList: () => request.get<RoleInfo[]>('/users/roles'),
|
||||
getRoleList: () =>
|
||||
request.get<RoleInfo[]>('/users/roles'),
|
||||
|
||||
getUserDetail: (id: number) => request.get<UserInfo>(`/users/${id}`),
|
||||
getUserDetail: (id: number) =>
|
||||
request.get<UserInfo>(`/users/${id}`),
|
||||
|
||||
createUser: (data: CreateUserParams) => request.post('/users', data),
|
||||
createUser: (data: CreateUserParams) =>
|
||||
request.post('/users', data),
|
||||
|
||||
updateUser: (id: number, data: UpdateUserParams) => request.put(`/users/${id}`, data),
|
||||
updateUser: (id: number, data: UpdateUserParams) =>
|
||||
request.put(`/users/${id}`, data),
|
||||
|
||||
deleteUser: (id: number) => request.delete(`/users/${id}`),
|
||||
deleteUser: (id: number) =>
|
||||
request.delete(`/users/${id}`),
|
||||
|
||||
batchDeleteUsers: (ids: number[]) => request.post('/users/batch-delete', { ids }),
|
||||
batchDeleteUsers: (ids: number[]) =>
|
||||
request.post('/users/batch-delete', { ids }),
|
||||
|
||||
updateStatus: (id: number, status: number) => request.put(`/users/${id}/status`, { status }),
|
||||
updateStatus: (id: number, status: number) =>
|
||||
request.put(`/users/${id}/status`, { status }),
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { App, Directive, DirectiveBinding } from 'vue';
|
||||
|
||||
const permissionDirective: Directive = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding<string>) {
|
||||
const { value } = binding;
|
||||
|
||||
if (!value) {
|
||||
console.warn('[v-permission] 需要提供权限码');
|
||||
return;
|
||||
}
|
||||
|
||||
const stores = import('@/stores/permission');
|
||||
stores.then(({ usePermissionStore }) => {
|
||||
const store = usePermissionStore();
|
||||
|
||||
if (store.userInfo?.roles.includes('super_admin')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!store.permissions.includes(value)) {
|
||||
el.parentNode?.removeChild(el);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default function setupPermissionDirective(app: App) {
|
||||
app.directive('permission', permissionDirective);
|
||||
}
|
||||
@@ -91,24 +91,29 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, h } from 'vue'
|
||||
import { ref, computed, watch, h, type Component, type VNode } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import {
|
||||
DashboardOutlined,
|
||||
SettingOutlined,
|
||||
UserOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
ToolOutlined,
|
||||
InfoCircleOutlined,
|
||||
LockOutlined,
|
||||
MenuOutlined,
|
||||
TeamOutlined,
|
||||
ProjectOutlined,
|
||||
ScheduleOutlined,
|
||||
MonitorOutlined,
|
||||
BarChartOutlined,
|
||||
BookOutlined,
|
||||
FileTextOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
HomeOutlined,
|
||||
DownOutlined,
|
||||
LogoutOutlined,
|
||||
LockOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
import { usePermissionStore, type MenuNode } from '@/stores/permission'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -119,57 +124,79 @@ const collapsed = ref(false)
|
||||
const selectedKeys = ref<string[]>([])
|
||||
const openKeys = ref<string[]>([])
|
||||
|
||||
// 菜单配置
|
||||
const menuItems = computed(() => [
|
||||
// 菜单图标映射:菜单 icon 字符串 → 图标组件,未知图标显示默认图标
|
||||
const iconMap: Record<string, Component> = {
|
||||
TeamOutlined,
|
||||
ProjectOutlined,
|
||||
ScheduleOutlined,
|
||||
MonitorOutlined,
|
||||
BarChartOutlined,
|
||||
SettingOutlined,
|
||||
UserOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
MenuOutlined,
|
||||
BookOutlined,
|
||||
FileTextOutlined,
|
||||
DashboardOutlined,
|
||||
}
|
||||
|
||||
function resolveIcon(icon?: string | null) {
|
||||
const iconComponent = icon ? iconMap[icon] : undefined
|
||||
return () => h(iconComponent || MenuOutlined)
|
||||
}
|
||||
|
||||
// 侧边栏菜单项结构(与 a-menu items 兼容)
|
||||
interface MenuItem {
|
||||
key: string
|
||||
icon: () => VNode
|
||||
label: string
|
||||
children?: MenuItem[]
|
||||
}
|
||||
|
||||
// 由后端菜单树构建侧边栏菜单:menuType=1 目录 → 子菜单组,menuType=2 菜单 → 菜单项
|
||||
function buildMenuItems(nodes: MenuNode[]): MenuItem[] {
|
||||
const items: MenuItem[] = []
|
||||
for (const node of nodes) {
|
||||
if (node.menuType === 1) {
|
||||
const children = (node.children || [])
|
||||
.filter(child => child.menuType === 2 && child.menuPath)
|
||||
.map(child => ({
|
||||
key: child.menuPath as string,
|
||||
icon: resolveIcon(child.icon),
|
||||
label: child.menuName,
|
||||
}))
|
||||
if (children.length) {
|
||||
items.push({
|
||||
key: node.menuPath || `dir-${node.id}`,
|
||||
icon: resolveIcon(node.icon),
|
||||
label: node.menuName,
|
||||
children,
|
||||
})
|
||||
}
|
||||
} else if (node.menuType === 2 && node.menuPath) {
|
||||
items.push({
|
||||
key: node.menuPath,
|
||||
icon: resolveIcon(node.icon),
|
||||
label: node.menuName,
|
||||
})
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// 菜单配置:仪表盘固定展示在最前 + 后端菜单树动态渲染
|
||||
const menuItems = computed<MenuItem[]>(() => [
|
||||
{
|
||||
key: '/dashboard',
|
||||
icon: () => h(DashboardOutlined),
|
||||
label: '仪表盘',
|
||||
},
|
||||
{
|
||||
key: '/system',
|
||||
icon: () => h(SettingOutlined),
|
||||
label: '系统管理',
|
||||
children: [
|
||||
{
|
||||
key: '/system/user',
|
||||
icon: () => h(UserOutlined),
|
||||
label: '用户管理',
|
||||
},
|
||||
{
|
||||
key: '/system/role',
|
||||
icon: () => h(SafetyCertificateOutlined),
|
||||
label: '角色权限',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: '/settings',
|
||||
icon: () => h(ToolOutlined),
|
||||
label: '系统设置',
|
||||
children: [
|
||||
{
|
||||
key: '/settings/basic',
|
||||
icon: () => h(InfoCircleOutlined),
|
||||
label: '基本设置',
|
||||
},
|
||||
{
|
||||
key: '/settings/security',
|
||||
icon: () => h(LockOutlined),
|
||||
label: '安全设置',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: '/log/operation',
|
||||
icon: () => h(FileTextOutlined),
|
||||
label: '操作日志',
|
||||
},
|
||||
...buildMenuItems(permissionStore.menus),
|
||||
])
|
||||
|
||||
// 计算属性
|
||||
const userInfo = computed(() => permissionStore.userInfo)
|
||||
const displayName = computed(() => userInfo.value?.nickname || userInfo.value?.username || '管理员')
|
||||
const displayName = computed(() => userInfo.value?.realName || userInfo.value?.username || '管理员')
|
||||
const userInitial = computed(() => (displayName.value?.charAt(0) || 'A').toUpperCase())
|
||||
|
||||
// 面包屑列表
|
||||
@@ -381,15 +408,15 @@ function handleUserMenuClick({ key }: { key: string }) {
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 16px;
|
||||
min-height: calc(100vh - 64px - 32px);
|
||||
margin: 0;
|
||||
min-height: calc(100vh - 64px);
|
||||
|
||||
.view-wrapper {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
min-height: calc(100vh - 64px - 32px - 48px); // 减去header、margin和padding
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
min-height: calc(100vh - 64px);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -435,11 +462,11 @@ function handleUserMenuClick({ key }: { key: string }) {
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 12px;
|
||||
margin: 0;
|
||||
|
||||
.view-wrapper {
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import "@/assets/styles/global.less";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import pinia from "./stores";
|
||||
import setupPermissionDirective from "@/directives/permission";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
@@ -13,6 +12,4 @@ app.use(Antd);
|
||||
app.use(router);
|
||||
app.use(pinia);
|
||||
|
||||
setupPermissionDirective(app);
|
||||
|
||||
app.mount("#app");
|
||||
+49
-37
@@ -17,6 +17,7 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
name: "Root",
|
||||
component: () => import("@/layouts/index.vue"),
|
||||
redirect: "/dashboard",
|
||||
children: [
|
||||
@@ -26,65 +27,76 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import("@/views/dashboard/index.vue"),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: "仪表盘",
|
||||
permission: "dashboard"
|
||||
title: "仪表盘"
|
||||
}
|
||||
},
|
||||
// 业务模块(未开发,先使用占位页;菜单 component 落地后替换为真实组件)
|
||||
{
|
||||
path: "customer",
|
||||
name: "Customer",
|
||||
component: () => import("@/views/Placeholder.vue"),
|
||||
meta: { requiresAuth: true, title: "客户管理", permission: "customer:list" }
|
||||
},
|
||||
{
|
||||
path: "project",
|
||||
name: "Project",
|
||||
component: () => import("@/views/Placeholder.vue"),
|
||||
meta: { requiresAuth: true, title: "项目管理", permission: "project:list" }
|
||||
},
|
||||
{
|
||||
path: "dispatch",
|
||||
name: "Dispatch",
|
||||
component: () => import("@/views/Placeholder.vue"),
|
||||
meta: { requiresAuth: true, title: "运营派单", permission: "dispatch:list" }
|
||||
},
|
||||
{
|
||||
path: "device",
|
||||
name: "Device",
|
||||
component: () => import("@/views/Placeholder.vue"),
|
||||
meta: { requiresAuth: true, title: "设备监控", permission: "device:list" }
|
||||
},
|
||||
{
|
||||
path: "report",
|
||||
name: "Report",
|
||||
component: () => import("@/views/Placeholder.vue"),
|
||||
meta: { requiresAuth: true, title: "统计报表", permission: "report:list" }
|
||||
},
|
||||
// 系统管理
|
||||
{
|
||||
path: "system/user",
|
||||
name: "UserList",
|
||||
name: "SystemUser",
|
||||
component: () => import("@/views/system/user/index.vue"),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: "用户管理",
|
||||
permission: "user:list"
|
||||
}
|
||||
meta: { requiresAuth: true, title: "用户管理", permission: "system:user" }
|
||||
},
|
||||
{
|
||||
path: "system/role",
|
||||
name: "RoleList",
|
||||
name: "SystemRole",
|
||||
component: () => import("@/views/system/role/index.vue"),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: "角色管理",
|
||||
permission: "role:list"
|
||||
}
|
||||
meta: { requiresAuth: true, title: "角色管理", permission: "system:role" }
|
||||
},
|
||||
{
|
||||
path: "settings/basic",
|
||||
name: "BasicSettings",
|
||||
component: () => import("@/views/settings/basic.vue"),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: "基本设置",
|
||||
permission: "system:basicSetting"
|
||||
}
|
||||
path: "system/menu",
|
||||
name: "SystemMenu",
|
||||
component: () => import("@/views/system/menu/index.vue"),
|
||||
meta: { requiresAuth: true, title: "菜单管理", permission: "system:menu" }
|
||||
},
|
||||
{
|
||||
path: "settings/security",
|
||||
name: "SecuritySettings",
|
||||
component: () => import("@/views/settings/security.vue"),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: "安全设置",
|
||||
permission: "system:securitySetting"
|
||||
}
|
||||
path: "system/dict",
|
||||
name: "SystemDict",
|
||||
component: () => import("@/views/system/dict/index.vue"),
|
||||
meta: { requiresAuth: true, title: "数据字典", permission: "system:dict" }
|
||||
},
|
||||
{
|
||||
path: "log/operation",
|
||||
name: "OperationLog",
|
||||
component: () => import("@/views/log/operation.vue"),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: "操作日志",
|
||||
permission: "log:operation"
|
||||
}
|
||||
component: () => import("@/views/log/operation/index.vue"),
|
||||
meta: { requiresAuth: true, title: "操作日志", permission: "system:log" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
redirect: "/login"
|
||||
redirect: "/403"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -3,18 +3,34 @@ import { ref, computed } from 'vue';
|
||||
import { authApi } from '@/api/auth';
|
||||
import router from '@/router';
|
||||
|
||||
// 后端菜单树节点结构(sys_menu)
|
||||
export interface MenuNode {
|
||||
id: number;
|
||||
parentId: number | null;
|
||||
menuName: string;
|
||||
menuPath?: string | null;
|
||||
component?: string | null;
|
||||
icon?: string | null;
|
||||
sortOrder: number;
|
||||
menuType: number; // 1目录(父级) 2菜单(叶子)
|
||||
permission?: string | null;
|
||||
visible: number;
|
||||
children?: MenuNode[];
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
avatar?: string;
|
||||
realName: string;
|
||||
roles: string[];
|
||||
permissions: string[];
|
||||
menus: MenuNode[];
|
||||
}
|
||||
|
||||
export const usePermissionStore = defineStore('permission', () => {
|
||||
const userInfo = ref<UserInfo | null>(null);
|
||||
const token = ref<string>(localStorage.getItem('access_token') || '');
|
||||
const menus = ref<MenuNode[]>([]);
|
||||
// 兼容旧调用方(如登录页 setPermissions),权限判断已改为基于菜单树
|
||||
const permissions = ref<string[]>([]);
|
||||
|
||||
const isLoggedIn = computed(() => !!token.value);
|
||||
@@ -23,23 +39,40 @@ export const usePermissionStore = defineStore('permission', () => {
|
||||
userInfo.value?.roles.includes(role) || false
|
||||
);
|
||||
|
||||
// 从菜单树展平所有节点的 permission 标识集合
|
||||
const permissionSet = computed(() => {
|
||||
const set = new Set<string>();
|
||||
const walk = (nodes: MenuNode[]) => {
|
||||
for (const node of nodes) {
|
||||
if (node.permission) {
|
||||
set.add(node.permission);
|
||||
}
|
||||
if (node.children?.length) {
|
||||
walk(node.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(menus.value);
|
||||
return set;
|
||||
});
|
||||
|
||||
const hasPermission = computed(() => (permission: string) =>
|
||||
permissions.value.includes(permission) ||
|
||||
userInfo.value?.roles.includes('super_admin') || false
|
||||
permissionSet.value.has(permission) || hasRole.value('ADMIN') || false
|
||||
);
|
||||
|
||||
async function login(username: string, password: string) {
|
||||
try {
|
||||
const res = await authApi.login({ username, password });
|
||||
if (res.code === 200) {
|
||||
const { accessToken, refreshToken, userInfo: info } = res.data;
|
||||
const { accessToken, refreshToken, userInfo: info, roles, menus: menuTree } = res.data;
|
||||
|
||||
token.value = accessToken;
|
||||
localStorage.setItem('access_token', accessToken);
|
||||
localStorage.setItem('refresh_token', refreshToken);
|
||||
|
||||
userInfo.value = info;
|
||||
permissions.value = info.permissions || [];
|
||||
// 后端将 roles/menus 作为 userInfo 的同级字段返回
|
||||
userInfo.value = { ...info, roles: roles || [], menus: menuTree || [] };
|
||||
menus.value = menuTree || [];
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -52,6 +85,7 @@ export const usePermissionStore = defineStore('permission', () => {
|
||||
function logout() {
|
||||
token.value = '';
|
||||
userInfo.value = null;
|
||||
menus.value = [];
|
||||
permissions.value = [];
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
@@ -63,7 +97,7 @@ export const usePermissionStore = defineStore('permission', () => {
|
||||
const res = await authApi.getCurrentUser();
|
||||
if (res.code === 200) {
|
||||
userInfo.value = res.data;
|
||||
permissions.value = res.data.permissions || [];
|
||||
menus.value = res.data.menus || [];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -80,6 +114,7 @@ export const usePermissionStore = defineStore('permission', () => {
|
||||
|
||||
function setUserInfo(info: UserInfo | null) {
|
||||
userInfo.value = info;
|
||||
menus.value = info?.menus || [];
|
||||
}
|
||||
|
||||
function setPermissions(perms: string[]) {
|
||||
@@ -89,6 +124,7 @@ export const usePermissionStore = defineStore('permission', () => {
|
||||
return {
|
||||
userInfo,
|
||||
token,
|
||||
menus,
|
||||
permissions,
|
||||
isLoggedIn,
|
||||
hasRole,
|
||||
|
||||
+19
-13
@@ -86,20 +86,26 @@ service.interceptors.response.use(
|
||||
}
|
||||
);
|
||||
|
||||
export default service;
|
||||
|
||||
export function get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.get(url, config);
|
||||
// 响应拦截器已解包 response.data,统一返回 { code, message, data } 结构
|
||||
export interface ApiResult<T = any> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export function post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.post(url, data, config);
|
||||
const request = {
|
||||
get<T = any>(url: string, config?: AxiosRequestConfig): Promise<ApiResult<T>> {
|
||||
return service.get(url, config) as unknown as Promise<ApiResult<T>>;
|
||||
},
|
||||
post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResult<T>> {
|
||||
return service.post(url, data, config) as unknown as Promise<ApiResult<T>>;
|
||||
},
|
||||
put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResult<T>> {
|
||||
return service.put(url, data, config) as unknown as Promise<ApiResult<T>>;
|
||||
},
|
||||
delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<ApiResult<T>> {
|
||||
return service.delete(url, config) as unknown as Promise<ApiResult<T>>;
|
||||
}
|
||||
};
|
||||
|
||||
export function put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.put(url, data, config);
|
||||
}
|
||||
|
||||
export function del<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.delete(url, config);
|
||||
}
|
||||
export default request;
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="placeholder-container">
|
||||
<div class="placeholder-content">
|
||||
<div class="placeholder-icon">
|
||||
<ToolOutlined />
|
||||
</div>
|
||||
<h2 class="placeholder-title">功能开发中</h2>
|
||||
<p class="placeholder-desc">该模块正在开发中,敬请期待</p>
|
||||
<a-button type="primary" @click="goBack">
|
||||
<ArrowLeftOutlined />
|
||||
返回
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ToolOutlined, ArrowLeftOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
router.back()
|
||||
} else {
|
||||
router.push('/dashboard')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.placeholder-container {
|
||||
min-height: calc(100vh - 200px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
text-align: center;
|
||||
padding: 60px 40px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 0 auto 24px;
|
||||
background: #e6f7ff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
:deep(.anticon) {
|
||||
font-size: 64px;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1f1f1f;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -4,19 +4,28 @@
|
||||
<a-form layout="inline" :model="queryParams">
|
||||
<a-form-item label="操作人">
|
||||
<a-input
|
||||
v-model:value="queryParams.operator"
|
||||
placeholder="请输入操作人"
|
||||
v-model:value="queryParams.userId"
|
||||
placeholder="请输入用户ID"
|
||||
allow-clear
|
||||
style="width: 140px"
|
||||
@pressEnter="handleSearch"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="操作模块">
|
||||
<a-input
|
||||
v-model:value="queryParams.module"
|
||||
placeholder="请输入操作模块"
|
||||
allow-clear
|
||||
style="width: 160px"
|
||||
@pressEnter="handleSearch"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="操作类型">
|
||||
<a-form-item label="操作动作">
|
||||
<a-select
|
||||
v-model:value="queryParams.actionType"
|
||||
placeholder="请选择操作类型"
|
||||
v-model:value="queryParams.action"
|
||||
placeholder="请选择操作动作"
|
||||
allow-clear
|
||||
style="width: 160px"
|
||||
style="width: 140px"
|
||||
>
|
||||
<a-select-option value="登录">登录</a-select-option>
|
||||
<a-select-option value="创建">创建</a-select-option>
|
||||
@@ -63,24 +72,14 @@
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'statusCode'">
|
||||
<a-tag :color="getStatusCodeColor(record.statusCode)">
|
||||
{{ record.statusCode }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'duration'">
|
||||
<span :class="{ 'duration-warning': record.duration > 1000 }">
|
||||
{{ record.duration }}ms
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'createdAt'">
|
||||
{{ formatTime(record.createdAt) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-tooltip :title="record.action">
|
||||
<span class="action-text">{{ record.action }}</span>
|
||||
<template v-if="column.key === 'params'">
|
||||
<a-tooltip :title="record.params || ''">
|
||||
<span class="params-text">{{ record.params || '-' }}</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'created_at'">
|
||||
{{ formatTime(record.created_at) }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
@@ -92,19 +91,20 @@ import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { SearchOutlined, ReloadOutlined, DownloadOutlined } from '@ant-design/icons-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { logApi, type LogItem } from '@/api/log'
|
||||
import { logApi } from '@/api/log'
|
||||
|
||||
const tableLoading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const logList = ref<LogItem[]>([])
|
||||
const logList = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const timeRange = ref<any[]>([])
|
||||
|
||||
const queryParams = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
operator: '',
|
||||
actionType: undefined as string | undefined,
|
||||
userId: '' as string | number,
|
||||
module: '',
|
||||
action: undefined as string | undefined,
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
})
|
||||
@@ -118,23 +118,23 @@ const columns = [
|
||||
},
|
||||
{
|
||||
title: '操作人',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作类型',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: 140,
|
||||
dataIndex: 'userId',
|
||||
key: 'userId',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作模块',
|
||||
dataIndex: 'module',
|
||||
key: 'module',
|
||||
width: 110,
|
||||
width: 130,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作动作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
@@ -145,29 +145,22 @@ const columns = [
|
||||
},
|
||||
{
|
||||
title: 'IP地址',
|
||||
dataIndex: 'ipAddress',
|
||||
key: 'ipAddress',
|
||||
dataIndex: 'ip',
|
||||
key: 'ip',
|
||||
width: 140,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '状态码',
|
||||
dataIndex: 'statusCode',
|
||||
key: 'statusCode',
|
||||
width: 90,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '耗时(ms)',
|
||||
dataIndex: 'duration',
|
||||
key: 'duration',
|
||||
width: 100,
|
||||
align: 'center' as const,
|
||||
title: '请求参数',
|
||||
dataIndex: 'params',
|
||||
key: 'params',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 180,
|
||||
},
|
||||
]
|
||||
@@ -182,13 +175,6 @@ const paginationConfig = reactive({
|
||||
showTotal: (t: number) => `共 ${t} 条记录`,
|
||||
})
|
||||
|
||||
function getStatusCodeColor(code: number): string {
|
||||
if (code >= 200 && code < 300) return 'success'
|
||||
if (code >= 400 && code < 500) return 'warning'
|
||||
if (code >= 500) return 'error'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
function formatTime(time: string): string {
|
||||
return dayjs(time).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
@@ -204,7 +190,14 @@ async function fetchLogList() {
|
||||
queryParams.endDate = ''
|
||||
}
|
||||
|
||||
const res: any = await logApi.getLogList(queryParams)
|
||||
const requestParams: any = { ...queryParams }
|
||||
if (requestParams.userId !== '') {
|
||||
requestParams.userId = Number(requestParams.userId)
|
||||
} else {
|
||||
delete requestParams.userId
|
||||
}
|
||||
|
||||
const res: any = await logApi.getLogList(requestParams)
|
||||
if (res.code === 200) {
|
||||
logList.value = res.data.list
|
||||
total.value = res.data.total
|
||||
@@ -224,8 +217,9 @@ function handleSearch() {
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
queryParams.operator = ''
|
||||
queryParams.actionType = undefined
|
||||
queryParams.userId = ''
|
||||
queryParams.module = ''
|
||||
queryParams.action = undefined
|
||||
queryParams.startDate = ''
|
||||
queryParams.endDate = ''
|
||||
timeRange.value = []
|
||||
@@ -244,8 +238,9 @@ async function handleExport() {
|
||||
exportLoading.value = true
|
||||
try {
|
||||
const params: any = {}
|
||||
if (queryParams.operator) params.operator = queryParams.operator
|
||||
if (queryParams.actionType) params.actionType = queryParams.actionType
|
||||
if (queryParams.userId !== '') params.userId = Number(queryParams.userId)
|
||||
if (queryParams.module) params.module = queryParams.module
|
||||
if (queryParams.action) params.action = queryParams.action
|
||||
if (timeRange.value && timeRange.value.length === 2) {
|
||||
params.startDate = timeRange.value[0].format('YYYY-MM-DD HH:mm:ss')
|
||||
params.endDate = timeRange.value[1].format('YYYY-MM-DD HH:mm:ss')
|
||||
@@ -292,12 +287,7 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.duration-warning {
|
||||
color: #cf1322;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
.params-text {
|
||||
max-width: 150px;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
@@ -68,7 +68,6 @@
|
||||
import { reactive, ref, onMounted, nextTick } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
UserOutlined,
|
||||
LockOutlined,
|
||||
@@ -121,22 +120,7 @@ async function handleLogin() {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
const response = await axios.post('/api/auth/login', {
|
||||
username: formState.username,
|
||||
password: formState.password
|
||||
})
|
||||
|
||||
const res = response.data
|
||||
|
||||
if (res.code === 200) {
|
||||
const { accessToken, refreshToken, userInfo } = res.data
|
||||
|
||||
localStorage.setItem('access_token', accessToken)
|
||||
localStorage.setItem('refresh_token', refreshToken)
|
||||
|
||||
permissionStore.setToken(accessToken)
|
||||
permissionStore.setUserInfo(userInfo)
|
||||
permissionStore.setPermissions(userInfo.permissions || [])
|
||||
await permissionStore.login(formState.username, formState.password)
|
||||
|
||||
if (formState.rememberMe) {
|
||||
localStorage.setItem('remembered_username', formState.username)
|
||||
@@ -148,9 +132,6 @@ async function handleLogin() {
|
||||
|
||||
const redirect = (route.query.redirect as string) || '/'
|
||||
router.push(redirect)
|
||||
} else {
|
||||
throw new Error(res.message || '登录失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('登录失败:', error)
|
||||
message.error(error.message || '登录失败,请检查用户名和密码')
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
<template>
|
||||
<div class="basic-settings">
|
||||
<a-card title="基本设置" :loading="loading">
|
||||
<a-form
|
||||
:model="formState"
|
||||
:rules="rules"
|
||||
ref="formRef"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 14 }"
|
||||
>
|
||||
<a-form-item label="站点名称" name="siteName">
|
||||
<a-input
|
||||
v-model:value="formState.siteName"
|
||||
placeholder="请输入站点名称"
|
||||
:maxlength="50"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="Logo" name="logo">
|
||||
<a-upload
|
||||
name="file"
|
||||
list-type="picture-card"
|
||||
class="logo-uploader"
|
||||
:show-upload-list="false"
|
||||
:before-upload="beforeUpload"
|
||||
:custom-request="handleUpload"
|
||||
>
|
||||
<img v-if="formState.logo" :src="formState.logo" alt="logo" style="width: 100%" />
|
||||
<div v-else>
|
||||
<plus-outlined />
|
||||
<div class="ant-upload-text">上传</div>
|
||||
</div>
|
||||
</a-upload>
|
||||
<div class="form-tip">支持 JPG、PNG 格式,建议尺寸 200x200px</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="会话超时时间" name="sessionTimeout">
|
||||
<a-input-number
|
||||
v-model:value="formState.sessionTimeout"
|
||||
:min="5"
|
||||
:max="1440"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<span class="form-tip">单位:分钟,范围 5-1440</span>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :wrapper-col="{ offset: 6, span: 14 }">
|
||||
<a-button type="primary" @click="handleSave" :loading="saving">
|
||||
保存设置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { systemApi, type BasicSettings } from '@/api/system';
|
||||
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
|
||||
const formState = reactive<BasicSettings>({
|
||||
siteName: '',
|
||||
logo: '',
|
||||
sessionTimeout: 120,
|
||||
});
|
||||
|
||||
const rules = {
|
||||
siteName: [
|
||||
{ required: true, message: '请输入站点名称', trigger: 'blur' },
|
||||
{ max: 50, message: '站点名称不能超过50个字符', trigger: 'blur' },
|
||||
],
|
||||
sessionTimeout: [
|
||||
{ required: true, message: '请输入会话超时时间', trigger: 'blur' },
|
||||
{ type: 'number', min: 5, max: 1440, message: '超时时间范围为5-1440分钟', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadSettings();
|
||||
});
|
||||
|
||||
async function loadSettings() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await systemApi.getSettings();
|
||||
if (res.code === 200 && res.data?.basic) {
|
||||
Object.assign(formState, res.data.basic);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载设置失败:', error);
|
||||
message.error('加载设置失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function beforeUpload(file: File) {
|
||||
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
|
||||
if (!isJpgOrPng) {
|
||||
message.error('只能上传 JPG/PNG 格式的图片!');
|
||||
return false;
|
||||
}
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
if (!isLt2M) {
|
||||
message.error('图片大小不能超过 2MB!');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleUpload(options: any) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
formState.logo = reader.result as string;
|
||||
};
|
||||
reader.readAsDataURL(options.file);
|
||||
options.onSuccess();
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
await formRef.value.validateFields();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const res: any = await systemApi.updateBasicSettings({
|
||||
siteName: formState.siteName,
|
||||
logo: formState.logo,
|
||||
sessionTimeout: formState.sessionTimeout,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success('基本设置保存成功');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
message.error('保存失败,请重试');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.basic-settings {
|
||||
padding: 20px;
|
||||
|
||||
.form-tip {
|
||||
margin-left: 10px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.logo-uploader {
|
||||
:deep(.ant-upload) {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-upload-text {
|
||||
margin-top: 8px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,151 +0,0 @@
|
||||
<template>
|
||||
<div class="security-settings">
|
||||
<a-card title="安全设置" :loading="loading">
|
||||
<a-form
|
||||
:model="formState"
|
||||
:rules="rules"
|
||||
ref="formRef"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 14 }"
|
||||
>
|
||||
<a-form-item label="密码最小长度" name="passwordMinLength">
|
||||
<a-input-number
|
||||
v-model:value="formState.passwordMinLength"
|
||||
:min="6"
|
||||
:max="20"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<span class="form-tip">范围 6-20 位</span>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="最大登录尝试次数" name="maxLoginAttempts">
|
||||
<a-input-number
|
||||
v-model:value="formState.maxLoginAttempts"
|
||||
:min="3"
|
||||
:max="10"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<span class="form-tip">超过此次数将锁定账户</span>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="账户锁定时长" name="lockoutDuration">
|
||||
<a-input-number
|
||||
v-model:value="formState.lockoutDuration"
|
||||
:min="5"
|
||||
:max="60"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<span class="form-tip">单位:分钟,范围 5-60</span>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="IP白名单" name="ipWhitelist">
|
||||
<a-select
|
||||
v-model:value="formState.ipWhitelist"
|
||||
mode="tags"
|
||||
placeholder="输入IP地址后按回车添加"
|
||||
style="width: 100%"
|
||||
:token-separators="[',']"
|
||||
/>
|
||||
<div class="form-tip">
|
||||
留空表示不限制IP访问,多个IP用回车或逗号分隔(如:192.168.1.1)
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :wrapper-col="{ offset: 6, span: 14 }">
|
||||
<a-button type="primary" @click="handleSave" :saving="saving">
|
||||
保存设置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { systemApi, type SecuritySettings } from '@/api/system';
|
||||
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
|
||||
const formState = reactive<SecuritySettings>({
|
||||
passwordMinLength: 6,
|
||||
maxLoginAttempts: 5,
|
||||
lockoutDuration: 30,
|
||||
ipWhitelist: [],
|
||||
});
|
||||
|
||||
const rules = {
|
||||
passwordMinLength: [
|
||||
{ required: true, message: '请输入密码最小长度', trigger: 'blur' },
|
||||
{ type: 'number', min: 6, max: 20, message: '长度范围为6-20', trigger: 'blur' },
|
||||
],
|
||||
maxLoginAttempts: [
|
||||
{ required: true, message: '请输入最大登录尝试次数', trigger: 'blur' },
|
||||
{ type: 'number', min: 3, max: 10, message: '范围为3-10次', trigger: 'blur' },
|
||||
],
|
||||
lockoutDuration: [
|
||||
{ required: true, message: '请输入账户锁定时长', trigger: 'blur' },
|
||||
{ type: 'number', min: 5, max: 60, message: '范围为5-60分钟', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadSettings();
|
||||
});
|
||||
|
||||
async function loadSettings() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await systemApi.getSettings();
|
||||
if (res.code === 200 && res.data?.security) {
|
||||
Object.assign(formState, res.data.security);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载设置失败:', error);
|
||||
message.error('加载设置失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
await formRef.value.validateFields();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const res: any = await systemApi.updateSecuritySettings({
|
||||
passwordMinLength: formState.passwordMinLength,
|
||||
maxLoginAttempts: formState.maxLoginAttempts,
|
||||
lockoutDuration: formState.lockoutDuration,
|
||||
ipWhitelist: formState.ipWhitelist,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success('安全设置保存成功');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
message.error('保存失败,请重试');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.security-settings {
|
||||
padding: 20px;
|
||||
|
||||
.form-tip {
|
||||
margin-left: 10px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,368 @@
|
||||
<template>
|
||||
<div class="dict-management">
|
||||
<a-card :bordered="false">
|
||||
<a-form layout="inline" :model="queryParams" class="search-form">
|
||||
<a-form-item label="字典类型">
|
||||
<a-select
|
||||
v-model:value="queryParams.dictType"
|
||||
placeholder="请选择字典类型"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
>
|
||||
<a-select-option v-for="type in dictTypeOptions" :key="type" :value="type">{{ type }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="关键词">
|
||||
<a-input v-model:value="queryParams.keyword" placeholder="字典标签/值" allow-clear style="width: 200px" @pressEnter="handleSearch" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态">
|
||||
<a-select v-model:value="queryParams.status" placeholder="请选择状态" allow-clear style="width: 120px">
|
||||
<a-select-option :value="1">启用</a-select-option>
|
||||
<a-select-option :value="0">禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<div class="table-operations">
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增字典
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-card :bordered="false">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'success' : 'default'">
|
||||
{{ record.status === 1 ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'created_at'">
|
||||
{{ formatTime(record.created_at) }}
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<EditOutlined />编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定删除该字典?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="handleDelete(record)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
<DeleteOutlined />删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
width="560px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="字典类型" name="dictType">
|
||||
<a-input v-model:value="formData.dictType" placeholder="如 sys_user_status" :disabled="isEdit" />
|
||||
</a-form-item>
|
||||
<a-form-item label="字典标签" name="dictLabel">
|
||||
<a-input v-model:value="formData.dictLabel" placeholder="请输入字典标签" />
|
||||
</a-form-item>
|
||||
<a-form-item label="字典值" name="dictValue">
|
||||
<a-input v-model:value="formData.dictValue" placeholder="请输入字典值" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" name="sortOrder">
|
||||
<a-input-number v-model:value="formData.sortOrder" :min="0" :max="999" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio :value="1">启用</a-radio>
|
||||
<a-radio :value="0">禁用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { dictApi } from '@/api/dict';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
interface DictItem {
|
||||
id: number;
|
||||
dictType: string;
|
||||
dictLabel: string;
|
||||
dictValue: string;
|
||||
sortOrder: number;
|
||||
status: number;
|
||||
created_at: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<DictItem[]>([]);
|
||||
const total = ref(0);
|
||||
const dictTypeOptions = ref<string[]>([]);
|
||||
|
||||
const queryParams = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
dictType: undefined as string | undefined,
|
||||
keyword: '',
|
||||
status: undefined as number | undefined,
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{ title: '字典类型', dataIndex: 'dictType', key: 'dictType', width: 160 },
|
||||
{ title: '字典标签', dataIndex: 'dictLabel', key: 'dictLabel', width: 160 },
|
||||
{ title: '字典值', dataIndex: 'dictValue', key: 'dictValue', width: 160 },
|
||||
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80, align: 'center' as const },
|
||||
{ title: '状态', key: 'status', width: 90, align: 'center' as const },
|
||||
{ title: '创建时间', key: 'created_at', width: 180 },
|
||||
{ title: '操作', key: 'action', width: 160, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const pagination = computed(() => ({
|
||||
current: queryParams.page,
|
||||
pageSize: queryParams.pageSize,
|
||||
total: total.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t: number) => `共 ${t} 条数据`,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
fetchDictList();
|
||||
fetchDictTypes();
|
||||
});
|
||||
|
||||
async function fetchDictTypes() {
|
||||
try {
|
||||
const res: any = await dictApi.getDictTypes();
|
||||
if (res.code === 200) {
|
||||
const data = res.data || [];
|
||||
dictTypeOptions.value = data.map((item: any) =>
|
||||
typeof item === 'string' ? item : item.dictType || item.type || item.label
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取字典类型失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDictList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await dictApi.getDictList(queryParams);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取字典列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
queryParams.page = 1;
|
||||
fetchDictList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
queryParams.dictType = undefined;
|
||||
queryParams.keyword = '';
|
||||
queryParams.status = undefined;
|
||||
queryParams.page = 1;
|
||||
fetchDictList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
queryParams.page = pag.current;
|
||||
queryParams.pageSize = pag.pageSize;
|
||||
fetchDictList();
|
||||
}
|
||||
|
||||
function formatTime(dateStr: string) {
|
||||
if (!dateStr) return '-';
|
||||
return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
const modalVisible = ref(false);
|
||||
const modalLoading = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
|
||||
const formData = reactive({
|
||||
dictType: '',
|
||||
dictLabel: '',
|
||||
dictValue: '',
|
||||
sortOrder: 0,
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
dictType: [{ required: true, message: '请输入字典类型', trigger: 'blur' }],
|
||||
dictLabel: [{ required: true, message: '请输入字典标签', trigger: 'blur' }],
|
||||
dictValue: [{ required: true, message: '请输入字典值', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
modalTitle.value = '新增字典';
|
||||
resetForm();
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: DictItem) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑字典';
|
||||
formData.dictType = record.dictType;
|
||||
formData.dictLabel = record.dictLabel;
|
||||
formData.dictValue = record.dictValue;
|
||||
formData.sortOrder = record.sortOrder ?? 0;
|
||||
formData.status = record.status;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.dictType = '';
|
||||
formData.dictLabel = '';
|
||||
formData.dictValue = '';
|
||||
formData.sortOrder = 0;
|
||||
formData.status = 1;
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const payload: any = {
|
||||
dictType: formData.dictType,
|
||||
dictLabel: formData.dictLabel,
|
||||
dictValue: formData.dictValue,
|
||||
sortOrder: formData.sortOrder,
|
||||
status: formData.status,
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await dictApi.updateDict(editId.value, payload);
|
||||
} else {
|
||||
res = await dictApi.createDict(payload);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchDictList();
|
||||
fetchDictTypes();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(record: DictItem) {
|
||||
try {
|
||||
const res: any = await dictApi.deleteDict(record.id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchDictList();
|
||||
fetchDictTypes();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.dict-management {
|
||||
padding: 24px;
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-operations {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,335 @@
|
||||
<template>
|
||||
<div class="menu-management">
|
||||
<a-card title="菜单管理">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="handleAdd()">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增菜单
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="menuList"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
:default-expand-all-rows="true"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'menuType'">
|
||||
<a-tag :color="record.menuType === 1 ? 'blue' : 'green'">
|
||||
{{ record.menuType === 1 ? '目录' : '菜单' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.dataIndex === 'visible'">
|
||||
<a-tag :color="record.visible === 1 ? 'success' : 'default'">
|
||||
{{ record.visible === 1 ? '显示' : '隐藏' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleAdd(record)">
|
||||
<PlusOutlined />新增子菜单
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<EditOutlined />编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定删除该菜单?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="handleDelete(record)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
<DeleteOutlined />删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
width="640px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="上级菜单" name="parentId">
|
||||
<a-tree-select
|
||||
v-model:value="formData.parentId"
|
||||
:tree-data="parentOptions"
|
||||
:field-names="{ label: 'menuName', value: 'id', children: 'children' }"
|
||||
allow-clear
|
||||
tree-default-expand-all
|
||||
placeholder="请选择上级菜单"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="菜单类型" name="menuType">
|
||||
<a-radio-group v-model:value="formData.menuType">
|
||||
<a-radio :value="1">目录</a-radio>
|
||||
<a-radio :value="2">菜单</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="菜单名称" name="menuName">
|
||||
<a-input v-model:value="formData.menuName" placeholder="请输入菜单名称" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="路由路径" name="menuPath">
|
||||
<a-input v-model:value="formData.menuPath" placeholder="如 /system/user" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="组件" name="component">
|
||||
<a-input v-model:value="formData.component" placeholder="如 system/user/index" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="图标" name="icon">
|
||||
<a-input v-model:value="formData.icon" placeholder="如 UserOutlined" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="排序" name="sortOrder">
|
||||
<a-input-number v-model:value="formData.sortOrder" :min="0" :max="999" style="width: 100%" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="权限标识" name="permission">
|
||||
<a-input v-model:value="formData.permission" placeholder="如 user:list" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="可见性" name="visible">
|
||||
<a-switch
|
||||
:checked="formData.visible === 1"
|
||||
checked-children="显示"
|
||||
un-checked-children="隐藏"
|
||||
@change="(checked: boolean) => (formData.visible = checked ? 1 : 0)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
||||
import { menuApi } from '@/api/menu';
|
||||
|
||||
interface MenuItem {
|
||||
id: number;
|
||||
parentId?: number | null;
|
||||
menuName: string;
|
||||
menuPath?: string;
|
||||
component?: string;
|
||||
icon?: string;
|
||||
menuType: number;
|
||||
sortOrder: number;
|
||||
permission?: string;
|
||||
visible: number;
|
||||
children?: MenuItem[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const menuList = ref<MenuItem[]>([]);
|
||||
|
||||
const columns = [
|
||||
{ title: '菜单名称', dataIndex: 'menuName', key: 'menuName' },
|
||||
{ title: '路由路径', dataIndex: 'menuPath', key: 'menuPath', width: 160 },
|
||||
{ title: '组件', dataIndex: 'component', key: 'component', width: 180 },
|
||||
{ title: '图标', dataIndex: 'icon', key: 'icon', width: 120 },
|
||||
{ title: '类型', dataIndex: 'menuType', key: 'menuType', width: 80, align: 'center' as const },
|
||||
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80, align: 'center' as const },
|
||||
{ title: '权限标识', dataIndex: 'permission', key: 'permission', width: 150 },
|
||||
{ title: '可见性', dataIndex: 'visible', key: 'visible', width: 90, align: 'center' as const },
|
||||
{ title: '操作', key: 'action', width: 240, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
// 上级菜单树(编辑时排除自身及后代节点,防止循环引用)
|
||||
const parentOptions = computed(() => {
|
||||
const excludeId = isEdit.value ? editId.value : null;
|
||||
const filtered = excludeId ? filterTree(menuList.value, excludeId) : menuList.value;
|
||||
return [{ id: 0, menuName: '根目录', children: filtered }];
|
||||
});
|
||||
|
||||
function filterTree(nodes: MenuItem[], excludeId: number): MenuItem[] {
|
||||
const result: MenuItem[] = [];
|
||||
for (const node of nodes) {
|
||||
if (node.id === excludeId) continue;
|
||||
const children = node.children?.length ? filterTree(node.children, excludeId) : undefined;
|
||||
if (children) {
|
||||
result.push({ ...node, children });
|
||||
} else {
|
||||
result.push(node);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const modalVisible = ref(false);
|
||||
const modalLoading = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
|
||||
const formData = reactive({
|
||||
parentId: 0,
|
||||
menuType: 2,
|
||||
menuName: '',
|
||||
menuPath: '',
|
||||
component: '',
|
||||
icon: '',
|
||||
sortOrder: 0,
|
||||
permission: '',
|
||||
visible: 1,
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
menuName: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchMenuTree();
|
||||
});
|
||||
|
||||
async function fetchMenuTree() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await menuApi.getMenuTree();
|
||||
if (res.code === 200) {
|
||||
menuList.value = res.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取菜单列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd(parent?: MenuItem) {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
modalTitle.value = '新增菜单';
|
||||
resetForm();
|
||||
if (parent) {
|
||||
formData.parentId = parent.id;
|
||||
formData.menuType = 2;
|
||||
}
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: MenuItem) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑菜单';
|
||||
formData.parentId = record.parentId || 0;
|
||||
formData.menuType = record.menuType;
|
||||
formData.menuName = record.menuName;
|
||||
formData.menuPath = record.menuPath || '';
|
||||
formData.component = record.component || '';
|
||||
formData.icon = record.icon || '';
|
||||
formData.sortOrder = record.sortOrder ?? 0;
|
||||
formData.permission = record.permission || '';
|
||||
formData.visible = record.visible ? 1 : 0;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.parentId = 0;
|
||||
formData.menuType = 2;
|
||||
formData.menuName = '';
|
||||
formData.menuPath = '';
|
||||
formData.component = '';
|
||||
formData.icon = '';
|
||||
formData.sortOrder = 0;
|
||||
formData.permission = '';
|
||||
formData.visible = 1;
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const payload: any = {
|
||||
parentId: formData.parentId || null,
|
||||
menuType: formData.menuType,
|
||||
menuName: formData.menuName,
|
||||
menuPath: formData.menuPath,
|
||||
component: formData.component,
|
||||
icon: formData.icon,
|
||||
sortOrder: formData.sortOrder,
|
||||
permission: formData.permission,
|
||||
visible: formData.visible,
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await menuApi.updateMenu(editId.value, payload);
|
||||
} else {
|
||||
res = await menuApi.createMenu(payload);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchMenuTree();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(record: MenuItem) {
|
||||
try {
|
||||
const res: any = await menuApi.deleteMenu(record.id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchMenuTree();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.menu-management {
|
||||
padding: 20px;
|
||||
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -30,16 +30,12 @@
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'name'">
|
||||
<span>{{ record.name }}</span>
|
||||
<template v-if="column.dataIndex === 'roleName'">
|
||||
<span>{{ record.roleName }}</span>
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'code'">
|
||||
<a-tag color="blue">{{ record.code }}</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'status'">
|
||||
<a-badge :status="record.status === 1 ? 'success' : 'error'" :text="record.status === 1 ? '启用' : '禁用'" />
|
||||
<template v-if="column.dataIndex === 'roleCode'">
|
||||
<a-tag color="blue">{{ record.roleCode }}</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'userCount'">
|
||||
@@ -88,12 +84,12 @@
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 17 }"
|
||||
>
|
||||
<a-form-item label="角色名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入角色名称" />
|
||||
<a-form-item label="角色名称" name="roleName">
|
||||
<a-input v-model:value="formData.roleName" placeholder="请输入角色名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="角色编码" name="code">
|
||||
<a-form-item label="角色编码" name="roleCode">
|
||||
<a-input
|
||||
v-model:value="formData.code"
|
||||
v-model:value="formData.roleCode"
|
||||
placeholder="请输入角色编码"
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
@@ -104,12 +100,6 @@
|
||||
<a-form-item label="排序" name="sortOrder">
|
||||
<a-input-number v-model:value="formData.sortOrder" :min="0" :max="999" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio :value="1">启用</a-radio>
|
||||
<a-radio :value="0">禁用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
@@ -124,17 +114,17 @@
|
||||
<a-tree
|
||||
v-if="!permissionTreeLoading"
|
||||
v-model:checkedKeys="checkedKeys"
|
||||
:tree-data="permissionTree"
|
||||
:tree-data="menuTree"
|
||||
checkable
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
:check-strictly="false"
|
||||
:field-names="{ title: 'menuName', key: 'id', children: 'children' }"
|
||||
:default-expand-all="true"
|
||||
style="max-height: 500px; overflow-y: auto;"
|
||||
>
|
||||
<template #title="{ name, icon }">
|
||||
<template #title="{ menuName, menuType }">
|
||||
<span>
|
||||
<component v-if="icon" :is="icon" style="margin-right: 8px" />
|
||||
{{ name }}
|
||||
<a-tag v-if="menuType === 1" color="blue">目录</a-tag>
|
||||
<a-tag v-else color="green">菜单</a-tag>
|
||||
{{ menuName }}
|
||||
</span>
|
||||
</template>
|
||||
</a-tree>
|
||||
@@ -153,9 +143,17 @@ import {
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { roleApi, type RoleInfo, type PermissionTreeNode } from '@/api/role';
|
||||
import { roleApi, type RoleInfo } from '@/api/role';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
interface MenuTreeNode {
|
||||
id: number;
|
||||
menuName: string;
|
||||
menuType?: number;
|
||||
children?: MenuTreeNode[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const roleList = ref<RoleInfo[]>([]);
|
||||
const keyword = ref('');
|
||||
@@ -171,13 +169,13 @@ const pagination = reactive({
|
||||
const columns = [
|
||||
{
|
||||
title: '角色名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
dataIndex: 'roleName',
|
||||
key: 'roleName',
|
||||
},
|
||||
{
|
||||
title: '角色编码',
|
||||
dataIndex: 'code',
|
||||
key: 'code',
|
||||
dataIndex: 'roleCode',
|
||||
key: 'roleCode',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
@@ -186,13 +184,6 @@ const columns = [
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '关联用户数',
|
||||
dataIndex: 'userCount',
|
||||
@@ -259,16 +250,15 @@ const currentRoleId = ref<number | null>(null);
|
||||
const formModalTitle = computed(() => (isEdit.value ? '编辑角色' : '新增角色'));
|
||||
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
code: '',
|
||||
roleName: '',
|
||||
roleCode: '',
|
||||
description: '',
|
||||
sortOrder: 0,
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
|
||||
roleName: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
|
||||
roleCode: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
function handleAdd() {
|
||||
@@ -281,20 +271,18 @@ function handleAdd() {
|
||||
function handleEdit(record: RoleInfo) {
|
||||
isEdit.value = true;
|
||||
currentRoleId.value = record.id;
|
||||
formData.name = record.name;
|
||||
formData.code = record.code;
|
||||
formData.roleName = record.roleName;
|
||||
formData.roleCode = record.roleCode;
|
||||
formData.description = record.description || '';
|
||||
formData.sortOrder = record.sortOrder;
|
||||
formData.status = record.status;
|
||||
formModalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.name = '';
|
||||
formData.code = '';
|
||||
formData.roleName = '';
|
||||
formData.roleCode = '';
|
||||
formData.description = '';
|
||||
formData.sortOrder = 0;
|
||||
formData.status = 1;
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
|
||||
@@ -310,18 +298,16 @@ async function handleSubmitForm() {
|
||||
let res: any;
|
||||
if (isEdit.value && currentRoleId.value) {
|
||||
res = await roleApi.updateRole(currentRoleId.value, {
|
||||
name: formData.name,
|
||||
roleName: formData.roleName,
|
||||
description: formData.description,
|
||||
sortOrder: formData.sortOrder,
|
||||
status: formData.status,
|
||||
});
|
||||
} else {
|
||||
res = await roleApi.createRole({
|
||||
name: formData.name,
|
||||
code: formData.code,
|
||||
roleName: formData.roleName,
|
||||
roleCode: formData.roleCode,
|
||||
description: formData.description,
|
||||
sortOrder: formData.sortOrder,
|
||||
status: formData.status,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -360,60 +346,39 @@ function handleViewUsers(_record: RoleInfo) {
|
||||
const permissionModalVisible = ref(false);
|
||||
const permissionLoading = ref(false);
|
||||
const permissionTreeLoading = ref(false);
|
||||
const permissionTree = ref<PermissionTreeNode[]>([]);
|
||||
const menuTree = ref<MenuTreeNode[]>([]);
|
||||
const checkedKeys = ref<number[]>([]);
|
||||
|
||||
async function handlePermission(record: RoleInfo) {
|
||||
currentRoleId.value = record.id;
|
||||
permissionModalVisible.value = true;
|
||||
checkedKeys.value = [];
|
||||
await loadPermissionTree();
|
||||
await loadMenuTree();
|
||||
}
|
||||
|
||||
async function loadPermissionTree() {
|
||||
async function loadMenuTree() {
|
||||
permissionTreeLoading.value = true;
|
||||
try {
|
||||
if (!currentRoleId.value) return;
|
||||
|
||||
const res: any = await roleApi.getRolePermissions(currentRoleId.value);
|
||||
const res: any = await roleApi.getRoleMenus(currentRoleId.value);
|
||||
if (res.code === 200) {
|
||||
permissionTree.value = res.data;
|
||||
extractCheckedKeys(res.data);
|
||||
menuTree.value = res.data.menuTree || [];
|
||||
checkedKeys.value = res.data.menuIds || [];
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取权限数据失败');
|
||||
message.error('获取菜单权限失败');
|
||||
} finally {
|
||||
permissionTreeLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function extractCheckedKeys(tree: PermissionTreeNode[]) {
|
||||
const keys: number[] = [];
|
||||
|
||||
function traverse(nodes: PermissionTreeNode[]) {
|
||||
for (const node of nodes) {
|
||||
if (node.checked) {
|
||||
keys.push(node.id);
|
||||
}
|
||||
if (node.children?.length) {
|
||||
traverse(node.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverse(tree);
|
||||
checkedKeys.value = keys;
|
||||
}
|
||||
|
||||
async function handleSavePermissions() {
|
||||
if (!currentRoleId.value) return;
|
||||
|
||||
permissionLoading.value = true;
|
||||
try {
|
||||
const res: any = await roleApi.updateRolePermissions(
|
||||
currentRoleId.value,
|
||||
checkedKeys.value as unknown as number[]
|
||||
);
|
||||
const res: any = await roleApi.updateRoleMenus(currentRoleId.value, checkedKeys.value);
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success('权限保存成功');
|
||||
|
||||
@@ -1,64 +1,53 @@
|
||||
<template>
|
||||
<div class="user-management">
|
||||
<a-card :bordered="false">
|
||||
<a-form layout="inline" :model="searchForm" class="search-form">
|
||||
<a-form-item label="关键词">
|
||||
<a-input v-model:value="searchForm.keyword" placeholder="用户名/昵称/手机号" allow-clear style="width: 220px" @pressEnter="handleSearch" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态">
|
||||
<a-select v-model:value="searchForm.status" placeholder="请选择状态" allow-clear style="width: 120px">
|
||||
<a-select-option :value="1">启用</a-select-option>
|
||||
<a-select-option :value="0">禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-card title="用户管理">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<div class="table-operations">
|
||||
<a-space>
|
||||
<a-button type="primary" v-permission="'user:create'" @click="handleAdd">
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增用户
|
||||
</a-button>
|
||||
<a-button danger v-permission="'user:delete'" :disabled="!selectedRowKeys.length" @click="handleBatchDelete">
|
||||
<a-button danger :disabled="!selectedRowKeys.length" @click="handleBatchDelete">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
批量删除 ({{ selectedRowKeys.length }})
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="用户名/姓名/手机号"
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-select v-model:value="searchForm.status" placeholder="状态" allow-clear style="width: 120px">
|
||||
<a-select-option :value="1">启用</a-select-option>
|
||||
<a-select-option :value="0">禁用</a-select-option>
|
||||
</a-select>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-card :bordered="false">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:row-selection="rowSelection"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:scroll="{ x: 1200 }"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'avatar'">
|
||||
<a-avatar :src="record.avatar" :size="40">{{ record.nickname?.charAt(0) }}</a-avatar>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'roles'">
|
||||
<a-space>
|
||||
<a-tag v-for="role in record.userRoles" :key="role.roleId" :color="getRoleColor(role.role?.code)">
|
||||
{{ role.role?.name }}
|
||||
<a-tag v-for="role in record.userRoles" :key="role.roleId" :color="getRoleColor(role.role?.roleCode)">
|
||||
{{ role.role?.roleName }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</template>
|
||||
@@ -69,18 +58,21 @@
|
||||
checked-children="启用"
|
||||
un-checked-children="禁用"
|
||||
@change="(checked: boolean) => handleStatusChange(record.id, checked ? 1 : 0)"
|
||||
v-permission="'user:update'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'created_at'">
|
||||
{{ formatDateTime(record.created_at) }}
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" v-permission="'user:update'" @click="handleEdit(record)">
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确定要删除该用户吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger v-permission="'user:delete'">
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
@@ -126,8 +118,8 @@
|
||||
<a-button type="link" size="small" @click="showPassword = true">修改密码</a-button>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="昵称" name="nickname">
|
||||
<a-input v-model:value="formData.nickname" placeholder="请输入昵称" />
|
||||
<a-form-item label="姓名" name="realName">
|
||||
<a-input v-model:value="formData.realName" placeholder="请输入姓名" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="手机号" name="phone">
|
||||
@@ -169,6 +161,7 @@ import {
|
||||
EditOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { userApi, type UserInfo, type UserQuery, type RoleInfo } from '@/api/user';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<UserInfo[]>([]);
|
||||
@@ -193,7 +186,7 @@ const formData = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
nickname: '',
|
||||
realName: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
roleIds: [] as number[],
|
||||
@@ -214,25 +207,25 @@ const formRules = {
|
||||
{ min: 6, message: '密码长度不能小于6位', trigger: 'blur' },
|
||||
],
|
||||
confirmPassword: [{ required: true, message: '请确认密码', trigger: 'blur' }, { validator: validateConfirmPassword, trigger: 'blur' }],
|
||||
nickname: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
|
||||
realName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
phone: [{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }],
|
||||
email: [{ type: 'email' as const, message: '邮箱格式不正确', trigger: 'blur' }],
|
||||
roleIds: [{ required: true, message: '请选择角色', trigger: 'change', type: 'array' as const }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '用户名', dataIndex: 'username', key: 'username', width: 120 },
|
||||
{ title: '昵称', dataIndex: 'nickname', key: 'nickname', width: 120 },
|
||||
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 130 },
|
||||
{ title: '邮箱', dataIndex: 'email', key: 'email', width: 180 },
|
||||
{ title: '角色', key: 'roles', width: 200 },
|
||||
{ title: '用户名', dataIndex: 'username', key: 'username', ellipsis: true },
|
||||
{ title: '姓名', dataIndex: 'realName', key: 'realName', ellipsis: true },
|
||||
{ title: '手机号', dataIndex: 'phone', key: 'phone', ellipsis: true },
|
||||
{ title: '邮箱', dataIndex: 'email', key: 'email', ellipsis: true },
|
||||
{ title: '角色', key: 'roles' },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center' as const,
|
||||
width: 80,
|
||||
},
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 180 },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', ellipsis: true },
|
||||
{ title: '操作', key: 'action', width: 160, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
@@ -277,7 +270,7 @@ async function fetchRoleList() {
|
||||
const res: any = await userApi.getRoleList();
|
||||
if (res.code === 200) {
|
||||
roleOptions.value = res.data.map((item: RoleInfo) => ({
|
||||
label: item.name,
|
||||
label: item.roleName,
|
||||
value: item.id,
|
||||
}));
|
||||
}
|
||||
@@ -321,7 +314,7 @@ function handleEdit(record: UserInfo) {
|
||||
resetForm();
|
||||
|
||||
formData.username = record.username;
|
||||
formData.nickname = record.nickname;
|
||||
formData.realName = record.realName;
|
||||
formData.phone = record.phone || '';
|
||||
formData.email = record.email || '';
|
||||
formData.status = record.status;
|
||||
@@ -334,7 +327,7 @@ function resetForm() {
|
||||
formData.username = '';
|
||||
formData.password = '';
|
||||
formData.confirmPassword = '';
|
||||
formData.nickname = '';
|
||||
formData.realName = '';
|
||||
formData.phone = '';
|
||||
formData.email = '';
|
||||
formData.roleIds = [];
|
||||
@@ -357,7 +350,7 @@ async function handleSubmit() {
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
const updateData: any = {
|
||||
nickname: formData.nickname,
|
||||
realName: formData.realName,
|
||||
phone: formData.phone,
|
||||
email: formData.email,
|
||||
roleIds: formData.roleIds,
|
||||
@@ -373,7 +366,7 @@ async function handleSubmit() {
|
||||
res = await userApi.createUser({
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
nickname: formData.nickname,
|
||||
realName: formData.realName,
|
||||
phone: formData.phone,
|
||||
email: formData.email,
|
||||
roleIds: formData.roleIds,
|
||||
@@ -463,37 +456,30 @@ function getRoleColor(code?: string): string {
|
||||
};
|
||||
return colorMap[code || ''] || 'default';
|
||||
}
|
||||
|
||||
function formatDateTime(dateStr: string) {
|
||||
if (!dateStr) return '-';
|
||||
return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
<style scoped>
|
||||
.user-management {
|
||||
padding: 24px;
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.table-operations {
|
||||
.search-bar {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
.ant-table-thead > tr > th {
|
||||
background-color: #fafafa;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-avatar) {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
</style>
|
||||
+3
-40
@@ -1307,51 +1307,14 @@ CREATE INDEX idx_file_business ON file_record (business_type, business_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 十三、初始数据
|
||||
-- 已迁移至独立脚本 db_seed.sql(幂等执行,便于单独重置种子数据)
|
||||
-- 执行顺序:先 db_init.sql 建表结构 → 再 db_seed.sql 写入种子数据
|
||||
-- ============================================================
|
||||
|
||||
-- 初始角色
|
||||
INSERT INTO sys_role (role_code, role_name, description, sort_order) VALUES
|
||||
('ADMIN', '运营管理员', '全部菜单权限,管理所有客户', 1),
|
||||
('SALESMAN', '业务员', '客户/项目/方案/合同/回访', 2),
|
||||
('WORKER', '共享电工', 'APP 接单处理,无后台菜单', 3),
|
||||
('CUSTOMER', '客户', '小程序查看用电/报警/报修', 4);
|
||||
|
||||
-- 初始菜单
|
||||
INSERT INTO sys_menu (menu_name, menu_path, menu_type, sort_order, permission) VALUES
|
||||
('客户管理', '/customer', 2, 1, 'customer:list'),
|
||||
('项目管理', '/project', 2, 2, 'project:list'),
|
||||
('运营派单', '/dispatch', 2, 3, 'dispatch:list'),
|
||||
('设备监控', '/device', 2, 4, 'device:list'),
|
||||
('统计报表', '/report', 2, 5, 'report:list'),
|
||||
('系统管理', '/system', 2, 6, 'system:list');
|
||||
|
||||
-- 初始字典
|
||||
INSERT INTO sys_dict (dict_type, dict_label, dict_value, sort_order) VALUES
|
||||
('customer_status', '意向', '1', 1),
|
||||
('customer_status', '签约', '2', 2),
|
||||
('customer_status', '运维中', '3', 3),
|
||||
('customer_status', '终止', '4', 4),
|
||||
('equip_type', '配电间', '配电间', 1),
|
||||
('equip_type', '配电井', '配电井', 2),
|
||||
('equip_type', '配电箱', '配电箱', 3),
|
||||
('device_type', '网关', '网关', 1),
|
||||
('device_type', '探测器', '探测器', 2),
|
||||
('device_type', '采集器', '采集器', 3),
|
||||
('dispatch_status', '已派发', '0', 1),
|
||||
('dispatch_status', '已接单', '1', 2),
|
||||
('dispatch_status', '已完成', '2', 3),
|
||||
('pay_cycle', '按月支付', '按月', 1),
|
||||
('pay_cycle', '按年支付', '按年', 2),
|
||||
('dispatch_type', '勘察派单', 'survey', 1),
|
||||
('dispatch_type', '安装派单', 'install', 2),
|
||||
('dispatch_type', '报警派单', 'alarm', 3),
|
||||
('dispatch_type', '维修派单', 'repair', 4),
|
||||
('dispatch_type', '维保派单', 'maintenance', 5);
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- ============================================================
|
||||
-- 完 · 37 张表 · 50+ 索引 · 3 个触发器 · 3 个分区管理函数 · 1 个物化视图 · 2 个 pg_cron 定时任务 · 初始数据
|
||||
-- 完 · 37 张表 · 50+ 索引 · 3 个触发器 · 3 个分区管理函数 · 1 个物化视图 · 2 个 pg_cron 定时任务(种子数据见 db_seed.sql)
|
||||
-- ============================================================
|
||||
|
||||
-- ============================================================
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
-- ============================================================
|
||||
-- 鑫潼智电系统 PostgreSQL 种子数据脚本 V1.0
|
||||
--
|
||||
-- 依赖: db_init.sql(须先执行,确保表结构已创建)
|
||||
--
|
||||
-- 用法:
|
||||
-- psql -U postgres -d xintong -f db_seed.sql
|
||||
--
|
||||
-- 特性:
|
||||
-- - 独立事务(BEGIN/COMMIT)
|
||||
-- - 幂等执行:
|
||||
-- sys_role / sys_menu / sys_user / sys_user_role / sys_role_menu
|
||||
-- 使用 ON CONFLICT (id / 联合主键) DO UPDATE | DO NOTHING
|
||||
-- sys_dict 无业务唯一键,使用 TRUNCATE RESTART IDENTITY 清空重建
|
||||
-- sys_user 可能被业务表外键引用,禁用 TRUNCATE CASCADE
|
||||
-- - 显式重置序列,确保 id 不漂移
|
||||
--
|
||||
-- 内容:
|
||||
-- 1. 初始角色(ADMIN / SALESMAN / WORKER / CUSTOMER)
|
||||
-- 2. 初始菜单(顶级 6 + 系统管理子菜单 5)
|
||||
-- 3. 初始字典(客户/设备/派单/支付等业务字典)
|
||||
-- 4. 管理员账户(admin / admin123456)
|
||||
-- 5. 用户-角色绑定 / 角色-菜单绑定
|
||||
-- ============================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 1. 初始角色(显式指定 id:1=ADMIN 2=SALESMAN 3=WORKER 4=CUSTOMER)
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO sys_role (id, role_code, role_name, description, sort_order) VALUES
|
||||
(1, 'ADMIN', '运营管理员', '全部菜单权限,管理所有客户', 1),
|
||||
(2, 'SALESMAN', '业务员', '客户/项目/方案/合同/回访', 2),
|
||||
(3, 'WORKER', '共享电工', 'APP 接单处理,无后台菜单', 3),
|
||||
(4, 'CUSTOMER', '客户', '小程序查看用电/报警/报修', 4)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
role_code = EXCLUDED.role_code,
|
||||
role_name = EXCLUDED.role_name,
|
||||
description = EXCLUDED.description,
|
||||
sort_order = EXCLUDED.sort_order;
|
||||
|
||||
-- 重置 sys_role 自增序列
|
||||
SELECT setval('sys_role_id_seq', 4, true);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 2. 初始菜单(顶级菜单,显式指定 id:1-6,系统管理为目录类型 menu_type=1,其余为 menu_type=2)
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO sys_menu (id, parent_id, menu_name, menu_path, component, icon, sort_order, menu_type, permission, visible) VALUES
|
||||
(1, NULL, '客户管理', '/customer', NULL, 'TeamOutlined', 1, 2, 'customer:list', 1),
|
||||
(2, NULL, '项目管理', '/project', NULL, 'ProjectOutlined', 2, 2, 'project:list', 1),
|
||||
(3, NULL, '运营派单', '/dispatch', NULL, 'ScheduleOutlined', 3, 2, 'dispatch:list', 1),
|
||||
(4, NULL, '设备监控', '/device', NULL, 'MonitorOutlined', 4, 2, 'device:list', 1),
|
||||
(5, NULL, '统计报表', '/report', NULL, 'BarChartOutlined', 5, 2, 'report:list', 1),
|
||||
(6, NULL, '系统管理', '/system', NULL, 'SettingOutlined', 6, 1, 'system:list', 1)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
parent_id = EXCLUDED.parent_id,
|
||||
menu_name = EXCLUDED.menu_name,
|
||||
menu_path = EXCLUDED.menu_path,
|
||||
component = EXCLUDED.component,
|
||||
icon = EXCLUDED.icon,
|
||||
sort_order = EXCLUDED.sort_order,
|
||||
menu_type = EXCLUDED.menu_type,
|
||||
permission = EXCLUDED.permission,
|
||||
visible = EXCLUDED.visible;
|
||||
|
||||
-- 初始菜单(系统管理子菜单,parent_id=6,显式指定 id:7-11)
|
||||
INSERT INTO sys_menu (id, parent_id, menu_name, menu_path, component, icon, sort_order, menu_type, permission, visible) VALUES
|
||||
(7, 6, '用户管理', '/system/user', 'system/user/index', 'UserOutlined', 1, 2, 'system:user', 1),
|
||||
(8, 6, '角色管理', '/system/role', 'system/role/index', 'SafetyCertificateOutlined', 2, 2, 'system:role', 1),
|
||||
(9, 6, '菜单管理', '/system/menu', 'system/menu/index', 'MenuOutlined', 3, 2, 'system:menu', 1),
|
||||
(10, 6, '数据字典', '/system/dict', 'system/dict/index', 'BookOutlined', 4, 2, 'system:dict', 1),
|
||||
(11, 6, '操作日志', '/log/operation', 'log/operation/index', 'FileTextOutlined', 5, 2, 'system:log', 1)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
parent_id = EXCLUDED.parent_id,
|
||||
menu_name = EXCLUDED.menu_name,
|
||||
menu_path = EXCLUDED.menu_path,
|
||||
component = EXCLUDED.component,
|
||||
icon = EXCLUDED.icon,
|
||||
sort_order = EXCLUDED.sort_order,
|
||||
menu_type = EXCLUDED.menu_type,
|
||||
permission = EXCLUDED.permission,
|
||||
visible = EXCLUDED.visible;
|
||||
|
||||
-- 重置 sys_menu 自增序列
|
||||
SELECT setval('sys_menu_id_seq', 11, true);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 3. 初始字典(sys_dict 无业务唯一键,清空重建以支持幂等执行)
|
||||
-- ------------------------------------------------------------
|
||||
TRUNCATE sys_dict RESTART IDENTITY;
|
||||
|
||||
INSERT INTO sys_dict (dict_type, dict_label, dict_value, sort_order) VALUES
|
||||
('customer_status', '意向', '1', 1),
|
||||
('customer_status', '签约', '2', 2),
|
||||
('customer_status', '运维中', '3', 3),
|
||||
('customer_status', '终止', '4', 4),
|
||||
('equip_type', '配电间', '配电间', 1),
|
||||
('equip_type', '配电井', '配电井', 2),
|
||||
('equip_type', '配电箱', '配电箱', 3),
|
||||
('device_type', '网关', '网关', 1),
|
||||
('device_type', '探测器', '探测器', 2),
|
||||
('device_type', '采集器', '采集器', 3),
|
||||
('dispatch_status', '已派发', '0', 1),
|
||||
('dispatch_status', '已接单', '1', 2),
|
||||
('dispatch_status', '已完成', '2', 3),
|
||||
('pay_cycle', '按月支付', '按月', 1),
|
||||
('pay_cycle', '按年支付', '按年', 2),
|
||||
('dispatch_type', '勘察派单', 'survey', 1),
|
||||
('dispatch_type', '安装派单', 'install', 2),
|
||||
('dispatch_type', '报警派单', 'alarm', 3),
|
||||
('dispatch_type', '维修派单', 'repair', 4),
|
||||
('dispatch_type', '维保派单', 'maintenance', 5);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 4. 管理员账户(显式指定 id=1,密码哈希为 bcrypt('admin123456'))
|
||||
-- sys_user 可能被业务表外键引用,使用 ON CONFLICT 而非 TRUNCATE,避免业务数据被级联清空
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO sys_user (id, username, password_hash, real_name, phone, email, status) VALUES
|
||||
(1, 'admin', '$2a$10$GC/0CaPf9Ti/yeNVSZ8L3OKvCS1MrAY.aohg9UerB6heyp78Fo17.', '运营管理员', NULL, NULL, 1)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
username = EXCLUDED.username,
|
||||
password_hash = EXCLUDED.password_hash,
|
||||
real_name = EXCLUDED.real_name,
|
||||
phone = EXCLUDED.phone,
|
||||
email = EXCLUDED.email,
|
||||
status = EXCLUDED.status;
|
||||
|
||||
-- 重置 sys_user 自增序列
|
||||
SELECT setval('sys_user_id_seq', 1, true);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 5. 用户-角色绑定:admin → ADMIN
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO sys_user_role (user_id, role_id) VALUES (1, 1)
|
||||
ON CONFLICT (user_id, role_id) DO NOTHING;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 6. 角色-菜单绑定:ADMIN 拥有全部 11 条菜单,SALESMAN 拥有业务菜单(客户/项目/统计报表)
|
||||
-- WORKER、CUSTOMER 不绑定任何菜单
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO sys_role_menu (role_id, menu_id) VALUES
|
||||
(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (1, 10), (1, 11),
|
||||
(2, 1), (2, 2), (2, 5)
|
||||
ON CONFLICT (role_id, menu_id) DO NOTHING;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- ============================================================
|
||||
-- 完 · 7 张系统表种子数据(4 角色 + 11 菜单 + 20 字典 + 1 管理员 + 绑定关系)
|
||||
-- ============================================================
|
||||
+3
-2
@@ -19,13 +19,14 @@
|
||||
"@midwayjs/validate": "^3.19.0",
|
||||
"@types/validator": "^13.15.10",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cache-manager-ioredis": "^2.1.5",
|
||||
"cache-manager-ioredis": "^2.1.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"ioredis": "^5.4.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mysql2": "^3.12.0",
|
||||
"pg": "^8.11.0",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"sequelize": "^6.37.5",
|
||||
"sequelize-typescript": "^2.1.6",
|
||||
|
||||
@@ -11,12 +11,12 @@ export default {
|
||||
sequelize: {
|
||||
dataSource: {
|
||||
default: {
|
||||
dialect: "mysql",
|
||||
host: process.env.MYSQL_HOST || "localhost",
|
||||
port: Number(process.env.MYSQL_PORT) || 3306,
|
||||
database: process.env.MYSQL_DATABASE || "smart_grid_iot",
|
||||
username: process.env.MYSQL_USERNAME || "root",
|
||||
password: process.env.MYSQL_PASSWORD || "123",
|
||||
dialect: "postgres",
|
||||
host: process.env.PG_HOST || "localhost",
|
||||
port: Number(process.env.PG_PORT) || 5432,
|
||||
database: process.env.PG_DATABASE || "xintong",
|
||||
username: process.env.PG_USERNAME || "postgres",
|
||||
password: process.env.PG_PASSWORD || "postgres",
|
||||
timezone: "+08:00",
|
||||
models: [join(__dirname, "../entity")],
|
||||
define: {
|
||||
@@ -62,22 +62,5 @@ export default {
|
||||
csrf: {
|
||||
enable: false
|
||||
}
|
||||
},
|
||||
|
||||
middleware: ["authMiddleware", "rbacMiddleware", "logMiddleware"],
|
||||
|
||||
middlewareConfig: {
|
||||
authMiddleware: {
|
||||
match: "/api/**",
|
||||
ignore: ["/api/auth/**", "/api/health"]
|
||||
},
|
||||
rbacMiddleware: {
|
||||
match: "/api/**",
|
||||
ignore: ["/api/auth/**", "/api/health", "/api/dashboard/**"]
|
||||
},
|
||||
logMiddleware: {
|
||||
match: "/api/**",
|
||||
ignore: ["/api/logs/**", "/api/health", "/api/auth/login"]
|
||||
}
|
||||
}
|
||||
} as MidwayConfig;
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
sequelize: {
|
||||
sync: true,
|
||||
sync: false,
|
||||
},
|
||||
};
|
||||
@@ -1,15 +1,23 @@
|
||||
import { Configuration, ILifeCycle } from "@midwayjs/core";
|
||||
import { Configuration, App, ILifeCycle } from "@midwayjs/core";
|
||||
import * as Koa from "@midwayjs/koa";
|
||||
import * as Sequelize from "@midwayjs/sequelize";
|
||||
import * as CacheManager from "@midwayjs/cache-manager";
|
||||
import { join } from "path";
|
||||
import { AuthMiddleware } from "./middleware/auth.middleware";
|
||||
import { RbacMiddleware } from "./middleware/rbac.middleware";
|
||||
import { LogMiddleware } from "./middleware/log.middleware";
|
||||
|
||||
@Configuration({
|
||||
imports: [Koa, Sequelize, CacheManager],
|
||||
importConfigs: [join(__dirname, "./config")]
|
||||
})
|
||||
export class ContainerConfigLifeCycle implements ILifeCycle {
|
||||
@App()
|
||||
app!: Koa.Application;
|
||||
|
||||
async onReady() {
|
||||
// 注册全局中间件(顺序:鉴权 → 权限 → 日志)
|
||||
this.app.useMiddleware([AuthMiddleware, RbacMiddleware, LogMiddleware]);
|
||||
console.log("智能物联网设备后台管理系统启动成功");
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import { AuthService } from "../service/auth.service";
|
||||
import { TokenUtil } from "../util/token";
|
||||
import { PermissionService } from "../service/permission.service";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import User from "../entity/user.entity";
|
||||
import UserRole from "../entity/userRole.entity";
|
||||
import Role from "../entity/role.entity";
|
||||
import SysUser from "../entity/sysUser.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
|
||||
@Controller("/api/auth")
|
||||
export class AuthController {
|
||||
@@ -22,14 +22,14 @@ export class AuthController {
|
||||
@Inject()
|
||||
ctx!: Context;
|
||||
|
||||
@InjectRepository(User)
|
||||
userModel!: typeof User;
|
||||
@InjectRepository(SysUser)
|
||||
userModel!: typeof SysUser;
|
||||
|
||||
@InjectRepository(UserRole)
|
||||
userRoleModel!: typeof UserRole;
|
||||
@InjectRepository(SysUserRole)
|
||||
userRoleModel!: typeof SysUserRole;
|
||||
|
||||
@InjectRepository(Role)
|
||||
roleModel!: typeof Role;
|
||||
@InjectRepository(SysRole)
|
||||
roleModel!: typeof SysRole;
|
||||
|
||||
@Post("/login")
|
||||
async login(@Body() body: { username: string; password: string }) {
|
||||
@@ -110,14 +110,7 @@ export class AuthController {
|
||||
const decoded = this.tokenUtil.verifyToken(token);
|
||||
const userId = decoded.userId;
|
||||
|
||||
const user = await this.userModel.findByPk(userId, {
|
||||
include: [
|
||||
{
|
||||
model: this.userRoleModel,
|
||||
include: [this.roleModel]
|
||||
}
|
||||
]
|
||||
});
|
||||
const user = await this.userModel.findByPk(userId);
|
||||
if (!user) {
|
||||
return {
|
||||
code: 404,
|
||||
@@ -125,8 +118,13 @@ export class AuthController {
|
||||
};
|
||||
}
|
||||
|
||||
const permissions = await this.permissionService.getUserPermissions(userId);
|
||||
const roles = (user as any).userRoles?.map((ur: any) => ur.role?.code).filter(Boolean) || [];
|
||||
const userRoles = await this.userRoleModel.findAll({
|
||||
where: { userId },
|
||||
include: [this.roleModel]
|
||||
});
|
||||
const roles = userRoles.map(ur => ur.role?.roleCode).filter(Boolean) || [];
|
||||
|
||||
const menus = await this.permissionService.getUserMenus(userId);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
@@ -134,10 +132,9 @@ export class AuthController {
|
||||
data: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
nickname: user.nickname || user.username,
|
||||
avatar: user.avatar,
|
||||
realName: user.realName,
|
||||
roles,
|
||||
permissions
|
||||
menus
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import { DictService } from "../service/dict.service";
|
||||
|
||||
@Controller("/api/dicts")
|
||||
export class DictController {
|
||||
@Inject()
|
||||
ctx!: Context;
|
||||
|
||||
@Inject()
|
||||
dictService!: DictService;
|
||||
|
||||
@Get("/")
|
||||
async list(@Query() query: { page?: number; pageSize?: number; dictType?: string; keyword?: string; status?: number }) {
|
||||
const result = await this.dictService.list(query);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get("/types")
|
||||
async types() {
|
||||
const result = await this.dictService.types();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Post("/")
|
||||
async create(@Body() body: any) {
|
||||
const result = await this.dictService.create(body);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Put("/:id")
|
||||
async update(@Param("id") id: string, @Body() body: any) {
|
||||
const result = await this.dictService.update(id, body);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Del("/:id")
|
||||
async remove(@Param("id") id: string) {
|
||||
const result = await this.dictService.remove(id);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,7 @@
|
||||
import { Controller, Get, Inject, Query, Param, All } from "@midwayjs/core";
|
||||
import { Controller, Get, Del, Inject, Query, Param, All } from "@midwayjs/core";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import { LogService } from "../service/log.service";
|
||||
|
||||
const Delete = (path?: string) => {
|
||||
return (target: any, key: string, descriptor: PropertyDescriptor) => {
|
||||
return descriptor;
|
||||
};
|
||||
};
|
||||
|
||||
@Controller("/api/logs")
|
||||
export class LogController {
|
||||
@Inject()
|
||||
@@ -17,7 +11,7 @@ export class LogController {
|
||||
logService!: LogService;
|
||||
|
||||
@Get("/")
|
||||
async list(@Query() query: { page?: number; pageSize?: number; moduleName?: string; actionType?: string; startDate?: string; endDate?: string; operator?: string }) {
|
||||
async list(@Query() query: { page?: number; pageSize?: number; userId?: string; module?: string; action?: string; startDate?: string; endDate?: string }) {
|
||||
const result = await this.logService.list(query);
|
||||
return result;
|
||||
}
|
||||
@@ -46,7 +40,7 @@ export class LogController {
|
||||
}
|
||||
}
|
||||
|
||||
@Delete("/clear")
|
||||
@Del("/clear")
|
||||
async clear() {
|
||||
const result = await this.logService.cleanExpiredLogs(0);
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Controller, Get, Post, Put, Del, Inject, Body, Param } from "@midwayjs/core";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import { MenuService } from "../service/menu.service";
|
||||
|
||||
@Controller("/api/menus")
|
||||
export class MenuController {
|
||||
@Inject()
|
||||
ctx!: Context;
|
||||
|
||||
@Inject()
|
||||
menuService!: MenuService;
|
||||
|
||||
@Get("/tree")
|
||||
async getMenuTree() {
|
||||
const result = await this.menuService.getMenuTree();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Post("/")
|
||||
async create(@Body() body: any) {
|
||||
const result = await this.menuService.create(body);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Put("/:id")
|
||||
async update(@Param("id") id: string, @Body() body: any) {
|
||||
const result = await this.menuService.update(id, body);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Del("/:id")
|
||||
async remove(@Param("id") id: string) {
|
||||
const result = await this.menuService.remove(id);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,7 @@
|
||||
import { Controller, Get, Post, Put, Inject, Query, Body, Param } from "@midwayjs/core";
|
||||
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import { RoleService } from "../service/role.service";
|
||||
|
||||
const Delete = (path?: string) => {
|
||||
return (target: any, key: string, descriptor: PropertyDescriptor) => {
|
||||
return descriptor;
|
||||
};
|
||||
};
|
||||
|
||||
@Controller("/api/roles")
|
||||
export class RoleController {
|
||||
@Inject()
|
||||
@@ -46,21 +40,21 @@ export class RoleController {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Delete("/:id")
|
||||
@Del("/:id")
|
||||
async delete(@Param("id") id: string) {
|
||||
const result = await this.roleService.delete(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get("/:id/permissions")
|
||||
async getPermissions(@Param("id") id: string) {
|
||||
const result = await this.roleService.getPermissions(Number(id));
|
||||
@Get("/:id/menus")
|
||||
async getMenus(@Param("id") id: string) {
|
||||
const result = await this.roleService.getMenus(Number(id));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Put("/:id/permissions")
|
||||
async updatePermissions(@Param("id") id: string, @Body() body: { permissionIds: number[] }) {
|
||||
const result = await this.roleService.updatePermissions(Number(id), body.permissionIds);
|
||||
@Put("/:id/menus")
|
||||
async updateMenus(@Param("id") id: string, @Body() body: { menuIds: number[] }) {
|
||||
const result = await this.roleService.updateMenus(Number(id), body.menuIds);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Controller, Get, Put, Inject, Body } from "@midwayjs/core";
|
||||
import { SystemService } from "../service/system.service";
|
||||
|
||||
@Controller("/api/settings")
|
||||
export class SystemController {
|
||||
@Inject()
|
||||
systemService!: SystemService;
|
||||
|
||||
@Get("/")
|
||||
async getSettings() {
|
||||
const result = await this.systemService.getSettings();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Put("/basic")
|
||||
async updateBasicSettings(@Body() body: any) {
|
||||
const result = await this.systemService.updateBasicSettings(body);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Put("/security")
|
||||
async updateSecuritySettings(@Body() body: any) {
|
||||
const result = await this.systemService.updateSecuritySettings(body);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Put("/notification")
|
||||
async updateNotificationSettings(@Body() body: any) {
|
||||
const result = await this.systemService.updateNotificationSettings(body);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,7 @@
|
||||
import { Controller, Get, Post, Put, Inject, Query, Body, Param } from "@midwayjs/core";
|
||||
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import { UserService } from "../service/user.service";
|
||||
|
||||
const Delete = (path?: string) => {
|
||||
return (target: any, key: string, descriptor: PropertyDescriptor) => {
|
||||
return descriptor;
|
||||
};
|
||||
};
|
||||
|
||||
@Controller("/api/users")
|
||||
export class UserController {
|
||||
@Inject()
|
||||
@@ -46,15 +40,17 @@ export class UserController {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Delete("/:id")
|
||||
@Del("/:id")
|
||||
async delete(@Param("id") id: string) {
|
||||
const result = await this.userService.delete(id);
|
||||
const currentUserId = this.ctx.state.user?.userId;
|
||||
const result = await this.userService.delete(id, currentUserId);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Post("/batch-delete")
|
||||
async batchDelete(@Body() body: { ids: number[] }) {
|
||||
const result = await this.userService.batchDelete(body.ids);
|
||||
const currentUserId = this.ctx.state.user?.userId;
|
||||
const result = await this.userService.batchDelete(body.ids, currentUserId);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { IsNotEmpty, IsString, IsOptional, IsInt, IsIn } from "class-validator";
|
||||
|
||||
export class CreateDictDTO {
|
||||
@IsNotEmpty({ message: "字典类型不能为空" })
|
||||
@IsString()
|
||||
dictType!: string;
|
||||
|
||||
@IsNotEmpty({ message: "字典标签不能为空" })
|
||||
@IsString()
|
||||
dictLabel!: string;
|
||||
|
||||
@IsNotEmpty({ message: "字典键值不能为空" })
|
||||
@IsString()
|
||||
dictValue!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sortOrder?: number = 0;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn([0, 1], { message: "状态只能为 0 或 1" })
|
||||
status?: number = 1;
|
||||
}
|
||||
|
||||
export class UpdateDictDTO {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
dictType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
dictLabel?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
dictValue?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sortOrder?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn([0, 1], { message: "状态只能为 0 或 1" })
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export class QueryDictDTO {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
page?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
pageSize?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
dictType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
keyword?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn([0, 1])
|
||||
status?: number;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { IsNotEmpty, IsString, IsOptional, IsInt, IsIn } from "class-validator";
|
||||
|
||||
export class CreateMenuDTO {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
parentId?: number;
|
||||
|
||||
@IsNotEmpty({ message: "菜单名称不能为空" })
|
||||
@IsString()
|
||||
menuName!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
menuPath?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
component?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
icon?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sortOrder?: number = 0;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn([1, 2])
|
||||
menuType?: number = 2;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
permission?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn([0, 1])
|
||||
visible?: number = 1;
|
||||
}
|
||||
|
||||
export class UpdateMenuDTO {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
parentId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
menuName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
menuPath?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
component?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
icon?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sortOrder?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn([1, 2])
|
||||
menuType?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
permission?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn([0, 1])
|
||||
visible?: number;
|
||||
}
|
||||
+12
-20
@@ -1,24 +1,18 @@
|
||||
import { IsNotEmpty, IsString, IsOptional, IsInt, Min, Max, IsArray } from "class-validator";
|
||||
import { IsNotEmpty, IsString, IsOptional, IsInt, IsArray } from "class-validator";
|
||||
|
||||
export class CreateRoleDTO {
|
||||
@IsNotEmpty({ message: "角色标识不能为空" })
|
||||
@IsNotEmpty({ message: "角色编码不能为空" })
|
||||
@IsString()
|
||||
code!: string;
|
||||
roleCode!: string;
|
||||
|
||||
@IsNotEmpty({ message: "角色名称不能为空" })
|
||||
@IsString()
|
||||
name!: string;
|
||||
roleName!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Max(1)
|
||||
status?: number = 1;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sortOrder?: number = 0;
|
||||
@@ -27,25 +21,23 @@ export class CreateRoleDTO {
|
||||
export class UpdateRoleDTO {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
roleCode?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
roleName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Max(1)
|
||||
status?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export class UpdatePermissionDTO {
|
||||
@IsNotEmpty({ message: "权限ID列表不能为空" })
|
||||
export class UpdateMenusDTO {
|
||||
@IsNotEmpty({ message: "菜单ID列表不能为空" })
|
||||
@IsArray()
|
||||
permissionIds!: number[];
|
||||
menuIds!: number[];
|
||||
}
|
||||
@@ -13,10 +13,11 @@ export class CreateUserDTO {
|
||||
@MaxLength(20, { message: "密码长度不能超过20位" })
|
||||
password!: string;
|
||||
|
||||
@IsNotEmpty({ message: "昵称不能为空" })
|
||||
@IsNotEmpty({ message: "真实姓名不能为空" })
|
||||
@IsString()
|
||||
@MinLength(2, { message: "昵称长度不能小于2位" })
|
||||
nickname!: string;
|
||||
@MinLength(2, { message: "真实姓名长度不能小于2位" })
|
||||
@MaxLength(64, { message: "真实姓名长度不能超过64位" })
|
||||
realName!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail({}, { message: "邮箱格式不正确" })
|
||||
@@ -39,8 +40,9 @@ export class CreateUserDTO {
|
||||
export class UpdateUserDTO {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(2, { message: "昵称长度不能小于2位" })
|
||||
nickname?: string;
|
||||
@MinLength(2, { message: "真实姓名长度不能小于2位" })
|
||||
@MaxLength(64, { message: "真实姓名长度不能超过64位" })
|
||||
realName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail({}, { message: "邮箱格式不正确" })
|
||||
@@ -50,10 +52,6 @@ export class UpdateUserDTO {
|
||||
@Matches(/^1[3-9]\d{9}$/, { message: "手机号格式不正确" })
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
avatar?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
roleIds?: number[];
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt } from "sequelize-typescript";
|
||||
|
||||
@Table({
|
||||
tableName: "operation_logs",
|
||||
comment: "操作日志表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: false
|
||||
})
|
||||
export default class OperationLog extends Model {
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
})
|
||||
id!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
field: "user_id",
|
||||
comment: "操作用户ID"
|
||||
})
|
||||
userId!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(50),
|
||||
field: "username",
|
||||
comment: "操作人用户名"
|
||||
})
|
||||
username!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(255),
|
||||
allowNull: false,
|
||||
field: "action",
|
||||
comment: "操作描述"
|
||||
})
|
||||
action!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(50),
|
||||
field: "module",
|
||||
comment: "操作模块"
|
||||
})
|
||||
module!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(10),
|
||||
field: "method",
|
||||
comment: "请求方法(GET/POST/PUT/DELETE等)"
|
||||
})
|
||||
method!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(500),
|
||||
field: "url",
|
||||
comment: "请求URL"
|
||||
})
|
||||
url!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(45),
|
||||
field: "ip_address",
|
||||
comment: "IP地址(支持IPv6)"
|
||||
})
|
||||
ipAddress!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(500),
|
||||
field: "user_agent",
|
||||
comment: "浏览器UA"
|
||||
})
|
||||
userAgent!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.TEXT,
|
||||
field: "request_params",
|
||||
comment: "请求参数(JSON格式)"
|
||||
})
|
||||
requestParams!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.TEXT,
|
||||
field: "response_data",
|
||||
comment: "响应数据(JSON格式)"
|
||||
})
|
||||
responseData!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.INTEGER,
|
||||
field: "status_code",
|
||||
comment: "HTTP响应状态码"
|
||||
})
|
||||
statusCode!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.INTEGER,
|
||||
field: "duration",
|
||||
comment: "请求耗时(毫秒)"
|
||||
})
|
||||
duration!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.TEXT,
|
||||
field: "error_message",
|
||||
comment: "错误信息(仅失败时记录)"
|
||||
})
|
||||
errorMessage!: string;
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, UpdatedAt, HasMany } from "sequelize-typescript";
|
||||
import RolePermission from "./rolePermission.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "permissions",
|
||||
comment: "权限表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: "updated_at"
|
||||
})
|
||||
export default class Permission extends Model {
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
})
|
||||
id!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(50),
|
||||
allowNull: false,
|
||||
comment: "权限名称"
|
||||
})
|
||||
name!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(100),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: "权限编码(唯一标识)"
|
||||
})
|
||||
code!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.ENUM("menu", "button", "api"),
|
||||
allowNull: false,
|
||||
defaultValue: "menu",
|
||||
comment: "权限类型: menu-菜单 button-按钮 api-接口"
|
||||
})
|
||||
type!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
field: "parent_id",
|
||||
comment: "父权限ID(NULL表示一级菜单)"
|
||||
})
|
||||
parentId!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
field: "sort_order",
|
||||
comment: "排序(数值越小越靠前)"
|
||||
})
|
||||
sortOrder!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(50),
|
||||
field: "icon",
|
||||
comment: "图标(菜单类型使用)"
|
||||
})
|
||||
icon!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(255),
|
||||
field: "route",
|
||||
comment: "路由路径(菜单类型使用)"
|
||||
})
|
||||
route!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(255),
|
||||
field: "component",
|
||||
comment: "组件路径(菜单类型使用)"
|
||||
})
|
||||
component!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.TINYINT,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: "状态: 0-禁用 1-启用"
|
||||
})
|
||||
status!: number;
|
||||
|
||||
@HasMany(() => RolePermission)
|
||||
rolePermissions!: RolePermission[];
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updated_at!: Date;
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, UpdatedAt, HasMany } from "sequelize-typescript";
|
||||
import UserRole from "./userRole.entity";
|
||||
import RolePermission from "./rolePermission.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "roles",
|
||||
comment: "角色表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: "updated_at"
|
||||
})
|
||||
export default class Role extends Model {
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
})
|
||||
id!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(50),
|
||||
allowNull: false,
|
||||
comment: "角色名称"
|
||||
})
|
||||
name!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: "角色编码(唯一标识)"
|
||||
})
|
||||
code!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(255),
|
||||
field: "description",
|
||||
comment: "描述"
|
||||
})
|
||||
description!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.TINYINT,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: "状态: 0-禁用 1-启用"
|
||||
})
|
||||
status!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
field: "sort_order",
|
||||
comment: "排序(数值越小越靠前)"
|
||||
})
|
||||
sortOrder!: number;
|
||||
|
||||
@HasMany(() => UserRole)
|
||||
userRoles!: UserRole[];
|
||||
|
||||
@HasMany(() => RolePermission)
|
||||
rolePermissions!: RolePermission[];
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updated_at!: Date;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
|
||||
import Role from "./role.entity";
|
||||
import Permission from "./permission.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "role_permissions",
|
||||
comment: "角色权限关联表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: false
|
||||
})
|
||||
export default class RolePermission extends Model {
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
})
|
||||
id!: number;
|
||||
|
||||
@ForeignKey(() => Role)
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
allowNull: false,
|
||||
field: "role_id",
|
||||
comment: "角色ID"
|
||||
})
|
||||
roleId!: number;
|
||||
|
||||
@ForeignKey(() => Permission)
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
allowNull: false,
|
||||
field: "permission_id",
|
||||
comment: "权限ID"
|
||||
})
|
||||
permissionId!: number;
|
||||
|
||||
@BelongsTo(() => Role)
|
||||
role!: Role;
|
||||
|
||||
@BelongsTo(() => Permission)
|
||||
permission!: Permission;
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt } from "sequelize-typescript";
|
||||
|
||||
@Table({
|
||||
tableName: "sys_dict",
|
||||
comment: "系统字典表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: false
|
||||
})
|
||||
export default class SysDict extends Model {
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
})
|
||||
id!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(64),
|
||||
allowNull: false,
|
||||
field: "dict_type",
|
||||
comment: "字典类型"
|
||||
})
|
||||
dictType!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(128),
|
||||
allowNull: false,
|
||||
field: "dict_label",
|
||||
comment: "字典标签"
|
||||
})
|
||||
dictLabel!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(128),
|
||||
allowNull: false,
|
||||
field: "dict_value",
|
||||
comment: "字典键值"
|
||||
})
|
||||
dictValue!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
field: "sort_order",
|
||||
comment: "排序(数值越小越靠前)"
|
||||
})
|
||||
sortOrder!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.SMALLINT,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: "状态: 0-禁用 1-启用"
|
||||
})
|
||||
status!: number;
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, HasMany } from "sequelize-typescript";
|
||||
import SysRoleMenu from "./sysRoleMenu.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "sys_menu",
|
||||
comment: "系统菜单表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: false
|
||||
})
|
||||
export default class SysMenu extends Model {
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
})
|
||||
id!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
field: "parent_id",
|
||||
comment: "父菜单ID(NULL表示一级菜单)"
|
||||
})
|
||||
parentId!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(64),
|
||||
allowNull: false,
|
||||
field: "menu_name",
|
||||
comment: "菜单名称"
|
||||
})
|
||||
menuName!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(256),
|
||||
field: "menu_path",
|
||||
comment: "菜单路由路径"
|
||||
})
|
||||
menuPath!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(256),
|
||||
field: "component",
|
||||
comment: "组件路径"
|
||||
})
|
||||
component!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(64),
|
||||
field: "icon",
|
||||
comment: "菜单图标"
|
||||
})
|
||||
icon!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
field: "sort_order",
|
||||
comment: "排序(数值越小越靠前)"
|
||||
})
|
||||
sortOrder!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.SMALLINT,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
field: "menu_type",
|
||||
comment: "菜单类型: 1-目录 2-菜单 3-按钮"
|
||||
})
|
||||
menuType!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(128),
|
||||
field: "permission",
|
||||
comment: "权限标识(按钮类型使用)"
|
||||
})
|
||||
permission!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.SMALLINT,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: "是否可见: 0-隐藏 1-显示"
|
||||
})
|
||||
visible!: number;
|
||||
|
||||
@HasMany(() => SysRoleMenu)
|
||||
sysRoleMenus!: SysRoleMenu[];
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt } from "sequelize-typescript";
|
||||
|
||||
@Table({
|
||||
tableName: "sys_operation_log",
|
||||
comment: "系统操作日志表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: false
|
||||
})
|
||||
export default class SysOperationLog extends Model {
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
})
|
||||
id!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
field: "user_id",
|
||||
comment: "操作用户ID"
|
||||
})
|
||||
userId!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(64),
|
||||
field: "module",
|
||||
comment: "操作模块"
|
||||
})
|
||||
module!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(64),
|
||||
field: "action",
|
||||
comment: "操作动作"
|
||||
})
|
||||
action!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(256),
|
||||
field: "method",
|
||||
comment: "请求方法(GET/POST/PUT/DELETE等)"
|
||||
})
|
||||
method!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.TEXT,
|
||||
field: "params",
|
||||
comment: "请求参数(JSON格式)"
|
||||
})
|
||||
params!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(64),
|
||||
field: "ip",
|
||||
comment: "操作IP(支持IPv6)"
|
||||
})
|
||||
ip!: string;
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, HasMany } from "sequelize-typescript";
|
||||
import SysUserRole from "./sysUserRole.entity";
|
||||
import SysRoleMenu from "./sysRoleMenu.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "sys_role",
|
||||
comment: "系统角色表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: false
|
||||
})
|
||||
export default class SysRole extends Model {
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
})
|
||||
id!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(32),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
field: "role_code",
|
||||
comment: "角色编码(唯一标识)"
|
||||
})
|
||||
roleCode!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(64),
|
||||
allowNull: false,
|
||||
field: "role_name",
|
||||
comment: "角色名称"
|
||||
})
|
||||
roleName!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.TEXT,
|
||||
field: "description",
|
||||
comment: "角色描述"
|
||||
})
|
||||
description!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
field: "sort_order",
|
||||
comment: "排序(数值越小越靠前)"
|
||||
})
|
||||
sortOrder!: number;
|
||||
|
||||
@HasMany(() => SysUserRole)
|
||||
sysUserRoles!: SysUserRole[];
|
||||
|
||||
@HasMany(() => SysRoleMenu)
|
||||
sysRoleMenus!: SysRoleMenu[];
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Column, Model, Table, DataType, ForeignKey, BelongsTo } from "sequelize-typescript";
|
||||
import SysRole from "./sysRole.entity";
|
||||
import SysMenu from "./sysMenu.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "sys_role_menu",
|
||||
comment: "系统角色菜单关联表",
|
||||
timestamps: false
|
||||
})
|
||||
export default class SysRoleMenu extends Model {
|
||||
@ForeignKey(() => SysRole)
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
field: "role_id",
|
||||
comment: "角色ID"
|
||||
})
|
||||
roleId!: number;
|
||||
|
||||
@ForeignKey(() => SysMenu)
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
field: "menu_id",
|
||||
comment: "菜单ID"
|
||||
})
|
||||
menuId!: number;
|
||||
|
||||
@BelongsTo(() => SysRole)
|
||||
role!: SysRole;
|
||||
|
||||
@BelongsTo(() => SysMenu)
|
||||
menu!: SysMenu;
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, UpdatedAt, HasMany } from "sequelize-typescript";
|
||||
import UserRole from "./userRole.entity";
|
||||
import SysUserRole from "./sysUserRole.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "users",
|
||||
comment: "用户表",
|
||||
tableName: "sys_user",
|
||||
comment: "系统用户表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: "updated_at"
|
||||
})
|
||||
export default class User extends Model {
|
||||
export default class SysUser extends Model {
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
autoIncrement: true,
|
||||
@@ -17,7 +17,7 @@ export default class User extends Model {
|
||||
id!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(50),
|
||||
type: DataType.STRING(64),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: "用户名"
|
||||
@@ -25,25 +25,20 @@ export default class User extends Model {
|
||||
username!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(255),
|
||||
type: DataType.STRING(256),
|
||||
allowNull: false,
|
||||
field: "password_hash",
|
||||
comment: "加密密码(bcrypt)"
|
||||
})
|
||||
password!: string;
|
||||
passwordHash!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(50),
|
||||
field: "nickname",
|
||||
comment: "昵称"
|
||||
type: DataType.STRING(64),
|
||||
allowNull: false,
|
||||
field: "real_name",
|
||||
comment: "真实姓名"
|
||||
})
|
||||
nickname!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(100),
|
||||
field: "email",
|
||||
comment: "邮箱"
|
||||
})
|
||||
email!: string;
|
||||
realName!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(20),
|
||||
@@ -53,14 +48,14 @@ export default class User extends Model {
|
||||
phone!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(255),
|
||||
field: "avatar",
|
||||
comment: "头像URL"
|
||||
type: DataType.STRING(128),
|
||||
field: "email",
|
||||
comment: "邮箱"
|
||||
})
|
||||
avatar!: string;
|
||||
email!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.TINYINT,
|
||||
type: DataType.SMALLINT,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: "状态: 0-禁用 1-启用"
|
||||
@@ -74,15 +69,8 @@ export default class User extends Model {
|
||||
})
|
||||
lastLoginAt!: Date;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(45),
|
||||
field: "last_login_ip",
|
||||
comment: "最后登录IP(支持IPv6)"
|
||||
})
|
||||
lastLoginIp!: string;
|
||||
|
||||
@HasMany(() => UserRole)
|
||||
userRoles!: UserRole[];
|
||||
@HasMany(() => SysUserRole)
|
||||
sysUserRoles!: SysUserRole[];
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Column, Model, Table, DataType, ForeignKey, BelongsTo } from "sequelize-typescript";
|
||||
import SysUser from "./sysUser.entity";
|
||||
import SysRole from "./sysRole.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "sys_user_role",
|
||||
comment: "系统用户角色关联表",
|
||||
timestamps: false
|
||||
})
|
||||
export default class SysUserRole extends Model {
|
||||
@ForeignKey(() => SysUser)
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
field: "user_id",
|
||||
comment: "用户ID"
|
||||
})
|
||||
userId!: number;
|
||||
|
||||
@ForeignKey(() => SysRole)
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
field: "role_id",
|
||||
comment: "角色ID"
|
||||
})
|
||||
roleId!: number;
|
||||
|
||||
@BelongsTo(() => SysUser)
|
||||
user!: SysUser;
|
||||
|
||||
@BelongsTo(() => SysRole)
|
||||
role!: SysRole;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
|
||||
import User from "./user.entity";
|
||||
import Role from "./role.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "user_roles",
|
||||
comment: "用户角色关联表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: false
|
||||
})
|
||||
export default class UserRole extends Model {
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
})
|
||||
id!: number;
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
allowNull: false,
|
||||
field: "user_id",
|
||||
comment: "用户ID"
|
||||
})
|
||||
userId!: number;
|
||||
|
||||
@ForeignKey(() => Role)
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
allowNull: false,
|
||||
field: "role_id",
|
||||
comment: "角色ID"
|
||||
})
|
||||
roleId!: number;
|
||||
|
||||
@BelongsTo(() => User)
|
||||
user!: User;
|
||||
|
||||
@BelongsTo(() => Role)
|
||||
role!: Role;
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import { TokenUtil } from '../util/token';
|
||||
|
||||
@Provide()
|
||||
export class AuthMiddleware implements IMiddleware<Context, NextFunction> {
|
||||
// 中间件匹配范围:全局生效,但跳过公开接口
|
||||
ignore = ['/api/auth/*', '/api/health'];
|
||||
|
||||
@Inject()
|
||||
tokenUtil!: TokenUtil;
|
||||
|
||||
|
||||
@@ -2,56 +2,41 @@ import { Middleware, IMiddleware } from '@midwayjs/core';
|
||||
import { Context, NextFunction } from '@midwayjs/koa';
|
||||
import { Provide, Inject } from '@midwayjs/core';
|
||||
import { InjectRepository } from '@midwayjs/sequelize';
|
||||
import OperationLog from '../entity/operationLog.entity';
|
||||
import SysOperationLog from '../entity/sysOperationLog.entity';
|
||||
|
||||
@Provide()
|
||||
export class LogMiddleware implements IMiddleware<Context, NextFunction> {
|
||||
// 中间件匹配范围:全局生效,跳过日志查询、健康检查与登录(登录由内部记录)
|
||||
ignore = ['/api/logs/*', '/api/health', '/api/auth/login'];
|
||||
|
||||
@InjectRepository(OperationLog)
|
||||
logModel!: typeof OperationLog;
|
||||
@InjectRepository(SysOperationLog)
|
||||
logModel!: typeof SysOperationLog;
|
||||
|
||||
resolve() {
|
||||
return async (ctx: Context, next: NextFunction) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
let logData: any = {};
|
||||
|
||||
try {
|
||||
logData = {
|
||||
method: ctx.method,
|
||||
url: ctx.url,
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.header['user-agent'],
|
||||
requestParams: JSON.stringify(ctx.request.body || {}),
|
||||
};
|
||||
|
||||
if (ctx.state.user) {
|
||||
logData.userId = ctx.state.user.userId;
|
||||
logData.username = ctx.state.user.username;
|
||||
}
|
||||
|
||||
await next();
|
||||
|
||||
logData.statusCode = ctx.status;
|
||||
logData.duration = Date.now() - startTime;
|
||||
// 仅记录写操作,且要求登录用户存在
|
||||
if (!['POST', 'PUT', 'DELETE'].includes(ctx.method)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (['POST', 'PUT', 'DELETE'].includes(ctx.method)) {
|
||||
logData.action = this.getActionDescription(ctx);
|
||||
logData.module = this.getModuleFromUrl(ctx.url);
|
||||
if (!ctx.state.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logData = {
|
||||
userId: ctx.state.user.userId,
|
||||
module: this.getModuleFromUrl(ctx.url),
|
||||
action: this.getActionDescription(ctx),
|
||||
method: ctx.method,
|
||||
params: JSON.stringify(ctx.request.body || {}),
|
||||
ip: ctx.ip,
|
||||
};
|
||||
|
||||
this.saveLog(logData).catch(err => {
|
||||
console.error('保存操作日志失败:', err);
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
logData.statusCode = 500;
|
||||
logData.duration = Date.now() - startTime;
|
||||
logData.errorMessage = error.message;
|
||||
|
||||
this.saveLog(logData).catch(() => {});
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,18 +44,25 @@ export class LogMiddleware implements IMiddleware<Context, NextFunction> {
|
||||
const actionMap: Record<string, Record<string, string>> = {
|
||||
'/api/auth/login': { POST: '用户登录' },
|
||||
'/api/auth/logout': { POST: '用户登出' },
|
||||
'/api/user': {
|
||||
'/api/users': {
|
||||
POST: '创建用户',
|
||||
PUT: '更新用户',
|
||||
DELETE: '删除用户',
|
||||
},
|
||||
'/api/role': {
|
||||
'/api/roles': {
|
||||
POST: '创建角色',
|
||||
PUT: '更新角色',
|
||||
DELETE: '删除角色',
|
||||
},
|
||||
'/api/settings': {
|
||||
PUT: '修改系统设置',
|
||||
'/api/menus': {
|
||||
POST: '创建菜单',
|
||||
PUT: '更新菜单',
|
||||
DELETE: '删除菜单',
|
||||
},
|
||||
'/api/dicts': {
|
||||
POST: '创建字典',
|
||||
PUT: '更新字典',
|
||||
DELETE: '删除字典',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -92,10 +84,10 @@ export class LogMiddleware implements IMiddleware<Context, NextFunction> {
|
||||
private getModuleFromUrl(url: string): string {
|
||||
const moduleMap: Record<string, string> = {
|
||||
'/api/auth': '认证',
|
||||
'/api/user': '用户管理',
|
||||
'/api/role': '角色管理',
|
||||
'/api/settings': '系统设置',
|
||||
'/api/log': '日志管理',
|
||||
'/api/users': '用户管理',
|
||||
'/api/roles': '角色管理',
|
||||
'/api/menus': '菜单管理',
|
||||
'/api/dicts': '数据字典',
|
||||
'/api/logs': '日志管理',
|
||||
'/api/dashboard': '仪表盘',
|
||||
};
|
||||
|
||||
@@ -1,14 +1,37 @@
|
||||
import { Middleware, IMiddleware } from '@midwayjs/core';
|
||||
import { Context, NextFunction } from '@midwayjs/koa';
|
||||
import { Provide, Inject } from '@midwayjs/core';
|
||||
import { InjectRepository } from '@midwayjs/sequelize';
|
||||
import { PermissionService } from '../service/permission.service';
|
||||
import SysUserRole from '../entity/sysUserRole.entity';
|
||||
import SysRole from '../entity/sysRole.entity';
|
||||
|
||||
// 请求路径前缀 → 菜单权限标识映射
|
||||
const PERMISSION_MAP: Record<string, string> = {
|
||||
'/api/users': 'system:user',
|
||||
'/api/roles': 'system:role',
|
||||
'/api/menus': 'system:menu',
|
||||
'/api/dicts': 'system:dict',
|
||||
'/api/logs': 'system:log',
|
||||
};
|
||||
|
||||
// 超级管理员角色编码,拥有全部权限
|
||||
const ADMIN_ROLE_CODE = 'ADMIN';
|
||||
|
||||
@Provide()
|
||||
export class RbacMiddleware implements IMiddleware<Context, NextFunction> {
|
||||
// 中间件匹配范围:全局生效,跳过公开接口与仪表盘
|
||||
ignore = ['/api/auth/*', '/api/health', '/api/dashboard/*'];
|
||||
|
||||
@Inject()
|
||||
permissionService!: PermissionService;
|
||||
|
||||
@InjectRepository(SysUserRole)
|
||||
userRoleModel!: typeof SysUserRole;
|
||||
|
||||
@InjectRepository(SysRole)
|
||||
roleModel!: typeof SysRole;
|
||||
|
||||
resolve() {
|
||||
return async (ctx: Context, next: NextFunction) => {
|
||||
const user = ctx.state.user;
|
||||
@@ -22,17 +45,21 @@ export class RbacMiddleware implements IMiddleware<Context, NextFunction> {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前请求路径和方法,转换为权限码
|
||||
// 例如:GET /api/user/list -> user:list
|
||||
const permissionCode = this.extractPermissionCode(ctx);
|
||||
|
||||
// 超级管理员拥有所有权限
|
||||
if (user.roles && user.roles.includes('super_admin')) {
|
||||
// 路径前缀匹配,未匹配到任何权限映射的请求直接放行
|
||||
const permissionCode = this.matchPermissionCode(ctx.path);
|
||||
if (!permissionCode) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查用户是否拥有该权限
|
||||
// 查询用户角色编码,超级管理员直接放行
|
||||
const roleCodes = await this.getUserRoleCodes(user.userId);
|
||||
if (roleCodes.includes(ADMIN_ROLE_CODE)) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查用户菜单中是否包含该权限标识
|
||||
const hasPermission = await this.permissionService.checkUserPermission(
|
||||
user.userId,
|
||||
permissionCode
|
||||
@@ -51,24 +78,21 @@ export class RbacMiddleware implements IMiddleware<Context, NextFunction> {
|
||||
};
|
||||
}
|
||||
|
||||
private extractPermissionCode(ctx: Context): string {
|
||||
const url = ctx.path.replace('/api/', '').replace(/^\//, '');
|
||||
const method = ctx.method.toLowerCase();
|
||||
|
||||
const methodMap: Record<string, string> = {
|
||||
post: 'create',
|
||||
put: 'update',
|
||||
delete: 'delete',
|
||||
get: 'list',
|
||||
};
|
||||
|
||||
const action = methodMap[method] || method;
|
||||
|
||||
const parts = url.split('/');
|
||||
if (parts.length >= 2) {
|
||||
return `${parts[0]}:${action}`;
|
||||
}
|
||||
|
||||
return `${url}:${action}`;
|
||||
private matchPermissionCode(path: string): string {
|
||||
for (const [prefix, code] of Object.entries(PERMISSION_MAP)) {
|
||||
if (path === prefix || path.startsWith(prefix + '/')) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private async getUserRoleCodes(userId: number): Promise<string[]> {
|
||||
const userRoles = await this.userRoleModel.findAll({
|
||||
where: { userId },
|
||||
include: [this.roleModel],
|
||||
});
|
||||
|
||||
return userRoles.map(ur => ur.role?.roleCode).filter(Boolean) as string[];
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,33 @@
|
||||
import { Provide, Inject } from "@midwayjs/core";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import User from "../entity/user.entity";
|
||||
import UserRole from "../entity/userRole.entity";
|
||||
import Role from "../entity/role.entity";
|
||||
import RolePermission from "../entity/rolePermission.entity";
|
||||
import Permission from "../entity/permission.entity";
|
||||
import SysUser from "../entity/sysUser.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
import { TokenUtil } from "../util/token";
|
||||
import { comparePassword } from "../util/crypto";
|
||||
import { PermissionService } from "./permission.service";
|
||||
|
||||
@Provide()
|
||||
export class AuthService {
|
||||
@Inject()
|
||||
tokenUtil!: TokenUtil;
|
||||
|
||||
@InjectRepository(User)
|
||||
userModel!: typeof User;
|
||||
@Inject()
|
||||
permissionService!: PermissionService;
|
||||
|
||||
@InjectRepository(UserRole)
|
||||
userRoleModel!: typeof UserRole;
|
||||
@InjectRepository(SysUser)
|
||||
userModel!: typeof SysUser;
|
||||
|
||||
@InjectRepository(Role)
|
||||
roleModel!: typeof Role;
|
||||
@InjectRepository(SysUserRole)
|
||||
userRoleModel!: typeof SysUserRole;
|
||||
|
||||
@InjectRepository(RolePermission)
|
||||
rolePermissionModel!: typeof RolePermission;
|
||||
|
||||
@InjectRepository(Permission)
|
||||
permissionModel!: typeof Permission;
|
||||
@InjectRepository(SysRole)
|
||||
roleModel!: typeof SysRole;
|
||||
|
||||
async login(username: string, password: string, ctx: Context): Promise<any> {
|
||||
const user = await this.userModel.findOne({
|
||||
where: { username },
|
||||
include: [
|
||||
{
|
||||
model: this.userRoleModel,
|
||||
include: [this.roleModel]
|
||||
}
|
||||
]
|
||||
where: { username }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
@@ -48,15 +38,20 @@ export class AuthService {
|
||||
throw new Error("账户已被禁用");
|
||||
}
|
||||
|
||||
const isPasswordValid = await comparePassword(password, user.password);
|
||||
const isPasswordValid = await comparePassword(password, user.passwordHash);
|
||||
if (!isPasswordValid) {
|
||||
throw new Error("密码错误");
|
||||
}
|
||||
|
||||
const roles = (user as any).userRoles?.map((ur: any) => ur.role) || [];
|
||||
const roleIds = roles.map((r: Role) => r.id);
|
||||
// 获取用户角色编码
|
||||
const userRoles = await this.userRoleModel.findAll({
|
||||
where: { userId: user.id },
|
||||
include: [this.roleModel]
|
||||
});
|
||||
const roleCodes = userRoles.map(ur => ur.role?.roleCode).filter(Boolean) as string[];
|
||||
|
||||
const permissions = await this.getUserPermissions(roleIds);
|
||||
// 获取用户菜单树
|
||||
const menus = await this.permissionService.getUserMenus(user.id);
|
||||
|
||||
const accessToken = this.tokenUtil.generateAccessToken({
|
||||
userId: user.id,
|
||||
@@ -68,8 +63,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
await user.update({
|
||||
lastLoginAt: new Date(),
|
||||
lastLoginIp: ctx.ip
|
||||
lastLoginAt: new Date()
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -78,11 +72,10 @@ export class AuthService {
|
||||
userInfo: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
nickname: user.nickname,
|
||||
avatar: user.avatar,
|
||||
roles: roles.map((r: Role) => r.code),
|
||||
permissions: permissions.map((p: Permission) => p.code)
|
||||
}
|
||||
realName: user.realName
|
||||
},
|
||||
roles: roleCodes,
|
||||
menus
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,17 +104,4 @@ export class AuthService {
|
||||
throw new Error("Refresh Token无效或已过期");
|
||||
}
|
||||
}
|
||||
|
||||
private async getUserPermissions(roleIds: number[]): Promise<Permission[]> {
|
||||
if (!roleIds || roleIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rolePermissions = await this.rolePermissionModel.findAll({
|
||||
where: { roleId: roleIds },
|
||||
include: [this.permissionModel]
|
||||
});
|
||||
|
||||
return [...new Map(rolePermissions.map((rp: any) => [rp.permission.id, rp.permission])).values()];
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,14 @@
|
||||
import { Provide, Inject } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import User from "../entity/user.entity";
|
||||
import OperationLog from "../entity/operationLog.entity";
|
||||
import SysUser from "../entity/sysUser.entity";
|
||||
import SysOperationLog from "../entity/sysOperationLog.entity";
|
||||
import dayjs from "dayjs";
|
||||
import { Op } from "sequelize";
|
||||
|
||||
@Provide()
|
||||
export class DashboardService {
|
||||
@InjectRepository(User)
|
||||
userModel!: typeof User;
|
||||
|
||||
@InjectRepository(OperationLog)
|
||||
logModel!: typeof OperationLog;
|
||||
@InjectRepository(SysOperationLog)
|
||||
logModel!: typeof SysOperationLog;
|
||||
|
||||
async getDashboardStats(): Promise<any> {
|
||||
const mockDeviceTotal = 1256;
|
||||
@@ -103,15 +100,15 @@ export class DashboardService {
|
||||
const logs = await this.logModel.findAll({
|
||||
order: [["created_at", "DESC"]],
|
||||
limit,
|
||||
attributes: ["id", "username", "action", "module", "ip_address", "created_at"]
|
||||
attributes: ["id", "action", "module", "ip", "created_at"]
|
||||
});
|
||||
|
||||
return logs.map((log) => ({
|
||||
id: log.id,
|
||||
user: log.username || "系统",
|
||||
user: String(log.userId || "系统"),
|
||||
action: log.action,
|
||||
module: log.module,
|
||||
ip: log.ipAddress,
|
||||
ip: log.ip,
|
||||
time: dayjs(log.created_at).format("MM-DD HH:mm")
|
||||
}));
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { Provide, Inject } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import SysDict from "../entity/sysDict.entity";
|
||||
import { Op } from "sequelize";
|
||||
|
||||
@Provide()
|
||||
export class DictService {
|
||||
@InjectRepository(SysDict)
|
||||
dictModel!: typeof SysDict;
|
||||
|
||||
async list(query: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
dictType?: string;
|
||||
keyword?: string;
|
||||
status?: number;
|
||||
}): Promise<any> {
|
||||
const page = query.page || 1;
|
||||
const pageSize = query.pageSize || 10;
|
||||
|
||||
const where: any = {};
|
||||
|
||||
if (query.dictType) {
|
||||
where.dictType = query.dictType;
|
||||
}
|
||||
|
||||
if (query.keyword) {
|
||||
where[Op.or] = [
|
||||
{ dictLabel: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ dictValue: { [Op.like]: `%${query.keyword}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
if (query.status !== undefined && query.status !== null) {
|
||||
where.status = query.status;
|
||||
}
|
||||
|
||||
const { count, rows } = await this.dictModel.findAndCountAll({
|
||||
where,
|
||||
order: [["sort_order", "ASC"], ["id", "ASC"]],
|
||||
limit: Number(pageSize),
|
||||
offset: (Number(page) - 1) * Number(pageSize),
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "获取字典列表成功",
|
||||
data: {
|
||||
list: rows,
|
||||
total: count,
|
||||
page: Number(page),
|
||||
pageSize: Number(pageSize),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async types(): Promise<any> {
|
||||
const rows = await this.dictModel.findAll({
|
||||
attributes: ["dictType"],
|
||||
group: ["dictType"],
|
||||
order: [["dictType", "ASC"]],
|
||||
});
|
||||
|
||||
const types = rows.map(row => row.dictType);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "获取字典类型成功",
|
||||
data: types,
|
||||
};
|
||||
}
|
||||
|
||||
async create(data: any): Promise<any> {
|
||||
const { dictType, dictLabel, dictValue, sortOrder = 0, status = 1 } = data;
|
||||
|
||||
if (!dictType || !dictLabel || !dictValue) {
|
||||
return {
|
||||
code: 400,
|
||||
message: "字典类型、字典标签、字典键值不能为空",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
const dict = await this.dictModel.create({
|
||||
dict_type: dictType,
|
||||
dict_label: dictLabel,
|
||||
dict_value: dictValue,
|
||||
sort_order: sortOrder,
|
||||
status,
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "创建字典成功",
|
||||
data: dict,
|
||||
};
|
||||
}
|
||||
|
||||
async update(id: string, data: any): Promise<any> {
|
||||
const dict = await this.dictModel.findByPk(Number(id));
|
||||
|
||||
if (!dict) {
|
||||
return {
|
||||
code: 404,
|
||||
message: "字典不存在",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
const updateData: any = {};
|
||||
if (data.dictType !== undefined) updateData.dict_type = data.dictType;
|
||||
if (data.dictLabel !== undefined) updateData.dict_label = data.dictLabel;
|
||||
if (data.dictValue !== undefined) updateData.dict_value = data.dictValue;
|
||||
if (data.sortOrder !== undefined) updateData.sort_order = data.sortOrder;
|
||||
if (data.status !== undefined) updateData.status = data.status;
|
||||
|
||||
await dict.update(updateData);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "更新字典成功",
|
||||
data: dict,
|
||||
};
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<any> {
|
||||
const dict = await this.dictModel.findByPk(Number(id));
|
||||
|
||||
if (!dict) {
|
||||
return {
|
||||
code: 404,
|
||||
message: "字典不存在",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
await dict.destroy();
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "删除字典成功",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,22 @@
|
||||
import { Provide, Inject } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import OperationLog from "../entity/operationLog.entity";
|
||||
import SysOperationLog from "../entity/sysOperationLog.entity";
|
||||
import { Op } from "sequelize";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
@Provide()
|
||||
export class LogService {
|
||||
@InjectRepository(OperationLog)
|
||||
logModel!: typeof OperationLog;
|
||||
@InjectRepository(SysOperationLog)
|
||||
logModel!: typeof SysOperationLog;
|
||||
|
||||
async list(query: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
moduleName?: string;
|
||||
actionType?: string;
|
||||
userId?: string | number;
|
||||
module?: string;
|
||||
action?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
operator?: string;
|
||||
}): Promise<any> {
|
||||
const page = query.page || 1;
|
||||
const pageSize = query.pageSize || 10;
|
||||
@@ -67,19 +67,22 @@ export class LogService {
|
||||
limit: 10000,
|
||||
});
|
||||
|
||||
const headers = ["序号", "操作人", "操作类型", "操作模块", "请求方法", "请求URL", "IP地址", "状态码", "耗时(ms)", "操作时间"];
|
||||
const rows = logs.map((log: any, index: number) => [
|
||||
const headers = ["序号", "操作人ID", "操作模块", "操作动作", "请求方法", "请求参数", "IP地址", "操作时间"];
|
||||
const rows = logs.map((log: any, index: number) => {
|
||||
const paramsText = (log.params || "").replace(/,/g, ",");
|
||||
const paramsCell = paramsText.length > 200 ? paramsText.substring(0, 200) + "..." : paramsText;
|
||||
|
||||
return [
|
||||
index + 1,
|
||||
log.username || "-",
|
||||
log.action || "-",
|
||||
log.userId || "-",
|
||||
log.module || "-",
|
||||
log.action || "-",
|
||||
log.method || "-",
|
||||
(log.url || "").replace(/,/g, ","),
|
||||
log.ipAddress || "-",
|
||||
log.statusCode || "-",
|
||||
log.duration || 0,
|
||||
paramsCell,
|
||||
log.ip || "-",
|
||||
dayjs(log.created_at).format("YYYY-MM-DD HH:mm:ss"),
|
||||
]);
|
||||
];
|
||||
});
|
||||
|
||||
const csvContent = [headers.join(","), ...rows.map(row => row.join(","))].join("\n");
|
||||
|
||||
@@ -100,18 +103,18 @@ export class LogService {
|
||||
|
||||
private buildWhereClause(query: any): any {
|
||||
const where: any = {};
|
||||
const { operator, actionType, moduleName, startDate, endDate } = query;
|
||||
const { userId, module, action, startDate, endDate } = query;
|
||||
|
||||
if (operator) {
|
||||
where.username = { [Op.like]: `%${operator}%` };
|
||||
if (userId) {
|
||||
where.userId = userId;
|
||||
}
|
||||
|
||||
if (actionType) {
|
||||
where.action = { [Op.like]: `%${actionType}%` };
|
||||
if (module) {
|
||||
where.module = { [Op.like]: `%${module}%` };
|
||||
}
|
||||
|
||||
if (moduleName) {
|
||||
where.module = { [Op.like]: `%${moduleName}%` };
|
||||
if (action) {
|
||||
where.action = { [Op.like]: `%${action}%` };
|
||||
}
|
||||
|
||||
if (startDate || endDate) {
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { Provide } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import SysMenu from "../entity/sysMenu.entity";
|
||||
|
||||
@Provide()
|
||||
export class MenuService {
|
||||
@InjectRepository(SysMenu)
|
||||
menuModel!: typeof SysMenu;
|
||||
|
||||
async getMenuTree(): Promise<any> {
|
||||
const menus = await this.menuModel.findAll({
|
||||
order: [["sort_order", "ASC"], ["id", "ASC"]],
|
||||
});
|
||||
|
||||
const tree = this.buildMenuTree(menus.map(menu => menu.toJSON()));
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "获取菜单树成功",
|
||||
data: tree,
|
||||
};
|
||||
}
|
||||
|
||||
async create(data: any): Promise<any> {
|
||||
const {
|
||||
parentId,
|
||||
menuName,
|
||||
menuPath,
|
||||
component,
|
||||
icon,
|
||||
sortOrder = 0,
|
||||
menuType = 2,
|
||||
permission,
|
||||
visible = 1,
|
||||
} = data;
|
||||
|
||||
if (!menuName) {
|
||||
return {
|
||||
code: 400,
|
||||
message: "菜单名称不能为空",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
const menu = await this.menuModel.create({
|
||||
parent_id: parentId,
|
||||
menu_name: menuName,
|
||||
menu_path: menuPath,
|
||||
component,
|
||||
icon,
|
||||
sort_order: sortOrder,
|
||||
menu_type: menuType,
|
||||
permission,
|
||||
visible,
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "创建菜单成功",
|
||||
data: menu,
|
||||
};
|
||||
}
|
||||
|
||||
async update(id: string, data: any): Promise<any> {
|
||||
const menu = await this.menuModel.findByPk(Number(id));
|
||||
|
||||
if (!menu) {
|
||||
return {
|
||||
code: 404,
|
||||
message: "菜单不存在",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
const updateData: any = {};
|
||||
if (data.parentId !== undefined) updateData.parent_id = data.parentId;
|
||||
if (data.menuName !== undefined) updateData.menu_name = data.menuName;
|
||||
if (data.menuPath !== undefined) updateData.menu_path = data.menuPath;
|
||||
if (data.component !== undefined) updateData.component = data.component;
|
||||
if (data.icon !== undefined) updateData.icon = data.icon;
|
||||
if (data.sortOrder !== undefined) updateData.sort_order = data.sortOrder;
|
||||
if (data.menuType !== undefined) updateData.menu_type = data.menuType;
|
||||
if (data.permission !== undefined) updateData.permission = data.permission;
|
||||
if (data.visible !== undefined) updateData.visible = data.visible;
|
||||
|
||||
await menu.update(updateData);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "更新菜单成功",
|
||||
data: menu,
|
||||
};
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<any> {
|
||||
const menu = await this.menuModel.findByPk(Number(id));
|
||||
|
||||
if (!menu) {
|
||||
return {
|
||||
code: 404,
|
||||
message: "菜单不存在",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
const childCount = await this.menuModel.count({
|
||||
where: { parentId: Number(id) },
|
||||
});
|
||||
|
||||
if (childCount > 0) {
|
||||
return {
|
||||
code: 400,
|
||||
message: "存在子菜单,无法删除",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
await menu.destroy();
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "删除菜单成功",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
private buildMenuTree(menus: any[]): any[] {
|
||||
const map = new Map<number, any>();
|
||||
const tree: any[] = [];
|
||||
|
||||
for (const menu of menus) {
|
||||
map.set(menu.id, {
|
||||
id: menu.id,
|
||||
parentId: menu.parentId,
|
||||
menuName: menu.menuName,
|
||||
menuPath: menu.menuPath,
|
||||
component: menu.component,
|
||||
icon: menu.icon,
|
||||
sortOrder: menu.sortOrder,
|
||||
menuType: menu.menuType,
|
||||
permission: menu.permission,
|
||||
visible: menu.visible,
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
|
||||
for (const menu of menus) {
|
||||
const node = map.get(menu.id)!;
|
||||
if (menu.parentId && map.has(menu.parentId)) {
|
||||
map.get(menu.parentId)!.children.push(node);
|
||||
} else {
|
||||
tree.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
}
|
||||
@@ -1,110 +1,111 @@
|
||||
import { Provide, Inject } from "@midwayjs/core";
|
||||
import { Provide } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import User from "../entity/user.entity";
|
||||
import UserRole from "../entity/userRole.entity";
|
||||
import Role from "../entity/role.entity";
|
||||
import RolePermission from "../entity/rolePermission.entity";
|
||||
import Permission from "../entity/permission.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRoleMenu from "../entity/sysRoleMenu.entity";
|
||||
import SysMenu from "../entity/sysMenu.entity";
|
||||
|
||||
@Provide()
|
||||
export class PermissionService {
|
||||
@InjectRepository(User)
|
||||
userModel!: typeof User;
|
||||
@InjectRepository(SysUserRole)
|
||||
userRoleModel!: typeof SysUserRole;
|
||||
|
||||
@InjectRepository(UserRole)
|
||||
userRoleModel!: typeof UserRole;
|
||||
@InjectRepository(SysRoleMenu)
|
||||
roleMenuModel!: typeof SysRoleMenu;
|
||||
|
||||
@InjectRepository(Role)
|
||||
roleModel!: typeof Role;
|
||||
@InjectRepository(SysMenu)
|
||||
menuModel!: typeof SysMenu;
|
||||
|
||||
@InjectRepository(RolePermission)
|
||||
rolePermissionModel!: typeof RolePermission;
|
||||
|
||||
@InjectRepository(Permission)
|
||||
permissionModel!: typeof Permission;
|
||||
|
||||
async checkUserPermission(userId: number, permissionCode: string): Promise<boolean> {
|
||||
/**
|
||||
* 获取用户可见菜单树(用户角色 → 角色菜单 → 可见菜单,按 sort_order/id 升序去重)
|
||||
*/
|
||||
async getUserMenus(userId: number): Promise<any[]> {
|
||||
const userRoles = await this.userRoleModel.findAll({
|
||||
where: { userId },
|
||||
include: [this.roleModel],
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
const roleIds = userRoles.map(ur => ur.roleId);
|
||||
if (!roleIds.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const roleMenus = await this.roleMenuModel.findAll({
|
||||
where: { roleId: roleIds },
|
||||
include: [
|
||||
{
|
||||
model: this.menuModel,
|
||||
where: { visible: 1 }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const menuMap = new Map<number, any>();
|
||||
for (const rm of roleMenus) {
|
||||
const menu = rm.menu;
|
||||
if (menu && !menuMap.has(menu.id)) {
|
||||
menuMap.set(menu.id, this.toMenuNode(menu));
|
||||
}
|
||||
}
|
||||
|
||||
const menus = Array.from(menuMap.values());
|
||||
menus.sort((a, b) => a.sortOrder - b.sortOrder || a.id - b.id);
|
||||
|
||||
return this.buildMenuTree(menus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户角色关联的菜单中是否存在指定权限标识的菜单
|
||||
*/
|
||||
async checkUserPermission(userId: number, permissionCode: string): Promise<boolean> {
|
||||
const userRoles = await this.userRoleModel.findAll({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
const roleIds = userRoles.map(ur => ur.roleId);
|
||||
if (!roleIds.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rolePermissions = await this.rolePermissionModel.findAll({
|
||||
const roleMenus = await this.roleMenuModel.findAll({
|
||||
where: { roleId: roleIds },
|
||||
include: [
|
||||
{
|
||||
model: this.permissionModel,
|
||||
where: { code: permissionCode },
|
||||
},
|
||||
],
|
||||
model: this.menuModel,
|
||||
where: { permission: permissionCode }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return rolePermissions.some(rp => rp.permission?.code === permissionCode);
|
||||
return roleMenus.some(rm => rm.menu?.permission === permissionCode);
|
||||
}
|
||||
|
||||
async getUserPermissions(userId: number): Promise<string[]> {
|
||||
const permissions = await this.getUserPermissionTree(userId);
|
||||
return this.flattenPermissions(permissions);
|
||||
private toMenuNode(menu: SysMenu): any {
|
||||
return {
|
||||
id: menu.id,
|
||||
parentId: menu.parentId,
|
||||
menuName: menu.menuName,
|
||||
menuPath: menu.menuPath,
|
||||
component: menu.component,
|
||||
icon: menu.icon,
|
||||
sortOrder: menu.sortOrder,
|
||||
menuType: menu.menuType,
|
||||
permission: menu.permission,
|
||||
visible: menu.visible,
|
||||
children: []
|
||||
};
|
||||
}
|
||||
|
||||
async getUserPermissionTree(userId: number): Promise<any[]> {
|
||||
const userRoles = await this.userRoleModel.findAll({
|
||||
where: { userId },
|
||||
include: [
|
||||
{
|
||||
model: this.roleModel,
|
||||
include: [
|
||||
{
|
||||
model: this.rolePermissionModel,
|
||||
include: [this.permissionModel],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const allPermissions = new Map<number, any>();
|
||||
|
||||
for (const ur of userRoles) {
|
||||
const role: any = ur.role;
|
||||
if (role && role.rolePermissions) {
|
||||
for (const rp of role.rolePermissions) {
|
||||
if (rp.permission && !allPermissions.has(rp.permission.id)) {
|
||||
allPermissions.set(rp.permission.id, rp.permission.toJSON());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.buildPermissionTree(Array.from(allPermissions.values()));
|
||||
}
|
||||
|
||||
async getAllPermissions(): Promise<any[]> {
|
||||
const permissions = await this.permissionModel.findAll({
|
||||
order: [["sort_order", "ASC"]],
|
||||
});
|
||||
|
||||
return this.buildPermissionTree(permissions.map(p => p.toJSON()));
|
||||
}
|
||||
|
||||
private buildPermissionTree(permissions: any[]): any[] {
|
||||
private buildMenuTree(menus: any[]): any[] {
|
||||
const map = new Map<number, any>();
|
||||
const tree: any[] = [];
|
||||
|
||||
for (const perm of permissions) {
|
||||
map.set(perm.id, { ...perm, children: [] });
|
||||
for (const menu of menus) {
|
||||
map.set(menu.id, { ...menu, children: [] });
|
||||
}
|
||||
|
||||
for (const perm of permissions) {
|
||||
const node = map.get(perm.id)!;
|
||||
if (perm.parentId && map.has(perm.parentId)) {
|
||||
map.get(perm.parentId)!.children.push(node);
|
||||
for (const menu of menus) {
|
||||
const node = map.get(menu.id)!;
|
||||
if (menu.parentId && map.has(menu.parentId)) {
|
||||
map.get(menu.parentId)!.children.push(node);
|
||||
} else {
|
||||
tree.push(node);
|
||||
}
|
||||
@@ -112,17 +113,4 @@ export class PermissionService {
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
private flattenPermissions(tree: any[]): string[] {
|
||||
const codes: string[] = [];
|
||||
|
||||
for (const node of tree) {
|
||||
codes.push(node.code);
|
||||
if (node.children?.length) {
|
||||
codes.push(...this.flattenPermissions(node.children));
|
||||
}
|
||||
}
|
||||
|
||||
return codes;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
import { Provide, Inject } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import Role from "../entity/role.entity";
|
||||
import UserRole from "../entity/userRole.entity";
|
||||
import RolePermission from "../entity/rolePermission.entity";
|
||||
import Permission from "../entity/permission.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRoleMenu from "../entity/sysRoleMenu.entity";
|
||||
import SysMenu from "../entity/sysMenu.entity";
|
||||
import { Op } from "sequelize";
|
||||
|
||||
@Provide()
|
||||
export class RoleService {
|
||||
@InjectRepository(Role)
|
||||
roleModel!: typeof Role;
|
||||
@InjectRepository(SysRole)
|
||||
roleModel!: typeof SysRole;
|
||||
|
||||
@InjectRepository(UserRole)
|
||||
userRoleModel!: typeof UserRole;
|
||||
@InjectRepository(SysUserRole)
|
||||
userRoleModel!: typeof SysUserRole;
|
||||
|
||||
@InjectRepository(RolePermission)
|
||||
rolePermissionModel!: typeof RolePermission;
|
||||
@InjectRepository(SysRoleMenu)
|
||||
roleMenuModel!: typeof SysRoleMenu;
|
||||
|
||||
@InjectRepository(Permission)
|
||||
permissionModel!: typeof Permission;
|
||||
@InjectRepository(SysMenu)
|
||||
menuModel!: typeof SysMenu;
|
||||
|
||||
async list(query: {
|
||||
page?: number;
|
||||
@@ -32,20 +32,17 @@ export class RoleService {
|
||||
const whereClause: any = {};
|
||||
if (query.keyword) {
|
||||
whereClause[Op.or] = [
|
||||
{ name: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ code: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ roleCode: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ roleName: { [Op.like]: `%${query.keyword}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
const { count, rows } = await this.roleModel.findAndCountAll({
|
||||
where: whereClause,
|
||||
attributes: ["id", "roleCode", "roleName", "description", "sortOrder"],
|
||||
offset,
|
||||
limit: pageSize,
|
||||
order: [["sort_order", "ASC"], ["created_at", "DESC"]],
|
||||
include: [{
|
||||
model: this.rolePermissionModel,
|
||||
include: [this.permissionModel],
|
||||
}],
|
||||
});
|
||||
|
||||
for (const role of rows) {
|
||||
@@ -80,12 +77,7 @@ export class RoleService {
|
||||
}
|
||||
|
||||
async detail(id: string): Promise<any> {
|
||||
const role = await this.roleModel.findByPk(Number(id), {
|
||||
include: [{
|
||||
model: this.rolePermissionModel,
|
||||
include: [this.permissionModel],
|
||||
}],
|
||||
});
|
||||
const role = await this.roleModel.findByPk(Number(id));
|
||||
|
||||
if (!role) {
|
||||
return {
|
||||
@@ -108,10 +100,10 @@ export class RoleService {
|
||||
}
|
||||
|
||||
async create(data: any): Promise<any> {
|
||||
const { name, code, description, status = 1, sortOrder = 0 } = data;
|
||||
const { roleCode, roleName, description, sortOrder = 0 } = data;
|
||||
|
||||
const existingRole = await this.roleModel.findOne({
|
||||
where: { code },
|
||||
where: { roleCode },
|
||||
});
|
||||
|
||||
if (existingRole) {
|
||||
@@ -123,10 +115,9 @@ export class RoleService {
|
||||
}
|
||||
|
||||
const role = await this.roleModel.create({
|
||||
name,
|
||||
code,
|
||||
roleCode,
|
||||
roleName,
|
||||
description,
|
||||
status,
|
||||
sort_order: sortOrder,
|
||||
});
|
||||
|
||||
@@ -148,9 +139,9 @@ export class RoleService {
|
||||
};
|
||||
}
|
||||
|
||||
if (data.code && data.code !== role.code) {
|
||||
if (data.roleCode && data.roleCode !== role.roleCode) {
|
||||
const existingRole = await this.roleModel.findOne({
|
||||
where: { code: data.code },
|
||||
where: { roleCode: data.roleCode },
|
||||
});
|
||||
|
||||
if (existingRole) {
|
||||
@@ -163,9 +154,9 @@ export class RoleService {
|
||||
}
|
||||
|
||||
const updateData: any = {};
|
||||
if (data.name !== undefined) updateData.name = data.name;
|
||||
if (data.roleCode !== undefined) updateData.roleCode = data.roleCode;
|
||||
if (data.roleName !== undefined) updateData.roleName = data.roleName;
|
||||
if (data.description !== undefined) updateData.description = data.description;
|
||||
if (data.status !== undefined) updateData.status = data.status;
|
||||
if (data.sortOrder !== undefined) updateData.sort_order = data.sortOrder;
|
||||
|
||||
await role.update(updateData);
|
||||
@@ -200,7 +191,7 @@ export class RoleService {
|
||||
};
|
||||
}
|
||||
|
||||
await this.rolePermissionModel.destroy({
|
||||
await this.roleMenuModel.destroy({
|
||||
where: { roleId: Number(id) },
|
||||
});
|
||||
|
||||
@@ -213,7 +204,7 @@ export class RoleService {
|
||||
};
|
||||
}
|
||||
|
||||
async getPermissions(roleId: number): Promise<any> {
|
||||
async getMenus(roleId: number): Promise<any> {
|
||||
const role = await this.roleModel.findByPk(roleId);
|
||||
|
||||
if (!role) {
|
||||
@@ -224,28 +215,32 @@ export class RoleService {
|
||||
};
|
||||
}
|
||||
|
||||
const rolePermissions = await this.rolePermissionModel.findAll({
|
||||
const roleMenus = await this.roleMenuModel.findAll({
|
||||
where: { roleId },
|
||||
});
|
||||
const assignedPermissionIds = rolePermissions.map(rp => rp.permissionId);
|
||||
const assignedMenuIds = roleMenus.map(rm => rm.menuId);
|
||||
|
||||
const allPermissions = await this.permissionModel.findAll({
|
||||
const menus = await this.menuModel.findAll({
|
||||
where: { menuType: { [Op.in]: [1, 2] } },
|
||||
order: [["sort_order", "ASC"], ["id", "ASC"]],
|
||||
});
|
||||
|
||||
const permissionTree = this.buildPermissionTree(
|
||||
allPermissions.map(p => p.toJSON()),
|
||||
assignedPermissionIds
|
||||
const menuTree = this.buildMenuTree(
|
||||
menus.map(m => m.toJSON()),
|
||||
assignedMenuIds
|
||||
);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "获取角色权限成功",
|
||||
data: permissionTree,
|
||||
message: "获取角色菜单成功",
|
||||
data: {
|
||||
menuTree,
|
||||
menuIds: assignedMenuIds,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async updatePermissions(roleId: number, permissionIds: number[]): Promise<any> {
|
||||
async updateMenus(roleId: number, menuIds: number[]): Promise<any> {
|
||||
const role = await this.roleModel.findByPk(roleId);
|
||||
|
||||
if (!role) {
|
||||
@@ -256,47 +251,57 @@ export class RoleService {
|
||||
};
|
||||
}
|
||||
|
||||
await this.rolePermissionModel.destroy({
|
||||
await this.roleMenuModel.destroy({
|
||||
where: { roleId },
|
||||
});
|
||||
|
||||
if (permissionIds && permissionIds.length > 0) {
|
||||
const rolePermissions = permissionIds.map(permissionId => ({
|
||||
if (menuIds && menuIds.length > 0) {
|
||||
const roleMenus = menuIds.map(menuId => ({
|
||||
roleId,
|
||||
permissionId,
|
||||
menuId,
|
||||
}));
|
||||
|
||||
await this.rolePermissionModel.bulkCreate(rolePermissions);
|
||||
await this.roleMenuModel.bulkCreate(roleMenus);
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "权限更新成功",
|
||||
message: "菜单更新成功",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
private buildPermissionTree(permissions: any[], checkedIds: number[]): any[] {
|
||||
const map = new Map<number, any>();
|
||||
private buildMenuTree(menus: any[], checkedIds: number[]): any[] {
|
||||
const nodeMap = new Map<number, any>();
|
||||
const menuIdSet = new Set(menus.map(m => m.id));
|
||||
const tree: any[] = [];
|
||||
|
||||
for (const perm of permissions) {
|
||||
map.set(perm.id, {
|
||||
...perm,
|
||||
for (const menu of menus) {
|
||||
nodeMap.set(menu.id, {
|
||||
...menu,
|
||||
children: [],
|
||||
checked: checkedIds.includes(perm.id),
|
||||
checked: checkedIds.includes(menu.id),
|
||||
});
|
||||
}
|
||||
|
||||
for (const perm of permissions) {
|
||||
const node = map.get(perm.id)!;
|
||||
if (perm.parent_id && map.has(perm.parent_id)) {
|
||||
map.get(perm.parent_id)!.children.push(node);
|
||||
for (const menu of menus) {
|
||||
const node = nodeMap.get(menu.id)!;
|
||||
const parent = menu.parentId ? nodeMap.get(menu.parentId) : undefined;
|
||||
|
||||
// 仅当父节点为一级节点时挂为其子节点,保证输出两级树;
|
||||
// 父节点不在列表或父节点本身是二级节点时,提升为一级节点
|
||||
if (parent && !menuIdSet.has(parent.parentId)) {
|
||||
parent.children.push(node);
|
||||
} else {
|
||||
tree.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
// 一级节点已在查询时按 sort_order 排序,此处对子节点排序
|
||||
for (const node of tree) {
|
||||
node.children.sort((a: any, b: any) => a.sortOrder - b.sortOrder);
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { Provide } from '@midwayjs/core';
|
||||
|
||||
@Provide()
|
||||
export class SystemService {
|
||||
private settingsCache: Map<string, any> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.initDefaultSettings();
|
||||
}
|
||||
|
||||
private initDefaultSettings() {
|
||||
this.settingsCache.set('basic', {
|
||||
siteName: '智能物联网设备管理系统',
|
||||
logo: '',
|
||||
sessionTimeout: 120,
|
||||
});
|
||||
|
||||
this.settingsCache.set('security', {
|
||||
passwordMinLength: 6,
|
||||
maxLoginAttempts: 5,
|
||||
lockoutDuration: 30,
|
||||
ipWhitelist: [] as string[],
|
||||
});
|
||||
|
||||
this.settingsCache.set('notification', {
|
||||
enableEmail: false,
|
||||
emailSmtpConfig: null as any,
|
||||
});
|
||||
}
|
||||
|
||||
async getSettings(): Promise<any> {
|
||||
return {
|
||||
code: 200,
|
||||
message: '获取系统设置成功',
|
||||
data: {
|
||||
basic: this.settingsCache.get('basic'),
|
||||
security: this.settingsCache.get('security'),
|
||||
notification: this.settingsCache.get('notification'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async updateBasicSettings(data: any): Promise<any> {
|
||||
const current = this.settingsCache.get('basic') || {};
|
||||
this.settingsCache.set('basic', { ...current, ...data });
|
||||
return {
|
||||
code: 200,
|
||||
message: '基本设置保存成功',
|
||||
};
|
||||
}
|
||||
|
||||
async updateSecuritySettings(data: any): Promise<any> {
|
||||
const current = this.settingsCache.get('security') || {};
|
||||
this.settingsCache.set('security', { ...current, ...data });
|
||||
return {
|
||||
code: 200,
|
||||
message: '安全设置保存成功',
|
||||
};
|
||||
}
|
||||
|
||||
async updateNotificationSettings(data: any): Promise<any> {
|
||||
const current = this.settingsCache.get('notification') || {};
|
||||
this.settingsCache.set('notification', { ...current, ...data });
|
||||
return {
|
||||
code: 200,
|
||||
message: '通知设置保存成功',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,22 @@
|
||||
import { Provide, Inject } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import { Repository } from "sequelize-typescript";
|
||||
import User from "../entity/user.entity";
|
||||
import UserRole from "../entity/userRole.entity";
|
||||
import Role from "../entity/role.entity";
|
||||
import SysUser from "../entity/sysUser.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
import { Op } from "sequelize";
|
||||
import { hashPassword } from "../util/crypto";
|
||||
|
||||
@Provide()
|
||||
export class UserService {
|
||||
@InjectRepository(User)
|
||||
userModel!: Repository<User>;
|
||||
@InjectRepository(SysUser)
|
||||
userModel!: Repository<SysUser>;
|
||||
|
||||
@InjectRepository(UserRole)
|
||||
userRoleModel!: Repository<UserRole>;
|
||||
@InjectRepository(SysUserRole)
|
||||
userRoleModel!: Repository<SysUserRole>;
|
||||
|
||||
@InjectRepository(Role)
|
||||
roleModel!: Repository<Role>;
|
||||
@InjectRepository(SysRole)
|
||||
roleModel!: Repository<SysRole>;
|
||||
|
||||
async list(query: { page?: any; pageSize?: any; keyword?: string; status?: number; roleId?: number }): Promise<any> {
|
||||
const page = Number(query.page) || 1;
|
||||
@@ -24,7 +24,7 @@ export class UserService {
|
||||
const where: any = {};
|
||||
|
||||
if (query.keyword) {
|
||||
where[Op.or] = [{ username: { [Op.like]: `%${query.keyword}%` } }, { nickname: { [Op.like]: `%${query.keyword}%` } }, { phone: { [Op.like]: `%${query.keyword}%` } }];
|
||||
where[Op.or] = [{ username: { [Op.like]: `%${query.keyword}%` } }, { realName: { [Op.like]: `%${query.keyword}%` } }, { phone: { [Op.like]: `%${query.keyword}%` } }];
|
||||
}
|
||||
|
||||
if (query.status !== undefined) {
|
||||
@@ -33,11 +33,11 @@ export class UserService {
|
||||
|
||||
const includeOptions: any = [
|
||||
{
|
||||
model: UserRole,
|
||||
model: SysUserRole,
|
||||
required: !!query.roleId,
|
||||
include: [
|
||||
{
|
||||
model: Role,
|
||||
model: SysRole,
|
||||
where: query.roleId ? { id: query.roleId } : {},
|
||||
required: !!query.roleId
|
||||
}
|
||||
@@ -57,7 +57,8 @@ export class UserService {
|
||||
|
||||
const list = rows.map((user) => {
|
||||
const u = user.toJSON();
|
||||
delete u.password;
|
||||
delete u.passwordHash;
|
||||
delete u.password_hash;
|
||||
return u;
|
||||
});
|
||||
|
||||
@@ -77,8 +78,8 @@ export class UserService {
|
||||
const user = await this.userModel.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: UserRole,
|
||||
include: [Role]
|
||||
model: SysUserRole,
|
||||
include: [SysRole]
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -92,7 +93,8 @@ export class UserService {
|
||||
}
|
||||
|
||||
const userData = user.toJSON();
|
||||
delete userData.password;
|
||||
delete userData.passwordHash;
|
||||
delete userData.password_hash;
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
@@ -102,7 +104,7 @@ export class UserService {
|
||||
}
|
||||
|
||||
async create(data: any): Promise<any> {
|
||||
const { username, password, nickname, email, phone, roleIds, status = 1 } = data;
|
||||
const { username, password, realName, email, phone, roleIds, status = 1 } = data;
|
||||
|
||||
const existingUser = await this.userModel.findOne({
|
||||
where: { username }
|
||||
@@ -120,8 +122,8 @@ export class UserService {
|
||||
|
||||
const user = await this.userModel.create({
|
||||
username,
|
||||
password: hashedPassword,
|
||||
nickname,
|
||||
passwordHash: hashedPassword,
|
||||
realName,
|
||||
email,
|
||||
phone,
|
||||
status
|
||||
@@ -134,14 +136,15 @@ export class UserService {
|
||||
const createdUser = await this.userModel.findByPk(user.id, {
|
||||
include: [
|
||||
{
|
||||
model: UserRole,
|
||||
include: [Role]
|
||||
model: SysUserRole,
|
||||
include: [SysRole]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const userData = createdUser!.toJSON();
|
||||
delete userData.password;
|
||||
delete userData.passwordHash;
|
||||
delete userData.password_hash;
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
@@ -163,14 +166,13 @@ export class UserService {
|
||||
|
||||
const updateData: any = {};
|
||||
|
||||
if (data.nickname !== undefined) updateData.nickname = data.nickname;
|
||||
if (data.realName !== undefined) updateData.realName = data.realName;
|
||||
if (data.email !== undefined) updateData.email = data.email;
|
||||
if (data.phone !== undefined) updateData.phone = data.phone;
|
||||
if (data.avatar !== undefined) updateData.avatar = data.avatar;
|
||||
if (data.status !== undefined) updateData.status = data.status;
|
||||
|
||||
if (data.password) {
|
||||
updateData.password = await hashPassword(data.password);
|
||||
updateData.passwordHash = await hashPassword(data.password);
|
||||
}
|
||||
|
||||
await user.update(updateData);
|
||||
@@ -182,14 +184,15 @@ export class UserService {
|
||||
const updatedUser = await this.userModel.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: UserRole,
|
||||
include: [Role]
|
||||
model: SysUserRole,
|
||||
include: [SysRole]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const userData = updatedUser!.toJSON();
|
||||
delete userData.password;
|
||||
delete userData.passwordHash;
|
||||
delete userData.password_hash;
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
@@ -198,7 +201,7 @@ export class UserService {
|
||||
};
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<any> {
|
||||
async delete(id: string, currentUserId?: number): Promise<any> {
|
||||
const user = await this.userModel.findByPk(id);
|
||||
|
||||
if (!user) {
|
||||
@@ -209,6 +212,22 @@ export class UserService {
|
||||
};
|
||||
}
|
||||
|
||||
if (Number(id) === 1) {
|
||||
return {
|
||||
code: 400,
|
||||
message: "内置管理员账号不允许删除",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
|
||||
if (currentUserId && Number(id) === Number(currentUserId)) {
|
||||
return {
|
||||
code: 400,
|
||||
message: "不能删除当前登录账号",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
|
||||
await this.userRoleModel.destroy({
|
||||
where: { userId: id }
|
||||
});
|
||||
@@ -222,7 +241,7 @@ export class UserService {
|
||||
};
|
||||
}
|
||||
|
||||
async batchDelete(ids: number[]): Promise<any> {
|
||||
async batchDelete(ids: number[], currentUserId?: number): Promise<any> {
|
||||
if (!ids || ids.length === 0) {
|
||||
return {
|
||||
code: 400,
|
||||
@@ -231,6 +250,22 @@ export class UserService {
|
||||
};
|
||||
}
|
||||
|
||||
if (ids.includes(1)) {
|
||||
return {
|
||||
code: 400,
|
||||
message: "内置管理员账号不允许删除",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
|
||||
if (currentUserId && ids.includes(Number(currentUserId))) {
|
||||
return {
|
||||
code: 400,
|
||||
message: "不能删除当前登录账号",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
|
||||
await this.userRoleModel.destroy({
|
||||
where: { userId: ids }
|
||||
});
|
||||
@@ -260,7 +295,8 @@ export class UserService {
|
||||
await user.update({ status });
|
||||
|
||||
const userData = user.toJSON();
|
||||
delete userData.password;
|
||||
delete userData.passwordHash;
|
||||
delete userData.password_hash;
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
@@ -271,18 +307,17 @@ export class UserService {
|
||||
|
||||
async getRoleList(): Promise<any> {
|
||||
const roles = await this.roleModel.findAll({
|
||||
where: { status: 1 },
|
||||
attributes: ["id", "roleCode", "roleName"],
|
||||
order: [
|
||||
["sort_order", "ASC"],
|
||||
["created_at", "DESC"]
|
||||
],
|
||||
raw: true
|
||||
]
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "获取角色列表成功",
|
||||
data: roles
|
||||
data: roles.map((role) => role.toJSON())
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+103
-63
@@ -1316,11 +1316,6 @@ asynckit@^0.4.0:
|
||||
resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
aws-ssl-profiles@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz#157dd77e9f19b1d123678e93f120e6f193022641"
|
||||
integrity sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==
|
||||
|
||||
babel-jest@^29.7.0:
|
||||
version "29.7.0"
|
||||
resolved "https://registry.npmmirror.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5"
|
||||
@@ -1486,7 +1481,7 @@ cache-content-type@^1.0.0:
|
||||
mime-types "^2.1.18"
|
||||
ylru "^1.2.0"
|
||||
|
||||
cache-manager-ioredis@^2.1.5:
|
||||
cache-manager-ioredis@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmmirror.com/cache-manager-ioredis/-/cache-manager-ioredis-2.1.0.tgz#69c9ab4cf72531f9015adf44d3822f9e2dcadc01"
|
||||
integrity sha512-TCxbp9ceuFveTKWuNaCX8QjoC41rAlHen4s63u9Yd+iXlw3efYmimc/u935PKPxSdhkXpnMes4mxtK3/yb0L4g==
|
||||
@@ -1764,7 +1759,7 @@ delegates@^1.0.0:
|
||||
resolved "https://registry.npmmirror.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
||||
integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==
|
||||
|
||||
denque@2.1.0, denque@^2.1.0:
|
||||
denque@2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
|
||||
integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
|
||||
@@ -2085,13 +2080,6 @@ function-bind@^1.1.2:
|
||||
resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
|
||||
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||
|
||||
generate-function@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f"
|
||||
integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==
|
||||
dependencies:
|
||||
is-property "^1.0.2"
|
||||
|
||||
generator-function@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmmirror.com/generator-function/-/generator-function-2.0.1.tgz#0e75dd410d1243687a0ba2e951b94eedb8f737a2"
|
||||
@@ -2290,13 +2278,6 @@ iconv-lite@0.4.24, iconv-lite@~0.4.24:
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
iconv-lite@^0.7.2:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz#d0bdeac3f12b4835b7359c2ad89c422a4d1cc72e"
|
||||
integrity sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3.0.0"
|
||||
|
||||
ignore@^5.1.1:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
|
||||
@@ -2430,11 +2411,6 @@ is-number@^7.0.0:
|
||||
resolved "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
|
||||
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
|
||||
|
||||
is-property@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
|
||||
integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==
|
||||
|
||||
is-regex@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22"
|
||||
@@ -3130,11 +3106,6 @@ lodash@^4.17.21:
|
||||
resolved "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c"
|
||||
integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==
|
||||
|
||||
long@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.npmmirror.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83"
|
||||
integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==
|
||||
|
||||
lru-cache@7.18.3:
|
||||
version "7.18.3"
|
||||
resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89"
|
||||
@@ -3147,11 +3118,6 @@ lru-cache@^5.1.1:
|
||||
dependencies:
|
||||
yallist "^3.0.2"
|
||||
|
||||
lru.min@^1.1.0, lru.min@^1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.4.tgz#6ea1737a8c1ba2300cc87ad46910a4bdffa0117b"
|
||||
integrity sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==
|
||||
|
||||
make-dir@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmmirror.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e"
|
||||
@@ -3298,27 +3264,6 @@ ms@^2.1.1, ms@^2.1.3:
|
||||
resolved "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
mysql2@^3.12.0:
|
||||
version "3.22.4"
|
||||
resolved "https://registry.npmmirror.com/mysql2/-/mysql2-3.22.4.tgz#2cfe5f6125d285755b90cbad9d7d35ceb526c417"
|
||||
integrity sha512-CtXYlmL7ZamiYKbmqkamQHWJROUHSfm+f3kByzGfknw7kW51mcB2ouMUqYq1XfYxbXmnWo6RhPydx6OCqdgcmQ==
|
||||
dependencies:
|
||||
aws-ssl-profiles "^1.1.2"
|
||||
denque "^2.1.0"
|
||||
generate-function "^2.3.1"
|
||||
iconv-lite "^0.7.2"
|
||||
long "^5.3.2"
|
||||
lru.min "^1.1.4"
|
||||
named-placeholders "^1.1.6"
|
||||
sql-escaper "^1.3.3"
|
||||
|
||||
named-placeholders@^1.1.6:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.6.tgz#c50c6920b43f258f59c16add1e56654f5cc02bb5"
|
||||
integrity sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==
|
||||
dependencies:
|
||||
lru.min "^1.1.0"
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
@@ -3470,11 +3415,74 @@ path-type@^4.0.0:
|
||||
resolved "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
|
||||
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
|
||||
|
||||
pg-cloudflare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmmirror.com/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz#4b4c20e6d8ae531d400730f4804571a8d62f1497"
|
||||
integrity sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==
|
||||
|
||||
pg-connection-string@^2.14.0:
|
||||
version "2.14.0"
|
||||
resolved "https://registry.npmmirror.com/pg-connection-string/-/pg-connection-string-2.14.0.tgz#abc26ee4f37c56c0f3ae0fcf0b0653cc4e1c0fd9"
|
||||
integrity sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==
|
||||
|
||||
pg-connection-string@^2.6.1:
|
||||
version "2.13.0"
|
||||
resolved "https://registry.npmmirror.com/pg-connection-string/-/pg-connection-string-2.13.0.tgz#8678113465a5af3cc977dcb51eadc847b27aa2de"
|
||||
integrity sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==
|
||||
|
||||
pg-hstore@^2.3.4:
|
||||
version "2.3.4"
|
||||
resolved "https://registry.npmmirror.com/pg-hstore/-/pg-hstore-2.3.4.tgz#4425e3e2a3e15d2a334c35581186c27cf2e9b8dd"
|
||||
integrity sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA==
|
||||
dependencies:
|
||||
underscore "^1.13.1"
|
||||
|
||||
pg-int8@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmmirror.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
|
||||
integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==
|
||||
|
||||
pg-pool@^3.14.0:
|
||||
version "3.14.0"
|
||||
resolved "https://registry.npmmirror.com/pg-pool/-/pg-pool-3.14.0.tgz#f35ae4eb846780cad71af24099b3edfa9781ad90"
|
||||
integrity sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==
|
||||
|
||||
pg-protocol@^1.16.0:
|
||||
version "1.16.0"
|
||||
resolved "https://registry.npmmirror.com/pg-protocol/-/pg-protocol-1.16.0.tgz#cffb008826561ee9770a8a15dc21f269d731b305"
|
||||
integrity sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==
|
||||
|
||||
pg-types@2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmmirror.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3"
|
||||
integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==
|
||||
dependencies:
|
||||
pg-int8 "1.0.1"
|
||||
postgres-array "~2.0.0"
|
||||
postgres-bytea "~1.0.0"
|
||||
postgres-date "~1.0.4"
|
||||
postgres-interval "^1.1.0"
|
||||
|
||||
pg@^8.11.0:
|
||||
version "8.23.0"
|
||||
resolved "https://registry.npmmirror.com/pg/-/pg-8.23.0.tgz#5c2026d32bd0cb4fbd9196bac1ecf5ae66607180"
|
||||
integrity sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==
|
||||
dependencies:
|
||||
pg-connection-string "^2.14.0"
|
||||
pg-pool "^3.14.0"
|
||||
pg-protocol "^1.16.0"
|
||||
pg-types "2.2.0"
|
||||
pgpass "1.0.5"
|
||||
optionalDependencies:
|
||||
pg-cloudflare "^1.4.0"
|
||||
|
||||
pgpass@1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.npmmirror.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d"
|
||||
integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==
|
||||
dependencies:
|
||||
split2 "^4.1.0"
|
||||
|
||||
picocolors@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||
@@ -3502,6 +3510,28 @@ pkg-dir@^4.2.0:
|
||||
dependencies:
|
||||
find-up "^4.0.0"
|
||||
|
||||
postgres-array@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmmirror.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e"
|
||||
integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==
|
||||
|
||||
postgres-bytea@~1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmmirror.com/postgres-bytea/-/postgres-bytea-1.0.1.tgz#c40b3da0222c500ff1e51c5d7014b60b79697c7a"
|
||||
integrity sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==
|
||||
|
||||
postgres-date@~1.0.4:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.npmmirror.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8"
|
||||
integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==
|
||||
|
||||
postgres-interval@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmmirror.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695"
|
||||
integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==
|
||||
dependencies:
|
||||
xtend "^4.0.0"
|
||||
|
||||
pretty-format@^29.0.0, pretty-format@^29.7.0:
|
||||
version "29.7.0"
|
||||
resolved "https://registry.npmmirror.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812"
|
||||
@@ -3667,7 +3697,7 @@ safe-stable-stringify@^2.5.0:
|
||||
resolved "https://registry.npmmirror.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd"
|
||||
integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0":
|
||||
"safer-buffer@>= 2.1.2 < 3":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
@@ -3819,16 +3849,16 @@ source-map@^0.6.0, source-map@^0.6.1:
|
||||
resolved "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
split2@^4.1.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
|
||||
integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
|
||||
|
||||
sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
|
||||
|
||||
sql-escaper@^1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.npmmirror.com/sql-escaper/-/sql-escaper-1.3.3.tgz#65faf89f048d26bb9a75566b82b5990ddf8a5b7f"
|
||||
integrity sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==
|
||||
|
||||
stack-utils@^2.0.3:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.npmmirror.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f"
|
||||
@@ -4066,6 +4096,11 @@ uglify-js@^3.1.4:
|
||||
resolved "https://registry.npmmirror.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f"
|
||||
integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==
|
||||
|
||||
underscore@^1.13.1:
|
||||
version "1.13.8"
|
||||
resolved "https://registry.npmmirror.com/underscore/-/underscore-1.13.8.tgz#a93a21186c049dbf0e847496dba72b7bd8c1e92b"
|
||||
integrity sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==
|
||||
|
||||
"undici-types@>=7.24.0 <7.24.7":
|
||||
version "7.24.6"
|
||||
resolved "https://registry.npmmirror.com/undici-types/-/undici-types-7.24.6.tgz#61275b485d7fd4e9d269c7cf04ec2873c9cc0f91"
|
||||
@@ -4194,6 +4229,11 @@ ws@^7.2.3:
|
||||
resolved "https://registry.npmmirror.com/ws/-/ws-7.5.11.tgz#9460daf1812bb81a423c5b9eac746941a86310fa"
|
||||
integrity sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==
|
||||
|
||||
xtend@^4.0.0:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||
|
||||
Reference in new issue
Block a user