Ver código fonte

优化退款功能,重构权限模块

skyline 1 mês atrás
pai
commit
adea416c10
26 arquivos alterados com 2033 adições e 540 exclusões
  1. 485 0
      docs/权限模块设计说明.md
  2. 20 0
      haha-admin-web/src/api/permission.ts
  3. 474 0
      haha-admin-web/src/views/system/permission/components/PermissionDefineDialog.vue
  4. 384 127
      haha-admin-web/src/views/system/permission/index.vue
  5. 217 36
      haha-admin-web/src/views/system/permission/utils/hook.tsx
  6. 34 8
      haha-admin-web/src/views/system/permission/utils/types.ts
  7. 66 202
      haha-admin-web/src/views/system/role/index.vue
  8. 4 143
      haha-admin-web/src/views/system/role/utils/hook.tsx
  9. 1 0
      haha-admin-web/src/views/system/role/utils/types.ts
  10. 1 0
      haha-admin/src/main/java/com/haha/admin/controller/CouponController.java
  11. 42 0
      haha-admin/src/main/java/com/haha/admin/controller/DataPermissionController.java
  12. 3 0
      haha-admin/src/main/java/com/haha/admin/controller/DeviceAlertController.java
  13. 3 0
      haha-admin/src/main/java/com/haha/admin/controller/DictController.java
  14. 3 6
      haha-admin/src/main/java/com/haha/admin/controller/LayerTemplateController.java
  15. 1 0
      haha-admin/src/main/java/com/haha/admin/controller/MarketingActivityController.java
  16. 157 0
      haha-admin/src/main/java/com/haha/admin/controller/RefundApplicationController.java
  17. 1 0
      haha-admin/src/main/java/com/haha/admin/controller/ShopController.java
  18. 1 0
      haha-admin/src/main/java/com/haha/admin/controller/TimedDiscountController.java
  19. 6 0
      haha-admin/src/main/java/com/haha/admin/controller/UserController.java
  20. 1 7
      haha-mapper/src/main/java/com/haha/mapper/RolePermissionMapper.java
  21. 0 9
      haha-mapper/src/main/resources/mapper/RolePermissionMapper.xml
  22. 2 0
      haha-miniapp/src/main/resources/application.yml
  23. 29 0
      haha-service/src/main/java/com/haha/service/DataPermissionService.java
  24. 1 1
      haha-service/src/main/java/com/haha/service/NewProductApplyService.java
  25. 89 0
      haha-service/src/main/java/com/haha/service/impl/DataPermissionServiceImpl.java
  26. 8 1
      haha-service/src/main/java/com/haha/service/impl/RoleServiceImpl.java

+ 485 - 0
docs/权限模块设计说明.md

