Forráskód Böngészése

chore(@six/wisdom-legacy): 系统管理

cc12458 1 hete
szülő
commit
f28cebd293
31 módosított fájl, 3140 hozzáadás és 0 törlés
  1. 3 0
      apps/wisdom-legacy/.env
  2. 2 0
      apps/wisdom-legacy/@types/variate.d.ts
  3. 168 0
      apps/wisdom-legacy/openApi/system/addDept.openapi.json
  4. 180 0
      apps/wisdom-legacy/openApi/system/addMenu.openapi.json
  5. 167 0
      apps/wisdom-legacy/openApi/system/addRole.openapi.json
  6. 409 0
      apps/wisdom-legacy/openApi/system/addUser.openapi.json
  7. 88 0
      apps/wisdom-legacy/src/api/system/department.api.ts
  8. 175 0
      apps/wisdom-legacy/src/api/system/department.schema.ts
  9. 4 0
      apps/wisdom-legacy/src/api/system/index.ts
  10. 70 0
      apps/wisdom-legacy/src/api/system/menu.api.ts
  11. 330 0
      apps/wisdom-legacy/src/api/system/menu.schema.ts
  12. 76 0
      apps/wisdom-legacy/src/api/system/role.api.ts
  13. 204 0
      apps/wisdom-legacy/src/api/system/role.schema.ts
  14. 35 0
      apps/wisdom-legacy/src/api/system/user.api.ts
  15. 151 0
      apps/wisdom-legacy/src/api/system/user.schema.ts
  16. 33 0
      apps/wisdom-legacy/src/preinstall.ts
  17. 55 0
      apps/wisdom-legacy/src/tools/remove-key.ts
  18. 47 0
      apps/wisdom-legacy/src/views/system/DepartmentList.vue
  19. 64 0
      apps/wisdom-legacy/src/views/system/MenuList.vue
  20. 30 0
      apps/wisdom-legacy/src/views/system/RoleList.vue
  21. 46 0
      apps/wisdom-legacy/src/views/system/UserList.vue
  22. 24 0
      apps/wisdom-legacy/src/views/system/components/MenuBadge.vue
  23. 86 0
      apps/wisdom-legacy/src/views/system/department.data.ts
  24. 283 0
      apps/wisdom-legacy/src/views/system/menu.data.ts
  25. 22 0
      apps/wisdom-legacy/src/views/system/modules/DepartmentEdit.vue
  26. 22 0
      apps/wisdom-legacy/src/views/system/modules/MenuEdit.vue
  27. 72 0
      apps/wisdom-legacy/src/views/system/modules/RoleEdit.vue
  28. 22 0
      apps/wisdom-legacy/src/views/system/modules/UserEdit.vue
  29. 19 0
      apps/wisdom-legacy/src/views/system/modules/UserPassword.vue
  30. 92 0
      apps/wisdom-legacy/src/views/system/role.data.ts
  31. 161 0
      apps/wisdom-legacy/src/views/system/user.data.ts

+ 3 - 0
apps/wisdom-legacy/.env

@@ -12,3 +12,6 @@ VITE_GLOB_API_URL=/manager/
 
 # 接口转换
 VITE_GLOB_API_FIELDS={"successCode":200,"messageField":"msg"}
+
+# 传承系统根部门(机构) 由后端分配
+VITE_GLOB_SYS_DEPARTMENT_PID=4

+ 2 - 0
apps/wisdom-legacy/@types/variate.d.ts

@@ -7,6 +7,8 @@ declare module '@vben/types/global' {
       messageField?: string;
       successCode?: number | string;
     };
+
+    sys_department_pid?: string;
   }
 }
 

+ 168 - 0
apps/wisdom-legacy/openApi/system/addDept.openapi.json