@@ -0,0 +1,485 @@
+# 权限模块设计说明
+
+## 1. 权限架构总览
+
+本系统采用**双层次权限控制架构**:
+
+| 层级 | 控制点 | 实现方式 | 范围 |
+|------|--------|----------|------|
+| 第一层 - 前端 | 路由访问、按钮显示 | 路由守卫(`meta.roles`)+ `Auth` 组件 / `v-auth` 指令 | 菜单可见性、按钮/操作可见性 |
+| 第二层 - 后端 | API 接口 | `@RequirePermission` 注解 + AOP 切面 | 接口访问权限校验 |
+
+两个层级独立校验,前端控制用户体验(隐藏无权限的元素),后端控制数据安全(阻止越权请求)。
+
+---
+
+## 2. 后端权限实现
+
+### 2.1 核心注解
+
+#### `@RequirePermission`
+
+```java
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RequirePermission {
+    String value();       // 权限编码,格式: module:operation
+    String message() default "无操作权限";  // 无权限时的提示信息
+}
+```
+
+**使用位置**:Controller 方法上,标注该接口所需的权限编码。
+
+#### `@SuperAdmin`
+
+```java
+public @interface SuperAdmin {
+    String message() default "仅超级管理员可访问";
+}
+```
+
+**使用位置**:Controller 方法上,标注仅超级管理员可访问的接口。
+
+### 2.2 权限校验原理(PermissionAspect)
+
+使用 Spring AOP `@Around` 切面拦截所有带有 `@RequirePermission` 的方法,执行流程:
+
+1. 通过 Sa-Token `StpUtil.getLoginId()` 获取当前登录用户
+2. 从 Session 中获取用户的角色 ID 列表
+3. **超级管理员豁免**:如果用户角色包含 roleId=1(超级管理员),直接放行
+4. 通过 `DataPermissionService.getPermissionCodesByRoleIds()` 查询用户拥有的所有权限编码
+5. 检查用户权限列表中是否包含接口所需的权限编码
+6. 包含则放行,不包含则返回 `Result.error(403, "无操作权限")`
+
+### 2.3 数据模型
+
+#### t_data_permission(权限定义表)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT | 主键(雪花算法) |
+| permission_code | VARCHAR | 权限编码,如 `product:create` |
+| permission_name | VARCHAR | 权限名称,如 `商品创建` |
+| module_code | VARCHAR | 模块编码,如 `product` |
+| module_name | VARCHAR | 模块名称,如 `商品管理` |
+| operation_type | VARCHAR | 操作类型: create/read/update/delete/execute |
+| description | VARCHAR | 权限描述 |
+| sort_order | INT | 排序 |
+| status | INT | 状态: 1-启用, 0-禁用 |
+| create_time | DATETIME | 创建时间 |
+| update_time | DATETIME | 更新时间 |
+
+#### t_role(角色表)
+
+角色通过 `role_id` 与 `t_role_data_permission` 关联,实现角色-权限多对多关系。
+
+#### t_role_data_permission(角色权限关联表)
+
+关联角色与权限的多对多关系。
+
+### 2.4 权限编码规则
+
+格式:`{module_code}:{operation_type}`
+
+| 操作类型 | 适用场景 | 说明 |
+|----------|----------|------|
+| `read` | 查询类操作 | 列表、详情、统计、导出 |
+| `create` | 创建类操作 | 新增记录 |
+| `update` | 更新类操作 | 编辑、修改状态、发布/下线、审核 |
+| `delete` | 删除类操作 | 删除、批量删除 |
+| `export` | 导出操作 | 导出数据 |
+| `import` | 导入操作 | 导入数据 |
+| `execute` | 执行操作 | 同步、手动执行 |
+| `distribute` | 发放操作 | 发放优惠券 |
+| `review` | 审核操作 | 审核提现 |
+| `generate` | 生成操作 | 生成报表 |
+
+### 2.5 权限编码清单
+
+#### 系统管理
+
+| 权限编码 | 权限名称 | 所属模块 |
+|----------|----------|----------|
+| admin:read | 管理员查询 | 管理员管理 |
+| admin:create | 管理员新增 | 管理员管理 |
+| admin:update | 管理员编辑 | 管理员管理 |
+| admin:delete | 管理员删除 | 管理员管理 |
+| role:read | 角色查询 | 角色管理 |
+| role:create | 角色新增 | 角色管理 |
+| role:update | 角色编辑 | 角色管理 |
+| role:delete | 角色删除 | 角色管理 |
+| dict:read | 字典查询 | 字典管理 |
+| dict:create | 字典新增 | 字典管理 |
+| dict:update | 字典编辑 | 字典管理 |
+| dict:delete | 字典删除 | 字典管理 |
+| dict:export | 字典导出 | 字典管理 |
+| dict:import | 字典导入 | 字典管理 |
+
+#### 运营监控
+
+| 权限编码 | 权限名称 | 所属模块 |
+|----------|----------|----------|
+| operation-log:read | 操作日志查询 | 操作日志 |
+| operation-log:delete | 操作日志删除 | 操作日志 |
+| sync:read | 同步记录查询 | 数据同步 |
+| sync:execute | 执行数据同步 | 数据同步 |
+
+#### 公告管理
+
+| 权限编码 | 权限名称 | 所属模块 |
+|----------|----------|----------|
+| announcement:read | 公告查询 | 公告管理 |
+| announcement:create | 公告新增 | 公告管理 |
+| announcement:update | 公告编辑 | 公告管理 |
+| announcement:delete | 公告删除 | 公告管理 |
+
+#### 订单管理
+
+| 权限编码 | 权限名称 | 所属模块 |
+|----------|----------|----------|
+| order:read | 订单查询 | 订单管理 |
+| order:update | 订单编辑/退款审核 | 订单管理 |
+
+#### 商品管理
+
+| 权限编码 | 权限名称 | 所属模块 |
+|----------|----------|----------|
+| product:read | 商品查询 | 商品管理 |
+| product:create | 商品新增 | 商品管理 |
+| product:update | 商品编辑/同步 | 商品管理 |
+| product:delete | 商品删除 | 商品管理 |
+| product-apply:read | 新品申请查询 | 新品申请 |
+| product-apply:create | 新品申请提交 | 新品申请 |
+| product-apply:update | 新品申请更新 | 新品申请 |
+| product-apply:delete | 新品申请删除 | 新品申请 |
+
+#### 门店管理
+
+| 权限编码 | 权限名称 | 所属模块 |
+|----------|----------|----------|
+| shop:read | 门店查询 | 门店管理 |
+| shop:create | 门店新增 | 门店管理 |
+| shop:update | 门店编辑/设备关联/补货员管理 | 门店管理 |
+| shop:delete | 门店删除 | 门店管理 |
+
+#### 设备管理
+
+| 权限编码 | 权限名称 | 所属模块 |
+|----------|----------|----------|
+| device:read | 设备查询/详情/统计 | 设备管理 |
+| device:update | 设备远程控制/配置 | 设备管理 |
+
+#### 设备告警
+
+| 权限编码 | 权限名称 | 所属模块 |
+|----------|----------|----------|
+| device-alert:read | 告警记录查询/统计 | 设备告警 |
+
+#### 库存管理
+
+| 权限编码 | 权限名称 | 所属模块 |
+|----------|----------|----------|
+| inventory:read | 库存查询/日志/统计 | 库存管理 |
+| inventory:create | 创建上货记录 | 库存管理 |
+| inventory:update | 库存调整/上货完成 | 库存管理 |
+
+#### 客户管理
+
+| 权限编码 | 权限名称 | 所属模块 |
+|----------|----------|----------|
+| user:read | 客户查询/详情/统计 | 客户管理 |
+| user:update | 客户状态/信用分更新 | 客户管理 |
+
+#### 营销中心
+
+| 权限编码 | 权限名称 | 所属模块 |
+|----------|----------|----------|
+| dashboard:read | 数据概览 | 数据概览 |
+| marketing:activity:read | 活动查询 | 营销活动 |
+| marketing:activity:create | 活动创建 | 营销活动 |
+| marketing:activity:edit | 活动编辑/暂停/恢复 | 营销活动 |
+| marketing:activity:publish | 活动发布 | 营销活动 |
+| marketing:activity:delete | 活动删除 | 营销活动 |
+| marketing:activity:list | 统一活动列表 | 统一活动 |
+| marketing:coupon:read | 优惠券查询 | 优惠券管理 |
+| marketing:coupon:create | 优惠券创建 | 优惠券管理 |
+| marketing:coupon:edit | 优惠券编辑 | 优惠券管理 |
+| marketing:coupon:delete | 优惠券删除 | 优惠券管理 |
+| marketing:coupon:distribute | 优惠券发放 | 优惠券管理 |
+| marketing:invite:read | 邀请活动查询 | 邀请活动 |
+| marketing:invite:create | 邀请活动创建 | 邀请活动 |
+| marketing:invite:edit | 邀请活动编辑 | 邀请活动 |
+| timed-discount:read | 定时折扣查询 | 定时折扣 |
+| timed-discount:create | 定时折扣创建 | 定时折扣 |
+| timed-discount:edit | 定时折扣编辑 | 定时折扣 |
+| timed-discount:delete | 定时折扣删除 | 定时折扣 |
+| timed-discount:execute | 定时折扣执行 | 定时折扣 |
+
+#### 分销管理
+
+| 权限编码 | 权限名称 | 所属模块 |
+|----------|----------|----------|
+| distribution:distributor:create | 分销员创建 | 分销管理 |
+| distribution:distributor:read | 分销员查询 | 分销管理 |
+| distribution:distributor:update | 分销员编辑/激活/停用 | 分销管理 |
+| distribution:distributor:delete | 分销员删除 | 分销管理 |
+| distribution:referral:read | 推荐记录查询 | 分销管理 |
+| distribution:commission:read | 佣金记录查询 | 分销管理 |
+| distribution:withdrawal:read | 提现记录查询 | 分销管理 |
+| distribution:withdrawal:review | 提现审核/确认到账 | 分销管理 |
+| distribution:report:read | 月度报表查询 | 分销管理 |
+| distribution:report:generate | 生成月度报表 | 分销管理 |
+| distribution:report:export | 导出月度报表 | 分销管理 |
+| distribution:dashboard:read | 分销概览 | 分销管理 |
+| distribution:config:read | 分销配置查询 | 分销管理 |
+| distribution:config:update | 分销配置更新 | 分销管理 |
+
+#### 统计报表
+
+| 权限编码 | 权限名称 | 所属模块 |
+|----------|----------|----------|
+| statistics:read | 统计概览 | 统计报表 |
+| statistics:category | 品类统计 | 统计报表 |
+| statistics:product | 商品统计 | 统计报表 |
+| statistics:device | 设备统计 | 统计报表 |
+| statistics:shop | 门店统计 | 统计报表 |
+| statistics:profit | 利润统计 | 统计报表 |
+| statistics:repurchase | 复购统计 | 统计报表 |
+| statistics:export | 统计导出 | 统计报表 |
+
+#### 设备层模版
+
+| 权限编码 | 权限名称 | 所属模块 |
+|----------|----------|----------|
+| layer-template:read | 层模版查询 | 层模版管理 |
+| layer-template:create | 层模版创建 | 层模版管理 |
+| layer-template:update | 层模版编辑/同步 | 层模版管理 |
+| layer-template:delete | 层模版删除 | 层模版管理 |
+
+#### 签到管理
+
+| 权限编码 | 权限名称 | 所属模块 |
+|----------|----------|----------|
+| checkin:read | 签到记录查询 | 签到管理 |
+| checkin:create | 签到提交/同步 | 签到管理 |
+
+### 2.6 业务异常抛出
+
+```java
+// 使用 ResponseEnum 枚举
+throw new BusinessException(ResponseEnum.ORDER_NOT_FOUND);
+
+// 自定义消息
+throw new BusinessException(404, "订单不存在");
+
+// 仅消息(默认500)
+throw new BusinessException("操作失败");
+```
+
+### 2.7 操作日志注解 `@Log`
+
+写操作(增/删/改)必须添加操作日志注解:
+
+```java
+@Log(module = "模块名", operation = OperationType.XXX, summary = "操作描述")
+```
+
+| OperationType | 说明 |
+|---------------|------|
+| INSERT | 新增操作 |
+| UPDATE | 更新操作 |
+| DELETE | 删除操作 |
+| SYNC | 同步操作 |
+| EXPORT | 导出操作 |
+| IMPORT | 导入操作 |
+| LOGIN | 登录操作 |
+| LOGOUT | 登出操作 |
+| OTHER | 其他操作 |
+
+---
+
+## 3. 前端权限实现
+
+### 3.1 路由级权限控制
+
+**路由配置**:在 `src/router/modules/` 下的路由配置文件中,通过 `meta.roles` 指定允许访问的角色:
+
+```ts
+meta: {
+  title: "管理员管理",
+  roles: ["admin"]  // 仅 admin 角色可访问
+}
+```
+
+**路由守卫**:`src/router/utils.ts` 中的 `filterTree` 和 `initRouter` 函数根据用户角色动态过滤路由。
+
+### 3.2 按钮级权限控制
+
+提供两种方式:
+
+#### `v-auth` 指令(基于路由 meta 中的 auth 配置)
+
+```vue
+<el-button v-auth="['btn.add']">新增</el-button>
+```
+
+在路由配置中定义按钮级权限码:
+
+```ts
+meta: {
+  auths: ["btn.add", "btn.edit", "btn.delete"]
+}
+```
+
+#### `v-perms` 指令(基于用户 permissions 列表)
+
+```vue
+<el-button v-perms="['product:create']">新增商品</el-button>
+```
+
+用户的 `permissions` 在登录时从后端获取并存储到 `userStore` 中。如果用户拥有 `*:*:*`(超级管理员权限),则所有操作均可执行。
+
+### 3.3 登录与权限加载流程
+
+1. 调用 `/login` API 登录
+2. 后端返回 `accessToken`、`refreshToken`、用户信息(包含角色和权限列表)
+3. 前端存储 Token 到 Cookie(`authorized-token`)和 localStorage(`user-info`)
+4. `useUserStoreHook().SET_ROLES/SET_PERMS` 存储角色和权限到 Pinia store
+5. 路由守卫 `initRouter()` 根据用户角色动态过滤可访问的路由
+6. 401 响应自动触发登出流程
+
+### 3.4 Token 管理
+
+- Token 存储在 Cookie 和 localStorage 双保险
+- 请求头格式:`Authorization: Bearer {token}`
+- 无感刷新机制:Token 过期时自动调用刷新接口
+- 401 响应自动清除 Token 并跳转登录页
+
+---
+
+## 4. 完整权限校验流程图
+
+```
+用户请求
+  |
+  v
+[前端路由守卫] ── 检查 meta.roles
+  |  无权限 → 跳转 403 页面
+  |  有权限 → 加载页面
+  v
+[前端按钮级 v-perms/v-auth]
+  |  无权限 → DOM 元素被移除
+  |  有权限 → 显示按钮
+  v
+[前端发起 API 请求] ── 携带 Bearer Token
+  |
+  v
+[Sa-Token 拦截器] ── 验证 Token 有效性
+  |  无效 → 返回 401
+  |  有效 → 继续
+  v
+[AOP @RequirePermission 切面]
+  |  无注解 → 放行
+  |  有注解:
+  |    获取用户角色列表
+  |    是超级管理员(roleId=1) → 放行
+  |    检查用户权限列表是否包含所需权限
+  |      不包含 → 返回 403
+  |      包含 → 放行
+  v
+[Controller 执行业务逻辑]
+```
+
+---
+
+## 5. 注意事项与最佳实践
+
+### 5.1 权限编码命名规范
+
+- 格式:`{module_code}:{operation_type}`
+- `module_code` 使用小写字母,多词用中划线连接(如 `timed-discount`、`operation-log`)
+- `operation_type` 使用小写字母
+
+### 5.2 新增功能权限纳入规范
+
+开发新功能时,必须按以下流程确保权限控制完整:
+
+1. Controller 中所有接口必须添加 `@RequirePermission` 注解
+2. 写操作接口(增/删/改)必须添加 `@Log` 注解
+3. 在 `t_data_permission` 表中添加对应的权限记录
+4. 前端菜单配置中添加 `meta.roles` 和按钮级权限码
+5. 本文档中补充新增的权限编码
+
+### 5.3 常见错误
+
+- **UserController 缺少权限注解**(已修复)—— 客户管理模块完全缺失 `@RequirePermission`
+- **DeviceAlertController 缺少权限注解**(已修复)—— 设备告警模块完全缺失 `@RequirePermission`
+- **LayerTemplateController 临时移除权限**(已修复)—— 三个接口的 `@RequirePermission` 被注释并标注 TODO
+- **辅助查询接口缺少权限**(已修复)—— 部分下拉选择接口未加权限注解
+
+### 5.4 接口分类与权限策略
+
+| 接口类型 | 权限策略 | 示例 |
+|----------|----------|------|
+| 查询列表 | `module:read` | `@RequirePermission("order:read")` |
+| 查询详情 | `module:read` | `@RequirePermission("order:read")` |
+| 新增 | `module:create` | `@RequirePermission("product:create")` |
+| 编辑 | `module:update` | `@RequirePermission("product:update")` |
+| 删除 | `module:delete` | `@RequirePermission("product:delete")` |
+| 状态变更 | `module:update` | `@RequirePermission("shop:update")` |
+| 审核 | `module:review` | `@RequirePermission("distribution:withdrawal:review")` |
+| 同步 | `module:update` | `@RequirePermission("layer-template:update")` |
+| 导出 | `module:export` | `@RequirePermission("statistics:export")` |
+| 登录 | 无需权限(`@SaIgnore`) | `AdminLoginController.login()` |
+
+---
+
+## 6. 前端菜单与后端权限映射
+
+| 前端菜单 | 后端 Controller | 权限前缀 |
+|----------|----------------|----------|
+| 数据概览 | DashboardController | dashboard:read |
+| 订单管理 | OrderController | order:read/update |
+| 退款管理 | RefundApplicationController | order:read/update |
+| 门店管理 | ShopController | shop:read/create/update/delete |
+| 设备管理 | DeviceController | device:read/update |
+| 门店分布地图 | (ShopController 复用) | shop:read |
+| 商品列表 | ProductController | product:read/create/update/delete |
+| 新品申请 | NewProductApplyController | product-apply:read/create/update/delete |
+| 库存管理 | InventoryController | inventory:read/create/update |
+| 客户管理 | UserController | user:read/update |
+| 营销活动 | MarketingActivityController / UnifiedActivityController | marketing:activity:* |
+| 优惠券管理 | CouponController | marketing:coupon:* |
+| 定时折扣 | TimedDiscountController | timed-discount:* |
+| 邀请活动 | InviteActivityAdminController | marketing:invite:* |
+| 分销管理 | DistributorAdminController | distribution:* |
+| 统计报表 | StatisticsController | statistics:* |
+| 管理员管理 | AdminController | admin:read/create/update/delete |
+| 角色管理 | RoleController | role:read/create/update/delete |
+| 权限管理 | DataPermissionController | role:read/update |
+| 公告管理 | AnnouncementController | announcement:* |
+| 操作日志 | OperationLogController | operation-log:read/delete |
+| 数据同步 | DataSyncController | sync:read/execute |
+| 字典管理 | DictController | dict:read/create/update/delete/export/import |
+| 签到管理 | CheckinController | checkin:read/create |
+| 设备告警 | DeviceAlertController | device-alert:read |
+| 层模版管理 | LayerTemplateController | layer-template:* |
+
+---
+
+## 7. 权限修复记录
+
+### 2026-04-29 修复清单
+
+| 文件 | 问题 | 修复内容 |
+|------|------|----------|
+| UserController.java | 5个接口完全缺失@RequirePermission | 新增 user:read(列表/详情/统计), user:update(状态/信用分) |
+| DeviceAlertController.java | 2个接口完全缺失@RequirePermission | 新增 device-alert:read(列表/统计) |
+| LayerTemplateController.java | 3个接口TEMP注释移除权限 | 恢复 layer-template:read(list), layer-template:update(sync/batchSync) |
+| DictController.java | getDictDataList缺少@RequirePermission | 新增 dict:read |
+| DictController.java | refreshCache缺少@RequirePermission和@Log | 新增 dict:update + 操作日志 |
+| MarketingActivityController.java | getOngoingActivities缺少@RequirePermission | 新增 marketing:activity:read |
+| TimedDiscountController.java | getEnabledActivities缺少@RequirePermission | 新增 timed-discount:read |
+| CouponController.java | getAvailableTemplates缺少@RequirePermission | 新增 marketing:coupon:read |
+| ShopController.java | getEnabledShops缺少@RequirePermission | 新增 shop:read |

+ 20 - 0
haha-admin-web/src/api/permission.ts

@@ -27,3 +27,23 @@ export const updateRolePermissions = (roleId: number, permissionIds: number[]) =
     data: permissionIds
   });
 };
+
+// ===== 权限定义CRUD =====
+
+export const createPermission = (data: any) => {
+  return http.request<Result>("post", "/permission/create", { data });
+};
+
+export const updatePermission = (data: any) => {
+  return http.request<Result>("put", "/permission/update", { data });
+};
+
+export const deletePermission = (id: number) => {
+  return http.request<Result>("delete", `/permission/${id}`);
+};
+
+export const togglePermissionStatus = (id: number, status: number) => {
+  return http.request<Result>("put", `/permission/${id}/status`, {
+    data: status
+  });
+};

+ 474 - 0
haha-admin-web/src/views/system/permission/components/PermissionDefineDialog.vue