@@ -0,0 +1,168 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "系统管理",
+    "description": "系统管理—相关接口文档",
+    "version": "1.0-SNAPSHOT"
+  },
+  "paths": {
+    "/dept": {
+      "post": {
+        "tags": ["部门管理API"],
+        "summary": "新增部门",
+        "operationId": "addDept",
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/DeptInfo"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "*/*": {
+                "schema": {
+                  "$ref": "#/components/schemas/AjaxResultLong"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "DeptInfo": {
+        "type": "object",
+        "properties": {
+          "deptId": {
+            "type": "integer",
+            "format": "int64",
+            "description": "部门ID"
+          },
+          "parentId": {
+            "type": "integer",
+            "format": "int64",
+            "description": "父部门ID"
+          },
+          "ancestors": {
+            "type": "string",
+            "description": "祖级列表"
+          },
+          "deptName": {
+            "type": "string",
+            "description": "部门名称",
+            "maxLength": 30,
+            "minLength": 0
+          },
+          "deptFullPathName": {
+            "type": "string",
+            "description": "部门名称(包含祖部门)"
+          },
+          "orderNum": {
+            "type": "integer",
+            "format": "int32",
+            "description": "显示顺序"
+          },
+          "leader": {
+            "type": "string",
+            "description": "负责人"
+          },
+          "phone": {
+            "type": "string",
+            "description": "联系电话",
+            "maxLength": 11,
+            "minLength": 0
+          },
+          "email": {
+            "type": "string",
+            "format": "email",
+            "description": "邮箱",
+            "maxLength": 50,
+            "minLength": 0
+          },
+          "status": {
+            "type": "string",
+            "description": "状态标志(详见字典:系统开关)",
+            "minLength": 1,
+            "pattern": "^[01]$"
+          },
+          "delFlag": {
+            "type": "string",
+            "description": "删除标志(详见字典:删除标志)"
+          },
+          "createIdBy": {
+            "type": "integer",
+            "format": "int64",
+            "description": "创建者ID"
+          },
+          "createBy": {
+            "type": "string",
+            "description": "创建者"
+          },
+          "createTime": {
+            "type": "string",
+            "format": "date-time",
+            "description": "创建时间"
+          },
+          "updateIdBy": {
+            "type": "integer",
+            "format": "int64",
+            "description": "更新者ID"
+          },
+          "updateBy": {
+            "type": "string",
+            "description": "更新者"
+          },
+          "updateTime": {
+            "type": "string",
+            "format": "date-time",
+            "description": "更新时间"
+          },
+          "remark": {
+            "type": "string",
+            "description": "备注"
+          },
+          "parentName": {
+            "type": "string",
+            "description": "父部门名称"
+          },
+          "children": {
+            "type": "array",
+            "description": "子部门",
+            "items": {
+              "$ref": "#/components/schemas/DeptInfo"
+            }
+          }
+        },
+        "required": ["deptName", "orderNum", "parentId", "status"]
+      },
+      "AjaxResultLong": {
+        "type": "object",
+        "description": "接口返回对象",
+        "properties": {
+          "code": {
+            "type": "integer",
+            "format": "int32",
+            "description": "状态码"
+          },
+          "msg": {
+            "type": "string",
+            "description": "提示语"
+          },
+          "data": {
+            "type": "integer",
+            "format": "int64",
+            "description": "数据对象"
+          }
+        }
+      }
+    }
+  }
+}

+ 180 - 0
apps/wisdom-legacy/openApi/system/addMenu.openapi.json

@@ -0,0 +1,180 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "系统管理",
+    "description": "系统管理—相关接口文档",
+    "version": "1.0-SNAPSHOT"
+  },
+  "paths": {
+    "/menu": {
+      "post": {
+        "tags": ["菜单管理API"],
+        "summary": "新增菜单",
+        "operationId": "add_2",
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/SysMenu"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "*/*": {
+                "schema": {
+                  "$ref": "#/components/schemas/AjaxResult"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "SysMenu": {
+        "type": "object",
+        "description": "菜单权限对象",
+        "properties": {
+          "createIdBy": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "createBy": {
+            "type": "string"
+          },
+          "createTime": {
+            "type": "string",
+            "format": "date-time"
+          },
+          "updateIdBy": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "updateBy": {
+            "type": "string"
+          },
+          "updateTime": {
+            "type": "string",
+            "format": "date-time"
+          },
+          "remark": {
+            "type": "string"
+          },
+          "params": {
+            "type": "object",
+            "additionalProperties": {}
+          },
+          "menuId": {
+            "type": "integer",
+            "format": "int64",
+            "description": "菜单ID"
+          },
+          "menuName": {
+            "type": "string",
+            "description": "菜单名称",
+            "maxLength": 50,
+            "minLength": 0
+          },
+          "parentName": {
+            "type": "string",
+            "description": "父菜单名称"
+          },
+          "parentId": {
+            "type": "integer",
+            "format": "int64",
+            "description": "父菜单ID"
+          },
+          "orderNum": {
+            "type": "integer",
+            "format": "int32",
+            "description": "显示顺序"
+          },
+          "path": {
+            "type": "string",
+            "description": "路由地址",
+            "maxLength": 200,
+            "minLength": 0
+          },
+          "component": {
+            "type": "string",
+            "description": "组件路径",
+            "maxLength": 200,
+            "minLength": 0
+          },
+          "query": {
+            "type": "string",
+            "description": "路由参数"
+          },
+          "routeName": {
+            "type": "string",
+            "description": "路由名称,默认和路由地址相同的驼峰格式"
+          },
+          "isFrame": {
+            "type": "string",
+            "description": "是否为外链(0是 1否)"
+          },
+          "isCache": {
+            "type": "string",
+            "description": "是否缓存(0缓存 1不缓存)"
+          },
+          "menuType": {
+            "type": "string",
+            "description": "类型(M目录 C菜单 F按钮)",
+            "minLength": 1
+          },
+          "visible": {
+            "type": "string",
+            "description": "显示状态(0显示 1隐藏)"
+          },
+          "status": {
+            "type": "string",
+            "description": "菜单状态(0正常 1停用)"
+          },
+          "perms": {
+            "type": "string",
+            "description": "权限字符串",
+            "maxLength": 100,
+            "minLength": 0
+          },
+          "icon": {
+            "type": "string",
+            "description": "菜单图标"
+          },
+          "children": {
+            "type": "array",
+            "description": "子菜单",
+            "items": {
+              "$ref": "#/components/schemas/SysMenu"
+            }
+          }
+        },
+        "required": ["menuName", "menuType", "orderNum"]
+      },
+      "AjaxResult": {
+        "type": "object",
+        "description": "接口返回对象",
+        "properties": {
+          "code": {
+            "type": "integer",
+            "format": "int32",
+            "description": "状态码"
+          },
+          "msg": {
+            "type": "string",
+            "description": "提示语"
+          },
+          "data": {
+            "description": "数据对象"
+          }
+        }
+      }
+    }
+  }
+}

+ 167 - 0
apps/wisdom-legacy/openApi/system/addRole.openapi.json

@@ -0,0 +1,167 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "系统管理",
+    "description": "系统管理—相关接口文档",
+    "version": "1.0-SNAPSHOT"
+  },
+  "paths": {
+    "/role": {
+      "post": {
+        "tags": ["角色管理API"],
+        "summary": "新增角色",
+        "operationId": "add_1",
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/SysRole"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "*/*": {
+                "schema": {
+                  "$ref": "#/components/schemas/AjaxResult"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "SysRole": {
+        "type": "object",
+        "description": "角色对象",
+        "properties": {
+          "createIdBy": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "createBy": {
+            "type": "string"
+          },
+          "createTime": {
+            "type": "string",
+            "format": "date-time"
+          },
+          "updateIdBy": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "updateBy": {
+            "type": "string"
+          },
+          "updateTime": {
+            "type": "string",
+            "format": "date-time"
+          },
+          "remark": {
+            "type": "string"
+          },
+          "params": {
+            "type": "object",
+            "additionalProperties": {}
+          },
+          "roleId": {
+            "type": "integer",
+            "format": "int64",
+            "description": "角色ID"
+          },
+          "roleName": {
+            "type": "string",
+            "description": "角色名称",
+            "maxLength": 30,
+            "minLength": 0
+          },
+          "roleKey": {
+            "type": "string",
+            "description": "角色权限",
+            "maxLength": 100,
+            "minLength": 0
+          },
+          "roleSort": {
+            "type": "integer",
+            "format": "int32",
+            "description": "角色排序"
+          },
+          "dataScope": {
+            "type": "string",
+            "description": "数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限)"
+          },
+          "menuCheckStrictly": {
+            "type": "boolean",
+            "description": "菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示)"
+          },
+          "deptCheckStrictly": {
+            "type": "boolean",
+            "description": "部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 )"
+          },
+          "status": {
+            "type": "string",
+            "description": "角色状态(0正常 1停用)"
+          },
+          "delFlag": {
+            "type": "string",
+            "description": "删除标志(0代表存在 2代表删除)"
+          },
+          "flag": {
+            "type": "boolean",
+            "description": "用户是否存在此角色标识 默认不存在"
+          },
+          "menuIds": {
+            "type": "array",
+            "description": "菜单组",
+            "items": {
+              "type": "integer",
+              "format": "int64"
+            }
+          },
+          "deptIds": {
+            "type": "array",
+            "description": "部门组(数据权限)",
+            "items": {
+              "type": "integer",
+              "format": "int64"
+            }
+          },
+          "permissions": {
+            "type": "array",
+            "description": "角色菜单权限",
+            "items": {
+              "type": "string"
+            },
+            "uniqueItems": true
+          }
+        },
+        "required": ["roleName"]
+      },
+      "AjaxResult": {
+        "type": "object",
+        "description": "接口返回对象",
+        "properties": {
+          "code": {
+            "type": "integer",
+            "format": "int32",
+            "description": "状态码"
+          },
+          "msg": {
+            "type": "string",
+            "description": "提示语"
+          },
+          "data": {
+            "description": "数据对象"
+          }
+        }
+      }
+    }
+  }
+}

+ 409 - 0
apps/wisdom-legacy/openApi/system/addUser.openapi.json

@@ -0,0 +1,409 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "系统管理",
+    "description": "系统管理—相关接口文档",
+    "version": "1.0-SNAPSHOT"
+  },
+  "paths": {
+    "/user": {
+      "post": {
+        "tags": ["用户管理API"],
+        "summary": "新增用户",
+        "operationId": "add",
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/SysUser"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "*/*": {
+                "schema": {
+                  "$ref": "#/components/schemas/AjaxResult"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "SysUser": {
+        "type": "object",
+        "properties": {
+          "createIdBy": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "createBy": {
+            "type": "string"
+          },
+          "createTime": {
+            "type": "string",
+            "format": "date-time"
+          },
+          "updateIdBy": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "updateBy": {
+            "type": "string"
+          },
+          "updateTime": {
+            "type": "string",
+            "format": "date-time"
+          },
+          "remark": {
+            "type": "string"
+          },
+          "params": {
+            "type": "object",
+            "additionalProperties": {}
+          },
+          "userId": {
+            "type": "integer",
+            "format": "int64",
+            "description": "用户ID"
+          },
+          "deptId": {
+            "type": "integer",
+            "format": "int64",
+            "description": "部门ID"
+          },
+          "userName": {
+            "type": "string",
+            "description": "用户账号",
+            "maxLength": 30,
+            "minLength": 0
+          },
+          "nickName": {
+            "type": "string",
+            "description": "用户昵称",
+            "maxLength": 30,
+            "minLength": 0
+          },
+          "userType": {
+            "type": "string",
+            "description": "用户类型(00系统用户)"
+          },
+          "email": {
+            "type": "string",
+            "format": "email",
+            "description": "用户邮箱",
+            "maxLength": 50,
+            "minLength": 0
+          },
+          "phonenumber": {
+            "type": "string",
+            "description": "手机号码",
+            "maxLength": 11,
+            "minLength": 0
+          },
+          "jobnumber": {
+            "type": "string",
+            "description": "工号",
+            "maxLength": 50,
+            "minLength": 0
+          },
+          "sex": {
+            "type": "string",
+            "description": "用户性别,0=男,1=女,2=未知"
+          },
+          "avatar": {
+            "type": "string",
+            "description": "用户头像"
+          },
+          "appletImg": {
+            "type": "string",
+            "description": "小程序码"
+          },
+          "password": {
+            "type": "string",
+            "description": "密码"
+          },
+          "status": {
+            "type": "string",
+            "description": "帐号状态(0正常 1停用)"
+          },
+          "delFlag": {
+            "type": "string",
+            "description": "删除标志(0代表存在 2代表删除)"
+          },
+          "loginIp": {
+            "type": "string",
+            "description": "最后登录IP"
+          },
+          "loginDate": {
+            "type": "string",
+            "format": "date-time",
+            "description": "最后登录时间"
+          },
+          "dept": {
+            "$ref": "#/components/schemas/DeptInfo",
+            "description": "部门对象"
+          },
+          "roles": {
+            "type": "array",
+            "description": "角色对象",
+            "items": {
+              "$ref": "#/components/schemas/SysRole"
+            }
+          },
+          "roleIds": {
+            "type": "array",
+            "description": "角色组",
+            "items": {
+              "type": "integer",
+              "format": "int64"
+            }
+          },
+          "roleId": {
+            "type": "integer",
+            "format": "int64",
+            "description": "角色ID"
+          }
+        },
+        "required": ["userName"]
+      },
+      "DeptInfo": {
+        "type": "object",
+        "properties": {
+          "deptId": {
+            "type": "integer",
+            "format": "int64",
+            "description": "部门ID"
+          },
+          "parentId": {
+            "type": "integer",
+            "format": "int64",
+            "description": "父部门ID"
+          },
+          "ancestors": {
+            "type": "string",
+            "description": "祖级列表"
+          },
+          "deptName": {
+            "type": "string",
+            "description": "部门名称",
+            "maxLength": 30,
+            "minLength": 0
+          },
+          "deptFullPathName": {
+            "type": "string",
+            "description": "部门名称(包含祖部门)"
+          },
+          "orderNum": {
+            "type": "integer",
+            "format": "int32",
+            "description": "显示顺序"
+          },
+          "leader": {
+            "type": "string",
+            "description": "负责人"
+          },
+          "phone": {
+            "type": "string",
+            "description": "联系电话",
+            "maxLength": 11,
+            "minLength": 0
+          },
+          "email": {
+            "type": "string",
+            "format": "email",
+            "description": "邮箱",
+            "maxLength": 50,
+            "minLength": 0
+          },
+          "status": {
+            "type": "string",
+            "description": "状态标志(详见字典:系统开关)",
+            "minLength": 1,
+            "pattern": "^[01]$"
+          },
+          "delFlag": {
+            "type": "string",
+            "description": "删除标志(详见字典:删除标志)"
+          },
+          "createIdBy": {
+            "type": "integer",
+            "format": "int64",
+            "description": "创建者ID"
+          },
+          "createBy": {
+            "type": "string",
+            "description": "创建者"
+          },
+          "createTime": {
+            "type": "string",
+            "format": "date-time",
+            "description": "创建时间"
+          },
+          "updateIdBy": {
+            "type": "integer",
+            "format": "int64",
+            "description": "更新者ID"
+          },
+          "updateBy": {
+            "type": "string",
+            "description": "更新者"
+          },
+          "updateTime": {
+            "type": "string",
+            "format": "date-time",
+            "description": "更新时间"
+          },
+          "remark": {
+            "type": "string",
+            "description": "备注"
+          },
+          "parentName": {
+            "type": "string",
+            "description": "父部门名称"
+          },
+          "children": {
+            "type": "array",
+            "description": "子部门",
+            "items": {
+              "$ref": "#/components/schemas/DeptInfo"
+            }
+          }
+        },
+        "required": ["deptName", "orderNum", "parentId", "status"]
+      },
+      "SysRole": {
+        "type": "object",
+        "description": "角色对象",
+        "properties": {
+          "createIdBy": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "createBy": {
+            "type": "string"
+          },
+          "createTime": {
+            "type": "string",
+            "format": "date-time"
+          },
+          "updateIdBy": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "updateBy": {
+            "type": "string"
+          },
+          "updateTime": {
+            "type": "string",
+            "format": "date-time"
+          },
+          "remark": {
+            "type": "string"
+          },
+          "params": {
+            "type": "object",
+            "additionalProperties": {}
+          },
+          "roleId": {
+            "type": "integer",
+            "format": "int64",
+            "description": "角色ID"
+          },
+          "roleName": {
+            "type": "string",
+            "description": "角色名称",
+            "maxLength": 30,
+            "minLength": 0
+          },
+          "roleKey": {
+            "type": "string",
+            "description": "角色权限",
+            "maxLength": 100,
+            "minLength": 0
+          },
+          "roleSort": {
+            "type": "integer",
+            "format": "int32",
+            "description": "角色排序"
+          },
+          "dataScope": {
+            "type": "string",
+            "description": "数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限)"
+          },
+          "menuCheckStrictly": {
+            "type": "boolean",
+            "description": "菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示)"
+          },
+          "deptCheckStrictly": {
+            "type": "boolean",
+            "description": "部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 )"
+          },
+          "status": {
+            "type": "string",
+            "description": "角色状态(0正常 1停用)"
+          },
+          "delFlag": {
+            "type": "string",
+            "description": "删除标志(0代表存在 2代表删除)"
+          },
+          "flag": {
+            "type": "boolean",
+            "description": "用户是否存在此角色标识 默认不存在"
+          },
+          "menuIds": {
+            "type": "array",
+            "description": "菜单组",
+            "items": {
+              "type": "integer",
+              "format": "int64"
+            }
+          },
+          "deptIds": {
+            "type": "array",
+            "description": "部门组(数据权限)",
+            "items": {
+              "type": "integer",
+              "format": "int64"
+            }
+          },
+          "permissions": {
+            "type": "array",
+            "description": "角色菜单权限",
+            "items": {
+              "type": "string"
+            },
+            "uniqueItems": true
+          }
+        },
+        "required": ["roleName"]
+      },
+      "AjaxResult": {
+        "type": "object",
+        "description": "接口返回对象",
+        "properties": {
+          "code": {
+            "type": "integer",
+            "format": "int32",
+            "description": "状态码"
+          },
+          "msg": {
+            "type": "string",
+            "description": "提示语"
+          },
+          "data": {
+            "description": "数据对象"
+          }
+        }
+      }
+    }
+  }
+}

+ 88 - 0
apps/wisdom-legacy/src/api/system/department.api.ts

@@ -0,0 +1,88 @@
+import type { DepartmentDTO, DepartmentVO } from './department.schema';
+
+import type { PageQueryMethodArgs, PageVO } from '#/request/schema';
+
+import { httpClient } from '#/request';
+import { decodeList, pageQueryArgsTransform } from '#/request/schema';
+import { buildTree } from '#/tools/tree';
+
+import {
+  decodeDepartment,
+  encodeDepartment,
+  encodeDepartmentQuery,
+  filterDepartmentsWhenSingleParent,
+} from './department.schema';
+
+export type { DepartmentVO } from './department.schema';
+export { DepartmentVOSchema } from './department.schema';
+
+export function listDepartmentMethod(...args: PageQueryMethodArgs) {
+  const { data } = pageQueryArgsTransform(args, encodeDepartmentQuery);
+  return httpClient.Post<PageVO<DepartmentVO>, DepartmentDTO[]>(
+    `/system/dept/list`,
+    data,
+    {
+      hitSource: /^department:(edit|delete)/,
+      params: data,
+      transform(data) {
+        const items = decodeList(
+          filterDepartmentsWhenSingleParent(data),
+          decodeDepartment,
+        );
+        return { total: items.length, items };
+      },
+    },
+  );
+}
+
+export function editDepartmentMethod(vo: DepartmentVO) {
+  const method = vo.id ? 'Put' : 'Post';
+  return httpClient[method]<{ id: string }, null | string>(
+    `/system/dept`,
+    encodeDepartment(vo),
+    {
+      name: 'department:edit',
+      transform(data) {
+        return data == null
+          ? { ...vo, id: vo.id ?? '/* [刷新] id 占位 */' }
+          : { id: data.toString() };
+      },
+    },
+  );
+}
+
+export function editDepartmentStatusMethod(vo: DepartmentVO) {
+  return httpClient.Put<null, null>(`/system/dept`, encodeDepartment(vo), {
+    name: 'department:edit:status',
+    meta: { ignoreError: true },
+  });
+}
+
+export function deleteDepartmentMethod(vo: Pick<DepartmentVO, 'id'>) {
+  return httpClient.Delete(
+    `/system/dept/${vo.id}`,
+    {},
+    {
+      name: 'department:delete',
+      meta: { ignoreError: true },
+    },
+  );
+}
+
+export function optionsDepartmentMethod() {
+  return httpClient.Post<DepartmentVO[], DepartmentDTO[]>(
+    `/system/dept/list`,
+    {},
+    {
+      hitSource: /^department:(edit|delete)/,
+      params: { tree: true },
+      transform(data) {
+        const list = decodeList(
+          filterDepartmentsWhenSingleParent(data),
+          decodeDepartment,
+        );
+        return buildTree(list, { getOrder: (node) => node.order ?? 0 });
+      },
+    },
+  );
+}

+ 175 - 0
apps/wisdom-legacy/src/api/system/department.schema.ts

@@ -0,0 +1,175 @@
+import type {
+  AuditRecordDTO,
+  AuditRecordVO,
+} from '#/request/schema/audit-record';
+
+import { $t } from '@vben/locales';
+
+import { z } from '#/adapter/form';
+import { globalVariate } from '#/preinstall';
+import { decodeAuditRecord } from '#/request/schema/audit-record';
+import {
+  decodeZeroFlag,
+  encodeZeroFlag,
+  encodeZeroFlagOptional,
+} from '#/request/schema/wire-flag';
+
+// ---------------------------------------------------------------------------
+// 常量
+// ---------------------------------------------------------------------------
+
+const { sys_department_pid } = globalVariate;
+
+export const defaultDepartmentPid = sys_department_pid?.toString() ?? '0';
+
+// ---------------------------------------------------------------------------
+// 类型 — DTO / VO
+// ---------------------------------------------------------------------------
+
+/** 部门 DTO,对应 OpenAPI `DeptInfo` */
+export interface DepartmentDTO extends AuditRecordDTO {
+  /** 部门 ID */
+  deptId?: number | string;
+  /** 部门名称 */
+  deptName: string;
+  /** 父部门 ID */
+  parentId: number | string;
+  /** 父部门名称 */
+  parentName?: string;
+  /** 子部门 */
+  children?: DepartmentDTO[];
+  /** 祖级列表 */
+  ancestors?: string;
+  /** 部门名称(包含祖部门) */
+  deptFullPathName?: string;
+  /** 邮箱 */
+  email?: string;
+  /** 负责人 */
+  leader?: string;
+  /** 联系电话 */
+  phone?: string;
+  /** 备注 */
+  remark?: string;
+  /**
+   * 状态标志
+   * - 0 启用
+   * - 1 禁用
+   */
+  status: '0' | '1';
+  /** 显示顺序 */
+  orderNum: number;
+  /** 部门编码 */
+  deptKey?: string;
+}
+
+/** 部门 VO */
+export interface DepartmentVO extends AuditRecordVO {
+  /** 父部门 ID,-> `parentId` */
+  pid?: string;
+  /** 部门 ID,-> `deptId` */
+  id?: string;
+  /** 部门名称,-> `deptName` */
+  name: string;
+  /** 部门编码,-> `deptKey` */
+  code?: string;
+  /** 子部门,-> `children` */
+  children?: DepartmentVO[];
+  /** 状态标志,-> `status` */
+  status: boolean;
+  /** 备注,-> `remark` */
+  remark?: string;
+  /** 显示顺序,-> `orderNum` */
+  order?: number;
+  /** 负责人,-> `leader` */
+  leader?: string;
+  /** 联系电话,-> `phone` */
+  phone?: string;
+  /** 邮箱,-> `email` */
+  email?: string;
+}
+
+// ---------------------------------------------------------------------------
+// Schema
+// ---------------------------------------------------------------------------
+
+export const DepartmentVOSchema = z.object({
+  pid: z.string().default(defaultDepartmentPid),
+  name: z
+    .string()
+    .min(1, `${$t('system.department.field.name')}不能为空`)
+    .max(30, `${$t('system.department.field.name')}不能超过30个字符`),
+  code: z
+    .string()
+    .min(1, `${$t('system.department.field.code')}不能为空`)
+    .max(100, `${$t('system.department.field.code')}不能超过50个字符`)
+    .optional(),
+  status: z.boolean().default(true),
+  remark: z
+    .string()
+    .max(256, $t('system.department.name', ['备注不能超过256个字符']))
+    .optional(),
+});
+
+// ---------------------------------------------------------------------------
+// 编解码
+// ---------------------------------------------------------------------------
+
+export const decodeDepartment = (dto: DepartmentDTO): DepartmentVO => {
+  return {
+    ...decodeAuditRecord(dto),
+    pid: dto.parentId?.toString(),
+    id: dto.deptId?.toString(),
+    name: dto.deptName,
+    code: dto.deptKey ?? dto.deptId?.toString(),
+    children: dto.children?.map((item) => decodeDepartment(item)),
+    status: decodeZeroFlag(dto.status),
+    remark: dto.remark,
+    order: dto.orderNum,
+    leader: dto.leader,
+    phone: dto.phone,
+    email: dto.email,
+  };
+};
+
+export const encodeDepartment = (vo: DepartmentVO): DepartmentDTO => {
+  return {
+    deptId: vo.id,
+    parentId: vo.pid ?? defaultDepartmentPid,
+    deptName: vo.name,
+    orderNum: vo.order ?? 1,
+    status: encodeZeroFlag(vo.status),
+    remark: vo.remark,
+  };
+};
+
+export const encodeDepartmentQuery = (
+  vo: Partial<DepartmentVO>,
+): Partial<DepartmentDTO> => {
+  return {
+    deptName: vo.name,
+    status: encodeZeroFlagOptional(vo.status),
+  };
+};
+
+// ---------------------------------------------------------------------------
+// 工具函数
+// ---------------------------------------------------------------------------
+
+/**
+ * 当列表中所有项的 `parentId` 相同(仅存在一个父级)时,
+ * 移除 `deptId` 等于传入 `parentId` 的项。
+ */
+export function filterDepartmentsWhenSingleParent(
+  items: DepartmentDTO[],
+  parentId: number | string = defaultDepartmentPid,
+): DepartmentDTO[] {
+  const TAG = '0';
+  if (items.length === 0 || parentId === TAG) return items;
+
+  const parentIds = new Set(items.map((item) => item.parentId?.toString()));
+  parentIds.delete(TAG);
+  if (parentIds.size !== 1) return items;
+
+  const parentKey = parentId.toString();
+  return items.filter((item) => item.deptId?.toString() !== parentKey);
+}

+ 4 - 0
apps/wisdom-legacy/src/api/system/index.ts

@@ -0,0 +1,4 @@
+export * from './department.api';
+export * from './menu.api';
+export * from './role.api';
+export * from './user.api';

+ 70 - 0
apps/wisdom-legacy/src/api/system/menu.api.ts

@@ -0,0 +1,70 @@
+import type { MenuDTO, MenuRecordVO } from './menu.schema';
+
+import type { PageQueryMethodArgs, PageVO } from '#/request/schema';
+
+import { httpClient } from '#/request';
+import { listPageTransform, pageQueryArgsTransform } from '#/request/schema';
+import { buildTree } from '#/tools/tree';
+
+import {
+  decodeMenuRecord,
+  encodeMenuQuery,
+  encodeMenuRecord,
+} from './menu.schema';
+
+export type { MenuRecordVO, MenuTypeVO } from './menu.schema';
+export { MenuTypeOptions, MenuVOSchema } from './menu.schema';
+
+export function listMenuMethod(...args: PageQueryMethodArgs) {
+  const { data } = pageQueryArgsTransform(args, encodeMenuQuery);
+  return httpClient.Get<PageVO<MenuRecordVO>, MenuDTO[]>(`/system/menu/list`, {
+    hitSource: /^menu:(edit|delete)/,
+    params: data,
+    transform: listPageTransform(decodeMenuRecord),
+  });
+}
+
+export function editMenuMethod(vo: MenuRecordVO) {
+  const method = vo.id ? 'Put' : 'Post';
+  return httpClient[method]<{ id: string }, null | string>(
+    `/system/menu`,
+    encodeMenuRecord(vo),
+    {
+      name: 'menu:edit',
+      transform(data) {
+        return data == null
+          ? { ...vo, id: vo.id ?? '/* 刷新占位符 */' }
+          : { id: data.toString() };
+      },
+    },
+  );
+}
+
+export function editMenuStatusMethod(vo: MenuRecordVO) {
+  return httpClient.Put<null, null>(`/system/menu`, encodeMenuRecord(vo), {
+    name: 'menu:edit:status',
+    meta: { ignoreError: true },
+  });
+}
+
+export function deleteMenuMethod(vo: Pick<MenuRecordVO, 'id'>) {
+  return httpClient.Delete(
+    `/system/menu/${vo.id}`,
+    {},
+    {
+      name: 'menu:delete',
+      meta: { ignoreError: true },
+    },
+  );
+}
+
+export function optionsMenuMethod() {
+  return httpClient.Get<MenuRecordVO[], MenuDTO[]>(`/system/menu/list`, {
+    hitSource: /^menu:(edit|delete)/,
+    params: { tree: true },
+    transform(data) {
+      const menus = data.map((item) => decodeMenuRecord(item));
+      return buildTree(menus, { getOrder: (node) => node.order ?? 0 });
+    },
+  });
+}

+ 330 - 0
apps/wisdom-legacy/src/api/system/menu.schema.ts

@@ -0,0 +1,330 @@
+import type {
+  AuditRecordDTO,
+  AuditRecordVO,
+} from '#/request/schema/audit-record';
+
+import { $t } from '@vben/locales';
+
+import { z } from '#/adapter/form';
+import { decodeAuditRecord } from '#/request/schema/audit-record';
+import {
+  decodeZeroFlag,
+  encodeZeroFlag,
+  encodeZeroFlagOptional,
+} from '#/request/schema/wire-flag';
+
+// ---------------------------------------------------------------------------
+// 字典 — 菜单类型
+// ---------------------------------------------------------------------------
+export const MenuTypeOptions = [
+  { label: '目录', value: 'catalog', color: 'purple' },
+  { label: '菜单', value: 'menu', color: 'blue' },
+  { label: '按钮', value: 'button', color: 'cyan' },
+  { label: '内嵌', value: 'embedded', color: 'red' },
+  { label: '外链', value: 'link', color: 'pink' },
+] as const;
+
+export type MenuTypeVO = (typeof MenuTypeOptions)[number]['value'];
+
+// ---------------------------------------------------------------------------
+// 类型 — DTO / VO
+// ---------------------------------------------------------------------------
+
+/** 菜单 DTO,对应 OpenAPI `SysMenu` */
+export interface MenuDTO extends AuditRecordDTO {
+  /** 菜单 ID */
+  menuId?: number | string;
+  /** 菜单名称 */
+  menuName: string;
+  /** 父菜单名称 */
+  parentName?: string;
+  /** 父菜单 ID */
+  parentId?: number | string;
+  /** 显示顺序 */
+  orderNum: number;
+  /** 路由地址 */
+  path?: string;
+  /** 组件路径 */
+  component?: string;
+  /** 路由参数 */
+  query?: string;
+  /** 路由名称 */
+  routeName?: string;
+  /** 是否为外链(0 是 1 否) */
+  isFrame?: '0' | '1';
+  /** 是否缓存(0 缓存 1 不缓存) */
+  isCache?: '0' | '1';
+  /** 类型(M 目录 C 菜单 F 按钮) */
+  menuType: 'C' | 'F' | 'M';
+  /** 显示状态(0 显示 1 隐藏) */
+  visible?: '0' | '1';
+  /**
+   * 菜单状态
+   * - 0 正常
+   * - 1 停用
+   */
+  status?: '0' | '1';
+  /** 权限字符串 */
+  perms?: string;
+  /** 菜单图标 */
+  icon?: string;
+  /** 子菜单 */
+  children?: MenuDTO[];
+  /** 备注 */
+  remark?: string;
+  /** 请求参数 */
+  params?: Record<string, unknown>;
+}
+
+/** @internal 菜单 VO 公共字段 */
+interface Menu {
+  /** 父菜单 ID,-> `parentId` */
+  pid?: string;
+  /** 菜单 ID,-> `menuId` */
+  id?: string;
+  /** 菜单名称,-> `routeName` */
+  name: string;
+  /** 路由地址,-> `path` */
+  path?: string;
+  /** 组件路径,-> `component` */
+  component?: string;
+  /** 重定向 */
+  redirect?: string;
+  /**
+   * 菜单类型,-> `menuType` -> `isFrame`
+   * @description
+   *  - catalog   -> M
+   *  - menu      -> C
+   *  - button    -> F
+   *  - embedded  -> F         1 (外链 1-否)
+   *  - link      -> F         0 (外链 0-是)
+   */
+  type: MenuTypeVO;
+  /** 权限字符串,-> `perms` */
+  authCode?: string;
+  /**
+   * Vben 框架创建的标记
+   */
+  isVben?: boolean;
+}
+
+/** 菜单 VO(路由 / 表单 meta 结构) */
+export interface MenuVO extends Menu {
+  meta: {
+    /** 菜单图标,-> `icon` */
+    icon?: string;
+    /** 内嵌 Iframe 的 URL,-> `path` */
+    iframeSrc?: string;
+    /** 是否缓存页面,-> `isCache` */
+    keepAlive?: boolean;
+    /** 外链页面的 URL,-> `path` */
+    link?: string;
+    /** 菜单排序,-> `orderNum` */
+    order?: number;
+    /** 路由参数,-> `query` */
+    query?: string;
+    /** 菜单标题,-> `menuName` */
+    title?: string;
+  };
+  /** 子菜单,-> `children` */
+  children?: MenuVO[];
+}
+
+/**
+ * 菜单列表 VO
+ *
+ * @remarks formSchema 无法稳定读取 `meta.*`,字段展平为顶层属性
+ */
+export interface MenuRecordVO extends AuditRecordVO, Menu {
+  /** 菜单标题,-> `menuName` */
+  title: string;
+  /** 菜单状态,-> `status` */
+  status: boolean;
+  /** 菜单排序,-> `orderNum` */
+  order?: number;
+  /** 备注,-> `remark` */
+  remark?: string;
+  /** 菜单图标,-> `icon` */
+  icon?: string;
+  /** 外链页面的 URL,-> `path` */
+  link?: string;
+  /** 是否缓存页面,-> `isCache` */
+  keepAlive?: boolean;
+  /** 子菜单,-> `children` */
+  children?: MenuRecordVO[];
+}
+
+// ---------------------------------------------------------------------------
+// Schema
+// ---------------------------------------------------------------------------
+
+export const MenuVOSchema = z.object({
+  name: z
+    .string()
+    .min(2, $t('ui.formRules.minLength', ['标题', 2]))
+    .max(30, $t('ui.formRules.maxLength', ['标题', 30])),
+  path: z
+    .string()
+    .min(2, $t('ui.formRules.minLength', ['路由地址', 2]))
+    .max(100, $t('ui.formRules.maxLength', ['路由地址', 100]))
+    .refine(
+      (value) => value.startsWith('/'),
+      $t('ui.formRules.startWith', ['路由地址', '/']),
+    ),
+});
+
+// ---------------------------------------------------------------------------
+// 编解码
+// ---------------------------------------------------------------------------
+
+export const decodeMenu = (dto: MenuDTO): MenuVO => {
+  const type = decodeMenuType(dto);
+  const path = isLinkMenuType(type) ? void 0 : dto.path;
+  return {
+    pid: dto.parentId?.toString(),
+    id: dto.menuId?.toString(),
+    name: dto.routeName || pathToRouteName(path) || dto.menuName,
+    type,
+    path,
+    component: dto.component,
+    authCode: dto.perms,
+    meta: {
+      icon: decodeMenuIcon(dto.icon),
+      order: dto.orderNum === 0 ? void 0 : dto.orderNum,
+      title: dto.menuName,
+      keepAlive: decodeZeroFlag(dto.isCache, true),
+      query: dto.query,
+      ...(type === 'link' ? { link: dto.path } : {}),
+      ...(type === 'embedded' ? { iframeSrc: dto.path } : {}),
+    },
+    children: dto.children?.map((item) => decodeMenu(item)),
+    isVben: dto.remark?.toLowerCase().includes('[vben]') ?? false,
+  };
+};
+
+export const decodeMenuRecord = (dto: MenuDTO): MenuRecordVO => {
+  const { meta, ...menu } = decodeMenu(dto);
+  let link: string | undefined;
+  if (menu.type === 'link') link = meta.link;
+  if (menu.type === 'embedded') link = meta.iframeSrc;
+  return {
+    ...decodeAuditRecord(dto),
+    ...menu,
+    ...meta,
+    link,
+    title: meta.title ?? menu.name,
+    status: decodeZeroFlag(dto.status, true),
+    remark: dto.remark,
+    children: dto.children?.map((item) => decodeMenuRecord(item)),
+  };
+};
+
+export const encodeMenuRecord = (vo: MenuRecordVO): MenuDTO => {
+  const { menuType, isFrame } = encodeMenuType(vo.type);
+  const isLink = isLinkMenuType(vo.type);
+  return {
+    parentId: vo.pid,
+    menuId: vo.id,
+    menuName: vo.title,
+    menuType,
+    isFrame,
+    perms: vo.authCode,
+    component: vo.component,
+    path: isLink ? vo.link : vo.path,
+    orderNum: vo.order ?? 0,
+    routeName: vo.name || pathToRouteName(isLink ? void 0 : vo.path),
+    status: encodeZeroFlag(vo.status, true),
+    isCache: encodeZeroFlag(vo.keepAlive, true),
+    icon: vo.icon,
+    remark: vo.remark,
+  };
+};
+
+export const encodeMenuQuery = (
+  vo: Partial<MenuRecordVO>,
+): Partial<MenuDTO> => {
+  return {
+    menuName: vo.title,
+    routeName: vo.name,
+    ...(vo.type == null ? {} : encodeMenuType(vo.type)),
+    status: encodeZeroFlagOptional(vo.status),
+  };
+};
+
+// ---------------------------------------------------------------------------
+// 内部工具
+// ---------------------------------------------------------------------------
+
+const HTTP_URL_RE = /^https?:\/\//i;
+
+/**
+ * @internal 路由 path → 路由名称(PascalCase 拼接)
+ *
+ * @example pathToRouteName('/system/dept') // => 'SystemDept'
+ * @example pathToRouteName('/zhcc/p/')     // => 'ZhccP'
+ */
+function pathToRouteName(path?: string): string | undefined {
+  if (!path?.trim() || !path.startsWith('/')) {
+    return undefined;
+  }
+
+  const pathname = path.split('?')[0]?.split('#')[0] ?? '';
+  const segments = pathname
+    .split('/')
+    .filter(Boolean)
+    .filter((segment) => !segment.startsWith(':'))
+    .map(
+      (segment) =>
+        segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase(),
+    );
+
+  return segments.length > 0 ? segments.join('') : undefined;
+}
+
+/** @internal DTO `menuType` + `isFrame` → VO `type` */
+function decodeMenuType(
+  dto: Pick<MenuDTO, 'component' | 'isFrame' | 'menuType' | 'path'>,
+): MenuTypeVO {
+  if (dto.menuType === 'M') return 'catalog';
+  if (dto.menuType === 'C') return 'menu';
+  if (dto.isFrame === '0') return 'link';
+
+  const path = dto.path ?? '';
+  if (HTTP_URL_RE.test(path)) return 'embedded';
+
+  const component = (dto.component ?? '').toLowerCase();
+  const isEmbedded =
+    component.includes('iframelayout') || component.includes('iframe');
+  return isEmbedded ? 'embedded' : 'button';
+}
+
+/** @internal VO `type` → DTO `menuType` + `isFrame` */
+function encodeMenuType(
+  type: MenuTypeVO,
+): Pick<MenuDTO, 'isFrame' | 'menuType'> {
+  switch (type) {
+    case 'button': {
+      return { menuType: 'F', isFrame: '1' };
+    }
+    case 'catalog': {
+      return { menuType: 'M', isFrame: '1' };
+    }
+    case 'embedded': {
+      return { menuType: 'F', isFrame: '1' };
+    }
+    case 'link': {
+      return { menuType: 'F', isFrame: '0' };
+    }
+    case 'menu': {
+      return { menuType: 'C', isFrame: '1' };
+    }
+  }
+}
+
+function isLinkMenuType(type: MenuTypeVO): boolean {
+  return type === 'link' || type === 'embedded';
+}
+
+function decodeMenuIcon(icon?: string): string | undefined {
+  return icon?.startsWith('#') ? undefined : icon;
+}

+ 76 - 0
apps/wisdom-legacy/src/api/system/role.api.ts

@@ -0,0 +1,76 @@
+import type { RoleVO } from './role.schema';
+
+import type { PageQueryMethodArgs } from '#/request/schema';
+
+import { getEnvelopeData, httpClient } from '#/request';
+import {
+  arrayTransform,
+  pageQueryArgsTransform,
+  paginateTransform,
+} from '#/request/schema';
+
+import { decodeRole, encodeRole, encodeRoleQuery } from './role.schema';
+
+export type { RoleVO } from './role.schema';
+export { dataScopeOptions, RoleVOSchema } from './role.schema';
+
+export function listRoleMethod(...args: PageQueryMethodArgs) {
+  const { params, data } = pageQueryArgsTransform(args, encodeRoleQuery);
+  return httpClient.Get(`/system/role/list`, {
+    hitSource: /^role:(edit|delete)/,
+    params: { ...params, ...data },
+    transform: paginateTransform(decodeRole),
+  });
+}
+
+export function editRoleMethod(vo: RoleVO) {
+  const method = vo.id ? 'Put' : 'Post';
+  return httpClient[method](`/system/role`, encodeRole(vo), {
+    name: `role:edit`,
+    transform(data) {
+      const payload = getEnvelopeData<null | string>(data);
+      return payload == null
+        ? { ...vo, id: vo.id ?? '/* 刷新占位符 */' }
+        : { id: String(payload) };
+    },
+  });
+}
+export function editRoleStatusMethod(vo: RoleVO) {
+  const { roleId, status } = encodeRole(vo);
+  return httpClient.Put(
+    `/system/role/changeStatus`,
+    { roleId, status },
+    {
+      name: `role:edit:status`,
+      meta: { ignoreError: true },
+    },
+  );
+}
+
+export function deleteRoleMethod(vo: Pick<RoleVO, 'id'>) {
+  return httpClient.Delete(
+    `/system/role/${vo.id}`,
+    {},
+    {
+      name: 'role:delete',
+      meta: { ignoreError: true },
+    },
+  );
+}
+
+export function optionsRoleMethod() {
+  return httpClient.Get(`/system/role/optionselect`, {
+    hitSource: /^role:(edit|delete)/,
+    transform: arrayTransform(decodeRole),
+  });
+}
+
+export function getRoleMenuMethod(vo: Partial<RoleVO>) {
+  return httpClient.Get(`/system/menu/roleMenuTreeselect/${vo.id}`, {
+    hitSource: /^role:(edit|delete)/,
+    transform(data: { checkedKeys?: string[] }) {
+      vo.menuIds = data?.checkedKeys?.map((value) => value.toString()) ?? [];
+      return vo;
+    },
+  });
+}

+ 204 - 0
apps/wisdom-legacy/src/api/system/role.schema.ts

@@ -0,0 +1,204 @@
+import type {
+  AuditRecordDTO,
+  AuditRecordVO,
+} from '#/request/schema/audit-record';
+
+import { $t } from '@vben/locales';
+
+import { z } from '#/adapter/form';
+import { decodeAuditRecord } from '#/request/schema/audit-record';
+import {
+  decodeZeroFlag,
+  encodeZeroFlag,
+  encodeZeroFlagOptional,
+} from '#/request/schema/wire-flag';
+
+// ---------------------------------------------------------------------------
+// 常量
+// ---------------------------------------------------------------------------
+
+/** 数据范围默认:本部门及以下数据权限 */
+const DEFAULT_DATA_SCOPE = '4';
+
+/**
+ * 树选择项默认:
+ * - true  父子节点选中状态不再关联
+ * - false 父子互相关联显示
+ */
+const DEFAULT_CHECK_STRICTLY = false;
+
+// ---------------------------------------------------------------------------
+// 字典 — 数据权限
+// ---------------------------------------------------------------------------
+export const dataScopeOptions = [
+  { label: '所有', value: '1' },
+  { label: `本${$t('system.department.name')}`, value: '3' },
+  { label: `本${$t('system.department.name')}及以下`, value: '4' },
+  { label: '仅本人', value: '5' },
+] as const;
+
+// ---------------------------------------------------------------------------
+// 类型 — DTO / VO
+// ---------------------------------------------------------------------------
+
+/** 角色 DTO,对应 OpenAPI `SysRole` */
+export interface RoleDTO extends AuditRecordDTO {
+  /** 角色 ID */
+  roleId?: number | string;
+  /** 角色名称 */
+  roleName: string;
+  /** 角色权限 / 编码 */
+  roleKey?: string;
+  /** 角色排序 */
+  roleSort?: number;
+  /**
+   * 数据范围
+   * - 1 所有数据权限
+   * - 2 自定义数据权限
+   * - 3 本部门数据权限
+   * - 4 本部门及以下数据权限
+   * - 5 仅本人数据权限
+   */
+  dataScope?: string;
+  /** 菜单树选择项是否关联显示 */
+  menuCheckStrictly?: boolean;
+  /** 部门树选择项是否关联显示 */
+  deptCheckStrictly?: boolean;
+  /**
+   * 角色状态
+   * - 0 正常
+   * - 1 停用
+   */
+  status?: '0' | '1';
+  /** 删除标志 */
+  delFlag?: string;
+  /** 用户是否存在此角色标识 */
+  flag?: boolean;
+  /** 菜单组 */
+  menuIds?: (number | string)[];
+  /** 部门组(数据权限) */
+  deptIds?: (number | string)[];
+  /** 角色菜单权限 */
+  permissions?: string[];
+  /** 备注 */
+  remark?: string;
+  /** 请求参数 */
+  params?: Record<string, unknown>;
+}
+
+/** 角色 VO */
+export interface RoleVO extends AuditRecordVO {
+  /** 角色 ID,-> `roleId` */
+  id?: string;
+  /** 角色名称,-> `roleName` */
+  name: string;
+  /** 角色权限 / 编码,-> `roleKey` */
+  code: string;
+  /** 角色排序,-> `roleSort` */
+  order?: number;
+  /** 数据范围,-> `dataScope` */
+  dataScope?: string;
+  /**
+   * 菜单树选择项是否关联显示,-> `menuCheckStrictly`
+   * @remarks 暂不启用(透传){@link DEFAULT_CHECK_STRICTLY }
+   */
+  menuCheckStrictly?: boolean;
+  /**
+   * 部门树选择项是否关联显示,-> `deptCheckStrictly`
+   * @remarks 暂不启用(透传){@link DEFAULT_CHECK_STRICTLY }
+   */
+  deptCheckStrictly?: boolean;
+  /** 角色状态,-> `status` */
+  status: boolean;
+  /** 备注,-> `remark` */
+  remark?: string;
+  /** 菜单组,-> `menuIds` */
+  menuIds?: string[];
+  /** 部门组(数据权限),-> `deptIds` */
+  deptIds?: string[];
+  /** 角色菜单权限,-> `permissions` */
+  permissions?: string[];
+  /** 用户是否存在此角色标识,-> `flag` */
+  flag?: boolean;
+}
+
+// ---------------------------------------------------------------------------
+// Schema
+// ---------------------------------------------------------------------------
+
+export const RoleVOSchema = z.object({
+  id: z.string().optional(),
+  name: z
+    .string()
+    .min(1, `${$t('system.role.field.name')}不能为空`)
+    .max(30, `${$t('system.role.field.name')}不能超过30个字符`),
+  code: z
+    .string()
+    .min(1, `${$t('system.role.field.code')}不能为空`)
+    .max(100, `${$t('system.role.field.code')}不能超过100个字符`),
+  order: z.number().int('角色排序必须为整数').optional(),
+  dataScope: z.string().default(DEFAULT_DATA_SCOPE),
+  menuCheckStrictly: z.boolean().default(DEFAULT_CHECK_STRICTLY),
+  deptCheckStrictly: z.boolean().default(DEFAULT_CHECK_STRICTLY),
+  status: z.boolean().default(true),
+  remark: z.string().max(256, '角色备注不能超过256个字符').optional(),
+  menuIds: z.array(z.coerce.string()).optional(),
+  deptIds: z.array(z.coerce.string()).optional(),
+  permissions: z.array(z.coerce.string()).optional(),
+  flag: z.boolean().optional(),
+});
+
+// ---------------------------------------------------------------------------
+// 编解码
+// ---------------------------------------------------------------------------
+
+export const decodeRole = (dto: RoleDTO): RoleVO => {
+  return {
+    ...decodeAuditRecord(dto),
+    id: dto.roleId?.toString(),
+    name: dto.roleName,
+    code: dto.roleKey ?? '',
+    order: dto.roleSort,
+    dataScope: dto.dataScope,
+    menuCheckStrictly: dto.menuCheckStrictly ?? DEFAULT_CHECK_STRICTLY,
+    deptCheckStrictly: dto.deptCheckStrictly ?? DEFAULT_CHECK_STRICTLY,
+    status: decodeZeroFlag(dto.status),
+    remark: dto.remark,
+    menuIds: dto.menuIds?.map((v) => v.toString()) ?? [],
+    deptIds: dto.deptIds?.map((v) => v.toString()) ?? [],
+    flag: dto.flag,
+  };
+};
+
+export const encodeRole = (vo: RoleVO): RoleDTO => {
+  return {
+    roleId: vo.id,
+    roleName: vo.name,
+    roleKey: vo.code,
+    roleSort: vo.order ?? 0,
+    dataScope: vo.dataScope ?? DEFAULT_DATA_SCOPE,
+    menuCheckStrictly: vo.menuCheckStrictly ?? DEFAULT_CHECK_STRICTLY,
+    status: encodeZeroFlag(vo.status, true),
+    remark: vo.remark,
+    menuIds: vo.menuIds ?? [],
+  };
+};
+
+export const encodeRoleDataPermission = (
+  vo: RoleVO,
+): Pick<RoleDTO, 'dataScope' | 'deptCheckStrictly' | 'deptIds' | 'roleId'> => {
+  return {
+    roleId: vo.id,
+    dataScope: vo.dataScope ?? DEFAULT_DATA_SCOPE,
+    deptCheckStrictly: vo.deptCheckStrictly ?? DEFAULT_CHECK_STRICTLY,
+    deptIds: vo.deptIds ?? [],
+  };
+};
+
+export const encodeRoleQuery = (vo: Partial<RoleVO>): Partial<RoleDTO> => {
+  return {
+    roleName: vo.name,
+    roleKey: vo.code,
+    status: encodeZeroFlagOptional(vo.status),
+  };
+};

+ 35 - 0
apps/wisdom-legacy/src/api/system/user.api.ts

@@ -0,0 +1,35 @@
+import type { UserDTO, UserVO } from './user.schema';
+
+import type { PageQueryMethodArgs } from '#/request/schema';
+
+import { httpClient } from '#/request';
+import { pageQueryArgsTransform, paginateTransform } from '#/request/schema';
+
+import { decodeUser, encodeUser, encodeUserQuery } from './user.schema';
+
+export type { UserVO } from './user.schema';
+export { encodeUserRoles } from './user.schema';
+
+export function listUserMethod(...args: PageQueryMethodArgs) {
+  const { params, data } = pageQueryArgsTransform(args, encodeUserQuery);
+  return httpClient.Get(`/system/user/list`, {
+    hitSource: /^user:(edit|delete)/,
+    params: { ...params, ...data },
+    transform: paginateTransform(decodeUser),
+  });
+}
+
+export function editUserMethod(vo: UserVO) {
+  const method = vo.id ? 'Put' : 'Post';
+  return httpClient[method](`/system/user`, encodeUser(vo), {
+    name: `user:edit`,
+  });
+}
+
+export function editUserPassword(vo: UserVO & { password: string }) {
+  return httpClient.Put<null, null>(`/system/user/resetPwd`, {
+    userId: vo.id,
+    userName: vo.userName,
+    password: vo.password,
+  } as UserDTO);
+}

+ 151 - 0
apps/wisdom-legacy/src/api/system/user.schema.ts

@@ -0,0 +1,151 @@
+import type { DepartmentDTO, DepartmentVO } from './department.schema';
+import type { RoleDTO, RoleVO } from './role.schema';
+
+import type {
+  AuditRecordDTO,
+  AuditRecordVO,
+} from '#/request/schema/audit-record';
+
+import { decodeAuditRecord } from '#/request/schema/audit-record';
+import {
+  decodeZeroFlag,
+  encodeZeroFlag,
+  encodeZeroFlagOptional,
+} from '#/request/schema/wire-flag';
+
+import { decodeDepartment } from './department.schema';
+import { decodeRole } from './role.schema';
+
+// ---------------------------------------------------------------------------
+// 类型 — DTO / VO
+// ---------------------------------------------------------------------------
+
+/** 用户 DTO,对应 OpenAPI `SysUser` */
+export interface UserDTO extends AuditRecordDTO {
+  /** 用户 ID */
+  userId?: number | string;
+  /** 用户账号 */
+  userName: string;
+  /** 用户昵称 */
+  nickName?: string;
+  /** 手机号码 */
+  phonenumber?: string;
+  /** 头像地址 */
+  avatar?: string;
+  /** 密码 */
+  password?: string;
+  /**
+   * 用户状态
+   * - 0 正常
+   * - 1 停用
+   */
+  status?: '0' | '1';
+  /** 部门 ID */
+  deptId?: string;
+  /** 部门对象 */
+  dept?: DepartmentDTO;
+  /** 角色列表 */
+  roles?: RoleDTO[];
+  /**
+   * 角色 ID 集合
+   */
+  roleIds?: (number | string)[];
+}
+
+/** 用户 VO */
+export interface UserVO extends AuditRecordVO {
+  /** 用户 ID,-> `userId` */
+  id?: string;
+  /** 用户账号,-> `userName` */
+  userName: string;
+  /**
+   * 用户密码,-> `password`
+   * @remarks 仅创建时有效
+   */
+  password?: string;
+  /** 用户昵称,-> `nickName` */
+  name: string;
+  /** 头像地址,-> `avatar` */
+  avatar?: string;
+  /** 用户状态,-> `status` */
+  status: boolean;
+  /** 手机号码,-> `phonenumber` */
+  phone?: string;
+  /** 部门,-> `dept` / `deptId` */
+  department?: DepartmentVO;
+  /** 角色列表,-> `roles` */
+  roles: RoleVO[];
+}
+
+// ---------------------------------------------------------------------------
+// 编解码
+// ---------------------------------------------------------------------------
+
+export const decodeUser = (dto: UserDTO): UserVO => {
+  return {
+    ...decodeAuditRecord(dto),
+    id: dto.userId?.toString(),
+    userName: dto.userName,
+    name: dto.nickName ?? '',
+    avatar: dto.avatar,
+    phone: dto.phonenumber,
+    status: decodeZeroFlag(dto.status),
+    department: decodeUserDepartment(dto.dept, dto.deptId),
+    roles: decodeUserRoles(dto.roles),
+  };
+};
+
+export const encodeUser = (vo: UserVO): UserDTO => {
+  const dto: UserDTO = {
+    userId: vo.id,
+    userName: vo.userName,
+    nickName: vo.name,
+    avatar: vo.avatar,
+    phonenumber: vo.phone,
+    status: encodeZeroFlag(vo.status),
+    deptId: vo.department?.id,
+    roleIds: encodeUserRoles(vo.roles),
+  };
+  if (!vo.id) dto.password = vo.password;
+  return dto;
+};
+
+export const encodeUserQuery = (vo: Partial<UserVO>): Partial<UserDTO> => {
+  return {
+    ...encodeUser(vo as UserVO),
+    status: encodeZeroFlagOptional(vo.status),
+  };
+};
+
+export function encodeUserRoles(roles?: (RoleVO | string)[]): string[] {
+  if (!Array.isArray(roles)) return [];
+  return roles.map((role) => {
+    if (typeof role === 'string') return role;
+    return role.id as string;
+  });
+}
+
+// ---------------------------------------------------------------------------
+// 内部工具
+// ---------------------------------------------------------------------------
+
+function decodeUserRoles(roles?: (RoleDTO | string)[]): RoleVO[] {
+  if (!Array.isArray(roles)) return [];
+  return roles.map((role) => {
+    if (typeof role === 'string') return { id: role } as RoleVO;
+    return decodeRole(role);
+  });
+}
+
+function decodeUserDepartment(
+  dept?: DepartmentDTO,
+  deptId?: string,
+): DepartmentVO | undefined {
+  if (dept != null) {
+    return decodeDepartment(dept);
+  }
+  if (deptId != null) {
+    return { id: deptId.toString() } as DepartmentVO;
+  }
+  return undefined;
+}

+ 33 - 0
apps/wisdom-legacy/src/preinstall.ts

@@ -0,0 +1,33 @@
+import type { ApplicationConfig } from '@vben/types/global';
+
+import { useAppConfig } from '@vben/hooks';
+
+const parse = (value: unknown) => {
+  try {
+    return JSON.parse(value as string);
+  } catch {
+    return value;
+  }
+};
+
+function load(tag = 'VITE_GLOB_'): Readonly<ApplicationConfig> {
+  const appConfig: ApplicationConfig = useAppConfig(
+    import.meta.env,
+    import.meta.env.PROD,
+  );
+
+  const config = import.meta.env.PROD
+    ? window._VBEN_ADMIN_PRO_APP_CONF_
+    : import.meta.env;
+
+  for (const [key, value] of Object.entries(config)) {
+    if (!key.includes(tag)) continue;
+
+    const k = key.slice(tag.length).toLowerCase();
+    (appConfig as any)[k] = parse(value);
+  }
+
+  return Object.freeze(appConfig);
+}
+
+export const globalVariate = load();

+ 55 - 0
apps/wisdom-legacy/src/tools/remove-key.ts

@@ -0,0 +1,55 @@
+/** 链式移除后续键:(key, when?) => chain */
+export type RemoveKeyChain<T extends Recordable> = <K extends keyof T>(
+  key: K,
+  when?: (value: T[K], key: K, obj: T) => boolean,
+) => RemoveKeyChain<T>;
+
+function removeKeyAt<T extends Recordable, K extends keyof T>(
+  obj: T,
+  key: K,
+  when?: (value: T[K], key: K, obj: T) => boolean,
+): void {
+  if (!Object.hasOwn(obj, key)) {
+    return;
+  }
+
+  if (when && !when(obj[key], key, obj)) {
+    return;
+  }
+
+  Reflect.deleteProperty(obj, key);
+}
+
+function createRemoveKeyChain<T extends Recordable>(obj: T): RemoveKeyChain<T> {
+  const chain = (<K extends keyof T>(
+    key: K,
+    when?: (value: T[K], key: K, obj: T) => boolean,
+  ) => {
+    removeKeyAt(obj, key, when);
+    return chain;
+  }) as RemoveKeyChain<T>;
+
+  return chain;
+}
+
+/**
+ * 从对象移除指定键,并返回链式函数以继续移除其他键。
+ *
+ * 传入 `when` 时,仅在其返回 `true` 时移除。
+ * 原地修改 `obj`;链式调用同样作用于该对象。
+ *
+ * @example
+ * ```ts
+ * removeKey(data, 'status');
+ *
+ * removeKey(data, 'pid', (value) => value === '0')('remark', (v) => v === '');
+ * ```
+ */
+export function removeKey<T extends Recordable, K extends keyof T>(
+  obj: T,
+  key: K,
+  when?: (value: T[K], key: K, obj: T) => boolean,
+): RemoveKeyChain<T> {
+  removeKeyAt(obj, key, when);
+  return createRemoveKeyChain(obj);
+}

+ 47 - 0
apps/wisdom-legacy/src/views/system/DepartmentList.vue

@@ -0,0 +1,47 @@
+<script setup lang="ts">
+import { Page } from '@vben/common-ui';
+import { IconifyIcon, Plus } from '@vben/icons';
+
+import { LoadingOutlined } from '@ant-design/icons-vue';
+
+import { editModal, useGridPage } from '#/adapter/vxe-table';
+import {
+  deleteDepartmentMethod,
+  editDepartmentStatusMethod,
+} from '#/api/system';
+
+import { departmentGrid } from './department.data';
+import DepartmentEdit from './modules/DepartmentEdit.vue';
+
+const { Grid, Edit, scope, actions, tree } = useGridPage(departmentGrid, {
+  edit: editModal(DepartmentEdit),
+  append: true,
+  delete: deleteDepartmentMethod,
+  status: editDepartmentStatusMethod,
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <Edit />
+    <Grid>
+      <template #toolbar-actions>
+        <button
+          v-if="tree"
+          title="展开/收起"
+          class="vxe-button type--button size--small is--circle ml-2"
+          @click="tree.toggle()"
+        >
+          <LoadingOutlined v-if="tree.loading" />
+          <IconifyIcon v-else icon="carbon:arrows-vertical" />
+        </button>
+      </template>
+      <template #toolbar-tools>
+        <a-button type="primary" @click="actions.create()">
+          <Plus class="size-5" />
+          {{ scope.createText }}
+        </a-button>
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 64 - 0
apps/wisdom-legacy/src/views/system/MenuList.vue

@@ -0,0 +1,64 @@
+<script setup lang="ts">
+import { Page } from '@vben/common-ui';
+import { IconifyIcon, Plus } from '@vben/icons';
+import { $t } from '@vben/locales';
+
+import { LoadingOutlined } from '@ant-design/icons-vue';
+
+import { editDrawer, useGridPage } from '#/adapter/vxe-table';
+import { deleteMenuMethod, editMenuStatusMethod } from '#/api/system';
+
+import MenuBadge from './components/MenuBadge.vue';
+import { menuGrid } from './menu.data';
+import MenuEdit from './modules/MenuEdit.vue';
+
+const { Grid, Edit, scope, actions, tree } = useGridPage(menuGrid, {
+  edit: editDrawer(MenuEdit),
+  append: true,
+  delete: deleteMenuMethod,
+  status: editMenuStatusMethod,
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <Edit />
+    <Grid>
+      <template #toolbar-actions>
+        <button
+          v-if="tree"
+          title="展开/收起"
+          class="vxe-button type--button size--small is--circle ml-2"
+          @click="tree.toggle()"
+        >
+          <LoadingOutlined v-if="tree.loading" />
+          <IconifyIcon v-else icon="carbon:arrows-vertical" />
+        </button>
+      </template>
+      <template #toolbar-tools>
+        <a-button type="primary" @click="actions.create()">
+          <Plus class="size-5" />
+          {{ scope.createText }}
+        </a-button>
+      </template>
+      <template #title="{ row }">
+        <div class="flex w-full items-center gap-1">
+          <div class="size-5 shrink-0">
+            <IconifyIcon
+              v-if="row.type === 'button'"
+              icon="carbon:security"
+              class="size-full"
+            />
+            <IconifyIcon
+              v-else-if="row?.icon"
+              :icon="row?.icon || 'carbon:circle-dash'"
+              class="size-full"
+            />
+          </div>
+          <span class="flex-auto">{{ $t(row?.title) }}</span>
+          <MenuBadge v-if="row.isVben" />
+        </div>
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 30 - 0
apps/wisdom-legacy/src/views/system/RoleList.vue

@@ -0,0 +1,30 @@
+<script setup lang="ts">
+import { Page } from '@vben/common-ui';
+import { Plus } from '@vben/icons';
+
+import { editDrawer, useGridPage } from '#/adapter/vxe-table';
+import { deleteRoleMethod, editRoleStatusMethod } from '#/api/system';
+
+import RoleEdit from './modules/RoleEdit.vue';
+import { roleGrid } from './role.data';
+
+const { Grid, Edit, scope, actions } = useGridPage(roleGrid, {
+  edit: editDrawer(RoleEdit),
+  delete: deleteRoleMethod,
+  status: editRoleStatusMethod,
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <Edit />
+    <Grid>
+      <template #toolbar-tools>
+        <a-button type="primary" @click="actions.create()">
+          <Plus class="size-5" />
+          {{ scope.createText }}
+        </a-button>
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 46 - 0
apps/wisdom-legacy/src/views/system/UserList.vue

@@ -0,0 +1,46 @@
+<script setup lang="ts">
+import type { UserVO } from '#/api/system';
+
+import { Page } from '@vben/common-ui';
+import { Plus } from '@vben/icons';
+
+import { useShell } from '#/adapter/shell';
+import { editModal, useGridPage } from '#/adapter/vxe-table';
+
+import UserEdit from './modules/UserEdit.vue';
+import UserPassword from './modules/UserPassword.vue';
+import { userGrid } from './user.data';
+
+const [Password, passwordApi] = useShell('modal', {
+  title: '重置密码',
+  connectedComponent: UserPassword,
+});
+
+const { Grid, Edit, scope, actions } = useGridPage(userGrid, {
+  edit: editModal(UserEdit),
+  rest: (row: UserVO) => {
+    passwordApi
+      .setData({
+        id: row.id,
+        userName: row.userName,
+      } satisfies Partial<UserVO>)
+      .open();
+    return void 0;
+  },
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <Password />
+    <Edit />
+    <Grid>
+      <template #toolbar-tools>
+        <a-button type="primary" @click="actions.create()">
+          <Plus class="size-5" />
+          {{ scope.createText }}
+        </a-button>
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 24 - 0
apps/wisdom-legacy/src/views/system/components/MenuBadge.vue

@@ -0,0 +1,24 @@
+<script setup lang="ts">
+defineOptions({ name: 'MenuBadge' });
+</script>
+
+<template>
+  <span class="menu-badge absolute">
+    <span class="relative mr-1 flex size-1.5">
+      <span
+        class="bg-green-500 absolute inline-flex size-full animate-ping rounded-full opacity-75"
+      ></span>
+      <span
+        class="bg-green-500 relative inline-flex size-1.5 rounded-full"
+      ></span>
+    </span>
+  </span>
+</template>
+
+<style scoped>
+.menu-badge {
+  top: 50%;
+  right: 0;
+  transform: translateY(-50%);
+}
+</style>

+ 86 - 0
apps/wisdom-legacy/src/views/system/department.data.ts

@@ -0,0 +1,86 @@
+import type { DepartmentVO } from '#/api/system';
+
+import { $t } from '@vben/locales';
+import { getPopupContainer } from '@vben/utils';
+
+import { defineStatusField } from '#/adapter/form';
+import { defineEditShell } from '#/adapter/shell/edit';
+import { defineGrid } from '#/adapter/vxe-table';
+import {
+  DepartmentVOSchema,
+  editDepartmentMethod,
+  listDepartmentMethod,
+  optionsDepartmentMethod,
+} from '#/api/system';
+
+export const departmentGrid = defineGrid<DepartmentVO>({
+  scope: 'system.department',
+  query: listDepartmentMethod,
+  tree: true,
+  fields: [
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: $t('system.department.field.name'),
+    },
+  ],
+  columns: (col) => [
+    col.seq({ width: 140, align: 'left', treeNode: true }),
+    {
+      field: 'name',
+      title: $t('system.department.field.name'),
+      width: 200,
+      align: 'left',
+    },
+    { field: 'remark', title: $t('ui.field.remark'), minWidth: 120 },
+    ...col.audit(),
+    col.status({ confirm: true }),
+    col.actions([{ code: 'append', text: '新增下级' }, 'edit', 'delete'], 180),
+  ],
+});
+
+export const departmentForm = defineEditShell<DepartmentVO>({
+  scope: 'system.department',
+  submit: editDepartmentMethod,
+  shell: 'modal',
+  prepare(data) {
+    if (data.pid === '0') Reflect.deleteProperty(data, 'pid');
+    return data;
+  },
+  schema: [
+    {
+      component: 'ApiTreeSelect',
+      componentProps: {
+        api: optionsDepartmentMethod,
+        filterTreeNode(input: string, node: Recordable<any>) {
+          if (!input || input.length === 0) return true;
+          const title: string = node.label.toString() ?? '';
+          if (!title) return false;
+          return title.includes(input) || $t(title).includes(input);
+        },
+        getPopupContainer,
+        class: 'w-full',
+        labelField: 'name',
+        valueField: 'id',
+        childrenField: 'children',
+        showSearch: true,
+        allowClear: true,
+      },
+      fieldName: 'pid',
+      label: $t('system.department.name', ['', '上级']),
+    },
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: $t('system.department.field.name'),
+      rules: DepartmentVOSchema.shape.name,
+    },
+    defineStatusField(),
+    {
+      component: 'Textarea',
+      fieldName: 'remark',
+      label: $t('ui.field.remark'),
+      rules: DepartmentVOSchema.shape.remark,
+    },
+  ],
+});

+ 283 - 0
apps/wisdom-legacy/src/views/system/menu.data.ts

@@ -0,0 +1,283 @@
+import type { VNodeChild } from 'vue';
+
+import type { TagOption } from '#/adapter/vxe-table';
+import type { MenuRecordVO } from '#/api/system/menu.schema';
+
+import { h } from 'vue';
+
+import { IconifyIcon } from '@vben/icons';
+import { $t } from '@vben/locales';
+import { getPopupContainer } from '@vben/utils';
+
+import { ApiOutlined } from '@ant-design/icons-vue';
+import { Tag } from 'ant-design-vue';
+
+import { defineStatusField, z } from '#/adapter/form';
+import { defineEditShell } from '#/adapter/shell/edit';
+import { defineGrid, defineTagRender } from '#/adapter/vxe-table';
+import {
+  DepartmentVOSchema,
+  editMenuMethod,
+  listMenuMethod,
+  MenuTypeOptions,
+  MenuVOSchema,
+  optionsMenuMethod,
+} from '#/api/system';
+
+import components from '../components';
+import MenuBadge from './components/MenuBadge.vue';
+
+export const menuGrid = defineGrid<MenuRecordVO>({
+  scope: 'system.menu',
+  query: listMenuMethod,
+  tree: true,
+  fields: [
+    {
+      component: 'Input',
+      fieldName: 'title',
+      label: '标题',
+    },
+  ],
+  columns: (col) => [
+    {
+      field: 'title',
+      title: '标题',
+      width: 256,
+      align: 'left',
+      fixed: 'left',
+      treeNode: true,
+      slots: { default: 'title' },
+    },
+    {
+      field: 'type',
+      title: '类型',
+      width: 100,
+      cellRender: defineTagRender([...MenuTypeOptions] as TagOption[]),
+    },
+    { field: 'authCode', title: '权限标识', minWidth: 120, align: 'left' },
+    { field: 'path', title: '路由地址', minWidth: 120, align: 'left' },
+    {
+      field: 'component',
+      title: '页面组件',
+      minWidth: 120,
+      align: 'left',
+      formatter: ({ row }) => {
+        switch (row.type) {
+          case 'catalog':
+          case 'menu': {
+            return row.component ?? '';
+          }
+          case 'embedded':
+          case 'link': {
+            return row.link ?? '';
+          }
+        }
+        return '';
+      },
+    },
+    {
+      field: 'remark',
+      title: $t('ui.field.remark'),
+      minWidth: 120,
+      visible: false,
+    },
+    ...col.audit(false),
+    col.status({ confirm: true }),
+    col.actions([{ code: 'append', text: '新增下级' }, 'edit', 'delete'], 180),
+  ],
+});
+
+export const menuForm = defineEditShell<MenuRecordVO>({
+  scope: 'system.menu',
+  submit: editMenuMethod,
+  shell: {
+    type: 'drawer',
+    class: 'w-full max-w-200',
+  },
+  form: {
+    commonConfig: {
+      colon: true,
+      formItemClass: 'col-span-2 md:col-span-1',
+    },
+    wrapperClass: 'grid-cols-2 gap-x-4',
+    submitOnEnter: false,
+  },
+  schema: [
+    {
+      component: 'RadioGroup',
+      componentProps: {
+        buttonStyle: 'solid',
+        optionType: 'button',
+        options: MenuTypeOptions,
+      },
+      defaultValue: 'menu',
+      fieldName: 'type',
+      label: '类型',
+      formItemClass: 'col-span-2 md:col-span-2',
+    },
+    {
+      component: 'ApiTreeSelect',
+      componentProps: {
+        api: optionsMenuMethod,
+        filterTreeNode(input: string, node: Recordable<any>) {
+          if (!input || input.length === 0) return true;
+          const title: string = node.label?.toString() ?? '';
+          if (!title) return false;
+          return title.includes(input) || $t(title).includes(input);
+        },
+        getPopupContainer,
+        class: 'w-full',
+        labelField: 'title',
+        valueField: 'id',
+        childrenField: 'children',
+        showSearch: true,
+        allowClear: true,
+        treeLine: true,
+        dropdownMatchSelectWidth: false,
+        treeDefaultExpandAll: true,
+      },
+      renderComponentContent() {
+        return {
+          title({
+            label,
+            icon,
+            type,
+            isVben,
+          }: MenuRecordVO & { label: string }) {
+            const coms: VNodeChild = [];
+            if (!label) return '';
+            if (icon) {
+              coms.push(h(IconifyIcon, { class: 'size-4', icon }));
+            }
+            coms.push(h('span', { class: '' }, $t(label)));
+            if (type) {
+              const props = MenuTypeOptions.find(
+                (option) => option.value === type,
+              );
+              coms.push(h(Tag, props, () => props?.label ?? type));
+            }
+            if (isVben) coms.push(h(MenuBadge));
+            return h('div', { class: 'flex items-center gap-1' }, coms);
+          },
+        };
+      },
+      fieldName: 'pid',
+      label: '上级菜单',
+    },
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: '菜单名称',
+      help: `可根据路由地址自动生成`,
+    },
+    {
+      component: 'Input',
+      fieldName: 'title',
+      label: '标题',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: 'carbon:name',
+        addonAfter: h(ApiOutlined, {
+          onClick(event) {
+            const url = `https://icon-sets.iconify.design`;
+            let category = '/carbon/';
+            let query = '';
+            try {
+              const target = event.target as HTMLElement;
+              const input = target
+                .closest('.ant-input-group-addon')
+                ?.parentElement?.querySelector('input');
+              const parts = input?.value?.split(':') ?? [];
+              if (parts.length >= 2) {
+                category = `/${parts[0]}/`;
+                query = `?icon-filter=${parts.slice(1).join(':')}`;
+              }
+            } catch {}
+            window.open(`${url}${category}${query}`, '_blank');
+          },
+        }),
+      },
+      fieldName: 'icon',
+      label: '图标',
+    },
+    {
+      component: 'Input',
+      fieldName: 'path',
+      label: '路由地址',
+      rules: MenuVOSchema.shape.path,
+      dependencies: {
+        if: (values) => ['catalog', 'embedded', 'menu'].includes(values.type),
+        triggerFields: ['type'],
+      },
+    },
+    {
+      component: 'Input',
+      fieldName: 'link',
+      label: '链接地址',
+      rules: z.string().url($t('ui.formRules.invalidURL')),
+      dependencies: {
+        if: (values) => ['embedded', 'link'].includes(values.type),
+        triggerFields: ['type'],
+      },
+    },
+    {
+      component: 'AutoComplete',
+      componentProps: {
+        allowClear: true,
+        class: 'w-full',
+        filterOption(input: string, option: { value: string }) {
+          return option.value.toLowerCase().includes(input.toLowerCase());
+        },
+        options: components,
+      },
+      fieldName: 'component',
+      label: '页面组件',
+      rules: 'required',
+      dependencies: {
+        if: (values) => values.type === 'menu',
+        triggerFields: ['type'],
+      },
+    },
+    {
+      component: 'Input',
+      fieldName: 'authCode',
+      label: '权限标识',
+      dependencies: {
+        rules: (values) => (values.type === 'button' ? 'required' : null),
+        if: (values) => !['link'].includes(values.type),
+        triggerFields: ['type'],
+      },
+    },
+    {
+      component: 'InputNumber',
+      fieldName: 'order',
+      label: '显示顺序',
+      help: '数值越小越靠前',
+    },
+    {
+      component: 'Checkbox',
+      dependencies: {
+        if: (values) => values.type === 'menu',
+        triggerFields: ['type'],
+      },
+      fieldName: 'keepAlive',
+      renderComponentContent() {
+        return {
+          default: () => '缓存标签页',
+        };
+      },
+      defaultValue: true,
+    },
+    defineStatusField(),
+    {
+      component: 'Textarea',
+      fieldName: 'remark',
+      label: $t('ui.field.remark'),
+      rules: DepartmentVOSchema.shape.remark,
+      formItemClass: 'col-span-2 md:col-span-2',
+    },
+  ],
+});

+ 22 - 0
apps/wisdom-legacy/src/views/system/modules/DepartmentEdit.vue

@@ -0,0 +1,22 @@
+<script setup lang="ts">
+import { $t } from '@vben/locales';
+
+import { useEditShell } from '#/adapter/shell';
+
+import { departmentForm } from '../department.data';
+
+const { Form, Shell, api } = useEditShell(departmentForm);
+</script>
+
+<template>
+  <Shell>
+    <Form class="mx-4" />
+    <template #prepend-footer>
+      <div class="flex-auto">
+        <a-button type="primary" danger @click="api.reset()">
+          {{ $t('common.reset') }}
+        </a-button>
+      </div>
+    </template>
+  </Shell>
+</template>

+ 22 - 0
apps/wisdom-legacy/src/views/system/modules/MenuEdit.vue

@@ -0,0 +1,22 @@
+<script setup lang="ts">
+import { computed } from 'vue';
+
+import { breakpointsTailwind, useBreakpoints } from '@vueuse/core';
+
+import { useEditShell } from '#/adapter/shell';
+
+import { menuForm } from '../menu.data';
+
+const { Form, Shell } = useEditShell(menuForm);
+
+const breakpoints = useBreakpoints(breakpointsTailwind);
+const layout = computed(() =>
+  breakpoints.greaterOrEqual('md').value ? 'horizontal' : 'vertical',
+);
+</script>
+
+<template>
+  <Shell>
+    <Form class="mx-4" :layout />
+  </Shell>
+</template>

+ 72 - 0
apps/wisdom-legacy/src/views/system/modules/RoleEdit.vue

@@ -0,0 +1,72 @@
+<script setup lang="ts">
+import { Tree } from '@vben/common-ui';
+import { IconifyIcon } from '@vben/icons';
+import { $t } from '@vben/locales';
+
+import { useRequest } from 'alova/client';
+import { Spin } from 'ant-design-vue';
+
+import { useEditShell } from '#/adapter/shell';
+import { optionsMenuMethod } from '#/api/system';
+
+import MenuBadge from '../components/MenuBadge.vue';
+import { roleForm } from '../role.data';
+
+const {
+  data: tree,
+  loading,
+  send: loadMenu,
+} = useRequest(optionsMenuMethod, { immediate: false });
+
+const { Form, Shell } = useEditShell(roleForm, { onLoaded: loadMenu });
+
+const getNodeClass = (node: Recordable) => {
+  const classes: string[] = [];
+  if (node.value?.type === 'button') classes.push('inline-flex w-[20%]');
+  return classes.join(' ');
+};
+</script>
+
+<template>
+  <Shell>
+    <Form class="mx-4">
+      <template #menuIds="slotProps">
+        <Spin class="w-full" :spinning="loading" :classes="{ root: 'w-full' }">
+          <Tree
+            :tree-data="tree"
+            multiple
+            bordered
+            :default-expanded-level="2"
+            :get-node-class="getNodeClass"
+            v-bind="slotProps"
+            value-field="id"
+            label-field="title"
+            icon-field="icon"
+          >
+            <template #node="{ value }">
+              <IconifyIcon v-if="value.icon" :icon="value.icon" />
+              {{ $t(value.title) }}
+              <div class="ml-4 relative">
+                <MenuBadge v-if="value.isVben" />
+              </div>
+            </template>
+          </Tree>
+        </Spin>
+      </template>
+    </Form>
+  </Shell>
+</template>
+
+<style lang="css" scoped>
+:deep(.ant-tree-title) {
+  .tree-actions {
+    @apply ml-5 hidden;
+  }
+}
+
+:deep(.ant-tree-title:hover) {
+  .tree-actions {
+    @apply ml-5 flex flex-auto justify-end;
+  }
+}
+</style>

+ 22 - 0
apps/wisdom-legacy/src/views/system/modules/UserEdit.vue

@@ -0,0 +1,22 @@
+<script setup lang="ts">
+import { $t } from '@vben/locales';
+
+import { useEditShell } from '#/adapter/shell';
+
+import { userForm } from '../user.data';
+
+const { Form, Shell, api } = useEditShell(userForm);
+</script>
+
+<template>
+  <Shell>
+    <Form class="mx-4" />
+    <template #prepend-footer>
+      <div class="flex-auto">
+        <a-button type="primary" danger @click="api.reset()">
+          {{ $t('common.reset') }}
+        </a-button>
+      </div>
+    </template>
+  </Shell>
+</template>

+ 19 - 0
apps/wisdom-legacy/src/views/system/modules/UserPassword.vue

@@ -0,0 +1,19 @@
+<script setup lang="ts">
+import { message } from 'ant-design-vue';
+
+import { useEditShell } from '#/adapter/shell';
+
+import { userPasswordForm } from '../user.data';
+
+const { Form, Shell } = useEditShell(userPasswordForm, {
+  onSuccess: () => {
+    message.success('重置密码成功');
+  },
+});
+</script>
+
+<template>
+  <Shell>
+    <Form class="mx-4" />
+  </Shell>
+</template>

+ 92 - 0
apps/wisdom-legacy/src/views/system/role.data.ts

@@ -0,0 +1,92 @@
+import type { RoleVO } from '#/api/system';
+
+import { $t } from '@vben/locales';
+
+import { defineStatusField } from '#/adapter/form';
+import { defineEditShell } from '#/adapter/shell/edit';
+import { defineGrid } from '#/adapter/vxe-table';
+import {
+  dataScopeOptions,
+  editRoleMethod,
+  getRoleMenuMethod,
+  listRoleMethod,
+  RoleVOSchema,
+} from '#/api/system';
+
+export const roleGrid = defineGrid<RoleVO>({
+  scope: 'system.role',
+  query: listRoleMethod,
+  fields: [
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: $t('system.role.field.name'),
+    },
+    {
+      component: 'Input',
+      fieldName: 'code',
+      label: $t('system.role.field.code'),
+    },
+  ],
+  columns: (col) => [
+    col.seq(),
+    { field: 'name', title: $t('system.role.field.name'), width: 160 },
+    { field: 'code', title: $t('system.role.field.code'), width: 140 },
+    {
+      field: 'remark',
+      title: $t('ui.field.remark'),
+      minWidth: 120,
+    },
+    ...col.audit(),
+    col.status(),
+    col.actions(['edit', 'delete']),
+  ],
+});
+
+export const roleForm = defineEditShell<RoleVO>({
+  scope: 'system.role',
+  load: getRoleMenuMethod,
+  submit: editRoleMethod,
+  shell: { type: 'drawer' },
+  form: {
+    layout: 'vertical',
+  },
+  schema: [
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: $t('system.role.field.name'),
+      rules: RoleVOSchema.shape.name,
+    },
+    {
+      component: 'Input',
+      fieldName: 'code',
+      label: $t('system.role.field.code'),
+      rules: RoleVOSchema.shape.code,
+    },
+    defineStatusField(),
+    {
+      component: 'Textarea',
+      fieldName: 'remark',
+      label: $t('ui.field.remark'),
+      rules: RoleVOSchema.shape.remark,
+    },
+    {
+      component: 'Select',
+      componentProps: { options: dataScopeOptions },
+      fieldName: 'dataScope',
+      label: `数据权限`,
+      rules: 'required',
+      wrapperClass: 'block',
+      controlClass: 'w-full',
+    },
+    {
+      component: 'Input',
+      fieldName: 'menuIds',
+      label: '菜单权限',
+      modelPropName: 'modelValue',
+      wrapperClass: 'block',
+      controlClass: 'w-full',
+    },
+  ],
+});

+ 161 - 0
apps/wisdom-legacy/src/views/system/user.data.ts

@@ -0,0 +1,161 @@
+import type { UserVO } from '#/api/system';
+
+import { $t } from '@vben/locales';
+import { getPopupContainer } from '@vben/utils';
+
+import { defineStatusField, password, z } from '#/adapter/form';
+import { defineEditShell, REFRESH_ID_PLACEHOLDER } from '#/adapter/shell/edit';
+import { defineGrid } from '#/adapter/vxe-table';
+import {
+  editUserMethod,
+  editUserPassword,
+  encodeUserRoles,
+  listUserMethod,
+  optionsDepartmentMethod,
+  optionsRoleMethod,
+} from '#/api/system';
+
+export const userGrid = defineGrid<UserVO>({
+  scope: 'system.user',
+  query: listUserMethod,
+  fields: [
+    {
+      component: 'Input',
+      fieldName: 'userName',
+      label: $t('system.user.field.account'),
+    },
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: $t('system.user.field.name'),
+    },
+  ],
+  columns: (col) => [
+    col.seq(),
+    { field: 'userName', title: $t('system.user.field.account') },
+    { field: 'name', title: $t('system.user.field.name') },
+    {
+      field: 'department.name',
+      title: $t('system.user.field.department'),
+    },
+    {
+      field: 'post.name',
+      title: $t('system.user.field.post'),
+    },
+    ...col.audit(),
+    col.status({ confirm: true }),
+    col.actions([{ code: 'rest', text: '重置密码' }, 'edit', 'delete'], 180),
+  ],
+});
+
+export const userForm = defineEditShell<UserVO>({
+  scope: 'system.user',
+  submit: editUserMethod,
+  shell: 'modal',
+  transform: (data) => {
+    if (data != null && typeof data === 'object' && 'id' in data) {
+      return data;
+    }
+    return { id: data ?? REFRESH_ID_PLACEHOLDER };
+  },
+  prepare(data) {
+    return { ...data, roles: encodeUserRoles(data.roles) } as any;
+  },
+  schema: (model) => [
+    {
+      component: 'ApiTreeSelect',
+      componentProps: {
+        api: optionsDepartmentMethod,
+        filterTreeNode(input: string, node: Recordable<any>) {
+          if (!input || input.length === 0) return true;
+          const title: string = node.label.toString() ?? '';
+          if (!title) return false;
+          return title.includes(input) || $t(title).includes(input);
+        },
+        getPopupContainer,
+        class: 'w-full',
+        labelField: 'name',
+        valueField: 'id',
+        childrenField: 'children',
+        showSearch: true,
+        allowClear: true,
+      },
+      fieldName: 'department.id',
+      label: $t('system.user.field.department'),
+      rules: 'selectRequired',
+    },
+    {
+      component: 'Input',
+      fieldName: 'userName',
+      label: $t('system.user.field.account'),
+      rules: 'required',
+    },
+    {
+      component: 'InputPassword',
+      componentProps: { placeholder: '请输入密码' },
+      fieldName: 'password',
+      label: '密码',
+      rules: password(),
+      hide: !!model?.id,
+    },
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: $t('system.user.field.name'),
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: { maxlength: 11 },
+      fieldName: 'phone',
+      label: $t('system.user.field.phone'),
+      rules: z
+        .string()
+        .regex(/^\d{11}$/, `请输入正确的号码`)
+        .default(''),
+    },
+    {
+      component: 'ApiSelect',
+      componentProps: {
+        api: optionsRoleMethod,
+        filterOption(input: string, option: Recordable<any>) {
+          if (!input || input.length === 0) return true;
+          const title: string = option.label.toString() ?? '';
+          if (!title) return false;
+          return title.includes(input) || $t(title).includes(input);
+        },
+        class: 'w-full',
+        labelField: 'name',
+        valueField: 'id',
+        showSearch: true,
+        allowClear: true,
+        mode: 'tags',
+      },
+      fieldName: 'roles',
+      label: $t('system.role.name'),
+    },
+    defineStatusField(model?.status),
+  ],
+});
+
+export const userPasswordForm = defineEditShell<UserVO & { password: string }>({
+  submit: editUserPassword,
+  shell: 'modal',
+  shouldTitle: false,
+  schema: [
+    {
+      component: 'Input',
+      fieldName: 'userName',
+      label: $t('system.user.field.account'),
+      rules: 'required',
+      disabled: true,
+    },
+    {
+      component: 'InputPassword',
+      componentProps: { placeholder: '请输入密码', autocomplete: 'off' },
+      fieldName: 'password',
+      label: '密码',
+      rules: password(),
+    },
+  ],
+});