@@ -0,0 +1,474 @@
+<script setup lang="tsx">
+import { ref, reactive, computed, onMounted } from "vue";
+import { message } from "@/utils/message";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import { addDialog } from "@/components/ReDialog";
+import {
+  getAllPermissions,
+  createPermission,
+  updatePermission,
+  deletePermission,
+  togglePermissionStatus
+} from "@/api/permission";
+import type { PermissionItem, PermissionForm } from "../utils/types";
+import { OPERATION_TYPES } from "../utils/types";
+
+import Delete from "~icons/ep/delete";
+import EditPen from "~icons/ep/edit-pen";
+import Refresh from "~icons/ep/refresh";
+import AddFill from "~icons/ri/add-circle-line";
+
+defineOptions({
+  name: "PermissionDefineDialog"
+});
+
+const props = withDefaults(defineProps<{
+  onSaved?: () => void;
+}>(), {});
+
+const loading = ref(false);
+const permissionList = ref<PermissionItem[]>([]);
+const activeModule = ref("");
+const tableRef = ref();
+const switchLoadMap = ref({});
+
+// 获取所有已启用的模块列表
+const permissionModules = computed(() => {
+  const moduleMap = new Map<string, string>();
+  permissionList.value.forEach(p => {
+    if (!moduleMap.has(p.moduleCode)) {
+      moduleMap.set(p.moduleCode, p.moduleName);
+    }
+  });
+  return Array.from(moduleMap.entries()).map(([code, name]) => ({
+    code,
+    name
+  }));
+});
+
+// 根据模块筛选
+const filteredList = computed(() => {
+  if (!activeModule.value) return permissionList.value;
+  return permissionList.value.filter(p => p.moduleCode === activeModule.value);
+});
+
+// 获取操作类型标签
+function getOperationTypeTag(type: string) {
+  const op = OPERATION_TYPES.find(t => t.value === type);
+  if (!op) return { text: type, type: "info" };
+  const colorMap: Record<string, string> = {
+    read: "primary",
+    create: "success",
+    update: "warning",
+    delete: "danger",
+    export: "",
+    import: "",
+    execute: "info",
+    distribute: "success",
+    review: "warning",
+    generate: ""
+  };
+  return { text: op.label, type: colorMap[type] || "info" };
+}
+
+// 表格列定义
+const columns: TableColumnList = [
+  {
+    label: "权限编码",
+    prop: "permissionCode",
+    minWidth: 160
+  },
+  {
+    label: "权限名称",
+    prop: "permissionName",
+    minWidth: 120
+  },
+  {
+    label: "所属模块",
+    prop: "moduleName",
+    minWidth: 100
+  },
+  {
+    label: "操作类型",
+    prop: "operationType",
+    minWidth: 90,
+    cellRenderer: ({ row }) => {
+      const tag = getOperationTypeTag(row.operationType);
+      return <el-tag type={tag.type as any} size="small">{tag.text}</el-tag>;
+    }
+  },
+  {
+    label: "排序",
+    prop: "sortOrder",
+    width: 70
+  },
+  {
+    label: "描述",
+    prop: "description",
+    minWidth: 140,
+    showOverflowTooltip: true
+  },
+  {
+    label: "状态",
+    minWidth: 90,
+    cellRenderer: ({ row, index }) => (
+      <el-switch
+        size="small"
+        loading={switchLoadMap.value[index]?.loading}
+        modelValue={row.status}
+        active-value={1}
+        inactive-value={0}
+        active-text="启用"
+        inactive-text="停用"
+        inline-prompt
+        onChange={(val) => handleToggleStatus(row, index, val)}
+      />
+    )
+  },
+  {
+    label: "操作",
+    fixed: "right",
+    width: 140,
+    cellRenderer: ({ row }) => (
+      <div class="flex gap-2">
+        <el-button
+          link
+          type="primary"
+          size="small"
+          icon={useRenderIcon(EditPen)}
+          onClick={() => openFormDialog("编辑", row)}
+        >
+          编辑
+        </el-button>
+        <el-popconfirm
+          title={`确认删除权限"${row.permissionName}"?`}
+          onConfirm={() => handleDelete(row)}
+        >
+          {{
+            reference: () => (
+              <el-button
+                link
+                type="danger"
+                size="small"
+                icon={useRenderIcon(Delete)}
+              >
+                删除
+              </el-button>
+            )
+          }}
+        </el-popconfirm>
+      </div>
+    )
+  }
+];
+
+// 获取数据
+async function fetchData() {
+  loading.value = true;
+  try {
+    const { data } = await getAllPermissions();
+    permissionList.value = data || [];
+  } catch (error) {
+    console.error("获取权限列表失败:", error);
+  } finally {
+    loading.value = false;
+  }
+}
+
+// 切换状态
+async function handleToggleStatus(row: PermissionItem, index: number, val: number) {
+  switchLoadMap.value[index] = { loading: true };
+  try {
+    const res = await togglePermissionStatus(row.id, val);
+    if (res.code === 200) {
+      row.status = val;
+      message(`已${val === 1 ? "启用" : "停用"}权限 ${row.permissionName}`, { type: "success" });
+    } else {
+      message(res.message || "操作失败", { type: "error" });
+    }
+  } catch {
+    message("操作失败", { type: "error" });
+  } finally {
+    switchLoadMap.value[index] = { loading: false };
+  }
+}
+
+// 删除
+async function handleDelete(row: PermissionItem) {
+  try {
+    const res = await deletePermission(row.id);
+    if (res.code === 200) {
+      message(`已删除权限 ${row.permissionName}`, { type: "success" });
+      await fetchData();
+    } else {
+      message(res.message || "删除失败", { type: "error" });
+    }
+  } catch {
+    message("删除失败", { type: "error" });
+  }
+}
+
+// 打开新增/编辑表单弹窗
+function openFormDialog(title: string, row?: PermissionItem) {
+  const formData = reactive<PermissionForm>({
+    permissionCode: row?.permissionCode || "",
+    permissionName: row?.permissionName || "",
+    moduleCode: row?.moduleCode || "",
+    moduleName: row?.moduleName || "",
+    operationType: row?.operationType || "read",
+    description: row?.description || "",
+    sortOrder: row?.sortOrder ?? 0,
+    status: row?.status ?? 1
+  });
+
+  // 从现有权限中提取所有模块列表作为建议选项
+  const moduleOptions = computed(() => {
+    const map = new Map<string, string>();
+    permissionList.value.forEach(p => {
+      if (!map.has(p.moduleCode)) {
+        map.set(p.moduleCode, p.moduleName);
+      }
+    });
+    return Array.from(map.entries()).map(([code, name]) => ({ code, name }));
+  });
+
+  const ruleFormRef = ref();
+
+  addDialog({
+    title: `${title}权限`,
+    width: "550px",
+    draggable: true,
+    closeOnClickModal: false,
+    contentRenderer: () => (
+      <el-form ref={ruleFormRef} model={formData} label-width="90px" label-position="right">
+        <el-row gutter={20}>
+          <el-col span={12}>
+            <el-form-item
+              label="权限编码"
+              prop="permissionCode"
+              rules={[
+                { required: true, message: "请输入权限编码", trigger: "blur" },
+                { pattern: /^[a-z][a-z0-9-]+:[a-z]+$/, message: "格式: module:operation (如 product:create)", trigger: "blur" }
+              ]}
+            >
+              <el-input
+                v-model={formData.permissionCode}
+                placeholder="如 product:create"
+                disabled={title === "编辑"}
+                clearable
+              />
+            </el-form-item>
+          </el-col>
+          <el-col span={12}>
+            <el-form-item
+              label="权限名称"
+              prop="permissionName"
+              rules={[{ required: true, message: "请输入权限名称", trigger: "blur" }]}
+            >
+              <el-input
+                v-model={formData.permissionName}
+                placeholder="如 商品创建"
+                clearable
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row gutter={20}>
+          <el-col span={12}>
+            <el-form-item
+              label="所属模块"
+              prop="moduleCode"
+              rules={[{ required: true, message: "请选择模块", trigger: "change" }]}
+            >
+              <el-select
+                v-model={formData.moduleCode}
+                placeholder="选择模块"
+                clearable
+                filterable
+                allow-create
+                style="width: 100%"
+                onChange={(val: string) => {
+                  if (title === "新增") {
+                    const found = moduleOptions.value.find(m => m.code === val);
+                    if (found) {
+                      formData.moduleName = found.name;
+                    } else {
+                      formData.moduleName = val;
+                    }
+                  }
+                }}
+              >
+                {moduleOptions.value.map(m => (
+                  <el-option key={m.code} label={`${m.name} (${m.code})`} value={m.code} />
+                ))}
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col span={12}>
+            <el-form-item label="模块名称" prop="moduleName">
+              <el-input
+                v-model={formData.moduleName}
+                placeholder="自动填充或手动输入"
+                clearable
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row gutter={20}>
+          <el-col span={12}>
+            <el-form-item
+              label="操作类型"
+              prop="operationType"
+              rules={[{ required: true, message: "请选择操作类型", trigger: "change" }]}
+            >
+              <el-select v-model={formData.operationType} placeholder="选择操作类型" style="width: 100%">
+                {OPERATION_TYPES.map(op => (
+                  <el-option key={op.value} label={op.label} value={op.value} />
+                ))}
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col span={12}>
+            <el-form-item label="排序号" prop="sortOrder">
+              <el-input
+                v-model={formData.sortOrder}
+                type="number"
+                min={0}
+                max={999}
+                style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model={formData.status}>
+            <el-radio value={1}>启用</el-radio>
+            <el-radio value={0}>停用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+
+        <el-form-item label="描述" prop="description">
+          <el-input
+            v-model={formData.description}
+            type="textarea"
+            placeholder="权限描述说明"
+            rows={2}
+          />
+        </el-form-item>
+      </el-form>
+    ),
+    beforeSure: async (done) => {
+      const valid = await ruleFormRef.value?.validate().catch(() => false);
+      if (!valid) return;
+
+      try {
+        if (title === "新增") {
+          const res = await createPermission({ ...formData });
+          if (res.code === 200) {
+            message(`新增权限 ${formData.permissionName} 成功`, { type: "success" });
+            done();
+            await fetchData();
+          } else {
+            message(res.message || "新增失败", { type: "error" });
+          }
+        } else {
+          const res = await updatePermission({ id: row.id, ...formData });
+          if (res.code === 200) {
+            message(`编辑权限 ${formData.permissionName} 成功`, { type: "success" });
+            done();
+            await fetchData();
+          } else {
+            message(res.message || "编辑失败", { type: "error" });
+          }
+        }
+      } catch (error) {
+        message("操作失败", { type: "error" });
+      }
+    }
+  });
+}
+
+onMounted(() => {
+  fetchData();
+});
+
+// 刷新
+function handleRefresh() {
+  fetchData();
+}
+
+defineExpose({
+  handleRefresh
+});
+</script>
+
+<template>
+  <div class="permission-define-dialog">
+    <!-- 模块筛选 -->
+    <el-form class="search-form" inline>
+      <el-form-item label="模块筛选:">
+        <el-radio-group v-model="activeModule">
+          <el-radio-button value="">全部</el-radio-button>
+          <el-radio-button
+            v-for="mod in permissionModules"
+            :key="mod.code"
+            :value="mod.code"
+          >
+            {{ mod.name }}
+          </el-radio-button>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon(AddFill)"
+          @click="openFormDialog('新增')"
+        >
+          新增权限
+        </el-button>
+        <el-button
+          :icon="useRenderIcon(Refresh)"
+          @click="handleRefresh"
+        >
+          刷新
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <pure-table
+      ref="tableRef"
+      row-key="id"
+      border
+      stripe
+      align-whole="center"
+      table-layout="auto"
+      :loading="loading"
+      :data="filteredList"
+      :columns="columns"
+      :header-cell-style="{
+        background: 'var(--el-fill-color-light)',
+        color: 'var(--el-text-color-primary)'
+      }"
+    />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.permission-define-dialog {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+
+  .search-form {
+    flex-shrink: 0;
+    margin-bottom: 12px;
+
+    :deep(.el-form-item) {
+      margin-bottom: 0;
+    }
+  }
+}
+</style>

+ 384 - 127
haha-admin-web/src/views/system/permission/index.vue

@@ -1,8 +1,18 @@
 <script setup lang="tsx">
+import { ref, computed, nextTick } from "vue";
 import { usePermission } from "./utils/hook";
-import { PureTableBar } from "@/components/RePureTableBar";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import { usePublicHooks } from "@/views/system/hooks";
+import { addDialog } from "@/components/ReDialog";
+import { deviceDetection, delay, subBefore, useResizeObserver } from "@pureadmin/utils";
+import PermissionDefineDialog from "./components/PermissionDefineDialog.vue";
+
 import Refresh from "~icons/ep/refresh";
+import Close from "~icons/ep/close";
+import Check from "~icons/ep/check";
+import Search from "~icons/ep/search";
+import Unlock from "~icons/ep/unlock";
+import Setting from "~icons/ep/setting";
 
 defineOptions({
   name: "SystemPermission"
@@ -10,156 +20,403 @@ defineOptions({
 
 const {
   loading,
-  permissionList,
-  permissionModules,
-  activeModule,
-  getPermissionTypeTag,
+  saving,
+  filteredRoles,
+  selectedRole,
+  roleSearch,
+  filteredPermissionTree,
+  checkedIds,
+  checkedCount,
+  hasChanges,
+  permissionSearch,
+  expandedModules,
+
+  selectRole,
+  togglePermission,
+  toggleModule,
+  isModuleAllChecked,
+  isModuleIndeterminate,
+  handleSave,
+  handleReset,
   refresh
 } = usePermission();
 
-// 表格列定义
-const columns: TableColumnList = [
-  {
-    label: "权限ID",
-    prop: "id",
-    width: 80
-  },
-  {
-    label: "权限名称",
-    prop: "name",
-    minWidth: 120
-  },
-  {
-    label: "权限标识",
-    prop: "code",
-    minWidth: 150
-  },
-  {
-    label: "所属模块",
-    prop: "moduleName",
-    minWidth: 100
-  },
-  {
-    label: "类型",
-    prop: "type",
-    minWidth: 80,
-    cellRenderer: ({ row }) => {
-      const tag = getPermissionTypeTag(row.type);
-      return <el-tag type={tag.type}>{tag.text}</el-tag>;
-    }
-  },
-  {
-    label: "路径",
-    prop: "path",
-    minWidth: 120,
-    showOverflowTooltip: true
-  },
-  {
-    label: "排序",
-    prop: "sort",
-    width: 80
-  },
-  {
-    label: "状态",
-    prop: "status",
-    minWidth: 80,
-    cellRenderer: ({ row }) => (
-      <el-tag type={row.status === 1 ? "success" : "danger"}>
-        {row.status === 1 ? "启用" : "停用"}
-      </el-tag>
+const { switchStyle } = usePublicHooks();
+const contentRef = ref();
+const leftPanelRef = ref();
+const rightPanelRef = ref();
+const panelHeight = ref(500);
+
+// 权限定义弹窗
+function openPermissionDefine() {
+  addDialog({
+    title: "权限定义管理",
+    width: "80%",
+    draggable: true,
+    closeOnClickModal: false,
+    fullscreen: deviceDetection(),
+    fullscreenIcon: true,
+    contentRenderer: () => (
+      <PermissionDefineDialog
+        onSaved={() => refresh()}
+      />
     )
-  }
+  });
+}
+
+// 获取操作类型标签样式
+function getOpTypeTag(opType: string) {
+  const colorMap: Record<string, string> = {
+    read: "primary",
+    create: "success",
+    update: "warning",
+    delete: "danger",
+    execute: "info",
+    export: "",
+    import: "",
+    distribute: "success",
+    review: "warning",
+    generate: ""
+  };
+  const tagType = colorMap[opType] || "info";
+  return { type: tagType as any, label: opType };
+}
+
+// 自适应高度
+const iconClass = [
+  "w-[22px]", "h-[22px]", "flex", "justify-center", "items-center",
+  "outline-hidden", "rounded-[4px]", "cursor-pointer", "transition-colors",
+  "hover:bg-[#0000000f]", "dark:hover:bg-[#ffffff1f]", "dark:hover:text-[#ffffffd9]"
 ];
 
-// 根据模块过滤权限
-const filteredPermissions = computed(() => {
-  if (!activeModule.value) return permissionList.value;
-  return permissionList.value.filter(p => p.moduleCode === activeModule.value);
-});
+// 切换模块展开/折叠
+function toggleModuleExpand(moduleId: string | number) {
+  const id = String(moduleId);
+  const list = expandedModules.value;
+  const idx = list.indexOf(id);
+  if (idx >= 0) {
+    list.splice(idx, 1);
+  } else {
+    list.push(id);
+  }
+}
 
-// 计算属性
-import { computed } from "vue";
+useResizeObserver(contentRef, async () => {
+  await nextTick();
+  delay(60).then(() => {
+    if (contentRef.value) {
+      const rect = contentRef.value.getBoundingClientRect();
+      if (rect.height > 0) {
+        panelHeight.value = rect.height - 20;
+      }
+    }
+  });
+});
 </script>
 
 <template>
-  <div class="main">
-    <el-form class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto">
-      <el-form-item label="权限模块:">
-        <el-radio-group v-model="activeModule">
-          <el-radio-button value="">全部</el-radio-button>
-          <el-radio-button
-            v-for="module in permissionModules"
-            :key="module.code"
-            :value="module.code"
-          >
-            {{ module.name }}
-          </el-radio-button>
-        </el-radio-group>
-      </el-form-item>
-    </el-form>
-
-    <PureTableBar
-      title="权限管理"
-      :columns="columns"
-      @refresh="refresh"
-    >
-      <template #buttons>
+  <div class="main" ref="contentRef">
+    <!-- 顶部操作栏 -->
+    <div class="top-bar bg-bg_color px-6 py-3 flex items-center justify-between flex-wrap gap-3">
+      <div class="flex items-center gap-3">
         <el-button
           type="primary"
+          plain
+          :icon="useRenderIcon(Setting)"
+          @click="openPermissionDefine"
+        >
+          权限定义管理
+        </el-button>
+        <el-button
           :icon="useRenderIcon(Refresh)"
           :loading="loading"
           @click="refresh"
         >
           刷新
         </el-button>
-      </template>
-      <template v-slot="{ size, dynamicColumns }">
-        <pure-table
-          row-key="id"
-          adaptive
-          :adaptiveConfig="{ offsetBottom: 108 }"
-          align-whole="center"
-          table-layout="auto"
-          :loading="loading"
-          :size="size"
-          :data="filteredPermissions"
-          :columns="dynamicColumns"
-          :header-cell-style="{
-            background: 'var(--el-fill-color-light)',
-            color: 'var(--el-text-color-primary)'
-          }"
-        />
-      </template>
-    </PureTableBar>
-
-    <!-- 权限说明 -->
-    <el-card shadow="never" class="mt-4">
-      <template #header>
-        <span class="font-bold">权限说明</span>
-      </template>
-      <el-descriptions :column="1" border>
-        <el-descriptions-item label="菜单权限">
-          控制用户是否能看到某个菜单项,在路由配置中进行控制
-        </el-descriptions-item>
-        <el-descriptions-item label="按钮权限">
-          控制用户是否能看到或操作某个按钮,使用 v-auth 指令或 hasPerms 函数进行控制
-        </el-descriptions-item>
-        <el-descriptions-item label="权限分配">
-          在角色管理页面,可以为每个角色分配对应的权限
-        </el-descriptions-item>
-      </el-descriptions>
-    </el-card>
+      </div>
+
+      <div class="flex items-center gap-3">
+        <template v-if="selectedRole">
+          <el-tag type="primary" effect="plain">
+            当前角色: {{ selectedRole.name }}
+          </el-tag>
+          <el-tag :type="checkedCount > 0 ? 'success' : 'info'" effect="plain">
+            已分配 {{ checkedCount }} 项权限
+          </el-tag>
+          <el-tag v-if="hasChanges" type="warning" effect="dark" class="animate-pulse">
+            有未保存的更改
+          </el-tag>
+        </template>
+        <template v-else>
+          <el-tag type="info" effect="plain">请从左侧选择一个角色</el-tag>
+        </template>
+      </div>
+
+      <div class="flex items-center gap-2">
+        <el-button
+          v-if="hasChanges"
+          :icon="useRenderIcon(Close)"
+          @click="handleReset"
+        >
+          重置
+        </el-button>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon(Check)"
+          :loading="saving"
+          :disabled="!hasChanges || !selectedRole"
+          @click="handleSave"
+        >
+          保存权限
+        </el-button>
+      </div>
+    </div>
+
+    <!-- 主体区域:分栏布局 -->
+    <div class="flex gap-3 px-6 pb-4" style="height: calc(100vh - 200px);">
+      <!-- 左栏:角色列表 -->
+      <div
+        class="left-panel bg-bg_color rounded-lg overflow-hidden flex flex-col"
+        style="width: 280px; min-width: 240px;"
+      >
+        <div class="panel-header px-4 py-3 border-b border-[var(--el-border-color-light)]">
+          <h3 class="text-sm font-bold flex items-center gap-2">
+            <IconifyIconOffline :icon="Unlock" width="16" />
+            角色列表
+          </h3>
+        </div>
+
+        <div class="p-3">
+          <el-input
+            v-model="roleSearch"
+            placeholder="搜索角色名称/标识..."
+            clearable
+            :prefix-icon="Search"
+            size="small"
+          />
+        </div>
+
+        <div class="flex-1 overflow-y-auto px-2 pb-2">
+          <div
+            v-for="role in filteredRoles"
+            :key="role.id"
+            class="role-item px-3 py-2.5 rounded-lg mb-1 cursor-pointer transition-all duration-200"
+            :class="{
+              'role-item-active': selectedRole?.id === role.id,
+              'role-item-hover': selectedRole?.id !== role.id
+            }"
+            @click="selectRole(role)"
+          >
+            <div class="flex items-center justify-between">
+              <div class="flex items-center gap-2 min-w-0">
+                <div
+                  class="w-2 h-2 rounded-full flex-shrink-0"
+                  :class="role.status === 1 ? 'bg-green-500' : 'bg-gray-300'"
+                />
+                <span class="text-sm font-medium truncate">{{ role.name }}</span>
+              </div>
+              <el-tag size="small" type="info" effect="plain" class="flex-shrink-0 ml-2">
+                {{ role.code }}
+              </el-tag>
+            </div>
+            <div class="text-xs text-gray-400 mt-1 pl-4">
+              标识: {{ role.code }}
+            </div>
+          </div>
+
+          <div
+            v-if="filteredRoles.length === 0"
+            class="flex items-center justify-center py-8 text-gray-400 text-sm"
+          >
+            <IconifyIconOffline :icon="Search" width="16" class="mr-1" />
+            暂无匹配角色
+          </div>
+        </div>
+      </div>
+
+      <!-- 右栏:权限分配面板 -->
+      <div class="right-panel bg-bg_color rounded-lg overflow-hidden flex flex-col flex-1">
+        <div class="panel-header px-4 py-3 border-b border-[var(--el-border-color-light)] flex items-center justify-between">
+          <h3 class="text-sm font-bold flex items-center gap-2">
+            <IconifyIconOffline :icon="Unlock" width="16" />
+            权限分配
+            <span v-if="selectedRole" class="text-gray-400 font-normal">
+              — {{ selectedRole.name }}
+            </span>
+          </h3>
+
+          <div class="flex items-center gap-3">
+            <el-input
+              v-model="permissionSearch"
+              placeholder="搜索权限..."
+              clearable
+              :prefix-icon="Search"
+              size="small"
+              style="width: 200px"
+            />
+          </div>
+        </div>
+
+        <div class="flex-1 overflow-y-auto p-4">
+          <template v-if="!selectedRole">
+            <div class="flex flex-col items-center justify-center h-full text-gray-400">
+              <IconifyIconOffline :icon="Unlock" width="48" class="mb-3 opacity-30" />
+              <p class="text-sm">请从左侧选择一个角色</p>
+              <p class="text-xs mt-1">选择角色后,可在此为其分配权限</p>
+            </div>
+          </template>
+
+          <template v-else-if="filteredPermissionTree.length === 0 && !loading">
+            <div class="flex flex-col items-center justify-center h-full text-gray-400">
+              <p class="text-sm">暂无权限数据</p>
+            </div>
+          </template>
+
+          <template v-else>
+            <div v-loading="loading" class="space-y-3">
+              <div
+                v-for="module in filteredPermissionTree"
+                :key="module.id"
+                class="module-card border border-[var(--el-border-color-light)] rounded-lg overflow-hidden transition-all duration-200 hover:shadow-sm"
+              >
+                <!-- 模块表头 -->
+                <div
+                  class="module-header flex items-center justify-between px-4 py-2.5 cursor-pointer select-none transition-colors duration-150"
+                  :class="isModuleIndeterminate(module) || isModuleAllChecked(module)
+                    ? 'bg-[var(--el-color-primary-light-9)]'
+                    : 'bg-[var(--el-fill-color-light)]'"
+                  @click="toggleModuleExpand(module.id)"
+                >
+                  <div class="flex items-center gap-3">
+                    <el-checkbox
+                      :model-value="isModuleAllChecked(module)"
+                      :indeterminate="isModuleIndeterminate(module)"
+                      @change="(val) => toggleModule(module, val as any)"
+                      @click.stop
+                    />
+                    <span class="text-sm font-semibold">{{ module.name }}</span>
+                    <el-tag size="small" type="info" effect="plain">
+                      {{ module.children?.length || 0 }}项
+                    </el-tag>
+                  </div>
+
+                  <div class="flex items-center gap-2">
+                    <span class="text-xs text-gray-400">
+                      已选 {{ module.children?.filter((p: any) => checkedIds.has(p.id)).length || 0 }}
+                    </span>
+                    <IconifyIconOffline
+                      :icon="expandedModules.includes(String(module.id)) ? 'ep:chevron-up' : 'ep:chevron-down'"
+                      width="14"
+                      class="text-gray-400 transition-transform duration-200"
+                    />
+                  </div>
+                </div>
+
+                <!-- 权限列表 -->
+                <template v-if="expandedModules.includes(String(module.id))">
+                  <div class="divide-y divide-[var(--el-border-color-light)]">
+                    <div
+                      v-for="perm in module.children"
+                      :key="perm.id"
+                      class="permission-item flex items-center justify-between px-4 py-2.5 transition-colors duration-150 hover:bg-[var(--el-color-primary-light-9)]"
+                      @click="togglePermission(perm.id, String(module.id))"
+                    >
+                      <div class="flex items-center gap-3 min-w-0">
+                        <el-checkbox
+                          :model-value="checkedIds.has(perm.id)"
+                          @click.stop
+                          @change="() => togglePermission(perm.id, String(module.id))"
+                        />
+                        <span class="text-sm">{{ perm.name }}</span>
+                        <el-tag size="small" :type="getOpTypeTag(perm.operationType || '').type" effect="light">
+                          {{ perm.operationType || '未知' }}
+                        </el-tag>
+                      </div>
+                      <el-tooltip :content="perm.permissionCode || ''" placement="left">
+                        <code class="text-xs text-gray-400 font-mono truncate max-w-[200px] ml-2">
+                          {{ perm.permissionCode || '' }}
+                        </code>
+                      </el-tooltip>
+                    </div>
+                  </div>
+                </template>
+              </div>
+            </div>
+          </template>
+        </div>
+      </div>
+    </div>
   </div>
 </template>
 
 <style lang="scss" scoped>
-.main-content {
-  margin: 24px 24px 0 !important;
+.main {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.top-bar {
+  flex-shrink: 0;
+  border-bottom: 1px solid var(--el-border-color-light);
+}
+
+.left-panel {
+  flex-shrink: 0;
+
+  .role-item {
+    border: 1px solid transparent;
+
+    &-active {
+      background: var(--el-color-primary-light-9);
+      border-color: var(--el-color-primary-light-5);
+
+      .text-sm.font-medium {
+        color: var(--el-color-primary);
+      }
+    }
+
+    &-hover {
+      &:hover {
+        background: var(--el-fill-color-light);
+      }
+    }
+  }
+}
+
+.right-panel {
+  min-width: 0;
 }
 
-.search-form {
-  :deep(.el-form-item) {
-    margin-bottom: 12px;
+.module-card {
+  .module-header {
+    &:hover {
+      background: var(--el-color-primary-light-9);
+    }
+  }
+
+  .permission-item {
+    cursor: pointer;
+
+    &:hover {
+      background: var(--el-color-primary-light-9);
+    }
+
+    &:active {
+      background: var(--el-color-primary-light-7);
+    }
   }
 }
+
+// 动画
+.animate-pulse {
+  animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+}
+
+@keyframes pulse {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.6; }
+}
 </style>

+ 217 - 36
haha-admin-web/src/views/system/permission/utils/hook.tsx

@@ -1,69 +1,250 @@
-import { ref, onMounted } from "vue";
-import { getAllPermissions, getPermissionsGrouped } from "@/api/permission";
-import type { PermissionItem, PermissionModule } from "./types";
+import { ref, reactive, computed, onMounted } from "vue";
+import { message } from "@/utils/message";
+import {
+  getPermissionsGrouped,
+  getRolePermissionIds,
+  updateRolePermissions
+} from "@/api/permission";
+import { getAllRoles } from "@/api/role";
 
 export function usePermission() {
   const loading = ref(false);
-  const permissionList = ref<PermissionItem[]>([]);
-  const permissionModules = ref<PermissionModule[]>([]);
-  const activeModule = ref("");
+  const saving = ref(false);
 
-  // 获取所有权限
-  async function fetchAllPermissions() {
+  // 角色列表
+  const roleList = ref<any[]>([]);
+  const selectedRole = ref<any>(null);
+  const roleSearch = ref("");
+
+  // 权限数据 - 按模块分组的树形结构
+  const permissionTree = ref<any[]>([]);
+  const checkedIds = ref<Set<number>>(new Set());
+  const originalCheckedIds = ref<number[]>([]);
+  const hasChanges = ref(false);
+  const permissionSearch = ref("");
+  const expandedModules = ref<string[]>([]);
+
+  // 计算所有权限ID(用于全选/取消全选)
+  const allPermissionIds = computed(() => {
+    const ids: number[] = [];
+    const collect = (list: any[]) => {
+      list.forEach(item => {
+        if (item.id > 0) ids.push(item.id);
+        if (item.children?.length) collect(item.children);
+      });
+    };
+    collect(permissionTree.value);
+    return ids;
+  });
+
+  // 已选权限数量
+  const checkedCount = computed(() => checkedIds.value.size);
+
+  // 筛选后的角色列表
+  const filteredRoles = computed(() => {
+    if (!roleSearch.value) return roleList.value;
+    const q = roleSearch.value.toLowerCase();
+    return roleList.value.filter(
+      r => r.name?.toLowerCase().includes(q) || r.code?.toLowerCase().includes(q)
+    );
+  });
+
+  // 筛选后的权限树
+  const filteredPermissionTree = computed(() => {
+    if (!permissionSearch.value) {
+      return permissionTree.value;
+    }
+    const q = permissionSearch.value.toLowerCase();
+    return permissionTree.value
+      .map(module => ({
+        ...module,
+        children: module.children?.filter(
+          (p: any) =>
+            p.permissionName?.toLowerCase().includes(q) ||
+            p.permissionCode?.toLowerCase().includes(q)
+        )
+      }))
+      .filter(module => module.children?.length > 0);
+  });
+
+  // 获取角色列表
+  async function fetchRoles() {
+    try {
+      const { data } = await getAllRoles();
+      roleList.value = data || [];
+    } catch (error) {
+      console.error("获取角色列表失败:", error);
+    }
+  }
+
+  // 获取权限树(按模块分组)
+  async function fetchPermissionTree() {
     loading.value = true;
     try {
-      const { data } = await getAllPermissions();
-      permissionList.value = data || [];
+      const { data } = await getPermissionsGrouped();
+      permissionTree.value = data || [];
+      // 默认展开所有模块
+      if (data?.length) {
+        expandedModules.value = data.map((m: any) => String(m.id));
+      }
     } catch (error) {
-      console.error("获取权限列表失败:", error);
+      console.error("获取权限失败:", error);
     } finally {
       loading.value = false;
     }
   }
 
-  // 获取分组权限
-  async function fetchPermissionsGrouped() {
+  // 选中角色,加载其权限
+  async function selectRole(role: any) {
+    if (selectedRole.value?.id === role.id) return;
+    selectedRole.value = role;
+    hasChanges.value = false;
     loading.value = true;
+
     try {
-      const { data } = await getPermissionsGrouped();
-      permissionModules.value = data || [];
-      if (data && data.length > 0) {
-        activeModule.value = data[0].code;
-      }
+      const { data } = await getRolePermissionIds(role.id);
+      const ids: number[] = data || [];
+      originalCheckedIds.value = ids;
+      checkedIds.value = new Set(ids);
     } catch (error) {
-      console.error("获取权限分组失败:", error);
+      console.error("获取角色权限失败:", error);
+      checkedIds.value = new Set();
+      originalCheckedIds.value = [];
     } finally {
       loading.value = false;
     }
   }
 
-  // 获取权限类型标签
-  function getPermissionTypeTag(type: number) {
-    switch (type) {
-      case 1:
-        return { text: "菜单", type: "primary" };
-      case 2:
-        return { text: "按钮", type: "success" };
-      default:
-        return { text: "未知", type: "info" };
+  // 切换单个权限选中状态
+  function togglePermission(permissionId: number, moduleId: string) {
+    const newSet = new Set(checkedIds.value);
+    if (newSet.has(permissionId)) {
+      newSet.delete(permissionId);
+    } else {
+      newSet.add(permissionId);
+    }
+    checkedIds.value = newSet;
+    hasChanges.value = true;
+
+    // 展开对应的模块
+    if (!expandedModules.value.includes(moduleId)) {
+      expandedModules.value.push(moduleId);
     }
   }
 
-  // 刷新数据
+  // 全选/取消全选某个模块下的所有权限
+  function toggleModule(module: any, checked: boolean) {
+    const newSet = new Set(checkedIds.value);
+    const modulePermissions = module.children || [];
+    const moduleIds = modulePermissions
+      .filter((p: any) => p.id > 0)
+      .map((p: any) => p.id);
+
+    if (checked) {
+      moduleIds.forEach((id: number) => newSet.add(id));
+    } else {
+      moduleIds.forEach((id: number) => newSet.delete(id));
+    }
+
+    checkedIds.value = newSet;
+    hasChanges.value = true;
+  }
+
+  // 检查某个模块是否全选
+  function isModuleAllChecked(module: any): boolean {
+    const modulePermissions = module.children || [];
+    const moduleIds = modulePermissions
+      .filter((p: any) => p.id > 0)
+      .map((p: any) => p.id);
+    if (moduleIds.length === 0) return false;
+    return moduleIds.every(id => checkedIds.value.has(id));
+  }
+
+  // 检查某个模块是否部分选中
+  function isModuleIndeterminate(module: any): boolean {
+    const modulePermissions = module.children || [];
+    const moduleIds = modulePermissions
+      .filter((p: any) => p.id > 0)
+      .map((p: any) => p.id);
+    if (moduleIds.length === 0) return false;
+    const checked = moduleIds.filter(id => checkedIds.value.has(id));
+    return checked.length > 0 && checked.length < moduleIds.length;
+  }
+
+  // 保存权限
+  async function handleSave() {
+    if (!selectedRole.value) {
+      message("请先选择一个角色", { type: "warning" });
+      return;
+    }
+    if (!hasChanges.value) {
+      message("没有需要保存的变更", { type: "info" });
+      return;
+    }
+
+    saving.value = true;
+    try {
+      const permissionIds = Array.from(checkedIds.value);
+      const res = await updateRolePermissions(selectedRole.value.id, permissionIds);
+      if (res.code === 200) {
+        message(`角色 ${selectedRole.value.name} 权限保存成功`, { type: "success" });
+        originalCheckedIds.value = [...permissionIds];
+        hasChanges.value = false;
+      } else {
+        message(res.message || "保存失败", { type: "error" });
+      }
+    } catch (error) {
+      message("保存失败", { type: "error" });
+    } finally {
+      saving.value = false;
+    }
+  }
+
+  // 重置当前角色的权限到保存状态
+  function handleReset() {
+    checkedIds.value = new Set(originalCheckedIds.value);
+    hasChanges.value = false;
+    message("已重置为保存状态", { type: "info" });
+  }
+
+  // 刷新所有数据
   async function refresh() {
-    await Promise.all([fetchAllPermissions(), fetchPermissionsGrouped()]);
+    await Promise.all([fetchRoles(), fetchPermissionTree()]);
+    // 如果有选中的角色,重新加载其权限
+    if (selectedRole.value) {
+      const role = selectedRole.value;
+      selectedRole.value = null;
+      await selectRole(role);
+    }
   }
 
-  onMounted(() => {
-    refresh();
+  onMounted(async () => {
+    await Promise.all([fetchRoles(), fetchPermissionTree()]);
   });
 
   return {
     loading,
-    permissionList,
-    permissionModules,
-    activeModule,
-    getPermissionTypeTag,
+    saving,
+    roleList,
+    selectedRole,
+    roleSearch,
+    filteredRoles,
+    permissionTree,
+    filteredPermissionTree,
+    checkedIds,
+    checkedCount,
+    hasChanges,
+    permissionSearch,
+    expandedModules,
+    allPermissionIds,
+
+    selectRole,
+    togglePermission,
+    toggleModule,
+    isModuleAllChecked,
+    isModuleIndeterminate,
+    handleSave,
+    handleReset,
     refresh
   };
 }

+ 34 - 8
haha-admin-web/src/views/system/permission/utils/types.ts

@@ -1,17 +1,16 @@
-// 权限项类型
+// 权限项类型 (对应后端 DataPermission 实体)
 export interface PermissionItem {
   id: number;
-  name: string;
-  code: string;
+  permissionCode: string;
+  permissionName: string;
   moduleCode: string;
   moduleName: string;
-  type: number; // 1: 菜单, 2: 按钮
-  parentId: number;
-  path?: string;
-  icon?: string;
-  sort: number;
+  operationType: string;
+  description?: string;
+  sortOrder: number;
   status: number;
   createTime?: string;
+  updateTime?: string;
 }
 
 // 权限模块类型
@@ -27,3 +26,30 @@ export interface RolePermission {
   roleName: string;
   permissionIds: number[];
 }
+
+// 权限表单类型(CRUD)
+export interface PermissionForm {
+  id?: number;
+  permissionCode: string;
+  permissionName: string;
+  moduleCode: string;
+  moduleName: string;
+  operationType: string;
+  description?: string;
+  sortOrder: number;
+  status: number;
+}
+
+// 操作类型选项
+export const OPERATION_TYPES = [
+  { value: "read", label: "查询" },
+  { value: "create", label: "新增" },
+  { value: "update", label: "编辑" },
+  { value: "delete", label: "删除" },
+  { value: "export", label: "导出" },
+  { value: "import", label: "导入" },
+  { value: "execute", label: "执行" },
+  { value: "distribute", label: "发放" },
+  { value: "review", label: "审核" },
+  { value: "generate", label: "生成" }
+];

+ 66 - 202
haha-admin-web/src/views/system/role/index.vue

@@ -1,91 +1,35 @@
 <script setup lang="ts">
 import { useRole } from "./utils/hook";
-import { ref, computed, nextTick, onMounted } from "vue";
+import { ref } from "vue";
 import { PureTableBar } from "@/components/RePureTableBar";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
-import { delay, subBefore, deviceDetection, useResizeObserver } from "@pureadmin/utils";
 
 import Delete from "~icons/ep/delete";
 import EditPen from "~icons/ep/edit-pen";
 import Refresh from "~icons/ep/refresh";
-import Menu from "~icons/ep/menu";
 import AddFill from "~icons/ri/add-circle-line";
-import Close from "~icons/ep/close";
-import Check from "~icons/ep/check";
 
 defineOptions({
   name: "SystemRole"
 });
 
-const iconClass = computed(() => {
-  return [
-    "w-[22px]",
-    "h-[22px]",
-    "flex",
-    "justify-center",
-    "items-center",
-    "outline-hidden",
-    "rounded-[4px]",
-    "cursor-pointer",
-    "transition-colors",
-    "hover:bg-[#0000000f]",
-    "dark:hover:bg-[#ffffff1f]",
-    "dark:hover:text-[#ffffffd9]"
-  ];
-});
-
-const treeRef = ref();
 const formRef = ref();
 const tableRef = ref();
-const contentRef = ref();
-const treeHeight = ref(400);
 
 const {
   form,
-  isShow,
-  curRow,
   loading,
   columns,
-  rowStyle,
   dataList,
-  treeData,
-  treeProps,
-  isLinkage,
   pagination,
-  isExpandAll,
-  isSelectAll,
-  treeSearchValue,
   onSearch,
   resetForm,
   openDialog,
-  handleMenu,
-  handleSave,
   handleDelete,
-  filterMethod,
-  onQueryChanged,
   handleSizeChange,
   handleCurrentChange,
   handleSelectionChange
-} = useRole(treeRef);
-
-onMounted(() => {
-  useResizeObserver(contentRef, async () => {
-    await nextTick();
-    delay(60).then(() => {
-      try {
-        const tableWrapper = tableRef.value?.getTableDoms?.()?.tableWrapper;
-        if (tableWrapper?.style?.height) {
-          const height = parseFloat(subBefore(tableWrapper.style.height, "px"));
-          if (height > 0) {
-            treeHeight.value = height;
-          }
-        }
-      } catch (e) {
-        console.warn("计算树高度失败", e);
-      }
-    });
-  });
-});
+} = useRole();
 </script>
 
 <template>
@@ -138,153 +82,72 @@ onMounted(() => {
       </el-form-item>
     </el-form>
 
-    <div
-      ref="contentRef"
-      :class="['flex', deviceDetection() ? 'flex-wrap' : '']"
+    <PureTableBar
+      title="角色管理"
+      :columns="columns"
+      @refresh="onSearch"
     >
-      <PureTableBar
-        :class="[isShow && !deviceDetection() ? 'w-[60vw]!' : 'w-full']"
-        style="transition: width 220ms cubic-bezier(0.4, 0, 0.2, 1)"
-        title="角色管理"
-        :columns="columns"
-        @refresh="onSearch"
-      >
-        <template #buttons>
-          <el-button
-            type="primary"
-            :icon="useRenderIcon(AddFill)"
-            @click="openDialog()"
-          >
-            新增角色
-          </el-button>
-        </template>
-        <template v-slot="{ size, dynamicColumns }">
-          <pure-table
-            ref="tableRef"
-            align-whole="center"
-            showOverflowTooltip
-            table-layout="auto"
-            :loading="loading"
-            :size="size"
-            adaptive
-            :row-style="rowStyle"
-            :adaptiveConfig="{ offsetBottom: 108 }"
-            :data="dataList"
-            :columns="dynamicColumns"
-            :pagination="{ ...pagination, size }"
-            :header-cell-style="{
-              background: 'var(--el-fill-color-light)',
-              color: 'var(--el-text-color-primary)'
-            }"
-            @selection-change="handleSelectionChange"
-            @page-size-change="handleSizeChange"
-            @page-current-change="handleCurrentChange"
-          >
-            <template #operation="{ row }">
-              <el-button
-                class="reset-margin"
-                link
-                type="primary"
-                :size="size"
-                :icon="useRenderIcon(EditPen)"
-                @click="openDialog('修改', row)"
-              >
-                修改
-              </el-button>
-              <el-popconfirm
-                :title="`是否确认删除角色 ${row.name}?`"
-                @confirm="handleDelete(row)"
-              >
-                <template #reference>
-                  <el-button
-                    class="reset-margin"
-                    link
-                    type="primary"
-                    :size="size"
-                    :icon="useRenderIcon(Delete)"
-                  >
-                    删除
-                  </el-button>
-                </template>
-              </el-popconfirm>
-              <el-button
-                class="reset-margin"
-                link
-                type="primary"
-                :size="size"
-                :icon="useRenderIcon(Menu)"
-                @click="handleMenu(row)"
-              >
-                权限
-              </el-button>
-            </template>
-          </pure-table>
-        </template>
-      </PureTableBar>
-
-      <div
-        v-if="isShow"
-        class="min-w-[calc(100vw-60vw-268px)]! w-full mt-2 px-2 pb-2 bg-bg_color ml-2 overflow-auto"
-      >
-        <div class="flex justify-between w-full px-3 pt-5 pb-4">
-          <div class="flex">
-            <span :class="iconClass">
-              <IconifyIconOffline
-                v-tippy="{
-                  content: '关闭'
-                }"
-                class="dark:text-white"
-                width="18px"
-                height="18px"
-                :icon="Close"
-                @click="handleMenu"
-              />
-            </span>
-            <span :class="[iconClass, 'ml-2']">
-              <IconifyIconOffline
-                v-tippy="{
-                  content: '保存权限'
-                }"
-                class="dark:text-white"
-                width="18px"
-                height="18px"
-                :icon="Check"
-                @click="handleSave"
-              />
-            </span>
-          </div>
-          <p class="font-bold truncate">
-            权限分配
-            {{ `${curRow?.name ? `(${curRow.name})` : ""}` }}
-          </p>
-        </div>
-        <el-input
-          v-model="treeSearchValue"
-          placeholder="请输入权限名称进行搜索"
-          class="mb-1"
-          clearable
-          @input="onQueryChanged"
-        />
-        <div class="flex flex-wrap">
-          <el-checkbox v-model="isExpandAll" label="展开/折叠" />
-          <el-checkbox v-model="isSelectAll" label="全选/全不选" />
-          <el-checkbox v-model="isLinkage" label="父子联动" />
-        </div>
-        <el-tree-v2
-          ref="treeRef"
-          show-checkbox
-          :data="treeData"
-          :props="treeProps"
-          :height="treeHeight"
-          :check-strictly="!isLinkage"
-          :filter-method="filterMethod"
+      <template #buttons>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon(AddFill)"
+          @click="openDialog()"
         >
-          <template #default="{ node }">
-            <span>{{ node.name || node.label }}</span>
+          新增角色
+        </el-button>
+      </template>
+      <template v-slot="{ size, dynamicColumns }">
+        <pure-table
+          ref="tableRef"
+          align-whole="center"
+          showOverflowTooltip
+          table-layout="auto"
+          :loading="loading"
+          :size="size"
+          adaptive
+          :adaptiveConfig="{ offsetBottom: 108 }"
+          :data="dataList"
+          :columns="dynamicColumns"
+          :pagination="{ ...pagination, size }"
+          :header-cell-style="{
+            background: 'var(--el-fill-color-light)',
+            color: 'var(--el-text-color-primary)'
+          }"
+          @selection-change="handleSelectionChange"
+          @page-size-change="handleSizeChange"
+          @page-current-change="handleCurrentChange"
+        >
+          <template #operation="{ row }">
+            <el-button
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(EditPen)"
+              @click="openDialog('修改', row)"
+            >
+              修改
+            </el-button>
+            <el-popconfirm
+              :title="`是否确认删除角色 ${row.name}?`"
+              @confirm="handleDelete(row)"
+            >
+              <template #reference>
+                <el-button
+                  class="reset-margin"
+                  link
+                  type="primary"
+                  :size="size"
+                  :icon="useRenderIcon(Delete)"
+                >
+                  删除
+                </el-button>
+              </template>
+            </el-popconfirm>
           </template>
-        </el-tree-v2>
-      </div>
-    </div>
+        </pure-table>
+      </template>
+    </PureTableBar>
   </div>
 </template>
 
@@ -299,3 +162,4 @@ onMounted(() => {
   }
 }
 </style>
+

+ 4 - 143
haha-admin-web/src/views/system/role/utils/hook.tsx

@@ -10,15 +10,11 @@ import {
   updateRole,
   deleteRole
 } from "@/api/role";
-import { getPermissionsGrouped, getRolePermissionIds, updateRolePermissions } from "@/api/permission";
 import {
-  type Ref,
-  h,
   ref,
   toRaw,
   reactive,
-  onMounted,
-  watch
+  onMounted
 } from "vue";
 import {
   ElForm,
@@ -30,32 +26,19 @@ import {
 } from "element-plus";
 import type { FormItemProps, SearchFormProps } from "./types";
 
-export function useRole(treeRef: Ref) {
+export function useRole() {
   const form = reactive<SearchFormProps>({
     name: "",
     code: "",
     status: ""
   });
-  const curRow = ref();
   const formRef = ref();
   const ruleFormRef = ref();
   const dataList = ref([]);
-  const treeIds = ref([]);
-  const treeData = ref([]);
-  const isShow = ref(false);
   const loading = ref(true);
-  const isLinkage = ref(false);
-  const treeSearchValue = ref();
   const switchLoadMap = ref({});
-  const isExpandAll = ref(false);
-  const isSelectAll = ref(false);
   const isDataLoaded = ref(false);
   const { switchStyle } = usePublicHooks();
-  const treeProps = {
-    value: "id",
-    label: "name",
-    children: "children"
-  };
   const pagination = reactive<PaginationProps>({
     total: 0,
     pageSize: 10,
@@ -113,7 +96,7 @@ export function useRole(treeRef: Ref) {
     {
       label: "操作",
       fixed: "right",
-      width: 210,
+      width: 160,
       slot: "operation"
     }
   ];
@@ -329,142 +312,20 @@ export function useRole(treeRef: Ref) {
     });
   }
 
-  // 权限分配
-  async function handleMenu(row?: any) {
-    const { id } = row;
-    if (id) {
-      curRow.value = row;
-      isShow.value = true;
-      try {
-        const { data } = await getRolePermissionIds(id);
-        treeRef.value.setCheckedKeys(data || []);
-      } catch (error) {
-        console.error("获取角色权限失败:", error);
-      }
-    } else {
-      curRow.value = null;
-      isShow.value = false;
-    }
-  }
-
-  // 高亮当前权限选中行
-  function rowStyle({ row: { id } }) {
-    return {
-      cursor: "pointer",
-      background: id === curRow.value?.id ? "var(--el-fill-color-light)" : ""
-    };
-  }
-
-  // 保存权限
-  async function handleSave() {
-    const { id, name } = curRow.value;
-    const permissionIds = treeRef.value.getCheckedKeys();
-    try {
-      const res = await updateRolePermissions(id, permissionIds);
-      if (res.code === 200) {
-        message(`角色 ${name} 的权限修改成功`, { type: "success" });
-      } else {
-        message(res.message || "权限修改失败", { type: "error" });
-      }
-    } catch (error) {
-      message("权限修改失败", { type: "error" });
-    }
-  }
-
-  // 搜索过滤
-  const filterMethod = (query: string, node) => {
-    return node.name?.includes(query);
-  };
-
-  const onQueryChanged = (query: string) => {
-    treeRef.value?.filter(query);
-  };
-
-  // 获取权限树
-  async function fetchPermissionTree() {
-    try {
-      const { data } = await getPermissionsGrouped();
-      // 后端返回的是 Map<moduleName, List<Permission>>,需要转换为树形结构
-      let treeList: any[] = [];
-      
-      if (Array.isArray(data)) {
-        // 如果已经是数组格式,直接使用
-        treeList = data;
-      } else if (data && typeof data === 'object') {
-        // 如果是对象格式,转换为树形结构
-        let moduleId = -1;
-        for (const [moduleName, permissions] of Object.entries(data)) {
-          moduleId--;
-          const moduleNode = {
-            id: moduleId,
-            name: moduleName,
-            children: (permissions as any[]).map((p: any) => ({
-              id: p.id,
-              name: p.permissionName
-            }))
-          };
-          treeList.push(moduleNode);
-        }
-      }
-      
-      treeData.value = treeList;
-      // 获取所有权限ID用于全选
-      const getAllIds = (list: any[]) => {
-        const ids: number[] = [];
-        list.forEach(item => {
-          ids.push(item.id);
-          if (item.children?.length) {
-            ids.push(...getAllIds(item.children));
-          }
-        });
-        return ids;
-      };
-      treeIds.value = getAllIds(treeList);
-    } catch (error) {
-      console.error("获取权限列表失败:", error);
-    }
-  }
-
-  onMounted(async () => {
-    await fetchPermissionTree();
+  onMounted(() => {
     onSearch();
   });
 
-  watch(isExpandAll, val => {
-    val
-      ? treeRef.value.setExpandedKeys(treeIds.value)
-      : treeRef.value.setExpandedKeys([]);
-  });
-
-  watch(isSelectAll, val => {
-    val
-      ? treeRef.value.setCheckedKeys(treeIds.value)
-      : treeRef.value.setCheckedKeys([]);
-  });
-
   return {
     form,
-    isShow,
-    curRow,
     loading,
     columns,
-    rowStyle,
     dataList,
-    treeData,
-    treeProps,
-    isLinkage,
     pagination,
-    isExpandAll,
-    isSelectAll,
-    treeSearchValue,
     onSearch,
     resetForm,
     openDialog,
-    handleMenu,
-    handleSave,
     handleDelete,
-    filterMethod,
-    onQueryChanged,
     handleSizeChange,
     handleCurrentChange,
     handleSelectionChange

+ 1 - 0
haha-admin-web/src/views/system/role/utils/types.ts

@@ -23,6 +23,7 @@ interface SearchFormProps {
   name: string;
   code: string;
   status: number | string;
+  remark?: string;
 }
 
 export type { FormItemProps, FormProps, SearchFormProps };

+ 1 - 0
haha-admin/src/main/java/com/haha/admin/controller/CouponController.java

@@ -101,6 +101,7 @@ public class CouponController {
         return Result.success("查询成功", stats);
     }
 
+    @RequirePermission("marketing:coupon:read")
     @GetMapping("/available")
     public Result<List<CouponTemplate>> getAvailableTemplates() {
         List<CouponTemplate> templates = templateService.getAvailableTemplates();

+ 42 - 0
haha-admin/src/main/java/com/haha/admin/controller/DataPermissionController.java

@@ -74,4 +74,46 @@ public class DataPermissionController {
             @RequestBody List<Long> permissionIds) {
         return roleService.updateRolePermissions(roleId, permissionIds);
     }
+
+    /**
+     * 创建权限定义
+     */
+    @RequirePermission("role:create")
+    @Log(module = "权限管理", operation = OperationType.INSERT, summary = "创建权限定义")
+    @PostMapping("/create")
+    public Result<Void> createPermission(@RequestBody DataPermission permission) {
+        return dataPermissionService.createPermission(permission);
+    }
+
+    /**
+     * 更新权限定义
+     */
+    @RequirePermission("role:update")
+    @Log(module = "权限管理", operation = OperationType.UPDATE, summary = "更新权限定义")
+    @PutMapping("/update")
+    public Result<Void> updatePermission(@RequestBody DataPermission permission) {
+        return dataPermissionService.updatePermission(permission);
+    }
+
+    /**
+     * 删除权限定义
+     */
+    @RequirePermission("role:delete")
+    @Log(module = "权限管理", operation = OperationType.DELETE, summary = "删除权限定义")
+    @DeleteMapping("/{id}")
+    public Result<Void> deletePermission(@PathVariable Long id) {
+        return dataPermissionService.deletePermission(id);
+    }
+
+    /**
+     * 切换权限状态
+     */
+    @RequirePermission("role:update")
+    @Log(module = "权限管理", operation = OperationType.UPDATE, summary = "切换权限状态")
+    @PutMapping("/{id}/status")
+    public Result<Void> togglePermissionStatus(
+            @PathVariable Long id,
+            @RequestBody Integer status) {
+        return dataPermissionService.togglePermissionStatus(id, status);
+    }
 }

+ 3 - 0
haha-admin/src/main/java/com/haha/admin/controller/DeviceAlertController.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.haha.common.enums.DeviceAlertType;
+import com.haha.admin.annotation.RequirePermission;
 import com.haha.common.vo.PageResult;
 import com.haha.common.vo.Result;
 import com.haha.entity.DeviceAlertRecord;
@@ -46,6 +47,7 @@ public class DeviceAlertController {
      * @param pageSize  每页数量
      * @return 分页结果
      */
+    @RequirePermission("device-alert:read")
     @GetMapping("/list")
     public Result<PageResult<DeviceAlertRecord>> list(
             @RequestParam(required = false) String deviceId,
@@ -83,6 +85,7 @@ public class DeviceAlertController {
      *
      * @return 统计数据
      */
+    @RequirePermission("device-alert:read")
     @GetMapping("/statistics")
     public Result<Map<String, Object>> statistics() {
         LocalDateTime todayStart = LocalDate.now().atStartOfDay();

+ 3 - 0
haha-admin/src/main/java/com/haha/admin/controller/DictController.java

@@ -78,6 +78,7 @@ public class DictController {
         return Result.success("批量删除成功");
     }
 
+    @RequirePermission("dict:read")
     @GetMapping("/data/list/{dictCode}")
     public Result<List<DictData>> getDictDataList(@PathVariable String dictCode) {
         return Result.success("查询成功", dictService.getDictDataList(dictCode));
@@ -146,6 +147,8 @@ public class DictController {
         return Result.success("查询成功", dictService.getDictValue(dictCode, itemLabel));
     }
 
+    @RequirePermission("dict:update")
+    @Log(module = "字典管理", operation = OperationType.UPDATE, summary = "刷新字典缓存")
     @PostMapping("/refresh-cache")
     public Result<Void> refreshCache() {
         dictService.refreshCache();

+ 3 - 6
haha-admin/src/main/java/com/haha/admin/controller/LayerTemplateController.java

@@ -34,8 +34,7 @@ public class LayerTemplateController {
      * @param queryDTO 查询参数
      * @return 层模版列表
      */
-    // TODO: 临时移除权限验证,修复后恢复
-    // @RequirePermission("layer-template:read")
+    @RequirePermission("layer-template:read")
     @GetMapping("/list")
     public Result<Map<String, Object>> list(LayerTemplateQueryDTO queryDTO) {
         queryDTO.validate();
@@ -134,8 +133,7 @@ public class LayerTemplateController {
      * @param deviceId 设备ID
      * @return 同步结果
      */
-    // TODO: 临时移除权限验证,修复后恢复
-    // @RequirePermission("layer-template:update")
+    @RequirePermission("layer-template:update")
     @Log(module = "设备层模版管理", operation = OperationType.SYNC, summary = "同步层模版")
     @PostMapping("/{deviceId}/sync")
     public Result<Void> sync(@PathVariable String deviceId) {
@@ -148,8 +146,7 @@ public class LayerTemplateController {
      * @param deviceIds 设备ID列表
      * @return 批量同步结果
      */
-    // TODO: 临时移除权限验证,修复后恢复
-    // @RequirePermission("layer-template:update")
+    @RequirePermission("layer-template:update")
     @Log(module = "设备层模版管理", operation = OperationType.SYNC, summary = "批量同步层模版")
     @PostMapping("/sync/batch")
     public Result<Map<String, Object>> batchSync(@RequestBody List<String> deviceIds) {

+ 1 - 0
haha-admin/src/main/java/com/haha/admin/controller/MarketingActivityController.java

@@ -109,6 +109,7 @@ public class MarketingActivityController {
         return Result.success("查询成功", stats);
     }
 
+    @RequirePermission("marketing:activity:read")
     @GetMapping("/ongoing")
     public Result<List<MarketingActivity>> getOngoingActivities() {
         List<MarketingActivity> activities = activityService.getOngoingActivities();

+ 157 - 0
haha-admin/src/main/java/com/haha/admin/controller/RefundApplicationController.java

@@ -0,0 +1,157 @@
+package com.haha.admin.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.haha.admin.annotation.RequirePermission;
+import com.haha.common.annotation.Log;
+import com.haha.common.enums.OperationType;
+import com.haha.common.vo.PageResult;
+import com.haha.common.vo.RefundApplicationVO;
+import com.haha.common.vo.RefundProductVO;
+import com.haha.common.vo.Result;
+import com.haha.entity.Order;
+import com.haha.entity.OrderGoods;
+import com.haha.service.OrderGoodsService;
+import com.haha.service.OrderService;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 退款申请管理控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/refund")
+@RequiredArgsConstructor
+public class RefundApplicationController {
+
+    private final OrderService orderService;
+    private final OrderGoodsService orderGoodsService;
+
+    /**
+     * 分页查询退款申请列表
+     */
+    @RequirePermission("order:read")
+    @GetMapping("/list")
+    public Result<PageResult<RefundApplicationVO>> list(
+            @RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "10") int pageSize,
+            @RequestParam(required = false) String orderNo,
+            @RequestParam(required = false) Integer status,
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
+
+        if (page < 1) page = 1;
+        if (pageSize > 100) pageSize = 10;
+
+        LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<Order>()
+                .isNotNull(Order::getRefundStatus)
+                .eq(orderNo != null && !orderNo.isEmpty(), Order::getOrderNo, orderNo)
+                .ge(startDate != null && !startDate.isEmpty(), Order::getCreateTime, startDate)
+                .le(endDate != null && !endDate.isEmpty(), Order::getCreateTime, endDate + " 23:59:59");
+
+        // 状态筛选:0-待审核,1-已通过,2-已拒绝
+        if (status != null) {
+            if (status == 0) {
+                wrapper.and(w -> w.ne(Order::getRefundStatus, "REFUNDED")
+                        .ne(Order::getRefundStatus, "REJECTED"));
+            } else if (status == 1) {
+                wrapper.eq(Order::getRefundStatus, "REFUNDED");
+            } else if (status == 2) {
+                wrapper.eq(Order::getRefundStatus, "REJECTED");
+            }
+        }
+
+        wrapper.orderByDesc(Order::getCreateTime);
+
+        IPage<Order> orderPage = orderService.page(new Page<>(page, pageSize), wrapper);
+
+        return Result.success("查询成功", PageResult.of(orderPage, this::toRefundApplicationVO));
+    }
+
+    /**
+     * 获取退款申请详情
+     */
+    @RequirePermission("order:read")
+    @GetMapping("/{id}")
+    public Result<RefundApplicationVO> getDetail(@PathVariable Long id) {
+        Order order = orderService.getDetailById(id);
+        if (order == null) {
+            return Result.error(404, "退款申请不存在");
+        }
+        return Result.success("查询成功", toRefundApplicationVO(order));
+    }
+
+    /**
+     * 审核退款申请
+     */
+    @RequirePermission("order:update")
+    @Log(module = "退款管理", operation = OperationType.UPDATE, summary = "审核退款申请")
+    @PostMapping("/{id}/review")
+    public Result<Void> review(@PathVariable Long id, @RequestBody ReviewRefundDTO dto) {
+        Order order = orderService.getById(id);
+        if (order == null) {
+            return Result.error(404, "退款申请不存在");
+        }
+
+        order.setRefundStatus(dto.isApproved() ? "REFUNDED" : "REJECTED");
+        order.setRefundReason(dto.getRemark());
+        orderService.updateById(order);
+
+        log.info("退款申请审核完成 - orderId: {}, approved: {}, remark: {}", id, dto.isApproved(), dto.getRemark());
+        return Result.success(dto.isApproved() ? "退款申请已通过" : "退款申请已拒绝", null);
+    }
+
+    /**
+     * 将 Order 转换为 RefundApplicationVO
+     */
+    private RefundApplicationVO toRefundApplicationVO(Order order) {
+        RefundApplicationVO vo = new RefundApplicationVO();
+        vo.setId(order.getId());
+        vo.setApplicationNo("RF" + order.getOrderNo());
+        vo.setOrderId(order.getId());
+        vo.setOrderNo(order.getOrderNo());
+        vo.setUserId(order.getUserId());
+        vo.setRefundAmount(order.getRefundAmount() != null ? order.getRefundAmount() : order.getTotalAmount());
+        vo.setReason(order.getRefundReason() != null ? order.getRefundReason() : "用户申请退款");
+        vo.setCreateTime(order.getCreateTime());
+        vo.setReviewTime(order.getRefundTime());
+
+        // 映射退款状态:REFUNDED->1(已通过), REJECTED->2(已拒绝), 其他->0(待审核)
+        String refundStatus = order.getRefundStatus();
+        if ("REFUNDED".equals(refundStatus)) {
+            vo.setStatus(1);
+        } else if ("REJECTED".equals(refundStatus)) {
+            vo.setStatus(2);
+        } else {
+            vo.setStatus(0);
+        }
+
+        // 查询商品列表(包含图片)
+        List<OrderGoods> goodsList = orderGoodsService.getByOrderId(order.getId());
+        List<RefundProductVO> products = goodsList.stream().map(g -> {
+            RefundProductVO p = new RefundProductVO();
+            p.setProductId(g.getProductId());
+            p.setProductName(g.getProductName());
+            p.setPic(g.getPic());
+            p.setQuantity(g.getProductNum() != null ? g.getProductNum() : 1);
+            p.setPrice(g.getPrice());
+            return p;
+        }).collect(Collectors.toList());
+        vo.setRefundProducts(products);
+
+        return vo;
+    }
+
+    @Data
+    public static class ReviewRefundDTO {
+        private boolean approved;
+        private String remark;
+    }
+}

+ 1 - 0
haha-admin/src/main/java/com/haha/admin/controller/ShopController.java

@@ -88,6 +88,7 @@ public class ShopController {
      * 获取所有启用的门店(下拉选择用)
      * @return 门店列表
      */
+    @RequirePermission("shop:read")
     @GetMapping("/enabled")
     public Result<List<Map<String, Object>>> getEnabledShops() {
         List<Map<String, Object>> shops = shopService.getAllEnabledShops();

+ 1 - 0
haha-admin/src/main/java/com/haha/admin/controller/TimedDiscountController.java

@@ -139,6 +139,7 @@ public class TimedDiscountController {
         return Result.success("查询成功", PageResult.of(pageResult));
     }
 
+    @RequirePermission("timed-discount:read")
     @GetMapping("/enabled")
     public Result<List<TimedDiscountActivity>> getEnabledActivities() {
         List<TimedDiscountActivity> activities = timedDiscountService.getEnabledActivities();

+ 6 - 0
haha-admin/src/main/java/com/haha/admin/controller/UserController.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.haha.entity.dto.CreditScoreDTO;
 import com.haha.entity.dto.StatusDTO;
 import com.haha.entity.dto.UserQueryDTO;
+import com.haha.admin.annotation.RequirePermission;
 import com.haha.common.annotation.Log;
 import com.haha.common.enums.OperationType;
 import com.haha.common.vo.PageResult;
@@ -34,6 +35,7 @@ public class UserController {
      * @param params 查询参数
      * @return 客户列表
      */
+    @RequirePermission("user:read")
     @GetMapping("/list")
     public Result<PageResult<User>> list(UserQueryDTO queryDTO) {
         try {
@@ -58,6 +60,7 @@ public class UserController {
      * @param id 客户ID
      * @return 客户详情
      */
+    @RequirePermission("user:read")
     @GetMapping("/{id}")
     public Result<User> getById(@PathVariable Long id) {
         try {
@@ -77,6 +80,7 @@ public class UserController {
      * 获取客户统计数据
      * @return 统计数据
      */
+    @RequirePermission("user:read")
     @GetMapping("/statistics")
     public Result<Map<String, Object>> getStatistics() {
         try {
@@ -94,6 +98,7 @@ public class UserController {
      * @param params 状态参数
      * @return 操作结果
      */
+    @RequirePermission("user:update")
     @Log(module = "客户管理", operation = OperationType.UPDATE, summary = "更新客户状态")
     @PutMapping("/{id}/status")
     public Result<String> updateStatus(@PathVariable Long id, @RequestBody StatusDTO dto) {
@@ -119,6 +124,7 @@ public class UserController {
      * @param params 信用分参数
      * @return 操作结果
      */
+    @RequirePermission("user:update")
     @Log(module = "客户管理", operation = OperationType.UPDATE, summary = "更新客户信用分")
     @PutMapping("/{id}/credit")
     public Result<String> updateCreditScore(@PathVariable Long id, @RequestBody CreditScoreDTO dto) {

+ 1 - 7
haha-mapper/src/main/java/com/haha/mapper/RolePermissionMapper.java

@@ -20,13 +20,7 @@ public interface RolePermissionMapper extends BaseMapper<RolePermission> {
      */
     int deleteByRoleId(@Param("roleId") Long roleId);
 
-    /**
-     * 批量插入角色权限关联
-     * @param roleId 角色ID
-     * @param permissionIds 权限ID列表
-     * @return 插入数量
-     */
-    int batchInsert(@Param("roleId") Long roleId, @Param("permissionIds") List<Long> permissionIds);
+
 
     /**
      * 根据角色ID查询权限ID列表

+ 0 - 9
haha-mapper/src/main/resources/mapper/RolePermissionMapper.xml

@@ -16,15 +16,6 @@
         WHERE role_id = #{roleId}
     </delete>
 
-    <!-- 批量插入角色权限关联 -->
-    <insert id="batchInsert">
-        INSERT INTO t_role_permission (role_id, permission_id, create_time)
-        VALUES
-        <foreach collection="permissionIds" item="permissionId" separator=",">
-            (#{roleId}, #{permissionId}, NOW())
-        </foreach>
-    </insert>
-
     <!-- 根据角色ID查询权限ID列表 -->
     <select id="selectPermissionIdsByRoleId" resultType="java.lang.Long">
         SELECT permission_id

+ 2 - 0
haha-miniapp/src/main/resources/application.yml

@@ -49,6 +49,8 @@ mybatis-plus:
     map-underscore-to-camel-case: true
     log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
   global-config:
+    db-config:
+      id-type: ASSIGN_ID
     enable-sql-runner: false
 
 # PageHelper 分页插件配置

+ 29 - 0
haha-service/src/main/java/com/haha/service/DataPermissionService.java

@@ -64,4 +64,33 @@ public interface DataPermissionService extends IService<DataPermission> {
      * @return 是否存在
      */
     boolean existsByPermissionCode(String permissionCode);
+
+    /**
+     * 创建权限
+     * @param permission 权限信息
+     * @return 创建结果
+     */
+    Result<Void> createPermission(DataPermission permission);
+
+    /**
+     * 更新权限
+     * @param permission 权限信息
+     * @return 更新结果
+     */
+    Result<Void> updatePermission(DataPermission permission);
+
+    /**
+     * 删除权限
+     * @param id 权限ID
+     * @return 删除结果
+     */
+    Result<Void> deletePermission(Long id);
+
+    /**
+     * 切换权限状态
+     * @param id 权限ID
+     * @param status 状态: 1-启用, 0-禁用
+     * @return 更新结果
+     */
+    Result<Void> togglePermissionStatus(Long id, Integer status);
 }

+ 1 - 1
haha-service/src/main/java/com/haha/service/NewProductApplyService.java

@@ -17,7 +17,7 @@ public interface NewProductApplyService extends IService<NewProductApply> {
      *
      * @param page     页码
      * @param pageSize 每页条数
-     * @param params   查询参数
+     * @param queryDTO   查询参数
      * @return 分页结果
      */
     IPage<NewProductApply> getPage(int page, int pageSize, NewProductApplyQueryDTO queryDTO);

+ 89 - 0
haha-service/src/main/java/com/haha/service/impl/DataPermissionServiceImpl.java

@@ -9,6 +9,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -80,6 +81,8 @@ public class DataPermissionServiceImpl extends ServiceImpl<DataPermissionMapper,
                 Map<String, Object> permissionNode = new LinkedHashMap<>();
                 permissionNode.put("id", permission.getId());
                 permissionNode.put("name", permission.getPermissionName());
+                permissionNode.put("permissionCode", permission.getPermissionCode());
+                permissionNode.put("operationType", permission.getOperationType());
                 @SuppressWarnings("unchecked")
                 List<Map<String, Object>> children = (List<Map<String, Object>>) moduleMap.get(moduleCode).get("children");
                 children.add(permissionNode);
@@ -148,4 +151,90 @@ public class DataPermissionServiceImpl extends ServiceImpl<DataPermissionMapper,
         DataPermission permission = dataPermissionMapper.selectByPermissionCode(permissionCode);
         return permission != null && permission.getStatus() == 1;
     }
+
+    @Override
+    public Result<Void> createPermission(DataPermission permission) {
+        try {
+            // 检查权限编码是否已存在
+            DataPermission exist = dataPermissionMapper.selectByPermissionCode(permission.getPermissionCode());
+            if (exist != null) {
+                return Result.error(400, "权限编码已存在");
+            }
+
+            permission.setCreateTime(LocalDateTime.now());
+            permission.setUpdateTime(LocalDateTime.now());
+            if (permission.getStatus() == null) {
+                permission.setStatus(1);
+            }
+
+            boolean success = this.save(permission);
+            return success ? Result.success("创建成功", null) : Result.error(500, "创建失败");
+        } catch (Exception e) {
+            log.error("创建权限失败", e);
+            return Result.error(500, "创建失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public Result<Void> updatePermission(DataPermission permission) {
+        try {
+            DataPermission exist = this.getById(permission.getId());
+            if (exist == null) {
+                return Result.error(404, "权限不存在");
+            }
+
+            // 如果修改了编码,检查是否重复
+            if (!exist.getPermissionCode().equals(permission.getPermissionCode())) {
+                DataPermission check = dataPermissionMapper.selectByPermissionCode(permission.getPermissionCode());
+                if (check != null) {
+                    return Result.error(400, "权限编码已存在");
+                }
+            }
+
+            permission.setUpdateTime(LocalDateTime.now());
+            boolean success = this.updateById(permission);
+            return success ? Result.success("更新成功", null) : Result.error(500, "更新失败");
+        } catch (Exception e) {
+            log.error("更新权限失败", e);
+            return Result.error(500, "更新失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public Result<Void> deletePermission(Long id) {
+        try {
+            DataPermission exist = this.getById(id);
+            if (exist == null) {
+                return Result.error(404, "权限不存在");
+            }
+
+            boolean success = this.removeById(id);
+            return success ? Result.success("删除成功", null) : Result.error(500, "删除失败");
+        } catch (Exception e) {
+            log.error("删除权限失败, id={}", id, e);
+            return Result.error(500, "删除失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public Result<Void> togglePermissionStatus(Long id, Integer status) {
+        try {
+            DataPermission exist = this.getById(id);
+            if (exist == null) {
+                return Result.error(404, "权限不存在");
+            }
+
+            if (status != 0 && status != 1) {
+                return Result.error(400, "状态值无效");
+            }
+
+            exist.setStatus(status);
+            exist.setUpdateTime(LocalDateTime.now());
+            boolean success = this.updateById(exist);
+            return success ? Result.success("状态更新成功", null) : Result.error(500, "状态更新失败");
+        } catch (Exception e) {
+            log.error("切换权限状态失败, id={}", id, e);
+            return Result.error(500, "状态更新失败: " + e.getMessage());
+        }
+    }
 }

+ 8 - 1
haha-service/src/main/java/com/haha/service/impl/RoleServiceImpl.java

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.haha.common.vo.Result;
 import com.haha.entity.Role;
+import com.haha.entity.RolePermission;
 import com.haha.mapper.RoleMapper;
 import com.haha.mapper.RolePermissionMapper;
 import com.haha.service.RoleService;
@@ -179,7 +180,13 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
             
             // 如果有新权限,批量插入
             if (permissionIds != null && !permissionIds.isEmpty()) {
-                rolePermissionMapper.batchInsert(roleId, permissionIds);
+                for (Long permissionId : permissionIds) {
+                    RolePermission rp = new RolePermission();
+                    rp.setRoleId(roleId);
+                    rp.setPermissionId(permissionId);
+                    rp.setCreateTime(LocalDateTime.now());
+                    rolePermissionMapper.insert(rp);
+                }
             }
             
             return Result.success("权限配置成功", null);