diff --git a/package.json b/package.json
index a33d0dc7..8f1d24e7 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
     "@zxcvbn-ts/core": "^2.2.1",
     "animate.css": "^4.1.1",
     "axios": "^1.3.4",
+    "benz-amr-recorder": "^1.1.5",
     "bpmn-js-token-simulation": "^0.10.0",
     "camunda-bpmn-moddle": "^7.0.1",
     "cropperjs": "^1.5.13",
diff --git a/src/api/bpm/userGroup/index.ts b/src/api/bpm/userGroup/index.ts
index c3399f27..035762bf 100644
--- a/src/api/bpm/userGroup/index.ts
+++ b/src/api/bpm/userGroup/index.ts
@@ -42,6 +42,6 @@ export const getUserGroupPage = async (params) => {
 }
 
 // 获取用户组精简信息列表
-export const listSimpleUserGroup = async () => {
+export const getSimpleUserGroupList = async (): Promise<UserGroupVO[]> => {
   return await request.get({ url: '/bpm/user-group/list-all-simple' })
 }
diff --git a/src/api/mp/message/index.ts b/src/api/mp/message/index.ts
index 8b7d3cbd..ad9b95dd 100644
--- a/src/api/mp/message/index.ts
+++ b/src/api/mp/message/index.ts
@@ -1,7 +1,7 @@
 import request from '@/config/axios'
 
 // 获得公众号消息分页
-export const getMessagePage = (query) => {
+export const getMessagePage = (query: PageParam) => {
   return request.get({
     url: '/mp/message/page',
     params: query
diff --git a/src/api/system/dept/index.ts b/src/api/system/dept/index.ts
index d66de3f1..e9c31fd7 100644
--- a/src/api/system/dept/index.ts
+++ b/src/api/system/dept/index.ts
@@ -18,7 +18,7 @@ export interface DeptPageReqVO {
 }
 
 // 查询部门(精简)列表
-export const listSimpleDeptApi = async () => {
+export const getSimpleDeptList = async (): Promise<DeptVO[]> => {
   return await request.get({ url: '/system/dept/list-all-simple' })
 }
 
diff --git a/src/api/system/menu/index.ts b/src/api/system/menu/index.ts
index 5913972b..13736215 100644
--- a/src/api/system/menu/index.ts
+++ b/src/api/system/menu/index.ts
@@ -18,18 +18,13 @@ export interface MenuVO {
   createTime: Date
 }
 
-export interface MenuPageReqVO {
-  name?: string
-  status?: number
-}
-
 // 查询菜单(精简)列表
-export const listSimpleMenusApi = () => {
+export const getSimpleMenusList = () => {
   return request.get({ url: '/system/menu/list-all-simple' })
 }
 
 // 查询菜单列表
-export const getMenuListApi = (params: MenuPageReqVO) => {
+export const getMenuList = (params) => {
   return request.get({ url: '/system/menu/list', params })
 }
 
@@ -39,16 +34,16 @@ export const getMenuApi = (id: number) => {
 }
 
 // 新增菜单
-export const createMenuApi = (data: MenuVO) => {
+export const createMenu = (data: MenuVO) => {
   return request.post({ url: '/system/menu/create', data })
 }
 
 // 修改菜单
-export const updateMenuApi = (data: MenuVO) => {
+export const updateMenu = (data: MenuVO) => {
   return request.put({ url: '/system/menu/update', data })
 }
 
 // 删除菜单
-export const deleteMenuApi = (id: number) => {
+export const deleteMenu = (id: number) => {
   return request.delete({ url: '/system/menu/delete?id=' + id })
 }
diff --git a/src/api/system/post/index.ts b/src/api/system/post/index.ts
index 98df227f..405db387 100644
--- a/src/api/system/post/index.ts
+++ b/src/api/system/post/index.ts
@@ -16,7 +16,7 @@ export const getPostPage = async (params: PageParam) => {
 }
 
 // 获取岗位精简信息列表
-export const getSimplePostList = async () => {
+export const getSimplePostList = async (): Promise<PostVO[]> => {
   return await request.get({ url: '/system/post/list-all-simple' })
 }
 
diff --git a/src/api/system/role/index.ts b/src/api/system/role/index.ts
index 0d477555..9692548a 100644
--- a/src/api/system/role/index.ts
+++ b/src/api/system/role/index.ts
@@ -28,7 +28,7 @@ export const getRolePageApi = async (params: RolePageReqVO) => {
 }
 
 // 查询角色(精简)列表
-export const listSimpleRolesApi = async () => {
+export const getSimpleRoleList = async (): Promise<RoleVO[]> => {
   return await request.get({ url: '/system/role/list-all-simple' })
 }
 
diff --git a/src/api/system/sensitiveWord/index.ts b/src/api/system/sensitiveWord/index.ts
index 7da2c28e..1116226f 100644
--- a/src/api/system/sensitiveWord/index.ts
+++ b/src/api/system/sensitiveWord/index.ts
@@ -1,4 +1,5 @@
 import request from '@/config/axios'
+import qs from 'qs'
 
 export interface SensitiveWordVO {
   id: number
@@ -9,22 +10,13 @@ export interface SensitiveWordVO {
   createTime: Date
 }
 
-export interface SensitiveWordPageReqVO extends PageParam {
-  name?: string
-  tag?: string
-  status?: number
-  createTime?: Date[]
-}
-
-export interface SensitiveWordExportReqVO {
-  name?: string
-  tag?: string
-  status?: number
-  createTime?: Date[]
+export interface SensitiveWordTestReqVO {
+  text: string
+  tag: string[]
 }
 
 // 查询敏感词列表
-export const getSensitiveWordPage = (params: SensitiveWordPageReqVO) => {
+export const getSensitiveWordPage = (params: PageParam) => {
   return request.get({ url: '/system/sensitive-word/page', params })
 }
 
@@ -49,16 +41,18 @@ export const deleteSensitiveWord = (id: number) => {
 }
 
 // 导出敏感词
-export const exportSensitiveWord = (params: SensitiveWordExportReqVO) => {
+export const exportSensitiveWord = (params) => {
   return request.download({ url: '/system/sensitive-word/export-excel', params })
 }
 
 // 获取所有敏感词的标签数组
-export const getSensitiveWordTags = () => {
+export const getSensitiveWordTagList = () => {
   return request.get({ url: '/system/sensitive-word/get-tags' })
 }
 
 // 获得文本所包含的不合法的敏感词数组
-export const validateText = (id: number) => {
-  return request.get({ url: '/system/sensitive-word/validate-text?' + id })
+export const validateText = (query: SensitiveWordTestReqVO) => {
+  return request.get({
+    url: '/system/sensitive-word/validate-text?' + qs.stringify(query, { arrayFormat: 'repeat' })
+  })
 }
diff --git a/src/api/system/user/index.ts b/src/api/system/user/index.ts
index 3cc0a84d..058b320a 100644
--- a/src/api/system/user/index.ts
+++ b/src/api/system/user/index.ts
@@ -86,6 +86,6 @@ export const updateUserStatusApi = (id: number, status: number) => {
 }
 
 // 获取用户精简信息列表
-export const getSimpleUserList = () => {
+export const getSimpleUserList = (): Promise<UserVO[]> => {
   return request.get({ url: '/system/user/list-all-simple' })
 }
diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts
index b1bdfafe..58d5601b 100644
--- a/src/router/modules/remaining.ts
+++ b/src/router/modules/remaining.ts
@@ -256,7 +256,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           hidden: true,
           canTo: true,
           title: '流程定义',
-          activeMenu: 'bpm/definition/index'
+          activeMenu: '/bpm/manager/model'
         }
       },
       {
diff --git a/src/types/auto-components.d.ts b/src/types/auto-components.d.ts
index 2b199f1a..04eb4d9e 100644
--- a/src/types/auto-components.d.ts
+++ b/src/types/auto-components.d.ts
@@ -23,13 +23,11 @@ declare module '@vue/runtime-core' {
     DictTag: typeof import('./../components/DictTag/src/DictTag.vue')['default']
     Echart: typeof import('./../components/Echart/src/Echart.vue')['default']
     Editor: typeof import('./../components/Editor/src/Editor.vue')['default']
-    ElAvatar: typeof import('element-plus/es')['ElAvatar']
     ElBadge: typeof import('element-plus/es')['ElBadge']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
     ElCard: typeof import('element-plus/es')['ElCard']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
-    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCol: typeof import('element-plus/es')['ElCol']
     ElCollapse: typeof import('element-plus/es')['ElCollapse']
     ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
@@ -54,6 +52,7 @@ declare module '@vue/runtime-core' {
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
     ElIcon: typeof import('element-plus/es')['ElIcon']
+    ElImage: typeof import('element-plus/es')['ElImage']
     ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
     ElInput: typeof import('element-plus/es')['ElInput']
     ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
@@ -68,11 +67,9 @@ declare module '@vue/runtime-core' {
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
     ElSelect: typeof import('element-plus/es')['ElSelect']
     ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
-    ElSpace: typeof import('element-plus/es')['ElSpace']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
     ElTable: typeof import('element-plus/es')['ElTable']
     ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
-    ElTableV2: typeof import('element-plus/es')['ElTableV2']
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
diff --git a/src/utils/dict.ts b/src/utils/dict.ts
index 395f7157..05c70dad 100644
--- a/src/utils/dict.ts
+++ b/src/utils/dict.ts
@@ -140,5 +140,9 @@ export enum DICT_TYPE {
   PAY_ORDER_STATUS = 'pay_order_status', // 商户支付订单状态
   PAY_ORDER_REFUND_STATUS = 'pay_order_refund_status', // 商户支付订单退款状态
   PAY_REFUND_ORDER_STATUS = 'pay_refund_order_status', // 退款订单状态
-  PAY_REFUND_ORDER_TYPE = 'pay_refund_order_type' // 退款订单类别
+  PAY_REFUND_ORDER_TYPE = 'pay_refund_order_type', // 退款订单类别
+
+  // ========== MP 模块 ==========
+  MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型
+  MP_MESSAGE_TYPE = 'mp_message_type' // 消息类型
 }
diff --git a/src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue b/src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue
new file mode 100644
index 00000000..a452cab9
--- /dev/null
+++ b/src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue
@@ -0,0 +1,247 @@
+<template>
+  <Dialog title="修改任务规则" v-model="modelVisible" width="600">
+    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
+      <el-form-item label="任务名称" prop="taskDefinitionName">
+        <el-input v-model="formData.taskDefinitionName" placeholder="请输入流标标识" disabled />
+      </el-form-item>
+      <el-form-item label="任务标识" prop="taskDefinitionKey">
+        <el-input v-model="formData.taskDefinitionKey" placeholder="请输入任务标识" disabled />
+      </el-form-item>
+      <el-form-item label="规则类型" prop="type">
+        <el-select v-model="formData.type" clearable style="width: 100%">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item v-if="formData.type === 10" label="指定角色" prop="roleIds">
+        <el-select v-model="formData.roleIds" multiple clearable style="width: 100%">
+          <el-option
+            v-for="item in roleOptions"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        label="指定部门"
+        prop="deptIds"
+        span="24"
+        v-if="formData.type === 20 || formData.type === 21"
+      >
+        <el-tree-select
+          ref="treeRef"
+          v-model="formData.deptIds"
+          node-key="id"
+          show-checkbox
+          :props="defaultProps"
+          :data="deptTreeOptions"
+          empty-text="加载中,请稍后"
+          multiple
+        />
+      </el-form-item>
+      <el-form-item label="指定岗位" prop="postIds" span="24" v-if="formData.type === 22">
+        <el-select v-model="formData.postIds" multiple clearable style="width: 100%">
+          <el-option
+            v-for="item in postOptions"
+            :key="parseInt(item.id)"
+            :label="item.name"
+            :value="parseInt(item.id)"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        label="指定用户"
+        prop="userIds"
+        span="24"
+        v-if="formData.type === 30 || formData.type === 31 || formData.type === 32"
+      >
+        <el-select v-model="formData.userIds" multiple clearable style="width: 100%">
+          <el-option
+            v-for="item in userOptions"
+            :key="parseInt(item.id)"
+            :label="item.nickname"
+            :value="parseInt(item.id)"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="指定用户组" prop="userGroupIds" v-if="formData.type === 40">
+        <el-select v-model="formData.userGroupIds" multiple clearable style="width: 100%">
+          <el-option
+            v-for="item in userGroupOptions"
+            :key="parseInt(item.id)"
+            :label="item.name"
+            :value="parseInt(item.id)"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="指定脚本" prop="scripts" v-if="formData.type === 50">
+        <el-select v-model="formData.scripts" multiple clearable style="width: 100%">
+          <el-option
+            v-for="dict in taskAssignScriptDictDatas"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <!-- 操作按钮 -->
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="modelVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { handleTree, defaultProps } from '@/utils/tree'
+import * as TaskAssignRuleApi from '@/api/bpm/taskAssignRule'
+import * as RoleApi from '@/api/system/role'
+import * as DeptApi from '@/api/system/dept'
+import * as PostApi from '@/api/system/post'
+import * as UserApi from '@/api/system/user'
+import * as UserGroupApi from '@/api/bpm/userGroup'
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const modelVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  type: Number(undefined),
+  modelId: '',
+  options: [],
+  roleIds: [],
+  deptIds: [],
+  postIds: [],
+  userIds: [],
+  userGroupIds: [],
+  scripts: []
+})
+const formRules = reactive({
+  type: [{ required: true, message: '规则类型不能为空', trigger: 'change' }],
+  roleIds: [{ required: true, message: '指定角色不能为空', trigger: 'change' }],
+  deptIds: [{ required: true, message: '指定部门不能为空', trigger: 'change' }],
+  postIds: [{ required: true, message: '指定岗位不能为空', trigger: 'change' }],
+  userIds: [{ required: true, message: '指定用户不能为空', trigger: 'change' }],
+  userGroupIds: [{ required: true, message: '指定用户组不能为空', trigger: 'change' }],
+  scripts: [{ required: true, message: '指定脚本不能为空', trigger: 'change' }]
+})
+const formRef = ref() // 表单 Ref
+const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
+const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
+const deptTreeOptions = ref() // 部门树
+const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
+const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
+const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT)
+
+/** 打开弹窗 */
+const open = async (modelId: string, row: TaskAssignRuleApi.TaskAssignVO) => {
+  // 1. 先重置表单
+  resetForm()
+  // 2. 再设置表单
+  formData.value = {
+    ...row,
+    modelId: modelId,
+    options: [],
+    roleIds: [],
+    deptIds: [],
+    postIds: [],
+    userIds: [],
+    userGroupIds: [],
+    scripts: []
+  }
+  // 将 options 赋值到对应的 roleIds 等选项
+  if (row.type === 10) {
+    formData.value.roleIds.push(...row.options)
+  } else if (row.type === 20 || row.type === 21) {
+    formData.value.deptIds.push(...row.options)
+  } else if (row.type === 22) {
+    formData.value.postIds.push(...row.options)
+  } else if (row.type === 30 || row.type === 31 || row.type === 32) {
+    formData.value.userIds.push(...row.options)
+  } else if (row.type === 40) {
+    formData.value.userGroupIds.push(...row.options)
+  } else if (row.type === 50) {
+    formData.value.scripts.push(...row.options)
+  }
+  // 打开弹窗
+  modelVisible.value = true
+
+  // 获得角色列表
+  roleOptions.value = await RoleApi.getSimpleRoleList()
+  // 获得部门列表
+  deptOptions.value = await DeptApi.getSimpleDeptList()
+  deptTreeOptions.value = handleTree(deptOptions.value, 'id')
+  // 获得岗位列表
+  postOptions.value = await PostApi.getSimplePostList()
+  // 获得用户列表
+  userOptions.value = await UserApi.getSimpleUserList()
+  // 获得用户组列表
+  userGroupOptions.value = await UserGroupApi.getSimpleUserGroupList()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+
+  // 构建表单
+  const form = {
+    ...formData.value,
+    taskDefinitionName: undefined
+  }
+  // 将 roleIds 等选项赋值到 options 中
+  if (form.type === 10) {
+    form.options = form.roleIds
+  } else if (form.type === 20 || form.type === 21) {
+    form.options = form.deptIds
+  } else if (form.type === 22) {
+    form.options = form.postIds
+  } else if (form.type === 30 || form.type === 31 || form.type === 32) {
+    form.options = form.userIds
+  } else if (form.type === 40) {
+    form.options = form.userGroupIds
+  } else if (form.type === 50) {
+    form.options = form.scripts
+  }
+  form.roleIds = undefined
+  form.deptIds = undefined
+  form.postIds = undefined
+  form.userIds = undefined
+  form.userGroupIds = undefined
+  form.scripts = undefined
+
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = form as unknown as TaskAssignRuleApi.TaskAssignVO
+    if (!data.id) {
+      await TaskAssignRuleApi.createTaskAssignRule(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await TaskAssignRuleApi.updateTaskAssignRule(data)
+      message.success(t('common.updateSuccess'))
+    }
+    modelVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/bpm/taskAssignRule/index.vue b/src/views/bpm/taskAssignRule/index.vue
index 8db7b578..feea80a2 100644
--- a/src/views/bpm/taskAssignRule/index.vue
+++ b/src/views/bpm/taskAssignRule/index.vue
@@ -1,186 +1,73 @@
 <template>
   <ContentWrap>
-    <!-- 列表 -->
-    <XTable @register="registerTable" ref="xGrid">
-      <template #options_default="{ row }">
-        <span :key="option" v-for="option in row.options">
-          <el-tag>
-            {{ getAssignRuleOptionName(row.type, option) }}
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="任务名" align="center" prop="taskDefinitionName" />
+      <el-table-column label="任务标识" align="center" prop="taskDefinitionKey" />
+      <el-table-column label="规则类型" align="center" prop="type">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE" :value="scope.row.type" />
+        </template>
+      </el-table-column>
+      <el-table-column label="规则范围" align="center" prop="options">
+        <template #default="scope">
+          <el-tag class="mr-5px" :key="option" v-for="option in scope.row.options">
+            {{ getAssignRuleOptionName(scope.row.type, option) }}
           </el-tag>
-          &nbsp;
-        </span>
-      </template>
-      <!-- 操作 -->
-      <template #actionbtns_default="{ row }" v-if="modelId">
-        <!-- 操作:修改 -->
-        <XTextButton
-          preIcon="ep:edit"
-          :title="t('action.edit')"
-          v-hasPermi="['bpm:task-assign-rule:update']"
-          @click="handleUpdate(row)"
-        />
-      </template>
-    </XTable>
-
-    <!-- 添加/修改弹窗 -->
-    <XModal v-model="dialogVisible" title="修改任务规则" width="800" height="35%">
-      <el-form ref="formRef" :model="formData" :rules="rules" label-width="80px">
-        <el-form-item label="任务名称" prop="taskDefinitionName">
-          <el-input v-model="formData.taskDefinitionName" placeholder="请输入流标标识" disabled />
-        </el-form-item>
-        <el-form-item label="任务标识" prop="taskDefinitionKey">
-          <el-input v-model="formData.taskDefinitionKey" placeholder="请输入任务标识" disabled />
-        </el-form-item>
-        <el-form-item label="规则类型" prop="type">
-          <el-select v-model="formData.type" clearable style="width: 100%">
-            <el-option
-              v-for="dict in getDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE)"
-              :key="parseInt(dict.value)"
-              :label="dict.label"
-              :value="parseInt(dict.value)"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item v-if="formData.type === 10" label="指定角色" prop="roleIds">
-          <el-select v-model="formData.roleIds" multiple clearable style="width: 100%">
-            <el-option
-              v-for="item in roleOptions"
-              :key="item.id"
-              :label="item.name"
-              :value="item.id"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item
-          label="指定部门"
-          prop="deptIds"
-          span="24"
-          v-if="formData.type === 20 || formData.type === 21"
-        >
-          <el-tree-select
-            ref="treeRef"
-            v-model="formData.deptIds"
-            node-key="id"
-            show-checkbox
-            :props="defaultProps"
-            :data="deptTreeOptions"
-            empty-text="加载中,请稍后"
-            multiple
-          />
-        </el-form-item>
-        <el-form-item label="指定岗位" prop="postIds" span="24" v-if="formData.type === 22">
-          <el-select v-model="formData.postIds" multiple clearable style="width: 100%">
-            <el-option
-              v-for="item in postOptions"
-              :key="parseInt(item.id)"
-              :label="item.name"
-              :value="parseInt(item.id)"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item
-          label="指定用户"
-          prop="userIds"
-          span="24"
-          v-if="formData.type === 30 || formData.type === 31 || formData.type === 32"
-        >
-          <el-select v-model="formData.userIds" multiple clearable style="width: 100%">
-            <el-option
-              v-for="item in userOptions"
-              :key="parseInt(item.id)"
-              :label="item.nickname"
-              :value="parseInt(item.id)"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="指定用户组" prop="userGroupIds" v-if="formData.type === 40">
-          <el-select v-model="formData.userGroupIds" multiple clearable style="width: 100%">
-            <el-option
-              v-for="item in userGroupOptions"
-              :key="parseInt(item.id)"
-              :label="item.name"
-              :value="parseInt(item.id)"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="指定脚本" prop="scripts" v-if="formData.type === 50">
-          <el-select v-model="formData.scripts" multiple clearable style="width: 100%">
-            <el-option
-              v-for="dict in taskAssignScriptDictDatas"
-              :key="parseInt(dict.value)"
-              :label="dict.label"
-              :value="parseInt(dict.value)"
-            />
-          </el-select>
-        </el-form-item>
-      </el-form>
-      <!-- 操作按钮 -->
-      <template #footer>
-        <!-- 按钮:保存 -->
-        <XButton
-          type="primary"
-          :title="t('action.save')"
-          :loading="actionLoading"
-          @click="submitForm"
-        />
-        <!-- 按钮:关闭 -->
-        <XButton
-          :loading="actionLoading"
-          :title="t('dialog.close')"
-          @click="dialogVisible = false"
-        />
-      </template>
-    </XModal>
+        </template>
+      </el-table-column>
+      <el-table-column v-if="queryParams.modelId" label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm(scope.row)"
+            v-hasPermi="['bpm:task-assign-rule:update']"
+          >
+            修改
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
   </ContentWrap>
+  <!-- 添加/修改弹窗 -->
+  <TaskAssignRuleForm ref="formRef" @success="getList" />
 </template>
 <script setup lang="ts" name="TaskAssignRule">
-// 全局相关的 import
-import { FormInstance } from 'element-plus'
-// 业务相关的 import
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import * as TaskAssignRuleApi from '@/api/bpm/taskAssignRule'
-import { listSimpleRolesApi } from '@/api/system/role'
-import { getSimplePostList } from '@/api/system/post'
-import { getSimpleUserList } from '@/api/system/user'
-import { listSimpleUserGroup } from '@/api/bpm/userGroup'
-import { listSimpleDeptApi } from '@/api/system/dept'
-import { DICT_TYPE, getDictOptions } from '@/utils/dict'
-import { handleTree, defaultProps } from '@/utils/tree'
-import { allSchemas, rules, idShowActionClick } from './taskAssignRule.data'
+import * as RoleApi from '@/api/system/role'
+import * as DeptApi from '@/api/system/dept'
+import * as PostApi from '@/api/system/post'
+import * as UserApi from '@/api/system/user'
+import * as UserGroupApi from '@/api/bpm/userGroup'
+import TaskAssignRuleForm from './TaskAssignRuleForm.vue'
+const { query } = useRoute() // 查询参数
 
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-const { query } = useRoute()
-const xGrid = ref()
-
-// ========== 列表相关 ==========
-
-const roleOptions = ref() // 角色列表
-const deptOptions = ref() // 部门列表
-const deptTreeOptions = ref()
-const postOptions = ref() // 岗位列表
-const userOptions = ref() // 用户列表
-const userGroupOptions = ref() // 用户组列表
-const taskAssignScriptDictDatas = getDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT)
-
-// 流程模型的编号。如果 modelId 非空,则用于流程模型的查看与配置
-const modelId = query.modelId
-// 流程定义的编号。如果 processDefinitionId 非空,则用于流程定义的查看,不支持配置
-const processDefinitionId = query.processDefinitionId
-let isShow = idShowActionClick(modelId)
-
-// 查询参数
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
 const queryParams = reactive({
-  modelId: modelId,
-  processDefinitionId: processDefinitionId
-})
-const [registerTable, { reload }] = useXTable({
-  allSchemas: allSchemas,
-  params: queryParams,
-  getListApi: TaskAssignRuleApi.getTaskAssignRuleList,
-  isList: true
+  modelId: query.modelId, // 流程模型的编号。如果 modelId 非空,则用于流程模型的查看与配置
+  processDefinitionId: query.processDefinitionId // 流程定义的编号。如果 processDefinitionId 非空,则用于流程定义的查看,不支持配置
 })
+const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
+const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
+const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
+const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
+const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT)
 
-// 翻译规则范围
+/** 查询参数列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    list.value = await TaskAssignRuleApi.getTaskAssignRuleList(queryParams)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 翻译规则范围 */
+// TODO 芋艿:各种 ts 报错
 const getAssignRuleOptionName = (type, option) => {
   if (type === 10) {
     for (const roleOption of roleOptions.value) {
@@ -223,136 +110,24 @@ const getAssignRuleOptionName = (type, option) => {
   return '未知(' + option + ')'
 }
 
-// ========== 修改相关 ==========
-
-// 修改任务责任表单
-const actionLoading = ref(false) // 遮罩层
-const dialogVisible = ref(false) // 是否显示弹出层
-const formRef = ref<FormInstance>()
-const formData = ref() // 表单数据
-
-// 提交按钮
-const submitForm = async () => {
-  // 参数校验
-  const elForm = unref(formRef)
-  if (!elForm) return
-  const valid = await elForm.validate()
-  if (!valid) return
-  // 构建表单
-  let form = {
-    ...formData.value,
-    taskDefinitionName: undefined
-  }
-  // 将 roleIds 等选项赋值到 options 中
-  if (form.type === 10) {
-    form.options = form.roleIds
-  } else if (form.type === 20 || form.type === 21) {
-    form.options = form.deptIds
-  } else if (form.type === 22) {
-    form.options = form.postIds
-  } else if (form.type === 30 || form.type === 31 || form.type === 32) {
-    form.options = form.userIds
-  } else if (form.type === 40) {
-    form.options = form.userGroupIds
-  } else if (form.type === 50) {
-    form.options = form.scripts
-  }
-  form.roleIds = undefined
-  form.deptIds = undefined
-  form.postIds = undefined
-  form.userIds = undefined
-  form.userGroupIds = undefined
-  form.scripts = undefined
-  // 设置提交中
-  actionLoading.value = true
-  // 提交请求
-  try {
-    const data = form as TaskAssignRuleApi.TaskAssignVO
-    // 新增
-    if (!data.id) {
-      await TaskAssignRuleApi.createTaskAssignRule(data)
-      message.success(t('common.createSuccess'))
-      // 修改
-    } else {
-      await TaskAssignRuleApi.updateTaskAssignRule(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-  } finally {
-    actionLoading.value = false
-    // 刷新列表
-    await reload()
-  }
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (row: TaskAssignRuleApi.TaskAssignVO) => {
+  formRef.value.open(queryParams.modelId, row)
 }
 
-// 修改任务分配规则
-const handleUpdate = (row) => {
-  // 1. 先重置表单
-  formData.value = {}
-  // 2. 再设置表单
-  formData.value = {
-    ...row,
-    modelId: modelId,
-    options: [],
-    roleIds: [],
-    deptIds: [],
-    postIds: [],
-    userIds: [],
-    userGroupIds: [],
-    scripts: []
-  }
-  // 将 options 赋值到对应的 roleIds 等选项
-  if (row.type === 10) {
-    formData.value.roleIds.push(...row.options)
-  } else if (row.type === 20 || row.type === 21) {
-    formData.value.deptIds.push(...row.options)
-  } else if (row.type === 22) {
-    formData.value.postIds.push(...row.options)
-  } else if (row.type === 30 || row.type === 31 || row.type === 32) {
-    formData.value.userIds.push(...row.options)
-  } else if (row.type === 40) {
-    formData.value.userGroupIds.push(...row.options)
-  } else if (row.type === 50) {
-    formData.value.scripts.push(...row.options)
-  }
-  // 打开弹窗
-  dialogVisible.value = true
-  actionLoading.value = false
-}
-
-// ========== 初始化 ==========
-onMounted(() => {
+/** 初始化 */
+onMounted(async () => {
+  await getList()
   // 获得角色列表
-  roleOptions.value = []
-  listSimpleRolesApi().then((data) => {
-    roleOptions.value.push(...data)
-  })
+  roleOptions.value = await RoleApi.getSimpleRoleList()
   // 获得部门列表
-  deptOptions.value = []
-  deptTreeOptions.value = []
-  listSimpleDeptApi().then((data) => {
-    deptOptions.value.push(...data)
-    deptTreeOptions.value.push(...handleTree(data, 'id'))
-  })
+  deptOptions.value = await DeptApi.getSimpleDeptList()
   // 获得岗位列表
-  postOptions.value = []
-  getSimplePostList().then((data) => {
-    postOptions.value.push(...data)
-  })
+  postOptions.value = await PostApi.getSimplePostList()
   // 获得用户列表
-  userOptions.value = []
-  getSimpleUserList().then((data) => {
-    userOptions.value.push(...data)
-  })
+  userOptions.value = await UserApi.getSimpleUserList()
   // 获得用户组列表
-  userGroupOptions.value = []
-  listSimpleUserGroup().then((data) => {
-    userGroupOptions.value.push(...data)
-  })
-  if (!isShow) {
-    setTimeout(() => {
-      xGrid.value.Ref.hideColumn('actionbtns')
-    }, 100)
-  }
+  userGroupOptions.value = await UserGroupApi.getSimpleUserGroupList()
 })
 </script>
diff --git a/src/views/bpm/taskAssignRule/taskAssignRule.data.ts b/src/views/bpm/taskAssignRule/taskAssignRule.data.ts
deleted file mode 100644
index cad74325..00000000
--- a/src/views/bpm/taskAssignRule/taskAssignRule.data.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
-
-// 表单校验
-export const rules = reactive({
-  type: [{ required: true, message: '规则类型不能为空', trigger: 'change' }],
-  roleIds: [{ required: true, message: '指定角色不能为空', trigger: 'change' }],
-  deptIds: [{ required: true, message: '指定部门不能为空', trigger: 'change' }],
-  postIds: [{ required: true, message: '指定岗位不能为空', trigger: 'change' }],
-  userIds: [{ required: true, message: '指定用户不能为空', trigger: 'change' }],
-  userGroupIds: [{ required: true, message: '指定用户组不能为空', trigger: 'change' }],
-  scripts: [{ required: true, message: '指定脚本不能为空', trigger: 'change' }]
-})
-
-// CrudSchema
-const crudSchemas = reactive<VxeCrudSchema>({
-  primaryKey: 'id',
-  primaryType: null,
-  action: true,
-  actionWidth: '200px',
-  columns: [
-    {
-      title: '任务名',
-      field: 'taskDefinitionName'
-    },
-    {
-      title: '任务标识',
-      field: 'taskDefinitionKey'
-    },
-    {
-      title: '规则类型',
-      field: 'type',
-      dictType: DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE,
-      dictClass: 'number'
-    },
-    {
-      title: '规则范围',
-      field: 'options',
-      table: {
-        slots: {
-          default: 'options_default'
-        }
-      }
-    }
-  ]
-})
-
-export const idShowActionClick = (modelId?: any) => {
-  if (modelId) {
-    return true
-  } else {
-    return false
-  }
-}
-export const { allSchemas } = useVxeCrudSchemas(crudSchemas)
diff --git a/src/views/infra/codegen/components/BasicInfoForm.vue b/src/views/infra/codegen/components/BasicInfoForm.vue
index 2009553f..5ab820a2 100644
--- a/src/views/infra/codegen/components/BasicInfoForm.vue
+++ b/src/views/infra/codegen/components/BasicInfoForm.vue
@@ -6,7 +6,7 @@ import { useForm } from '@/hooks/web/useForm'
 import { FormSchema } from '@/types/form'
 import { CodegenTableVO } from '@/api/infra/codegen/types'
 import { getIntDictOptions } from '@/utils/dict'
-import { listSimpleMenusApi } from '@/api/system/menu'
+import { getSimpleMenusList } from '@/api/system/menu'
 import { handleTree, defaultProps } from '@/utils/tree'
 import { PropType } from 'vue'
 
@@ -21,7 +21,7 @@ const templateTypeOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_T
 const sceneOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE)
 const menuOptions = ref<any>([]) // 树形结构
 const getTree = async () => {
-  const res = await listSimpleMenusApi()
+  const res = await getSimpleMenusList()
   menuOptions.value = handleTree(res)
 }
 
diff --git a/src/views/mp/components/img.png b/src/views/mp/components/img.png
new file mode 100644
index 00000000..c25a6e76
Binary files /dev/null and b/src/views/mp/components/img.png differ
diff --git a/src/views/mp/components/wx-location/main.vue b/src/views/mp/components/wx-location/main.vue
new file mode 100644
index 00000000..47eab571
--- /dev/null
+++ b/src/views/mp/components/wx-location/main.vue
@@ -0,0 +1,71 @@
+<!--
+  【微信消息 - 定位】
+-->
+<template>
+  <div>
+    <el-link
+      type="primary"
+      target="_blank"
+      :href="
+        'https://map.qq.com/?type=marker&isopeninfowin=1&markertype=1&pointx=' +
+        locationY +
+        '&pointy=' +
+        locationX +
+        '&name=' +
+        label +
+        '&ref=yudao'
+      "
+    >
+      <el-col>
+        <el-row>
+          <img
+            :src="
+              'https://apis.map.qq.com/ws/staticmap/v2/?zoom=10&markers=color:blue|label:A|' +
+              locationX +
+              ',' +
+              locationY +
+              '&key=' +
+              qqMapKey +
+              '&size=250*180'
+            "
+          />
+        </el-row>
+        <el-row>
+          <el-icon><Location /></el-icon>
+          <Icon icon="ep:location" />
+          {{ label }}
+        </el-row>
+      </el-col>
+    </el-link>
+  </div>
+</template>
+<script setup lang="ts" name="WxLocation">
+const props = defineProps({
+  locationX: {
+    required: true,
+    type: Number
+  },
+  locationY: {
+    required: true,
+    type: Number
+  },
+  label: {
+    // 地名
+    required: true,
+    type: String
+  },
+  qqMapKey: {
+    // QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
+    required: false,
+    type: String,
+    default: 'TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E' // 需要自定义
+  }
+})
+
+defineExpose({
+  locationX: props.locationX,
+  locationY: props.locationY,
+  label: props.label,
+  qqMapKey: props.qqMapKey
+})
+</script>
diff --git a/src/views/mp/components/wx-msg/card.scss b/src/views/mp/components/wx-msg/card.scss
new file mode 100644
index 00000000..67ac9219
--- /dev/null
+++ b/src/views/mp/components/wx-msg/card.scss
@@ -0,0 +1,101 @@
+.avue-card{
+  &__item{
+    margin-bottom: 16px;
+    border: 1px solid #e8e8e8;
+    background-color: #fff;
+    box-sizing: border-box;
+    color: rgba(0,0,0,.65);
+    font-size: 14px;
+    font-variant: tabular-nums;
+    line-height: 1.5;
+    list-style: none;
+    font-feature-settings: "tnum";
+    cursor: pointer;
+    height:200px;
+    &:hover{
+      border-color: rgba(0,0,0,.09);
+      box-shadow: 0 2px 8px rgba(0,0,0,.09);
+    }
+    &--add{
+      border:1px dashed #000;
+      width: 100%;
+      color: rgba(0,0,0,.45);
+      background-color: #fff;
+      border-color: #d9d9d9;
+      border-radius: 2px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 16px;
+      i{
+        margin-right: 10px;
+      }
+      &:hover{
+        color: #40a9ff;
+        background-color: #fff;
+        border-color: #40a9ff;
+      }
+    }
+  }
+  &__body{
+    display: flex;
+    padding: 24px;
+  }
+  &__detail{
+    flex:1
+  }
+  &__avatar{
+    width: 48px;
+    height: 48px;
+    border-radius: 48px;
+    overflow: hidden;
+    margin-right: 12px;
+    img{
+      width: 100%;
+      height: 100%;
+    }
+  }
+  &__title{
+    color: rgba(0,0,0,.85);
+    margin-bottom: 12px;
+    font-size: 16px;
+    &:hover{
+      color:#1890ff;
+    }
+  }
+  &__info{
+    color: rgba(0,0,0,.45);
+    display: -webkit-box;
+    -webkit-box-orient: vertical;
+    -webkit-line-clamp: 3;
+    overflow: hidden;
+    height: 64px;
+  }
+  &__menu{
+    display: flex;
+    justify-content:space-around;
+    height: 50px;
+    background: #f7f9fa;
+    color: rgba(0,0,0,.45);
+    text-align: center;
+    line-height: 50px;
+    &:hover{
+      color:#1890ff;
+    }
+  }
+}
+
+/** joolun 额外加的 */
+.avue-comment__main {
+  flex: unset!important;
+  border-radius: 5px!important;
+  margin: 0 8px!important;
+}
+.avue-comment__header {
+  border-top-left-radius: 5px;
+  border-top-right-radius: 5px;
+}
+.avue-comment__body {
+  border-bottom-right-radius: 5px;
+  border-bottom-left-radius: 5px;
+}
diff --git a/src/views/mp/components/wx-msg/comment.scss b/src/views/mp/components/wx-msg/comment.scss
new file mode 100644
index 00000000..3f1341b2
--- /dev/null
+++ b/src/views/mp/components/wx-msg/comment.scss
@@ -0,0 +1,88 @@
+/* 来自 https://github.com/nmxiaowei/avue/blob/master/styles/src/element-ui/comment.scss  */
+.avue-comment{
+  margin-bottom: 30px;
+  display: flex;
+  align-items: flex-start;
+  &--reverse{
+    flex-direction:row-reverse;
+    .avue-comment__main{
+      &:before,&:after{
+        left: auto;
+        right: -8px;
+        border-width: 8px 0 8px 8px;
+      }
+      &:before{
+        border-left-color: #dedede;
+      }
+      &:after{
+        border-left-color: #f8f8f8;
+        margin-right: 1px;
+        margin-left: auto;
+      }
+    }
+  }
+  &__avatar{
+    width: 48px;
+    height: 48px;
+    border-radius: 50%;
+    border: 1px solid transparent;
+    box-sizing: border-box;
+    vertical-align: middle;
+  }
+  &__header{
+    padding: 5px 15px;
+    background: #f8f8f8;
+    border-bottom: 1px solid #eee;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+  &__author{
+    font-weight: 700;
+    font-size: 14px;
+    color: #999;
+  }
+  &__main{
+    flex:1;
+    margin: 0 20px;
+    position: relative;
+    border: 1px solid #dedede;
+    border-radius: 2px;
+    &:before,&:after{
+      position: absolute;
+      top: 10px;
+      left: -8px;
+      right: 100%;
+      width: 0;
+      height: 0;
+      display: block;
+      content: " ";
+      border-color: transparent;
+      border-style: solid solid outset;
+      border-width: 8px 8px 8px 0;
+      pointer-events: none;
+    }
+    &:before {
+      border-right-color: #dedede;
+      z-index: 1;
+    }
+    &:after{
+      border-right-color: #f8f8f8;
+      margin-left: 1px;
+      z-index: 2;
+    }
+  }
+  &__body{
+    padding: 15px;
+    overflow: hidden;
+    background: #fff;
+    font-family: Segoe UI,Lucida Grande,Helvetica,Arial,Microsoft YaHei,FreeSans,Arimo,Droid Sans,wenquanyi micro hei,Hiragino Sans GB,Hiragino Sans GB W3,FontAwesome,sans-serif;color: #333;
+    font-size: 14px;
+  }
+  blockquote{
+    margin:0;
+    font-family: Georgia,Times New Roman,Times,Kai,Kaiti SC,KaiTi,BiauKai,FontAwesome,serif;
+    padding: 1px 0 1px 15px;
+    border-left: 4px solid #ddd;
+  }
+}
diff --git a/src/views/mp/components/wx-msg/main.vue b/src/views/mp/components/wx-msg/main.vue
new file mode 100644
index 00000000..b514a73e
--- /dev/null
+++ b/src/views/mp/components/wx-msg/main.vue
@@ -0,0 +1,338 @@
+<!--
+  - Copyright (C) 2018-2019
+  - All rights reserved, Designed By www.joolun.com
+  芋道源码:
+  ① 移除暂时用不到的 websocket
+  ② 代码优化,补充注释,提升阅读性
+-->
+<template>
+  <div class="msg-main">
+    <div class="msg-div" :id="'msg-div' + nowStr">
+      <!-- 加载更多 -->
+      <div v-loading="loading"></div>
+      <div v-if="!loading">
+        <div class="el-table__empty-block" v-if="loadMore" @click="loadingMore"
+          ><span class="el-table__empty-text">点击加载更多</span></div
+        >
+        <div class="el-table__empty-block" v-if="!loadMore"
+          ><span class="el-table__empty-text">没有更多了</span></div
+        >
+      </div>
+      <!-- 消息列表 -->
+      <div class="execution" v-for="item in list" :key="item.id">
+        <div class="avue-comment" :class="item.sendFrom === 2 ? 'avue-comment--reverse' : ''">
+          <div class="avatar-div">
+            <img
+              :src="item.sendFrom === 1 ? user.avatar : mp.avatar"
+              class="avue-comment__avatar"
+            />
+            <div class="avue-comment__author">{{
+              item.sendFrom === 1 ? user.nickname : mp.nickname
+            }}</div>
+          </div>
+          <div class="avue-comment__main">
+            <div class="avue-comment__header">
+              <div class="avue-comment__create_time">{{ parseTime(item.createTime) }}</div>
+            </div>
+            <div
+              class="avue-comment__body"
+              :style="item.sendFrom === 2 ? 'background: #6BED72;' : ''"
+            >
+              <!-- 【事件】区域 -->
+              <div v-if="item.type === 'event' && item.event === 'subscribe'">
+                <el-tag type="success" size="mini">关注</el-tag>
+              </div>
+              <div v-else-if="item.type === 'event' && item.event === 'unsubscribe'">
+                <el-tag type="danger" size="mini">取消关注</el-tag>
+              </div>
+              <div v-else-if="item.type === 'event' && item.event === 'CLICK'">
+                <el-tag size="mini">点击菜单</el-tag>【{{ item.eventKey }}】
+              </div>
+              <div v-else-if="item.type === 'event' && item.event === 'VIEW'">
+                <el-tag size="mini">点击菜单链接</el-tag>【{{ item.eventKey }}】
+              </div>
+              <div v-else-if="item.type === 'event' && item.event === 'scancode_waitmsg'">
+                <el-tag size="mini">扫码结果</el-tag>【{{ item.eventKey }}】
+              </div>
+              <div v-else-if="item.type === 'event' && item.event === 'scancode_push'">
+                <el-tag size="mini">扫码结果</el-tag>【{{ item.eventKey }}】
+              </div>
+              <div v-else-if="item.type === 'event' && item.event === 'pic_sysphoto'">
+                <el-tag size="mini">系统拍照发图</el-tag>
+              </div>
+              <div v-else-if="item.type === 'event' && item.event === 'pic_photo_or_album'">
+                <el-tag size="mini">拍照或者相册</el-tag>
+              </div>
+              <div v-else-if="item.type === 'event' && item.event === 'pic_weixin'">
+                <el-tag size="mini">微信相册</el-tag>
+              </div>
+              <div v-else-if="item.type === 'event' && item.event === 'location_select'">
+                <el-tag size="mini">选择地理位置</el-tag>
+              </div>
+              <div v-else-if="item.type === 'event'">
+                <el-tag type="danger" size="mini">未知事件类型</el-tag>
+              </div>
+              <!-- 【消息】区域 -->
+              <div v-else-if="item.type === 'text'">{{ item.content }}</div>
+              <div v-else-if="item.type === 'voice'">
+                <wx-voice-player :url="item.mediaUrl" :content="item.recognition" />
+              </div>
+              <div v-else-if="item.type === 'image'">
+                <a target="_blank" :href="item.mediaUrl">
+                  <img :src="item.mediaUrl" style="width: 100px" />
+                </a>
+              </div>
+              <div
+                v-else-if="item.type === 'video' || item.type === 'shortvideo'"
+                style="text-align: center"
+              >
+                <wx-video-player :url="item.mediaUrl" />
+              </div>
+              <div v-else-if="item.type === 'link'" class="avue-card__detail">
+                <el-link type="success" :underline="false" target="_blank" :href="item.url">
+                  <div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div>
+                </el-link>
+                <div class="avue-card__info" style="height: unset">{{ item.description }}</div>
+              </div>
+              <!-- TODO 芋艿:待完善 -->
+              <div v-else-if="item.type === 'location'">
+                <wx-location
+                  :label="item.label"
+                  :location-y="item.locationY"
+                  :location-x="item.locationX"
+                />
+              </div>
+              <div v-else-if="item.type === 'news'" style="width: 300px">
+                <!-- TODO 芋艿:待测试;详情页也存在类似的情况 -->
+                <wx-news :articles="item.articles" />
+              </div>
+              <div v-else-if="item.type === 'music'">
+                <wx-music
+                  :title="item.title"
+                  :description="item.description"
+                  :thumb-media-url="item.thumbMediaUrl"
+                  :music-url="item.musicUrl"
+                  :hq-music-url="item.hqMusicUrl"
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="msg-send" v-loading="sendLoading">
+      <wx-reply-select ref="replySelect" :objData="objData" />
+      <el-button type="success" size="small" class="send-but" @click="sendMsg">发送(S)</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getMessagePage, sendMessage } from '@/api/mp/message'
+import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
+import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
+import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
+import WxNews from '@/views/mp/components/wx-news/main.vue'
+import WxLocation from '@/views/mp/components/wx-location/main.vue'
+import WxMusic from '@/views/mp/components/wx-music/main.vue'
+import { getUser } from '@/api/mp/mpuser'
+
+export default {
+  name: 'WxMsg',
+  components: {
+    WxReplySelect,
+    WxVideoPlayer,
+    WxVoicePlayer,
+    WxNews,
+    WxLocation,
+    WxMusic
+  },
+  props: {
+    userId: {
+      type: Number,
+      required: true
+    }
+  },
+  data() {
+    return {
+      nowStr: new Date().getTime(), // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处
+      loading: false, // 消息列表是否正在加载中
+      loadMore: true, // 是否可以加载更多
+      list: [], // 消息列表
+      queryParams: {
+        pageNo: 1, // 当前页数
+        pageSize: 14, // 每页显示多少条
+        accountId: undefined
+      },
+      user: {
+        // 由于微信不再提供昵称,直接使用“用户”展示
+        nickname: '用户',
+        avatar: require('@/assets/images/profile.jpg'),
+        accountId: 0 // 公众号账号编号
+      },
+      mp: {
+        nickname: '公众号',
+        avatar: require('@/assets/images/wechat.png')
+      },
+
+      // ========= 消息发送 =========
+      sendLoading: false, // 发送消息是否加载中
+      objData: {
+        // 微信发送消息
+        type: 'text'
+      }
+    }
+  },
+  created() {
+    // 获得用户信息
+    getUser(this.userId).then((response) => {
+      this.user.nickname =
+        response.data.nickname && response.data.nickname.length > 0
+          ? response.data.nickname
+          : this.user.nickname
+      this.user.avatar =
+        response.data.avatar && this.user.avatar.length > 0
+          ? response.data.avatar
+          : this.user.avatar
+      this.user.accountId = response.data.accountId
+      // 设置公众号账号编号
+      this.queryParams.accountId = response.data.accountId
+      this.objData.accountId = response.data.accountId
+
+      // 加载消息
+      console.log(this.queryParams)
+      this.refreshChange()
+    })
+  },
+  methods: {
+    sendMsg() {
+      if (!this.objData) {
+        return
+      }
+      // 公众号限制:客服消息,公众号只允许发送一条
+      if (this.objData.type === 'news' && this.objData.articles.length > 1) {
+        this.objData.articles = [this.objData.articles[0]]
+        this.$message({
+          showClose: true,
+          message: '图文消息条数限制在 1 条以内,已默认发送第一条',
+          type: 'success'
+        })
+      }
+
+      // 执行发送
+      this.sendLoading = true
+      sendMessage(
+        Object.assign(
+          {
+            userId: this.userId
+          },
+          {
+            ...this.objData
+          }
+        )
+      )
+        .then((response) => {
+          this.sendLoading = false
+          // 添加到消息列表,并滚动
+          this.list = [...this.list, ...[response.data]]
+          this.scrollToBottom()
+          // 重置 objData 状态
+          this.$refs['replySelect'].deleteObj() // 重置,避免 tab 的数据未清理
+        })
+        .catch(() => {
+          this.sendLoading = false
+        })
+    },
+    loadingMore() {
+      this.queryParams.pageNo++
+      this.getPage(this.queryParams)
+    },
+    getPage(page, params) {
+      this.loading = true
+      getMessagePage(
+        Object.assign(
+          {
+            pageNo: page.pageNo,
+            pageSize: page.pageSize,
+            userId: this.userId,
+            accountId: page.accountId
+          },
+          params
+        )
+      ).then((response) => {
+        // 计算当前的滚动高度
+        const msgDiv = document.getElementById('msg-div' + this.nowStr)
+        let scrollHeight = 0
+        if (msgDiv) {
+          scrollHeight = msgDiv.scrollHeight
+        }
+
+        // 处理数据
+        const data = response.data.list.reverse()
+        this.list = [...data, ...this.list]
+        this.loading = false
+        if (data.length < this.queryParams.pageSize || data.length === 0) {
+          this.loadMore = false
+        }
+        this.queryParams.pageNo = page.pageNo
+        this.queryParams.pageSize = page.pageSize
+
+        // 滚动到原来的位置
+        if (this.queryParams.pageNo === 1) {
+          // 定位到消息底部
+          this.scrollToBottom()
+        } else if (data.length !== 0) {
+          // 定位滚动条
+          this.$nextTick(() => {
+            if (scrollHeight !== 0) {
+              msgDiv.scrollTop =
+                document.getElementById('msg-div' + this.nowStr).scrollHeight - scrollHeight - 100
+            }
+          })
+        }
+      })
+    },
+    /**
+     * 刷新回调
+     */
+    refreshChange() {
+      this.getPage(this.queryParams)
+    },
+    /** 定位到消息底部 */
+    scrollToBottom: function () {
+      this.$nextTick(() => {
+        let div = document.getElementById('msg-div' + this.nowStr)
+        div.scrollTop = div.scrollHeight
+      })
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+/* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc  */
+@import './comment.scss';
+@import './card.scss';
+
+.msg-main {
+  margin-top: -30px;
+  padding: 10px;
+}
+.msg-div {
+  height: 50vh;
+  overflow: auto;
+  background-color: #eaeaea;
+  margin-left: 10px;
+  margin-right: 10px;
+}
+.msg-send {
+  padding: 10px;
+}
+.avatar-div {
+  text-align: center;
+  width: 80px;
+}
+.send-but {
+  float: right;
+  margin-top: 8px !important;
+}
+</style>
diff --git a/src/views/mp/components/wx-music/main.vue b/src/views/mp/components/wx-music/main.vue
new file mode 100644
index 00000000..52555f15
--- /dev/null
+++ b/src/views/mp/components/wx-music/main.vue
@@ -0,0 +1,60 @@
+<!--
+  【微信消息 - 音乐】
+-->
+<template>
+  <div>
+    <el-link
+      type="success"
+      :underline="false"
+      target="_blank"
+      :href="hqMusicUrl ? hqMusicUrl : musicUrl"
+    >
+      <div
+        class="avue-card__body"
+        style="padding: 10px; background-color: #fff; border-radius: 5px"
+      >
+        <div class="avue-card__avatar">
+          <img :src="thumbMediaUrl" alt="" />
+        </div>
+        <div class="avue-card__detail">
+          <div class="avue-card__title" style="margin-bottom: unset">{{ title }}</div>
+          <div class="avue-card__info" style="height: unset">{{ description }}</div>
+        </div>
+      </div>
+    </el-link>
+  </div>
+</template>
+
+<script setup lang="ts" name="WxMusic">
+const props = defineProps({
+  title: {
+    required: false,
+    type: String
+  },
+  description: {
+    required: false,
+    type: String
+  },
+  musicUrl: {
+    required: false,
+    type: String
+  },
+  hqMusicUrl: {
+    required: false,
+    type: String
+  },
+  thumbMediaUrl: {
+    required: true,
+    type: String
+  }
+})
+
+defineExpose({
+  musicUrl: props.musicUrl
+})
+</script>
+
+<style lang="scss" scoped>
+/* 因为 joolun 实现依赖 avue 组件,该页面使用了 card.scc  */
+@import '../wx-msg/card.scss';
+</style>
diff --git a/src/views/mp/components/wx-news/main.vue b/src/views/mp/components/wx-news/main.vue
new file mode 100644
index 00000000..d08e2813
--- /dev/null
+++ b/src/views/mp/components/wx-news/main.vue
@@ -0,0 +1,107 @@
+<!--
+  - Copyright (C) 2018-2019
+  - All rights reserved, Designed By www.joolun.com
+  【微信消息 - 图文】
+  芋道源码:
+  ① 代码优化,补充注释,提升阅读性
+-->
+<template>
+  <div class="news-home">
+    <div v-for="(article, index) in articles" :key="index" class="news-div">
+      <!-- 头条 -->
+      <a target="_blank" :href="article.url" v-if="index === 0">
+        <div class="news-main">
+          <div class="news-content">
+            <el-image
+              class="material-img"
+              style="width: 100%; height: 120px"
+              :src="article.picUrl"
+            />
+            <div class="news-content-title">
+              <span>{{ article.title }}</span>
+            </div>
+          </div>
+        </div>
+      </a>
+      <!-- 二条/三条等等 -->
+      <a target="_blank" :href="article.url" v-else>
+        <div class="news-main-item">
+          <div class="news-content-item">
+            <div class="news-content-item-title">{{ article.title }}</div>
+            <div class="news-content-item-img">
+              <img class="material-img" :src="article.picUrl" height="100%" />
+            </div>
+          </div>
+        </div>
+      </a>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+const props = defineProps({
+  articles: {
+    type: Array,
+    default: () => null
+  }
+})
+
+defineExpose({
+  articles: props.articles
+})
+</script>
+
+<style lang="scss" scoped>
+.news-home {
+  background-color: #ffffff;
+  width: 100%;
+  margin: auto;
+}
+.news-main {
+  width: 100%;
+  margin: auto;
+}
+.news-content {
+  background-color: #acadae;
+  width: 100%;
+  position: relative;
+}
+.news-content-title {
+  display: inline-block;
+  font-size: 12px;
+  color: #ffffff;
+  position: absolute;
+  left: 0;
+  bottom: 0;
+  background-color: black;
+  width: 98%;
+  padding: 1%;
+  opacity: 0.65;
+  white-space: normal;
+  box-sizing: unset !important;
+}
+.news-main-item {
+  background-color: #ffffff;
+  padding: 5px 0;
+  border-top: 1px solid #eaeaea;
+}
+.news-content-item {
+  position: relative;
+}
+.news-content-item-title {
+  display: inline-block;
+  font-size: 10px;
+  width: 70%;
+  margin-left: 1%;
+  white-space: normal;
+}
+.news-content-item-img {
+  display: inline-block;
+  width: 25%;
+  background-color: #acadae;
+  margin-right: 1%;
+}
+.material-img {
+  width: 100%;
+}
+</style>
diff --git a/src/views/mp/components/wx-reply/main.vue b/src/views/mp/components/wx-reply/main.vue
new file mode 100644
index 00000000..57a3cd84
--- /dev/null
+++ b/src/views/mp/components/wx-reply/main.vue
@@ -0,0 +1,634 @@
+<!--&lt;!&ndash;-->
+<!--  - Copyright (C) 2018-2019-->
+<!--  - All rights reserved, Designed By www.joolun.com-->
+<!--  芋道源码:-->
+<!--  ① 移除多余的 rep 为前缀的变量,让 message 消息更简单-->
+<!--  ② 代码优化,补充注释,提升阅读性-->
+<!--  ③ 优化消息的临时缓存策略,发送消息时,只清理被发送消息的 tab,不会强制切回到 text 输入-->
+<!--  ④ 支持发送【视频】消息时,支持新建视频-->
+<!--&ndash;&gt;-->
+<!--<template>-->
+<!--  <el-tabs type="border-card" v-model="objData.type" @tab-click="handleClick">-->
+<!--    &lt;!&ndash; 类型 1:文本 &ndash;&gt;-->
+<!--    <el-tab-pane name="text">-->
+<!--      <span slot="label"><i class="el-icon-document"></i> 文本</span>-->
+<!--      <el-input-->
+<!--        type="textarea"-->
+<!--        :rows="5"-->
+<!--        placeholder="请输入内容"-->
+<!--        v-model="objData.content"-->
+<!--        @input="inputContent"-->
+<!--      />-->
+<!--    </el-tab-pane>-->
+<!--    &lt;!&ndash; 类型 2:图片 &ndash;&gt;-->
+<!--    <el-tab-pane name="image">-->
+<!--      <span slot="label"><i class="el-icon-picture"></i> 图片</span>-->
+<!--      <el-row>-->
+<!--        &lt;!&ndash; 情况一:已经选择好素材、或者上传好图片 &ndash;&gt;-->
+<!--        <div class="select-item" v-if="objData.url">-->
+<!--          <img class="material-img" :src="objData.url" />-->
+<!--          <p class="item-name" v-if="objData.name">{{ objData.name }}</p>-->
+<!--          <el-row class="ope-row">-->
+<!--            <el-button type="danger" icon="el-icon-delete" circle @click="deleteObj" />-->
+<!--          </el-row>-->
+<!--        </div>-->
+<!--        &lt;!&ndash; 情况二:未做完上述操作 &ndash;&gt;-->
+<!--        <div v-else>-->
+<!--          <el-row style="text-align: center">-->
+<!--            &lt;!&ndash; 选择素材 &ndash;&gt;-->
+<!--            <el-col :span="12" class="col-select">-->
+<!--              <el-button type="success" @click="openMaterial">-->
+<!--                素材库选择<i class="el-icon-circle-check el-icon&#45;&#45;right"></i>-->
+<!--              </el-button>-->
+<!--              <el-dialog-->
+<!--                title="选择图片"-->
+<!--                v-model:visible="dialogImageVisible"-->
+<!--                width="90%"-->
+<!--                append-to-body-->
+<!--              >-->
+<!--                <wx-material-select :obj-data="objData" @selectMaterial="selectMaterial" />-->
+<!--              </el-dialog>-->
+<!--            </el-col>-->
+<!--            &lt;!&ndash; 文件上传 &ndash;&gt;-->
+<!--            <el-col :span="12" class="col-add">-->
+<!--              <el-upload-->
+<!--                :action="actionUrl"-->
+<!--                :headers="headers"-->
+<!--                multiple-->
+<!--                :limit="1"-->
+<!--                :file-list="fileList"-->
+<!--                :data="uploadData"-->
+<!--                :before-upload="beforeImageUpload"-->
+<!--                :on-success="handleUploadSuccess"-->
+<!--              >-->
+<!--                <el-button type="primary">上传图片</el-button>-->
+<!--                <div slot="tip" class="el-upload__tip"-->
+<!--                  >支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div-->
+<!--                >-->
+<!--              </el-upload>-->
+<!--            </el-col>-->
+<!--          </el-row>-->
+<!--        </div>-->
+<!--      </el-row>-->
+<!--    </el-tab-pane>-->
+<!--    &lt;!&ndash; 类型 3:语音 &ndash;&gt;-->
+<!--    <el-tab-pane name="voice">-->
+<!--      <span slot="label"><i class="el-icon-phone"></i> 语音</span>-->
+<!--      <el-row>-->
+<!--        <div class="select-item2" v-if="objData.url">-->
+<!--          <p class="item-name">{{ objData.name }}</p>-->
+<!--          <div class="item-infos">-->
+<!--            <wx-voice-player :url="objData.url" />-->
+<!--          </div>-->
+<!--          <el-row class="ope-row">-->
+<!--            <el-button type="danger" icon="el-icon-delete" circle @click="deleteObj" />-->
+<!--          </el-row>-->
+<!--        </div>-->
+<!--        <div v-else>-->
+<!--          <el-row style="text-align: center">-->
+<!--            &lt;!&ndash; 选择素材 &ndash;&gt;-->
+<!--            <el-col :span="12" class="col-select">-->
+<!--              <el-button type="success" @click="openMaterial">-->
+<!--                素材库选择<i class="el-icon-circle-check el-icon&#45;&#45;right"></i>-->
+<!--              </el-button>-->
+<!--              <el-dialog-->
+<!--                title="选择语音"-->
+<!--                v-model:visible="dialogVoiceVisible"-->
+<!--                width="90%"-->
+<!--                append-to-body-->
+<!--              >-->
+<!--                <WxMaterialSelect :objData="objData" @selectMaterial="selectMaterial" />-->
+<!--              </el-dialog>-->
+<!--            </el-col>-->
+<!--            &lt;!&ndash; 文件上传 &ndash;&gt;-->
+<!--            <el-col :span="12" class="col-add">-->
+<!--              <el-upload-->
+<!--                :action="actionUrl"-->
+<!--                :headers="headers"-->
+<!--                multiple-->
+<!--                :limit="1"-->
+<!--                :file-list="fileList"-->
+<!--                :data="uploadData"-->
+<!--                :before-upload="beforeVoiceUpload"-->
+<!--                :on-success="handleUploadSuccess"-->
+<!--              >-->
+<!--                <el-button type="primary">点击上传</el-button>-->
+<!--                <div slot="tip" class="el-upload__tip"-->
+<!--                  >格式支持 mp3/wma/wav/amr,文件大小不超过 2M,播放长度不超过 60s</div-->
+<!--                >-->
+<!--              </el-upload>-->
+<!--            </el-col>-->
+<!--          </el-row>-->
+<!--        </div>-->
+<!--      </el-row>-->
+<!--    </el-tab-pane>-->
+<!--    &lt;!&ndash; 类型 4:视频 &ndash;&gt;-->
+<!--    <el-tab-pane name="video">-->
+<!--      <span slot="label"><i class="el-icon-share"></i> 视频</span>-->
+<!--      <el-row>-->
+<!--        <el-input v-model="objData.title" placeholder="请输入标题" @input="inputContent" />-->
+<!--        <div style="margin: 20px 0"></div>-->
+<!--        <el-input v-model="objData.description" placeholder="请输入描述" @input="inputContent" />-->
+<!--        <div style="margin: 20px 0"></div>-->
+<!--        <div style="text-align: center">-->
+<!--          <wx-video-player v-if="objData.url" :url="objData.url" />-->
+<!--        </div>-->
+<!--        <div style="margin: 20px 0"></div>-->
+<!--        <el-row style="text-align: center">-->
+<!--          &lt;!&ndash; 选择素材 &ndash;&gt;-->
+<!--          <el-col :span="12">-->
+<!--            <el-button type="success" @click="openMaterial">-->
+<!--              素材库选择<i class="el-icon-circle-check el-icon&#45;&#45;right"></i>-->
+<!--            </el-button>-->
+<!--            <el-dialog-->
+<!--              title="选择视频"-->
+<!--              v-model:visible="dialogVideoVisible"-->
+<!--              width="90%"-->
+<!--              append-to-body-->
+<!--            >-->
+<!--              <wx-material-select :objData="objData" @selectMaterial="selectMaterial" />-->
+<!--            </el-dialog>-->
+<!--          </el-col>-->
+<!--          &lt;!&ndash; 文件上传 &ndash;&gt;-->
+<!--          <el-col :span="12">-->
+<!--            <el-upload-->
+<!--              :action="actionUrl"-->
+<!--              :headers="headers"-->
+<!--              multiple-->
+<!--              :limit="1"-->
+<!--              :file-list="fileList"-->
+<!--              :data="uploadData"-->
+<!--              :before-upload="beforeVideoUpload"-->
+<!--              :on-success="handleUploadSuccess"-->
+<!--            >-->
+<!--              <el-button type="primary"-->
+<!--                >新建视频<i class="el-icon-upload el-icon&#45;&#45;right"></i-->
+<!--              ></el-button>-->
+<!--            </el-upload>-->
+<!--          </el-col>-->
+<!--        </el-row>-->
+<!--      </el-row>-->
+<!--    </el-tab-pane>-->
+<!--    &lt;!&ndash; 类型 5:图文 &ndash;&gt;-->
+<!--    <el-tab-pane name="news">-->
+<!--      <span slot="label"><i class="el-icon-news"></i> 图文</span>-->
+<!--      <el-row>-->
+<!--        <div class="select-item" v-if="objData.articles">-->
+<!--          <wx-news :articles="objData.articles" />-->
+<!--          <el-row class="ope-row">-->
+<!--            <el-button type="danger" icon="el-icon-delete" circle @click="deleteObj" />-->
+<!--          </el-row>-->
+<!--        </div>-->
+<!--        &lt;!&ndash; 选择素材 &ndash;&gt;-->
+<!--        <div v-if="!objData.content">-->
+<!--          <el-row style="text-align: center">-->
+<!--            <el-col :span="24">-->
+<!--              <el-button type="success" @click="openMaterial"-->
+<!--                >{{ newsType === '1' ? '选择已发布图文' : '选择草稿箱图文'-->
+<!--                }}<i class="el-icon-circle-check el-icon&#45;&#45;right"></i-->
+<!--              ></el-button>-->
+<!--            </el-col>-->
+<!--          </el-row>-->
+<!--        </div>-->
+<!--        <el-dialog title="选择图文" v-model:visible="dialogNewsVisible" width="90%" append-to-body>-->
+<!--          <wx-material-select-->
+<!--            :objData="objData"-->
+<!--            @selectMaterial="selectMaterial"-->
+<!--            :newsType="newsType"-->
+<!--          />-->
+<!--        </el-dialog>-->
+<!--      </el-row>-->
+<!--    </el-tab-pane>-->
+<!--    &lt;!&ndash; 类型 6:音乐 &ndash;&gt;-->
+<!--    <el-tab-pane name="music">-->
+<!--      <span slot="label"><i class="el-icon-service"></i> 音乐</span>-->
+<!--      <el-row>-->
+<!--        <el-col :span="6">-->
+<!--          <div class="thumb-div">-->
+<!--            <img style="width: 100px" v-if="objData.thumbMediaUrl" :src="objData.thumbMediaUrl" />-->
+<!--            <i v-else class="el-icon-plus avatar-uploader-icon"></i>-->
+<!--            <div class="thumb-but">-->
+<!--              <el-upload-->
+<!--                :action="actionUrl"-->
+<!--                :headers="headers"-->
+<!--                multiple-->
+<!--                :limit="1"-->
+<!--                :file-list="fileList"-->
+<!--                :data="uploadData"-->
+<!--                :before-upload="beforeThumbImageUpload"-->
+<!--                :on-success="handleUploadSuccess"-->
+<!--              >-->
+<!--                <el-button slot="trigger" size="mini" type="text">本地上传</el-button>-->
+<!--                <el-button size="mini" type="text" @click="openMaterial" style="margin-left: 5px"-->
+<!--                  >素材库选择</el-button-->
+<!--                >-->
+<!--              </el-upload>-->
+<!--            </div>-->
+<!--          </div>-->
+<!--          <el-dialog-->
+<!--            title="选择图片"-->
+<!--            v-model:visible="dialogThumbVisible"-->
+<!--            width="80%"-->
+<!--            append-to-body-->
+<!--          >-->
+<!--            <wx-material-select-->
+<!--              :objData="{ type: 'image', accountId: objData.accountId }"-->
+<!--              @selectMaterial="selectMaterial"-->
+<!--            />-->
+<!--          </el-dialog>-->
+<!--        </el-col>-->
+<!--        <el-col :span="18">-->
+<!--          <el-input v-model="objData.title" placeholder="请输入标题" @input="inputContent" />-->
+<!--          <div style="margin: 20px 0"></div>-->
+<!--          <el-input v-model="objData.description" placeholder="请输入描述" @input="inputContent" />-->
+<!--        </el-col>-->
+<!--      </el-row>-->
+<!--      <div style="margin: 20px 0"></div>-->
+<!--      <el-input v-model="objData.musicUrl" placeholder="请输入音乐链接" @input="inputContent" />-->
+<!--      <div style="margin: 20px 0"></div>-->
+<!--      <el-input-->
+<!--        v-model="objData.hqMusicUrl"-->
+<!--        placeholder="请输入高质量音乐链接"-->
+<!--        @input="inputContent"-->
+<!--      />-->
+<!--    </el-tab-pane>-->
+<!--  </el-tabs>-->
+<!--</template>-->
+
+<!--<script>-->
+<!--import WxNews from '@/views/mp/components/wx-news/main.vue'-->
+<!--import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'-->
+<!--import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'-->
+<!--import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'-->
+
+<!--import { getAccessToken } from '@/utils/auth'-->
+
+<!--export default {-->
+<!--  name: 'WxReplySelect',-->
+<!--  components: {-->
+<!--    WxNews,-->
+<!--    WxMaterialSelect,-->
+<!--    WxVoicePlayer,-->
+<!--    WxVideoPlayer-->
+<!--  },-->
+<!--  props: {-->
+<!--    objData: {-->
+<!--      // 消息对象。-->
+<!--      type: Object, // 设置为 Object 的原因,方便属性的传递-->
+<!--      required: true-->
+<!--    },-->
+<!--    newsType: {-->
+<!--      // 图文类型:1、已发布图文;2、草稿箱图文-->
+<!--      type: String,-->
+<!--      default: '1'-->
+<!--    }-->
+<!--  },-->
+<!--  data() {-->
+<!--    return {-->
+<!--      tempPlayerObj: {-->
+<!--        type: '2'-->
+<!--      },-->
+
+<!--      tempObj: new Map().set(-->
+<!--        // 临时缓存,切换消息类型的 tab 的时候,可以保存对应的数据;-->
+<!--        this.objData.type, // 消息类型-->
+<!--        Object.assign({}, this.objData)-->
+<!--      ), // 消息内容-->
+
+<!--      // ========== 素材选择的弹窗,是否可见 ==========-->
+<!--      dialogNewsVisible: false, // 图文-->
+<!--      dialogImageVisible: false, // 图片-->
+<!--      dialogVoiceVisible: false, // 语音-->
+<!--      dialogVideoVisible: false, // 视频-->
+<!--      dialogThumbVisible: false, // 缩略图-->
+
+<!--      // ========== 文件上传(图片、语音、视频) ==========-->
+<!--      fileList: [], // 文件列表-->
+<!--      uploadData: {-->
+<!--        accountId: undefined,-->
+<!--        type: this.objData.type,-->
+<!--        title: '',-->
+<!--        introduction: ''-->
+<!--      },-->
+<!--      actionUrl: process.env.VUE_APP_BASE_API + '/admin-api/mp/material/upload-temporary',-->
+<!--      headers: { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部-->
+<!--    }-->
+<!--  },-->
+<!--  methods: {-->
+<!--    beforeThumbImageUpload(file) {-->
+<!--      const isType =-->
+<!--        file.type === 'image/jpeg' ||-->
+<!--        file.type === 'image/png' ||-->
+<!--        file.type === 'image/gif' ||-->
+<!--        file.type === 'image/bmp' ||-->
+<!--        file.type === 'image/jpg'-->
+<!--      if (!isType) {-->
+<!--        this.$message.error('上传图片格式不对!')-->
+<!--        return false-->
+<!--      }-->
+<!--      const isLt = file.size / 1024 / 1024 < 2-->
+<!--      if (!isLt) {-->
+<!--        this.$message.error('上传图片大小不能超过 2M!')-->
+<!--        return false-->
+<!--      }-->
+<!--      this.uploadData.accountId = this.objData.accountId-->
+<!--      return true-->
+<!--    },-->
+<!--    beforeVoiceUpload(file) {-->
+<!--      // 校验格式-->
+<!--      const isType =-->
+<!--        file.type === 'audio/mp3' ||-->
+<!--        file.type === 'audio/mpeg' ||-->
+<!--        file.type === 'audio/wma' ||-->
+<!--        file.type === 'audio/wav' ||-->
+<!--        file.type === 'audio/amr'-->
+<!--      if (!isType) {-->
+<!--        this.$message.error('上传语音格式不对!' + file.type)-->
+<!--        return false-->
+<!--      }-->
+<!--      // 校验大小-->
+<!--      const isLt = file.size / 1024 / 1024 < 2-->
+<!--      if (!isLt) {-->
+<!--        this.$message.error('上传语音大小不能超过 2M!')-->
+<!--        return false-->
+<!--      }-->
+<!--      this.uploadData.accountId = this.objData.accountId-->
+<!--      return true-->
+<!--    },-->
+<!--    beforeImageUpload(file) {-->
+<!--      // 校验格式-->
+<!--      const isType =-->
+<!--        file.type === 'image/jpeg' ||-->
+<!--        file.type === 'image/png' ||-->
+<!--        file.type === 'image/gif' ||-->
+<!--        file.type === 'image/bmp' ||-->
+<!--        file.type === 'image/jpg'-->
+<!--      if (!isType) {-->
+<!--        this.$message.error('上传图片格式不对!')-->
+<!--        return false-->
+<!--      }-->
+<!--      // 校验大小-->
+<!--      const isLt = file.size / 1024 / 1024 < 2-->
+<!--      if (!isLt) {-->
+<!--        this.$message.error('上传图片大小不能超过 2M!')-->
+<!--        return false-->
+<!--      }-->
+<!--      this.uploadData.accountId = this.objData.accountId-->
+<!--      return true-->
+<!--    },-->
+<!--    beforeVideoUpload(file) {-->
+<!--      // 校验格式-->
+<!--      const isType = file.type === 'video/mp4'-->
+<!--      if (!isType) {-->
+<!--        this.$message.error('上传视频格式不对!')-->
+<!--        return false-->
+<!--      }-->
+<!--      // 校验大小-->
+<!--      const isLt = file.size / 1024 / 1024 < 10-->
+<!--      if (!isLt) {-->
+<!--        this.$message.error('上传视频大小不能超过 10M!')-->
+<!--        return false-->
+<!--      }-->
+<!--      this.uploadData.accountId = this.objData.accountId-->
+<!--      return true-->
+<!--    },-->
+<!--    handleUploadSuccess(response, file, fileList) {-->
+<!--      if (response.code !== 0) {-->
+<!--        this.$message.error('上传出错:' + response.msg)-->
+<!--        return false-->
+<!--      }-->
+
+<!--      // 清空上传时的各种数据-->
+<!--      this.fileList = []-->
+<!--      this.uploadData.title = ''-->
+<!--      this.uploadData.introduction = ''-->
+
+<!--      // 上传好的文件,本质是个素材,所以可以进行选中-->
+<!--      let item = response.data-->
+<!--      this.selectMaterial(item)-->
+<!--    },-->
+<!--    /**-->
+<!--     * 切换消息类型的 tab-->
+<!--     *-->
+<!--     * @param tab tab-->
+<!--     */-->
+<!--    handleClick(tab) {-->
+<!--      // 设置后续文件上传的文件类型-->
+<!--      this.uploadData.type = this.objData.type-->
+<!--      if (this.uploadData.type === 'music') {-->
+<!--        // 【音乐】上传的是缩略图-->
+<!--        this.uploadData.type = 'thumb'-->
+<!--      }-->
+
+<!--      // 从 tempObj 临时缓存中,获取对应的数据,并设置回 objData-->
+<!--      let tempObjItem = this.tempObj.get(this.objData.type)-->
+<!--      if (tempObjItem) {-->
+<!--        this.objData.content = tempObjItem.content ? tempObjItem.content : null-->
+<!--        this.objData.mediaId = tempObjItem.mediaId ? tempObjItem.mediaId : null-->
+<!--        this.objData.url = tempObjItem.url ? tempObjItem.url : null-->
+<!--        this.objData.name = tempObjItem.url ? tempObjItem.name : null-->
+<!--        this.objData.title = tempObjItem.title ? tempObjItem.title : null-->
+<!--        this.objData.description = tempObjItem.description ? tempObjItem.description : null-->
+<!--        return-->
+<!--      }-->
+<!--      // 如果获取不到,需要把 objData 复原-->
+<!--      // 必须使用 $set 赋值,不然 input 无法输入内容-->
+<!--      this.$set(this.objData, 'content', '')-->
+<!--      this.$delete(this.objData, 'mediaId')-->
+<!--      this.$delete(this.objData, 'url')-->
+<!--      this.$set(this.objData, 'title', '')-->
+<!--      this.$set(this.objData, 'description', '')-->
+<!--    },-->
+<!--    /**-->
+<!--     * 选择素材,将设置设置到 objData 变量-->
+<!--     *-->
+<!--     * @param item 素材-->
+<!--     */-->
+<!--    selectMaterial(item) {-->
+<!--      // 选择好素材,所以隐藏弹窗-->
+<!--      this.closeMaterial()-->
+
+<!--      // 创建 tempObjItem 对象,并设置对应的值-->
+<!--      let tempObjItem = {}-->
+<!--      tempObjItem.type = this.objData.type-->
+<!--      if (this.objData.type === 'news') {-->
+<!--        tempObjItem.articles = item.content.newsItem-->
+<!--        this.objData.articles = item.content.newsItem-->
+<!--      } else if (this.objData.type === 'music') {-->
+<!--        // 音乐需要特殊处理,因为选择的是图片的缩略图-->
+<!--        tempObjItem.thumbMediaId = item.mediaId-->
+<!--        this.objData.thumbMediaId = item.mediaId-->
+<!--        tempObjItem.thumbMediaUrl = item.url-->
+<!--        this.objData.thumbMediaUrl = item.url-->
+<!--        // title、introduction、musicUrl、hqMusicUrl:从 objData 到 tempObjItem,避免上传素材后,被覆盖掉-->
+<!--        tempObjItem.title = this.objData.title || ''-->
+<!--        tempObjItem.introduction = this.objData.introduction || ''-->
+<!--        tempObjItem.musicUrl = this.objData.musicUrl || ''-->
+<!--        tempObjItem.hqMusicUrl = this.objData.hqMusicUrl || ''-->
+<!--      } else if (this.objData.type === 'image' || this.objData.type === 'voice') {-->
+<!--        tempObjItem.mediaId = item.mediaId-->
+<!--        this.objData.mediaId = item.mediaId-->
+<!--        tempObjItem.url = item.url-->
+<!--        this.objData.url = item.url-->
+<!--        tempObjItem.name = item.name-->
+<!--        this.objData.name = item.name-->
+<!--      } else if (this.objData.type === 'video') {-->
+<!--        tempObjItem.mediaId = item.mediaId-->
+<!--        this.objData.mediaId = item.mediaId-->
+<!--        tempObjItem.url = item.url-->
+<!--        this.objData.url = item.url-->
+<!--        tempObjItem.name = item.name-->
+<!--        this.objData.name = item.name-->
+<!--        // title、introduction:从 item 到 tempObjItem,因为素材里有 title、introduction-->
+<!--        if (item.title) {-->
+<!--          this.objData.title = item.title || ''-->
+<!--          tempObjItem.title = item.title || ''-->
+<!--        }-->
+<!--        if (item.introduction) {-->
+<!--          this.objData.description = item.introduction || '' // 消息使用的是 description,素材使用的是 introduction,所以转换下-->
+<!--          tempObjItem.description = item.introduction || ''-->
+<!--        }-->
+<!--      } else if (this.objData.type === 'text') {-->
+<!--        this.objData.content = item.content || ''-->
+<!--      }-->
+<!--      // 最终设置到临时缓存-->
+<!--      this.tempObj.set(this.objData.type, tempObjItem)-->
+<!--    },-->
+<!--    openMaterial() {-->
+<!--      if (this.objData.type === 'news') {-->
+<!--        this.dialogNewsVisible = true-->
+<!--      } else if (this.objData.type === 'image') {-->
+<!--        this.dialogImageVisible = true-->
+<!--      } else if (this.objData.type === 'voice') {-->
+<!--        this.dialogVoiceVisible = true-->
+<!--      } else if (this.objData.type === 'video') {-->
+<!--        this.dialogVideoVisible = true-->
+<!--      } else if (this.objData.type === 'music') {-->
+<!--        this.dialogThumbVisible = true-->
+<!--      }-->
+<!--    },-->
+<!--    closeMaterial() {-->
+<!--      this.dialogNewsVisible = false-->
+<!--      this.dialogImageVisible = false-->
+<!--      this.dialogVoiceVisible = false-->
+<!--      this.dialogVideoVisible = false-->
+<!--      this.dialogThumbVisible = false-->
+<!--    },-->
+<!--    deleteObj() {-->
+<!--      if (this.objData.type === 'news') {-->
+<!--        this.$delete(this.objData, 'articles')-->
+<!--      } else if (this.objData.type === 'image') {-->
+<!--        this.objData.mediaId = null-->
+<!--        this.$delete(this.objData, 'url')-->
+<!--        this.objData.name = null-->
+<!--      } else if (this.objData.type === 'voice') {-->
+<!--        this.objData.mediaId = null-->
+<!--        this.$delete(this.objData, 'url')-->
+<!--        this.objData.name = null-->
+<!--      } else if (this.objData.type === 'video') {-->
+<!--        this.objData.mediaId = null-->
+<!--        this.$delete(this.objData, 'url')-->
+<!--        this.objData.name = null-->
+<!--        this.objData.title = null-->
+<!--        this.objData.description = null-->
+<!--      } else if (this.objData.type === 'music') {-->
+<!--        this.objData.thumbMediaId = null-->
+<!--        this.objData.thumbMediaUrl = null-->
+<!--        this.objData.title = null-->
+<!--        this.objData.description = null-->
+<!--        this.objData.musicUrl = null-->
+<!--        this.objData.hqMusicUrl = null-->
+<!--      } else if (this.objData.type === 'text') {-->
+<!--        this.objData.content = null-->
+<!--      }-->
+<!--      // 覆盖缓存-->
+<!--      this.tempObj.set(this.objData.type, Object.assign({}, this.objData))-->
+<!--    },-->
+<!--    /**-->
+<!--     * 输入时,缓存每次 objData 到 tempObj 中-->
+<!--     *-->
+<!--     * why?不确定为什么 v-model="objData.content" 不能自动缓存,所以通过这样的方式-->
+<!--     */-->
+<!--    inputContent(str) {-->
+<!--      // 覆盖缓存-->
+<!--      this.tempObj.set(this.objData.type, Object.assign({}, this.objData))-->
+<!--    }-->
+<!--  }-->
+<!--}-->
+<!--</script>-->
+
+<!--<style lang="scss" scoped>-->
+<!--.public-account-management {-->
+<!--  .el-input {-->
+<!--    width: 70%;-->
+<!--    margin-right: 2%;-->
+<!--  }-->
+<!--}-->
+<!--.pagination {-->
+<!--  text-align: right;-->
+<!--  margin-right: 25px;-->
+<!--}-->
+<!--.select-item {-->
+<!--  width: 280px;-->
+<!--  padding: 10px;-->
+<!--  margin: 0 auto 10px auto;-->
+<!--  border: 1px solid #eaeaea;-->
+<!--}-->
+<!--.select-item2 {-->
+<!--  padding: 10px;-->
+<!--  margin: 0 auto 10px auto;-->
+<!--  border: 1px solid #eaeaea;-->
+<!--}-->
+<!--.ope-row {-->
+<!--  padding-top: 10px;-->
+<!--  text-align: center;-->
+<!--}-->
+<!--.item-name {-->
+<!--  font-size: 12px;-->
+<!--  overflow: hidden;-->
+<!--  text-overflow: ellipsis;-->
+<!--  white-space: nowrap;-->
+<!--  text-align: center;-->
+<!--}-->
+<!--.el-form-item__content {-->
+<!--  line-height: unset !important;-->
+<!--}-->
+<!--.col-select {-->
+<!--  border: 1px solid rgb(234, 234, 234);-->
+<!--  padding: 50px 0px;-->
+<!--  height: 160px;-->
+<!--  width: 49.5%;-->
+<!--}-->
+<!--.col-select2 {-->
+<!--  border: 1px solid rgb(234, 234, 234);-->
+<!--  padding: 50px 0px;-->
+<!--  height: 160px;-->
+<!--}-->
+<!--.col-add {-->
+<!--  border: 1px solid rgb(234, 234, 234);-->
+<!--  padding: 50px 0px;-->
+<!--  height: 160px;-->
+<!--  width: 49.5%;-->
+<!--  float: right;-->
+<!--}-->
+<!--.avatar-uploader-icon {-->
+<!--  border: 1px solid #d9d9d9;-->
+<!--  font-size: 28px;-->
+<!--  color: #8c939d;-->
+<!--  width: 100px !important;-->
+<!--  height: 100px !important;-->
+<!--  line-height: 100px !important;-->
+<!--  text-align: center;-->
+<!--}-->
+<!--.material-img {-->
+<!--  width: 100%;-->
+<!--}-->
+<!--.thumb-div {-->
+<!--  display: inline-block;-->
+<!--  text-align: center;-->
+<!--}-->
+<!--.item-infos {-->
+<!--  width: 30%;-->
+<!--  margin: auto;-->
+<!--}-->
+<!--</style>-->
diff --git a/src/views/mp/components/wx-video-play/main.vue b/src/views/mp/components/wx-video-play/main.vue
new file mode 100644
index 00000000..880d10f8
--- /dev/null
+++ b/src/views/mp/components/wx-video-play/main.vue
@@ -0,0 +1,117 @@
+<!--
+  - Copyright (C) 2018-2019
+  - All rights reserved, Designed By www.joolun.com
+  【微信消息 - 视频】
+  芋道源码:
+  ① bug 修复:
+    1)joolun 的做法:使用 mediaId 从微信公众号,下载对应的 mp4 素材,从而播放内容;
+      存在的问题:mediaId 有效期是 3 天,超过时间后无法播放
+    2)重构后的做法:后端接收到微信公众号的视频消息后,将视频消息的 media_id 的文件内容保存到文件服务器中,这样前端可以直接使用 URL 播放。
+  ② 体验优化:弹窗关闭后,自动暂停视频的播放
+-->
+<template>
+  <div>
+    <!-- 提示 -->
+    <div @click="playVideo()">
+      <el-icon>
+        <VideoPlay />
+      </el-icon>
+      <p>点击播放视频</p>
+    </div>
+
+    <!-- 弹窗播放 -->
+    <el-dialog
+      title="视频播放"
+      v-model:visible="dialogVideo"
+      width="40%"
+      append-to-body
+      @close="closeDialog"
+    >
+      <video-player
+        v-if="playerOptions.sources[0].src"
+        class="video-player vjs-custom-skin"
+        ref="videoPlayerRef"
+        :playsinline="true"
+        :options="playerOptions"
+        @play="onPlayerPlay($event)"
+        @pause="onPlayerPause($event)"
+      />
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts" name="WxVideoPlayer">
+// 引入 videoPlayer 相关组件。教程:https://juejin.cn/post/6923056942281654285
+import { videoPlayer } from 'vue-video-player'
+import 'video.js/dist/video-js.css'
+import 'vue-video-player/src/custom-theme.css'
+import { VideoPlay } from '@element-plus/icons-vue'
+
+const props = defineProps({
+  url: {
+    // 视频地址,例如说:https://www.iocoder.cn/xxx.mp4
+    type: String,
+    required: true
+  }
+})
+const videoPlayerRef = ref()
+const dialogVideo = ref(false)
+const playerOptions = reactive({
+  playbackRates: [0.5, 1.0, 1.5, 2.0], // 播放速度
+  autoplay: false, // 如果 true,浏览器准备好时开始回放。
+  muted: false, // 默认情况下将会消除任何音频。
+  loop: false, // 导致视频一结束就重新开始。
+  preload: 'auto', // 建议浏览器在 <video> 加载元素后是否应该开始下载视频数据。auto 浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
+  language: 'zh-CN',
+  aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
+  fluid: true, // 当true时,Video.js player 将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
+  sources: [
+    {
+      type: 'video/mp4',
+      src: '' // 你的视频地址(必填)【重要】
+    }
+  ],
+  poster: '', // 你的封面地址
+  width: document.documentElement.clientWidth,
+  notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖 Video.js 无法播放媒体源时显示的默认信息。
+  controlBar: {
+    timeDivider: true,
+    durationDisplay: true,
+    remainingTimeDisplay: false,
+    fullscreenToggle: true //全屏按钮
+  }
+})
+
+const playVideo = () => {
+  dialogVideo.value = true
+  playerOptions.sources[0].src = props.url
+}
+const closeDialog = () => {
+  // 暂停播放
+  // videoPlayerRef.player.pause()
+}
+//   onPlayerPlay(player) {},
+//   // // eslint-disable-next-line @typescript-eslint/no-unused-vars
+//   // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
+//   onPlayerPause(player) {}
+
+// methods: {
+//   playVideo() {
+//     this.dialogVideo = true
+//     // 设置地址
+//     this.playerOptions.sources[0]['src'] = this.url
+//   },
+//   closeDialog() {
+//     // 暂停播放
+//     this.$refs.videoPlayer.player.pause()
+//   },
+//
+//   //todo player组件引入可能有问题
+//
+//   // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
+//   onPlayerPlay(player) {},
+//   // // eslint-disable-next-line @typescript-eslint/no-unused-vars
+//   // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
+//   onPlayerPause(player) {}
+// }
+</script>
diff --git a/src/views/mp/components/wx-voice-play/main.vue b/src/views/mp/components/wx-voice-play/main.vue
new file mode 100644
index 00000000..3260fc05
--- /dev/null
+++ b/src/views/mp/components/wx-voice-play/main.vue
@@ -0,0 +1,97 @@
+<!--
+  - Copyright (C) 2018-2019
+  - All rights reserved, Designed By www.joolun.com
+  【微信消息 - 语音】
+   芋道源码:
+  ① bug 修复:
+    1)joolun 的做法:使用 mediaId 从微信公众号,下载对应的 mp4 素材,从而播放内容;
+      存在的问题:mediaId 有效期是 3 天,超过时间后无法播放
+    2)重构后的做法:后端接收到微信公众号的视频消息后,将视频消息的 media_id 的文件内容保存到文件服务器中,这样前端可以直接使用 URL 播放。
+  ② 代码优化:将 props 中的 objData 调成为 data 中对应的属性,并补充相关注释
+-->
+<template>
+  <div class="wx-voice-div" @click="playVoice">
+    <el-icon>
+      <Icon v-if="playing !== true" icon="ep:video-play" />
+      <Icon v-else icon="ep:video-pause" />
+      <span class="amr-duration" v-if="duration">{{ duration }} 秒</span>
+    </el-icon>
+    <div v-if="content">
+      <el-tag type="success" size="mini">语音识别</el-tag>
+      {{ content }}
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts" name="WxVoicePlayer">
+// 因为微信语音是 amr 格式,所以需要用到 amr 解码器:https://www.npmjs.com/package/benz-amr-recorder
+import BenzAMRRecorder from 'benz-amr-recorder'
+
+const props = defineProps({
+  url: {
+    type: String, // 语音地址,例如说:https://www.iocoder.cn/xxx.amr
+    required: true
+  },
+  content: {
+    type: String, // 语音文本
+    required: false
+  }
+})
+
+const amr = ref()
+const playing = ref(false)
+const duration = ref()
+
+/** 处理点击,播放或暂停 */
+const playVoice = () => {
+  // 情况一:未初始化,则创建 BenzAMRRecorder
+  if (amr.value === undefined) {
+    amrInit()
+    return
+  }
+  // 情况二:已经初始化,则根据情况播放或暂时
+  if (amr.value.isPlaying()) {
+    amrStop()
+  } else {
+    amrPlay()
+  }
+}
+
+/** 音频初始化 */
+const amrInit = () => {
+  amr.value = new BenzAMRRecorder()
+  // 设置播放
+  amr.value.initWithUrl(props.url).then(function () {
+    amrPlay()
+    duration.value = amr.value.getDuration()
+  })
+  // 监听暂停
+  amr.value.onEnded(function () {
+    playing.value = false
+  })
+}
+
+/** 音频播放 */
+const amrPlay = () => {
+  playing.value = true
+  amr.value.play()
+}
+
+/** 音频暂停 */
+const amrStop = () => {
+  playing.value = false
+  amr.value.stop()
+}
+// TODO 芋艿:下面样式有点问题
+</script>
+<style lang="scss" scoped>
+.wx-voice-div {
+  padding: 5px;
+  background-color: #eaeaea;
+  border-radius: 10px;
+}
+.amr-duration {
+  font-size: 11px;
+  margin-left: 5px;
+}
+</style>
diff --git a/src/views/mp/freePublish/index.vue b/src/views/mp/freePublish/index.vue
index 497f72ec..6ef4a303 100644
--- a/src/views/mp/freePublish/index.vue
+++ b/src/views/mp/freePublish/index.vue
@@ -1,3 +1,392 @@
 <template>
-  <span>开发中</span>
+  <!-- 搜索工作栏 -->
+  <content-wrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="公众号" prop="accountId">
+        <el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px">
+          <el-option
+            v-for="item in accountList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </content-wrap>
+
+  <!-- 列表 -->
+  <content-wrap>
+    <div class="waterfall" v-loading="loading">
+      <div
+        class="waterfall-item"
+        v-show="item.content && item.content.newsItem"
+        v-for="item in list"
+        :key="item.articleId"
+      >
+        <wx-news :articles="item.content.newsItem" />
+        <!-- 操作 -->
+        <el-row justify="center" class="ope-row">
+          <el-button
+            type="danger"
+            circle
+            @click="handleDelete(item)"
+            v-hasPermi="['mp:free-publish:delete']"
+          >
+            <Icon icon="ep:delete" />
+          </el-button>
+        </el-row>
+      </div>
+    </div>
+    <!-- 分页组件 -->
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </content-wrap>
 </template>
+
+<script setup lang="ts" name="freePublish">
+import { getFreePublishPage, deleteFreePublish } from '@/api/mp/freePublish'
+import * as MpAccountApi from '@/api/mp/account'
+import WxNews from '@/views/mp/components/wx-news/main.vue'
+const message = useMessage() // 消息弹窗
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  currentPage: 1, // 当前页数
+  pageNo: 1, // 当前页数
+  accountId: undefined // 当前页数
+})
+const queryFormRef = ref() // 搜索的表单
+const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表
+
+/** 查询列表 */
+const getList = async () => {
+  // 如果没有选中公众号账号,则进行提示。
+  if (!queryParams.accountId) {
+    message.error('未选中公众号,无法查询已发表图文')
+    return false
+  }
+  // TODO 改成 await 形式
+  loading.value = true
+  getFreePublishPage(queryParams)
+    .then((data) => {
+      console.log(data)
+      // 将 thumbUrl 转成 picUrl,保证 wx-news 组件可以预览封面
+      data.list.forEach((item) => {
+        console.log(item)
+        const newsItem = item.content.newsItem
+        newsItem.forEach((article) => {
+          article.picUrl = article.thumbUrl
+        })
+      })
+      list.value = data.list
+      total.value = data.total
+    })
+    .finally(() => {
+      loading.value = false
+    })
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  // 默认选中第一个
+  if (accountList.value.length > 0) {
+    // @ts-ignore
+    queryParams.accountId = accountList.value[0].id
+  }
+  handleQuery()
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (item) => {
+  {
+    // TODO 改成 await 形式
+    const articleId = item.articleId
+    const accountId = queryParams.accountId
+    message
+      .confirm('删除后用户将无法访问此页面,确定删除?')
+      .then(function () {
+        return deleteFreePublish(accountId, articleId)
+      })
+      .then(() => {
+        getList()
+        message.success('删除成功')
+      })
+      .catch(() => {})
+  }
+}
+
+onMounted(async () => {
+  accountList.value = await MpAccountApi.getSimpleAccountList()
+  // 选中第一个
+  if (accountList.value.length > 0) {
+    // @ts-ignore
+    queryParams.accountId = accountList.value[0].id
+  }
+  await getList()
+})
+</script>
+<style lang="scss" scoped>
+.pagination {
+  float: right;
+  margin-right: 25px;
+}
+
+.add_but {
+  padding: 10px;
+}
+
+.ope-row {
+  margin-top: 5px;
+  text-align: center;
+  border-top: 1px solid #eaeaea;
+  padding-top: 5px;
+}
+
+.item-name {
+  font-size: 12px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  text-align: center;
+}
+
+.el-upload__tip {
+  margin-left: 5px;
+}
+
+/* 新增图文 */
+.left {
+  display: inline-block;
+  width: 35%;
+  vertical-align: top;
+  margin-top: 200px;
+}
+
+.right {
+  display: inline-block;
+  width: 60%;
+  margin-top: -40px;
+}
+
+.avatar-uploader {
+  width: 20%;
+  display: inline-block;
+}
+
+.avatar-uploader .el-upload {
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  text-align: unset !important;
+}
+
+.avatar-uploader .el-upload:hover {
+  border-color: #165dff;
+}
+
+.avatar-uploader-icon {
+  border: 1px solid #d9d9d9;
+  font-size: 28px;
+  color: #8c939d;
+  width: 120px;
+  height: 120px;
+  line-height: 120px;
+  text-align: center;
+}
+
+.avatar {
+  width: 230px;
+  height: 120px;
+}
+
+.avatar1 {
+  width: 120px;
+  height: 120px;
+}
+
+.digest {
+  width: 60%;
+  display: inline-block;
+  vertical-align: top;
+}
+
+/*新增图文*/
+/*瀑布流样式*/
+.waterfall {
+  width: 100%;
+  column-gap: 10px;
+  column-count: 5;
+  margin: 0 auto;
+}
+
+.waterfall-item {
+  padding: 10px;
+  margin-bottom: 10px;
+  break-inside: avoid;
+  border: 1px solid #eaeaea;
+}
+
+p {
+  line-height: 30px;
+}
+
+@media (min-width: 992px) and (max-width: 1300px) {
+  .waterfall {
+    column-count: 3;
+  }
+  p {
+    color: red;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .waterfall {
+    column-count: 2;
+  }
+  p {
+    color: orange;
+  }
+}
+
+@media (max-width: 767px) {
+  .waterfall {
+    column-count: 1;
+  }
+}
+
+/*瀑布流样式*/
+.news-main {
+  background-color: #ffffff;
+  width: 100%;
+  margin: auto;
+  height: 120px;
+}
+
+.news-content {
+  background-color: #acadae;
+  width: 100%;
+  height: 120px;
+  position: relative;
+}
+
+.news-content-title {
+  display: inline-block;
+  font-size: 15px;
+  color: #ffffff;
+  position: absolute;
+  left: 0px;
+  bottom: 0px;
+  background-color: black;
+  width: 98%;
+  padding: 1%;
+  opacity: 0.65;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  height: 25px;
+}
+
+.news-main-item {
+  background-color: #ffffff;
+  padding: 5px 0px;
+  border-top: 1px solid #eaeaea;
+  width: 100%;
+  margin: auto;
+}
+
+.news-content-item {
+  position: relative;
+  margin-left: -3px;
+}
+
+.news-content-item-title {
+  display: inline-block;
+  font-size: 12px;
+  width: 70%;
+}
+
+.news-content-item-img {
+  display: inline-block;
+  width: 25%;
+  background-color: #acadae;
+}
+
+.input-tt {
+  padding: 5px;
+}
+
+.activeAddNews {
+  border: 5px solid #2bb673;
+}
+
+.news-main-plus {
+  width: 280px;
+  text-align: center;
+  margin: auto;
+  height: 50px;
+}
+
+.icon-plus {
+  margin: 10px;
+  font-size: 25px;
+}
+
+.select-item {
+  width: 60%;
+  padding: 10px;
+  margin: 0 auto 10px auto;
+  border: 1px solid #eaeaea;
+}
+
+.father .child {
+  display: none;
+  text-align: center;
+  position: relative;
+  bottom: 25px;
+}
+
+.father:hover .child {
+  display: block;
+}
+
+.thumb-div {
+  display: inline-block;
+  width: 30%;
+  text-align: center;
+}
+
+.thumb-but {
+  margin: 5px;
+}
+
+.material-img {
+  width: 100%;
+  height: 100%;
+}
+</style>
diff --git a/src/views/mp/message/index.vue b/src/views/mp/message/index.vue
index 497f72ec..10145221 100644
--- a/src/views/mp/message/index.vue
+++ b/src/views/mp/message/index.vue
@@ -1,3 +1,261 @@
 <template>
-  <span>开发中</span>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="公众号" prop="accountId">
+        <el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px">
+          <el-option
+            v-for="item in accountList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="消息类型" prop="type">
+        <el-select v-model="queryParams.type" placeholder="请选择消息类型" class="!w-240px">
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="用户标识" prop="openid">
+        <el-input
+          v-model="queryParams.openid"
+          placeholder="请输入用户标识"
+          clearable
+          :v-on="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          style="width: 240px"
+          value-format="yyyy-MM-dd HH:mm:ss"
+          type="daterange"
+          range-separator="-"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="['00:00:00', '23:59:59']"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column
+        label="发送时间"
+        align="center"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="消息类型" align="center" prop="type" width="80" />
+      <el-table-column label="发送方" align="center" prop="sendFrom" width="80">
+        <template #default="scope">
+          <el-tag v-if="scope.row.sendFrom === 1" type="success">粉丝</el-tag>
+          <el-tag v-else type="info">公众号</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="用户标识" align="center" prop="openid" width="300" />
+      <el-table-column label="内容" prop="content">
+        <template #default="scope">
+          <!-- 【事件】区域 -->
+          <div v-if="scope.row.type === 'event' && scope.row.event === 'subscribe'">
+            <el-tag type="success">关注</el-tag>
+          </div>
+          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'unsubscribe'">
+            <el-tag type="danger">取消关注</el-tag>
+          </div>
+          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'CLICK'">
+            <el-tag>点击菜单</el-tag>【{{ scope.row.eventKey }}】
+          </div>
+          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'VIEW'">
+            <el-tag>点击菜单链接</el-tag>【{{ scope.row.eventKey }}】
+          </div>
+          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_waitmsg'">
+            <el-tag>扫码结果</el-tag>【{{ scope.row.eventKey }}】
+          </div>
+          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_push'">
+            <el-tag>扫码结果</el-tag>【{{ scope.row.eventKey }}】
+          </div>
+          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_sysphoto'">
+            <el-tag>系统拍照发图</el-tag>
+          </div>
+          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_photo_or_album'">
+            <el-tag>拍照或者相册</el-tag>
+          </div>
+          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_weixin'">
+            <el-tag>微信相册</el-tag>
+          </div>
+          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'location_select'">
+            <el-tag>选择地理位置</el-tag>
+          </div>
+          <div v-else-if="scope.row.type === 'event'">
+            <el-tag type="danger">未知事件类型</el-tag>
+          </div>
+          <!-- 【消息】区域 -->
+          <div v-else-if="scope.row.type === 'text'">{{ scope.row.content }}</div>
+          <div v-else-if="scope.row.type === 'voice'">
+            <wx-voice-player :url="scope.row.mediaUrl" :content="scope.row.recognition" />
+          </div>
+          <div v-else-if="scope.row.type === 'image'">
+            <a target="_blank" :href="scope.row.mediaUrl">
+              <img :src="scope.row.mediaUrl" style="width: 100px" />
+            </a>
+          </div>
+          <div v-else-if="scope.row.type === 'video' || scope.row.type === 'shortvideo'">
+            <wx-video-player :url="scope.row.mediaUrl" style="margin-top: 10px" />
+          </div>
+          <div v-else-if="scope.row.type === 'link'">
+            <el-tag>链接</el-tag>:
+            <a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
+          </div>
+          <div v-else-if="scope.row.type === 'location'">
+            <wx-location
+              :label="scope.row.label"
+              :location-y="scope.row.locationY"
+              :location-x="scope.row.locationX"
+            />
+          </div>
+          <div v-else-if="scope.row.type === 'music'">
+            <wx-music
+              :title="scope.row.title"
+              :description="scope.row.description"
+              :thumb-media-url="scope.row.thumbMediaUrl"
+              :music-url="scope.row.musicUrl"
+              :hq-music-url="scope.row.hqMusicUrl"
+            />
+          </div>
+          <div v-else-if="scope.row.type === 'news'">
+            <wx-news :articles="scope.row.articles" />
+          </div>
+          <div v-else>
+            <el-tag type="danger">未知消息类型</el-tag>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="handleSend(scope.row)"
+            v-hasPermi="['mp:message:send']"
+          >
+            消息
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 发送消息的弹窗 -->
+    <el-dialog title="粉丝消息列表" v-model:visible="open" width="50%">
+      <wx-msg :user-id="userId" v-if="open" />
+    </el-dialog>
+  </ContentWrap>
 </template>
+<script setup lang="ts" name="MpMessage">
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+// import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
+import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
+// import WxMsg from '@/views/mp/components/wx-msg/main.vue'
+import WxLocation from '@/views/mp/components/wx-location/main.vue'
+// import WxMusic from '@/views/mp/components/wx-music/main.vue'
+// import WxNews from '@/views/mp/components/wx-news/main.vue'
+import * as MpAccountApi from '@/api/mp/account'
+import * as MpMessageApi from '@/api/mp/message'
+const message = useMessage() // 消息弹窗
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  openid: null,
+  accountId: null,
+  type: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+// TODO 芋艿:下面应该移除
+const open = ref(false) // 是否显示弹出层
+const userId = ref(0) // 操作的用户编号
+const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表
+
+/** 查询参数列表 */
+const getList = async () => {
+  // 如果没有选中公众号账号,则进行提示。
+  if (!queryParams.accountId) {
+    await message.error('未选中公众号,无法查询消息')
+    return
+  }
+  try {
+    loading.value = true
+    const data = await MpMessageApi.getMessagePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = async () => {
+  queryFormRef.value.resetFields()
+  // 默认选中第一个
+  if (accountList.value.length > 0) {
+    // @ts-ignore
+    queryParams.accountId = accountList.value[0].id
+  }
+  handleQuery()
+}
+const handleSend = async (row) => {
+  userId.value = row.userId
+  open.value = true
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  accountList.value = await MpAccountApi.getSimpleAccountList()
+  // 选中第一个
+  if (accountList.value.length > 0) {
+    // @ts-ignore
+    queryParams.accountId = accountList.value[0].id
+  }
+  await getList()
+})
+</script>
diff --git a/src/views/system/dept/DeptForm.vue b/src/views/system/dept/DeptForm.vue
index 188ecb79..f2c3bc02 100644
--- a/src/views/system/dept/DeptForm.vue
+++ b/src/views/system/dept/DeptForm.vue
@@ -166,7 +166,7 @@ const resetForm = () => {
 /** 获得部门树 */
 const getTree = async () => {
   deptTree.value = []
-  const data = await DeptApi.listSimpleDeptApi()
+  const data = await DeptApi.getSimpleDeptList()
   let dept: Tree = { id: 0, name: '顶级部门', children: [] }
   dept.children = handleTree(data)
   deptTree.value.push(dept)
diff --git a/src/views/system/menu/MenuForm.vue b/src/views/system/menu/MenuForm.vue
new file mode 100644
index 00000000..45bcedfb
--- /dev/null
+++ b/src/views/system/menu/MenuForm.vue
@@ -0,0 +1,253 @@
+<template>
+  <Dialog :title="modelTitle" v-model="modelVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="上级菜单">
+        <el-tree-select
+          node-key="id"
+          v-model="formData.parentId"
+          :props="defaultProps"
+          :data="menuTree"
+          :default-expanded-keys="[0]"
+          check-strictly
+        />
+      </el-form-item>
+      <el-form-item label="菜单名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入菜单名称" clearable />
+      </el-form-item>
+      <el-form-item label="菜单类型" prop="type">
+        <el-radio-group v-model="formData.type">
+          <el-radio-button
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE)"
+            :key="dict.label"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio-button>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="菜单图标" v-if="formData.type !== 3">
+        <IconSelect v-model="formData.icon" clearable />
+      </el-form-item>
+      <el-form-item label="路由地址" prop="path" v-if="formData.type !== 3">
+        <template #label>
+          <Tooltip
+            titel="路由地址"
+            message="访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头"
+          />
+        </template>
+        <el-input v-model="formData.path" placeholder="请输入路由地址" clearable />
+      </el-form-item>
+      <el-form-item label="组件地址" prop="component" v-if="formData.type === 2">
+        <el-input v-model="formData.component" placeholder="例如说:system/user/index" clearable />
+      </el-form-item>
+      <el-form-item label="组件名字" prop="componentName" v-if="formData.type === 2">
+        <el-input v-model="formData.componentName" placeholder="例如说:SystemUser" clearable />
+      </el-form-item>
+      <el-form-item label="权限标识" prop="permission" v-if="formData.type !== 1">
+        <template #label>
+          <Tooltip
+            titel="权限标识"
+            message="Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission('system:user:list')`)"
+          />
+        </template>
+        <el-input v-model="formData.permission" placeholder="请输入权限标识" clearable />
+      </el-form-item>
+      <el-form-item label="显示排序" prop="sort">
+        <el-input-number v-model="formData.sort" controls-position="right" :min="0" clearable />
+      </el-form-item>
+      <el-form-item label="菜单状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.label"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="显示状态" prop="visible" v-if="formData.type !== 3">
+        <template #label>
+          <Tooltip titel="显示状态" message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问" />
+        </template>
+        <el-radio-group v-model="formData.visible">
+          <el-radio border key="true" :label="true">显示</el-radio>
+          <el-radio border key="false" :label="false">隐藏</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="总是显示" prop="alwaysShow" v-if="formData.type !== 3">
+        <template #label>
+          <Tooltip
+            titel="总是显示"
+            message="选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单"
+          />
+        </template>
+        <el-radio-group v-model="formData.alwaysShow">
+          <el-radio border key="true" :label="true">总是</el-radio>
+          <el-radio border key="false" :label="false">不是</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="缓存状态" prop="keepAlive" v-if="formData.type === 2">
+        <template #label>
+          <Tooltip
+            titel="缓存状态"
+            message="选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段"
+          />
+        </template>
+        <el-radio-group v-model="formData.keepAlive">
+          <el-radio border key="true" :label="true">缓存</el-radio>
+          <el-radio border key="false" :label="false">不缓存</el-radio>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="modelVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as MenuApi from '@/api/system/menu'
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { SystemMenuTypeEnum, CommonStatusEnum } from '@/utils/constants'
+import { handleTree, defaultProps } from '@/utils/tree'
+const { wsCache } = useCache()
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const modelVisible = ref(false) // 弹窗的是否展示
+const modelTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: 0,
+  name: '',
+  permission: '',
+  type: SystemMenuTypeEnum.DIR,
+  sort: Number(undefined),
+  parentId: 0,
+  path: '',
+  icon: '',
+  component: '',
+  componentName: '',
+  status: CommonStatusEnum.ENABLE,
+  visible: true,
+  keepAlive: true,
+  alwaysShow: true
+})
+const formRules = reactive({
+  name: [{ required: true, message: '菜单名称不能为空', trigger: 'blur' }],
+  sort: [{ required: true, message: '菜单顺序不能为空', trigger: 'blur' }],
+  path: [{ required: true, message: '路由地址不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number, parentId?: number) => {
+  modelVisible.value = true
+  modelTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  if (parentId) {
+    formData.value.parentId = parentId
+  }
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await MenuApi.getMenuApi(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+  // 获得菜单列表
+  await getTree()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    if (
+      formData.value.type === SystemMenuTypeEnum.DIR ||
+      formData.value.type === SystemMenuTypeEnum.MENU
+    ) {
+      if (!isExternal(formData.value.path)) {
+        if (formData.value.parentId === 0 && formData.value.path.charAt(0) !== '/') {
+          message.error('路径必须以 / 开头')
+          return
+        } else if (formData.value.parentId !== 0 && formData.value.path.charAt(0) === '/') {
+          message.error('路径不能以 / 开头')
+          return
+        }
+      }
+    }
+    const data = formData.value as unknown as MenuApi.MenuVO
+    if (formType.value === 'create') {
+      await MenuApi.createMenu(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await MenuApi.updateMenu(data)
+      message.success(t('common.updateSuccess'))
+    }
+    modelVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+    // 清空,从而触发刷新
+    wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
+  }
+}
+
+/** 获取下拉框[上级菜单]的数据  */
+const menuTree = ref<Tree[]>([]) // 树形结构
+const getTree = async () => {
+  menuTree.value = []
+  const res = await MenuApi.getSimpleMenusList()
+  let menu: Tree = { id: 0, name: '主类目', children: [] }
+  menu.children = handleTree(res)
+  menuTree.value.push(menu)
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: 0,
+    name: '',
+    permission: '',
+    type: SystemMenuTypeEnum.DIR,
+    sort: Number(undefined),
+    parentId: 0,
+    path: '',
+    icon: '',
+    component: '',
+    componentName: '',
+    status: CommonStatusEnum.ENABLE,
+    visible: true,
+    keepAlive: true,
+    alwaysShow: true
+  }
+  formRef.value?.resetFields()
+}
+
+/** 判断 path 是不是外部的 HTTP 等链接 */
+const isExternal = (path: string) => {
+  return /^(https?:|mailto:|tel:)/.test(path)
+}
+</script>
diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue
index 0604aa93..3baf3148 100644
--- a/src/views/system/menu/index.vue
+++ b/src/views/system/menu/index.vue
@@ -1,351 +1,183 @@
 <template>
+  <!-- 搜索工作栏 -->
   <ContentWrap>
-    <!-- 列表 -->
-    <XTable ref="xGrid" @register="registerTable" show-overflow>
-      <template #toolbar_buttons>
-        <!-- 操作:新增 -->
-        <XButton
-          type="primary"
-          preIcon="ep:zoom-in"
-          :title="t('action.add')"
-          v-hasPermi="['system:menu:create']"
-          @click="handleCreate()"
-        />
-        <XButton title="展开所有" @click="xGrid?.Ref.setAllTreeExpand(true)" />
-        <XButton title="关闭所有" @click="xGrid?.Ref.clearTreeExpand()" />
-      </template>
-      <template #name_default="{ row }">
-        <Icon :icon="row.icon" />
-        <span class="ml-3">{{ row.name }}</span>
-      </template>
-      <template #actionbtns_default="{ row }">
-        <!-- 操作:修改 -->
-        <XTextButton
-          preIcon="ep:edit"
-          :title="t('action.edit')"
-          v-hasPermi="['system:menu:update']"
-          @click="handleUpdate(row.id)"
-        />
-        <!-- 操作:删除 -->
-        <XTextButton
-          preIcon="ep:delete"
-          :title="t('action.del')"
-          v-hasPermi="['system:menu:delete']"
-          @click="deleteData(row.id)"
-        />
-      </template>
-    </XTable>
-  </ContentWrap>
-  <!-- 添加或修改菜单对话框 -->
-  <XModal id="menuModel" v-model="dialogVisible" :title="dialogTitle">
-    <!-- 对话框(添加 / 修改) -->
     <el-form
-      ref="formRef"
-      :model="menuForm"
-      :rules="rules"
-      label-width="100px"
-      label-position="right"
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
     >
-      <el-form-item label="上级菜单">
-        <el-tree-select
-          node-key="id"
-          v-model="menuForm.parentId"
-          :props="defaultProps"
-          :data="menuOptions"
-          :default-expanded-keys="[0]"
-          check-strictly
+      <el-form-item label="菜单名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入菜单名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
         />
       </el-form-item>
-      <el-col :span="16">
-        <el-form-item label="菜单名称" prop="name">
-          <el-input v-model="menuForm.name" placeholder="请输入菜单名称" clearable />
-        </el-form-item>
-      </el-col>
-      <el-form-item label="菜单类型" prop="type">
-        <el-radio-group v-model="menuForm.type">
-          <el-radio-button
-            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE)"
-            :key="dict.label"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-radio-button>
-        </el-radio-group>
+      <el-form-item label="状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择菜单状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['system:menu:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button type="danger" plain @click="toggleExpandAll">
+          <Icon icon="ep:sort" class="mr-5px" /> 展开/折叠
+        </el-button>
       </el-form-item>
-      <template v-if="menuForm.type !== 3">
-        <el-form-item label="菜单图标">
-          <IconSelect v-model="menuForm.icon" clearable />
-        </el-form-item>
-        <el-col :span="16">
-          <el-form-item label="路由地址" prop="path">
-            <template #label>
-              <Tooltip
-                titel="路由地址"
-                message="访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头"
-              />
-            </template>
-            <el-input v-model="menuForm.path" placeholder="请输入路由地址" clearable />
-          </el-form-item>
-        </el-col>
-      </template>
-      <template v-if="menuForm.type === 2">
-        <el-col :span="16">
-          <el-form-item label="组件地址" prop="component">
-            <el-input
-              v-model="menuForm.component"
-              placeholder="例如说:system/user/index"
-              clearable
-            />
-          </el-form-item>
-        </el-col>
-        <el-col :span="16">
-          <el-form-item label="组件名字" prop="componentName">
-            <el-input v-model="menuForm.componentName" placeholder="例如说:SystemUser" clearable />
-          </el-form-item>
-        </el-col>
-      </template>
-      <template v-if="menuForm.type !== 1">
-        <el-col :span="16">
-          <el-form-item label="权限标识" prop="permission">
-            <template #label>
-              <Tooltip
-                titel="权限标识"
-                message="Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission('system:user:list')`)"
-              />
-            </template>
-            <el-input v-model="menuForm.permission" placeholder="请输入权限标识" clearable />
-          </el-form-item>
-        </el-col>
-      </template>
-      <el-col :span="16">
-        <el-form-item label="显示排序" prop="sort">
-          <el-input-number v-model="menuForm.sort" controls-position="right" :min="0" clearable />
-        </el-form-item>
-      </el-col>
-      <el-col :span="16">
-        <el-form-item label="菜单状态" prop="status">
-          <el-radio-group v-model="menuForm.status">
-            <el-radio
-              border
-              v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
-              :key="dict.label"
-              :label="dict.value"
-            >
-              {{ dict.label }}
-            </el-radio>
-          </el-radio-group>
-        </el-form-item>
-      </el-col>
-      <template v-if="menuForm.type !== 3">
-        <el-col :span="16">
-          <el-form-item label="显示状态" prop="visible">
-            <template #label>
-              <Tooltip
-                titel="显示状态"
-                message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问"
-              />
-            </template>
-            <el-radio-group v-model="menuForm.visible">
-              <el-radio border key="true" :label="true">显示</el-radio>
-              <el-radio border key="false" :label="false">隐藏</el-radio>
-            </el-radio-group>
-          </el-form-item>
-        </el-col>
-      </template>
-      <template v-if="menuForm.type !== 3">
-        <el-col :span="16">
-          <el-form-item label="总是显示" prop="alwaysShow">
-            <template #label>
-              <Tooltip
-                titel="总是显示"
-                message="选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单"
-              />
-            </template>
-            <el-radio-group v-model="menuForm.alwaysShow">
-              <el-radio border key="true" :label="true">总是</el-radio>
-              <el-radio border key="false" :label="false">不是</el-radio>
-            </el-radio-group>
-          </el-form-item>
-        </el-col>
-      </template>
-      <template v-if="menuForm.type === 2">
-        <el-col :span="16">
-          <el-form-item label="缓存状态" prop="keepAlive">
-            <template #label>
-              <Tooltip
-                titel="缓存状态"
-                message="选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段"
-              />
-            </template>
-            <el-radio-group v-model="menuForm.keepAlive">
-              <el-radio border key="true" :label="true">缓存</el-radio>
-              <el-radio border key="false" :label="false">不缓存</el-radio>
-            </el-radio-group>
-          </el-form-item>
-        </el-col>
-      </template>
     </el-form>
-    <template #footer>
-      <!-- 按钮:保存 -->
-      <XButton
-        v-if="['create', 'update'].includes(actionType)"
-        type="primary"
-        :loading="actionLoading"
-        @click="submitForm()"
-        :title="t('action.save')"
-      />
-      <!-- 按钮:关闭 -->
-      <XButton :loading="actionLoading" @click="dialogVisible = false" :title="t('dialog.close')" />
-    </template>
-  </XModal>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      v-loading="loading"
+      :data="list"
+      row-key="id"
+      v-if="refreshTable"
+      :default-expand-all="isExpandAll"
+    >
+      <el-table-column prop="name" label="菜单名称" :show-overflow-tooltip="true" width="250" />
+      <el-table-column prop="icon" label="图标" align="center" width="100">
+        <template #default="scope">
+          <Icon :icon="scope.row.icon" />
+        </template>
+      </el-table-column>
+      <el-table-column prop="sort" label="排序" width="60" />
+      <el-table-column prop="permission" label="权限标识" :show-overflow-tooltip="true" />
+      <el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true" />
+      <el-table-column prop="componentName" label="组件名称" :show-overflow-tooltip="true" />
+      <el-table-column prop="status" label="状态" width="80">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['system:menu:update']"
+          >
+            修改
+          </el-button>
+          <el-button
+            link
+            type="primary"
+            @click="openForm('create', undefined, scope.row.id)"
+            v-hasPermi="['system:menu:create']"
+          >
+            新增
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['system:menu:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <MenuForm ref="formRef" @success="getList" />
 </template>
 <script setup lang="ts" name="Menu">
-import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-import { FormInstance } from 'element-plus'
-// 业务相关的 import
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { SystemMenuTypeEnum, CommonStatusEnum } from '@/utils/constants'
-import { handleTree, defaultProps } from '@/utils/tree'
+import { handleTree } from '@/utils/tree'
 import * as MenuApi from '@/api/system/menu'
-import { allSchemas, rules } from './menu.data'
-
+import MenuForm from './MenuForm.vue'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
-const { wsCache } = useCache()
 
-const xGrid = ref<any>(null)
-
-// 列表相关的变量
-const treeConfig = {
-  transform: true,
-  rowField: 'id',
-  parentField: 'parentId',
-  expandAll: false
-}
-const [registerTable, { reload, deleteData }] = useXTable({
-  allSchemas: allSchemas,
-  treeConfig: treeConfig,
-  getListApi: MenuApi.getMenuListApi,
-  deleteApi: MenuApi.deleteMenuApi
-})
-// 弹窗相关的变量
-const dialogVisible = ref(false) // 是否显示弹出层
-const dialogTitle = ref('edit') // 弹出层标题
-const actionType = ref('') // 操作按钮的类型
-const actionLoading = ref(false) // 遮罩层
-// 新增和修改的表单值
-const formRef = ref<FormInstance>()
-const menuForm = ref<MenuApi.MenuVO>({
-  id: 0,
-  name: '',
-  permission: '',
-  type: SystemMenuTypeEnum.DIR,
-  sort: 1,
-  parentId: 0,
-  path: '',
-  icon: '',
-  component: '',
-  componentName: '',
-  status: CommonStatusEnum.ENABLE,
-  visible: true,
-  keepAlive: true,
-  alwaysShow: true,
-  createTime: new Date()
+const loading = ref(true) // 列表的加载中
+const list = ref<any>([]) // 列表的数据
+const queryParams = reactive({
+  name: undefined,
+  status: undefined
 })
+const queryFormRef = ref() // 搜索的表单
+const isExpandAll = ref(false) // 是否展开,默认全部折叠
+const refreshTable = ref(true) // 重新渲染表格状态
 
-// ========== 下拉框[上级菜单] ==========
-const menuOptions = ref<any[]>([]) // 树形结构
-// 获取下拉框[上级菜单]的数据
-const getTree = async () => {
-  menuOptions.value = []
-  const res = await MenuApi.listSimpleMenusApi()
-  let menu: Tree = { id: 0, name: '主类目', children: [] }
-  menu.children = handleTree(res)
-  menuOptions.value.push(menu)
-}
-
-// ========== 新增/修改 ==========
-
-// 设置标题
-const setDialogTile = async (type: string) => {
-  await getTree()
-  dialogTitle.value = t('action.' + type)
-  actionType.value = type
-  dialogVisible.value = true
-}
-
-// 新增操作
-const handleCreate = () => {
-  setDialogTile('create')
-  // 重置表单
-  formRef.value?.resetFields()
-  menuForm.value = {
-    id: 0,
-    name: '',
-    permission: '',
-    type: SystemMenuTypeEnum.DIR,
-    sort: 1,
-    parentId: 0,
-    path: '',
-    icon: '',
-    component: '',
-    componentName: '',
-    status: CommonStatusEnum.ENABLE,
-    visible: true,
-    keepAlive: true,
-    alwaysShow: true,
-    createTime: new Date()
-  }
-}
-
-// 修改操作
-const handleUpdate = async (rowId: number) => {
-  await setDialogTile('update')
-  // 设置数据
-  const res = await MenuApi.getMenuApi(rowId)
-  menuForm.value = res
-  // TODO 芋艿:这块要优化下,部分字段未重置,无法修改
-  menuForm.value.componentName = res.componentName || ''
-  menuForm.value.alwaysShow = res.alwaysShow !== undefined ? res.alwaysShow : true
-}
-
-// 提交新增/修改的表单
-const submitForm = async () => {
-  actionLoading.value = true
-  // 提交请求
+/** 查询参数列表 */
+const getList = async () => {
+  loading.value = true
   try {
-    if (
-      menuForm.value.type === SystemMenuTypeEnum.DIR ||
-      menuForm.value.type === SystemMenuTypeEnum.MENU
-    ) {
-      if (!isExternal(menuForm.value.path)) {
-        if (menuForm.value.parentId === 0 && menuForm.value.path.charAt(0) !== '/') {
-          message.error('路径必须以 / 开头')
-          return
-        } else if (menuForm.value.parentId !== 0 && menuForm.value.path.charAt(0) === '/') {
-          message.error('路径不能以 / 开头')
-          return
-        }
-      }
-    }
-    if (actionType.value === 'create') {
-      await MenuApi.createMenuApi(menuForm.value)
-      message.success(t('common.createSuccess'))
-    } else {
-      await MenuApi.updateMenuApi(menuForm.value)
-      message.success(t('common.updateSuccess'))
-    }
+    const data = await MenuApi.getMenuList(queryParams)
+    list.value = handleTree(data)
   } finally {
-    dialogVisible.value = false
-    actionLoading.value = false
-    wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
-    // 操作成功,重新加载列表
-    await reload()
+    loading.value = false
   }
 }
 
-// 判断 path 是不是外部的 HTTP 等链接
-const isExternal = (path: string) => {
-  return /^(https?:|mailto:|tel:)/.test(path)
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  getList()
 }
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number, parentId?: number) => {
+  formRef.value.open(type, id, parentId)
+}
+
+/** 展开/折叠操作 */
+const toggleExpandAll = () => {
+  refreshTable.value = false
+  isExpandAll.value = !isExpandAll.value
+  nextTick(() => {
+    refreshTable.value = true
+  })
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await MenuApi.deleteMenu(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
 </script>
diff --git a/src/views/system/menu/menu.data.ts b/src/views/system/menu/menu.data.ts
deleted file mode 100644
index 753c1211..00000000
--- a/src/views/system/menu/menu.data.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
-const { t } = useI18n() // 国际化
-
-// 新增和修改的表单校验
-export const rules = reactive({
-  name: [required],
-  sort: [required],
-  path: [required],
-  status: [required]
-})
-
-// CrudSchema
-const crudSchemas = reactive<VxeCrudSchema>({
-  primaryKey: 'id',
-  primaryType: null,
-  action: true,
-  columns: [
-    {
-      title: '上级菜单',
-      field: 'parentId',
-      isTable: false
-    },
-    {
-      title: '菜单名称',
-      field: 'name',
-      isSearch: true,
-      table: {
-        treeNode: true,
-        align: 'left',
-        width: '200px',
-        slots: {
-          default: 'name_default'
-        }
-      }
-    },
-    {
-      title: '菜单类型',
-      field: 'type',
-      dictType: DICT_TYPE.SYSTEM_MENU_TYPE
-    },
-    {
-      title: '路由地址',
-      field: 'path'
-    },
-    {
-      title: '组件路径',
-      field: 'component'
-    },
-    {
-      title: '组件名字',
-      field: 'componentName'
-    },
-    {
-      title: '权限标识',
-      field: 'permission'
-    },
-    {
-      title: '排序',
-      field: 'sort'
-    },
-    {
-      title: t('common.status'),
-      field: 'status',
-      dictType: DICT_TYPE.COMMON_STATUS,
-      dictClass: 'number',
-      isSearch: true
-    },
-    {
-      title: t('common.createTime'),
-      field: 'createTime',
-      formatter: 'formatDate',
-      isTable: false
-    }
-  ]
-})
-export const { allSchemas } = useVxeCrudSchemas(crudSchemas)
diff --git a/src/views/system/oauth2/client/client.data.ts b/src/views/system/oauth2/client/client.data.ts
deleted file mode 100644
index 52ee8895..00000000
--- a/src/views/system/oauth2/client/client.data.ts
+++ /dev/null
@@ -1,197 +0,0 @@
-import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
-import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
-const { t } = useI18n() // 国际化
-
-const authorizedGrantOptions = getStrDictOptions(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE)
-
-// 表单校验
-export const rules = reactive({
-  clientId: [required],
-  secret: [required],
-  name: [required],
-  status: [required],
-  accessTokenValiditySeconds: [required],
-  refreshTokenValiditySeconds: [required],
-  redirectUris: [required],
-  authorizedGrantTypes: [required]
-})
-
-// CrudSchema
-const crudSchemas = reactive<VxeCrudSchema>({
-  primaryKey: 'clientId',
-  primaryType: null,
-  action: true,
-  columns: [
-    {
-      title: '客户端端号',
-      field: 'clientId'
-    },
-    {
-      title: '客户端密钥',
-      field: 'secret'
-    },
-    {
-      title: '应用名',
-      field: 'name',
-      isSearch: true
-    },
-    {
-      title: '应用图标',
-      field: 'logo',
-      table: {
-        cellRender: {
-          name: 'XImg'
-        }
-      },
-      form: {
-        component: 'UploadImg'
-      }
-    },
-    {
-      title: t('common.status'),
-      field: 'status',
-      dictType: DICT_TYPE.COMMON_STATUS,
-      dictClass: 'number',
-      isSearch: true
-    },
-    {
-      title: '访问令牌的有效期',
-      field: 'accessTokenValiditySeconds',
-      form: {
-        component: 'InputNumber'
-      },
-      table: {
-        slots: {
-          default: 'accessTokenValiditySeconds_default'
-        }
-      }
-    },
-    {
-      title: '刷新令牌的有效期',
-      field: 'refreshTokenValiditySeconds',
-      form: {
-        component: 'InputNumber'
-      },
-      table: {
-        slots: {
-          default: 'refreshTokenValiditySeconds_default'
-        }
-      }
-    },
-    {
-      title: '授权类型',
-      field: 'authorizedGrantTypes',
-      table: {
-        width: 400,
-        slots: {
-          default: 'authorizedGrantTypes_default'
-        }
-      },
-      form: {
-        component: 'Select',
-        componentProps: {
-          options: authorizedGrantOptions,
-          multiple: true,
-          filterable: true
-        }
-      }
-    },
-    {
-      title: '授权范围',
-      field: 'scopes',
-      isTable: false,
-      form: {
-        component: 'Select',
-        componentProps: {
-          options: [],
-          multiple: true,
-          filterable: true,
-          allowCreate: true,
-          defaultFirstOption: true
-        }
-      }
-    },
-    {
-      title: '自动授权范围',
-      field: 'autoApproveScopes',
-      isTable: false,
-      form: {
-        component: 'Select',
-        componentProps: {
-          options: [],
-          multiple: true,
-          filterable: true,
-          allowCreate: true,
-          defaultFirstOption: true
-        }
-      }
-    },
-    {
-      title: '可重定向的 URI 地址',
-      field: 'redirectUris',
-      isTable: false,
-      form: {
-        component: 'Select',
-        componentProps: {
-          options: [],
-          multiple: true,
-          filterable: true,
-          allowCreate: true,
-          defaultFirstOption: true
-        }
-      }
-    },
-    {
-      title: '权限',
-      field: 'authorities',
-      isTable: false,
-      form: {
-        component: 'Select',
-        componentProps: {
-          options: [],
-          multiple: true,
-          filterable: true,
-          allowCreate: true,
-          defaultFirstOption: true
-        }
-      }
-    },
-    {
-      title: '资源',
-      field: 'resourceIds',
-      isTable: false,
-      form: {
-        component: 'Select',
-        componentProps: {
-          options: [],
-          multiple: true,
-          filterable: true,
-          allowCreate: true,
-          defaultFirstOption: true
-        }
-      }
-    },
-    {
-      title: '附加信息',
-      field: 'additionalInformation',
-      isTable: false,
-      form: {
-        component: 'Input',
-        componentProps: {
-          type: 'textarea',
-          rows: 4
-        },
-        colProps: {
-          span: 24
-        }
-      }
-    },
-    {
-      title: t('common.createTime'),
-      field: 'createTime',
-      formatter: 'formatDate',
-      isForm: false
-    }
-  ]
-})
-export const { allSchemas } = useVxeCrudSchemas(crudSchemas)
diff --git a/src/views/system/oauth2/client/form.vue b/src/views/system/oauth2/client/form.vue
new file mode 100644
index 00000000..0822e59f
--- /dev/null
+++ b/src/views/system/oauth2/client/form.vue
@@ -0,0 +1,259 @@
+<template>
+  <Dialog :title="modelTitle" v-model="modelVisible" width="800">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="客户端编号" prop="secret">
+        <el-input v-model="formData.clientId" placeholder="请输入客户端编号" />
+      </el-form-item>
+      <el-form-item label="客户端密钥" prop="secret">
+        <el-input v-model="formData.secret" placeholder="请输入客户端密钥" />
+      </el-form-item>
+      <el-form-item label="应用名" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入应用名" />
+      </el-form-item>
+      <el-form-item label="应用图标">
+        <UploadImg v-model="formData.logo" :limit="1" />
+      </el-form-item>
+      <el-form-item label="应用描述">
+        <el-input type="textarea" v-model="formData.description" placeholder="请输入应用名" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="parseInt(dict.value)"
+            >{{ dict.label }}</el-radio
+          >
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="访问令牌的有效期" prop="accessTokenValiditySeconds">
+        <el-input-number v-model="formData.accessTokenValiditySeconds" placeholder="单位:秒" />
+      </el-form-item>
+      <el-form-item label="刷新令牌的有效期" prop="refreshTokenValiditySeconds">
+        <el-input-number v-model="formData.refreshTokenValiditySeconds" placeholder="单位:秒" />
+      </el-form-item>
+      <el-form-item label="授权类型" prop="authorizedGrantTypes">
+        <el-select
+          v-model="formData.authorizedGrantTypes"
+          multiple
+          filterable
+          placeholder="请输入授权类型"
+          style="width: 500px"
+        >
+          <el-option
+            v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="授权范围" prop="scopes">
+        <el-select
+          v-model="formData.scopes"
+          multiple
+          filterable
+          allow-create
+          placeholder="请输入授权范围"
+          style="width: 500px"
+        >
+          <el-option v-for="scope in formData.scopes" :key="scope" :label="scope" :value="scope" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="自动授权范围" prop="autoApproveScopes">
+        <el-select
+          v-model="formData.autoApproveScopes"
+          multiple
+          filterable
+          placeholder="请输入授权范围"
+          style="width: 500px"
+        >
+          <el-option v-for="scope in formData.scopes" :key="scope" :label="scope" :value="scope" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="可重定向的 URI 地址" prop="redirectUris">
+        <el-select
+          v-model="formData.redirectUris"
+          multiple
+          filterable
+          allow-create
+          placeholder="请输入可重定向的 URI 地址"
+          style="width: 500px"
+        >
+          <el-option
+            v-for="redirectUri in formData.redirectUris"
+            :key="redirectUri"
+            :label="redirectUri"
+            :value="redirectUri"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="权限" prop="authorities">
+        <el-select
+          v-model="formData.authorities"
+          multiple
+          filterable
+          allow-create
+          placeholder="请输入权限"
+          style="width: 500px"
+        >
+          <el-option
+            v-for="authority in formData.authorities"
+            :key="authority"
+            :label="authority"
+            :value="authority"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="资源" prop="resourceIds">
+        <el-select
+          v-model="formData.resourceIds"
+          multiple
+          filterable
+          allow-create
+          placeholder="请输入资源"
+          style="width: 500px"
+        >
+          <el-option
+            v-for="resourceId in formData.resourceIds"
+            :key="resourceId"
+            :label="resourceId"
+            :value="resourceId"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="附加信息" prop="additionalInformation">
+        <el-input
+          type="textarea"
+          v-model="formData.additionalInformation"
+          placeholder="请输入附加信息,JSON 格式数据"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+        <el-button @click="modelVisible = false">取 消</el-button>
+      </div>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE, getDictOptions } from '@/utils/dict'
+import * as ClientApi from '@/api/system/oauth2/client'
+import UploadImg from '@/components/UploadFile'
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const modelVisible = ref(false) // 弹窗的是否展示
+const modelTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  clientId: undefined,
+  secret: undefined,
+  name: undefined,
+  logo: undefined,
+  description: undefined,
+  status: DICT_TYPE.COMMON_STATUS,
+  accessTokenValiditySeconds: 30 * 60,
+  refreshTokenValiditySeconds: 30 * 24 * 60,
+  redirectUris: [],
+  authorizedGrantTypes: [],
+  scopes: [],
+  autoApproveScopes: [],
+  authorities: [],
+  resourceIds: [],
+  additionalInformation: undefined
+})
+const formRules = reactive({
+  clientId: [{ required: true, message: '客户端编号不能为空', trigger: 'blur' }],
+  secret: [{ required: true, message: '客户端密钥不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '应用名不能为空', trigger: 'blur' }],
+  logo: [{ required: true, message: '应用图标不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
+  accessTokenValiditySeconds: [
+    { required: true, message: '访问令牌的有效期不能为空', trigger: 'blur' }
+  ],
+  refreshTokenValiditySeconds: [
+    { required: true, message: '刷新令牌的有效期不能为空', trigger: 'blur' }
+  ],
+  redirectUris: [{ required: true, message: '可重定向的 URI 地址不能为空', trigger: 'blur' }],
+  authorizedGrantTypes: [{ required: true, message: '授权类型不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const openModal = async (type: string, id?: number) => {
+  modelVisible.value = true
+  modelTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await ClientApi.getOAuth2ClientApi(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as ClientApi.OAuth2ClientVO
+    if (formType.value === 'create') {
+      await ClientApi.createOAuth2ClientApi(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ClientApi.updateOAuth2ClientApi(data)
+      message.success(t('common.updateSuccess'))
+    }
+    modelVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    clientId: undefined,
+    secret: undefined,
+    name: undefined,
+    logo: undefined,
+    description: undefined,
+    status: DICT_TYPE.COMMON_STATUS,
+    accessTokenValiditySeconds: 30 * 60,
+    refreshTokenValiditySeconds: 30 * 24 * 60,
+    redirectUris: [],
+    authorizedGrantTypes: [],
+    scopes: [],
+    autoApproveScopes: [],
+    authorities: [],
+    resourceIds: [],
+    additionalInformation: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/oauth2/client/index.vue b/src/views/system/oauth2/client/index.vue
index 9ff44692..c88af726 100644
--- a/src/views/system/oauth2/client/index.vue
+++ b/src/views/system/oauth2/client/index.vue
@@ -133,7 +133,6 @@
 import type { FormExpose } from '@/components/Form'
 // 业务相关的 import
 import * as ClientApi from '@/api/system/oauth2/client'
-import { rules, allSchemas } from './client.data'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
diff --git a/src/views/system/role/MenuPermissionForm.vue b/src/views/system/role/MenuPermissionForm.vue
new file mode 100644
index 00000000..4628ef24
--- /dev/null
+++ b/src/views/system/role/MenuPermissionForm.vue
@@ -0,0 +1,202 @@
+<template>
+  <Dialog :title="dialogScopeTitle" v-model="dialogScopeVisible" width="800">
+    <el-form
+      ref="menuPermissionFormRef"
+      :model="dataScopeForm"
+      :inline="true"
+      label-width="80px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="角色名称">
+        <el-tag>{{ dataScopeForm.name }}</el-tag>
+      </el-form-item>
+      <el-form-item label="角色标识">
+        <el-tag>{{ dataScopeForm.code }}</el-tag>
+      </el-form-item>
+      <!-- 分配角色的数据权限对话框 -->
+      <el-form-item label="权限范围" v-if="actionScopeType === 'data'">
+        <el-select v-model="dataScopeForm.dataScope">
+          <el-option
+            v-for="item in dataScopeDictDatas"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
+      <!-- 分配角色的菜单权限对话框 -->
+      <el-row>
+        <el-col :span="24">
+          <el-form-item
+            label="权限范围"
+            v-if="
+              actionScopeType === 'menu' ||
+              dataScopeForm.dataScope === SystemDataScopeEnum.DEPT_CUSTOM
+            "
+            style="display: flex"
+          >
+            <el-card class="card" shadow="never">
+              <template #header>
+                父子联动(选中父节点,自动选择子节点):
+                <el-switch
+                  v-model="checkStrictly"
+                  inline-prompt
+                  active-text="是"
+                  inactive-text="否"
+                />
+                全选/全不选:
+                <el-switch
+                  v-model="treeNodeAll"
+                  inline-prompt
+                  active-text="是"
+                  inactive-text="否"
+                  @change="handleCheckedTreeNodeAll()"
+                />
+              </template>
+              <el-tree
+                ref="treeRef"
+                node-key="id"
+                show-checkbox
+                :check-strictly="!checkStrictly"
+                :props="defaultProps"
+                :data="dataScopeForm"
+                empty-text="加载中,请稍后"
+              />
+            </el-card>
+          </el-form-item> </el-col
+      ></el-row>
+    </el-form>
+    <!-- 操作按钮 -->
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button
+          :title="t('action.save')"
+          :loading="actionLoading"
+          @click="submitScope()"
+          type="primary"
+          :disabled="formLoading"
+        >
+          保存
+        </el-button>
+        <el-button
+          :loading="actionLoading"
+          :title="t('dialog.close')"
+          @click="dialogScopeVisible = false"
+          >取 消</el-button
+        >
+      </div>
+    </template>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import * as RoleApi from '@/api/system/role'
+import type { ElTree } from 'element-plus'
+import type { FormExpose } from '@/components/Form'
+import { handleTree, defaultProps } from '@/utils/tree'
+import { SystemDataScopeEnum } from '@/utils/constants'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { listSimpleMenusApi } from '@/api/system/menu'
+import { listSimpleDeptApi } from '@/api/system/dept'
+import * as PermissionApi from '@/api/system/permission'
+// ========== CRUD 相关 ==========
+const actionLoading = ref(false) // 遮罩层
+const menuPermissionFormRef = ref<FormExpose>() // 表单 Ref
+const { t } = useI18n() // 国际化
+const dialogScopeTitle = ref('菜单权限')
+const dataScopeDictDatas = ref()
+const message = useMessage() // 消息弹窗
+const actionScopeType = ref('')
+// 选项
+const checkStrictly = ref(true)
+const treeNodeAll = ref(false)
+const dialogScopeVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const treeOptions = ref<any[]>([]) // 菜单树形结构
+const treeRef = ref<InstanceType<typeof ElTree>>()
+// ========== 数据权限 ==========
+const dataScopeForm = reactive({
+  id: 0,
+  name: '',
+  code: '',
+  dataScope: 0,
+  checkList: []
+})
+
+/** 打开弹窗 */
+const openModal = async (type: string, row: RoleApi.RoleVO) => {
+  dataScopeForm.id = row.id
+  dataScopeForm.name = row.name
+  dataScopeForm.code = row.code
+  actionScopeType.value = type
+  dialogScopeVisible.value = true
+  if (type === 'menu') {
+    const menuRes = await listSimpleMenusApi()
+    treeOptions.value = handleTree(menuRes)
+    const role = await PermissionApi.listRoleMenusApi(row.id)
+    if (role) {
+      role?.forEach((item: any) => {
+        unref(treeRef)?.setChecked(item, true, false)
+      })
+    }
+  } else if (type === 'data') {
+    const deptRes = await listSimpleDeptApi()
+    treeOptions.value = handleTree(deptRes)
+    const role = await RoleApi.getRole(row.id)
+    dataScopeForm.dataScope = role.dataScope
+    if (role.dataScopeDeptIds) {
+      role.dataScopeDeptIds?.forEach((item: any) => {
+        unref(treeRef)?.setChecked(item, true, false)
+      })
+    }
+  }
+}
+
+// 保存权限
+const submitScope = async () => {
+  if ('data' === actionScopeType.value) {
+    const data = ref<PermissionApi.PermissionAssignRoleDataScopeReqVO>({
+      roleId: dataScopeForm.id,
+      dataScope: dataScopeForm.dataScope,
+      dataScopeDeptIds:
+        dataScopeForm.dataScope !== SystemDataScopeEnum.DEPT_CUSTOM
+          ? []
+          : (treeRef.value!.getCheckedKeys(false) as unknown as Array<number>)
+    })
+    await PermissionApi.assignRoleDataScopeApi(data.value)
+  } else if ('menu' === actionScopeType.value) {
+    const data = ref<PermissionApi.PermissionAssignRoleMenuReqVO>({
+      roleId: dataScopeForm.id,
+      menuIds: [
+        ...(treeRef.value!.getCheckedKeys(false) as unknown as Array<number>),
+        ...(treeRef.value!.getHalfCheckedKeys() as unknown as Array<number>)
+      ]
+    })
+    await PermissionApi.assignRoleMenuApi(data.value)
+  }
+  message.success(t('common.updateSuccess'))
+  dialogScopeVisible.value = false
+}
+
+// 全选/全不选
+const handleCheckedTreeNodeAll = () => {
+  treeRef.value!.setCheckedNodes(treeNodeAll.value ? treeOptions.value : [])
+}
+
+const init = () => {
+  dataScopeDictDatas.value = getIntDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE)
+}
+
+defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗
+// ========== 初始化 ==========
+onMounted(() => {
+  init()
+})
+</script>
+<style scoped>
+.card {
+  width: 100%;
+  max-height: 400px;
+  overflow-y: scroll;
+}
+</style>
diff --git a/src/views/system/role/RoleForm.vue b/src/views/system/role/RoleForm.vue
new file mode 100644
index 00000000..0fdb130b
--- /dev/null
+++ b/src/views/system/role/RoleForm.vue
@@ -0,0 +1,148 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="modelVisible" width="800">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="角色名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入角色名称" />
+      </el-form-item>
+      <el-form-item label="角色类型" prop="type">
+        <el-input :model-value="formData.type" placeholder="请输入角色类型" height="150px" />
+      </el-form-item>
+      <el-form-item label="角色标识" prop="code">
+        <el-input :model-value="formData.code" placeholder="请输入角色标识" height="150px" />
+      </el-form-item>
+      <el-form-item label="显示顺序" prop="sort">
+        <el-input :model-value="formData.sort" placeholder="请输入显示顺序" height="150px" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="formData.status" placeholder="请选择状态" clearable>
+          <el-option
+            v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="parseInt(dict.value)"
+            :label="dict.label"
+            :value="parseInt(dict.value)"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input v-model="formData.remark" type="textarea" placeholder="请输备注" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+        <el-button @click="modelVisible = false">取 消</el-button>
+      </div>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import type { FormExpose } from '@/components/Form'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as RoleApi from '@/api/system/role'
+// ========== CRUD 相关 ==========
+const dialogTitle = ref('edit') // 弹出层标题
+const formRef = ref<FormExpose>() // 表单 Ref
+const { t } = useI18n() // 国际化
+const dataScopeDictDatas = ref()
+const message = useMessage() // 消息弹窗
+
+const modelVisible = ref(false) // 弹窗的是否展示
+const modelTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+
+const formData = ref({
+  id: undefined,
+  name: '',
+  type: '',
+  code: '',
+  sort: undefined,
+  status: CommonStatusEnum.ENABLE,
+  remark: ''
+})
+const formRules = reactive({
+  name: [{ required: true, message: '岗位标题不能为空', trigger: 'blur' }],
+  code: [{ required: true, message: '岗位编码不能为空', trigger: 'change' }],
+  sort: [{ required: true, message: '岗位顺序不能为空', trigger: 'change' }],
+  status: [{ required: true, message: '岗位状态不能为空', trigger: 'change' }],
+  remark: [{ required: false, message: '岗位内容不能为空', trigger: 'blur' }]
+})
+
+/** 打开弹窗 */
+const openModal = async (type: string, id?: number) => {
+  modelVisible.value = true
+  modelTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await RoleApi.getRole(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: '',
+    code: '',
+    sort: undefined,
+    status: CommonStatusEnum.ENABLE,
+    remark: ''
+  }
+  formRef.value?.resetFields()
+}
+defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as RoleApi.RoleVO
+    if (formType.value === 'create') {
+      await RoleApi.createRole(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await RoleApi.updateRole(data)
+      message.success(t('common.updateSuccess'))
+    }
+    modelVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+const init = () => {
+  dataScopeDictDatas.value = getIntDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE)
+}
+// ========== 初始化 ==========
+onMounted(() => {
+  init()
+})
+</script>
+<style scoped>
+.card {
+  width: 100%;
+  max-height: 400px;
+  overflow-y: scroll;
+}
+</style>
diff --git a/src/views/system/role/index.vue b/src/views/system/role/index.vue
index da4b8389..12ef7845 100644
--- a/src/views/system/role/index.vue
+++ b/src/views/system/role/index.vue
@@ -1,331 +1,249 @@
 <template>
   <ContentWrap>
-    <!-- 列表 -->
-    <XTable @register="registerTable">
-      <!-- 操作:新增 -->
-      <template #toolbar_buttons>
-        <XButton
-          type="primary"
-          preIcon="ep:zoom-in"
-          :title="t('action.add')"
-          v-hasPermi="['system:role:create']"
-          @click="handleCreate()"
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="角色名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入角色名称"
+          clearable
+          @keyup.enter="handleQuery"
         />
-      </template>
-      <template #actionbtns_default="{ row }">
-        <!-- 操作:编辑 -->
-        <XTextButton
-          preIcon="ep:edit"
-          :title="t('action.edit')"
-          v-hasPermi="['system:role:update']"
-          @click="handleUpdate(row.id)"
-        />
-        <!-- 操作:详情 -->
-        <XTextButton
-          preIcon="ep:view"
-          :title="t('action.detail')"
-          v-hasPermi="['system:role:query']"
-          @click="handleDetail(row.id)"
-        />
-        <!-- 操作:菜单权限 -->
-        <XTextButton
-          preIcon="ep:basketball"
-          title="菜单权限"
-          v-hasPermi="['system:permission:assign-role-menu']"
-          @click="handleScope('menu', row)"
-        />
-        <!-- 操作:数据权限 -->
-        <XTextButton
-          preIcon="ep:coin"
-          title="数据权限"
-          v-hasPermi="['system:permission:assign-role-data-scope']"
-          @click="handleScope('data', row)"
-        />
-        <!-- 操作:删除 -->
-        <XTextButton
-          preIcon="ep:delete"
-          :title="t('action.del')"
-          v-hasPermi="['system:role:delete']"
-          @click="deleteData(row.id)"
-        />
-      </template>
-    </XTable>
-  </ContentWrap>
-
-  <XModal v-model="dialogVisible" :title="dialogTitle">
-    <!-- 对话框(添加 / 修改) -->
-    <Form
-      v-if="['create', 'update'].includes(actionType)"
-      :schema="allSchemas.formSchema"
-      :rules="rules"
-      ref="formRef"
-    />
-    <!-- 对话框(详情) -->
-    <Descriptions
-      v-if="actionType === 'detail'"
-      :schema="allSchemas.detailSchema"
-      :data="detailData"
-    />
-    <!-- 操作按钮 -->
-    <template #footer>
-      <XButton
-        v-if="['create', 'update'].includes(actionType)"
-        type="primary"
-        :title="t('action.save')"
-        :loading="actionLoading"
-        @click="submitForm()"
-      />
-      <XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
-    </template>
-  </XModal>
-
-  <XModal v-model="dialogScopeVisible" :title="dialogScopeTitle">
-    <el-form :model="dataScopeForm" label-width="140px" :inline="true">
-      <el-form-item label="角色名称">
-        <el-tag>{{ dataScopeForm.name }}</el-tag>
       </el-form-item>
-      <el-form-item label="角色标识">
-        <el-tag>{{ dataScopeForm.code }}</el-tag>
+      <el-form-item label="角色标识" prop="code">
+        <el-input
+          v-model="queryParams.code"
+          placeholder="请输入角色标识"
+          clearable
+          @keyup.enter="handleQuery"
+        />
       </el-form-item>
-      <!-- 分配角色的数据权限对话框 -->
-      <el-form-item label="权限范围" v-if="actionScopeType === 'data'">
-        <el-select v-model="dataScopeForm.dataScope">
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
           <el-option
-            v-for="item in dataScopeDictDatas"
-            :key="item.value"
-            :label="item.label"
-            :value="item.value"
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
           />
         </el-select>
       </el-form-item>
-      <!-- 分配角色的菜单权限对话框 -->
-      <el-row>
-        <el-col :span="24">
-          <el-form-item
-            label="权限范围"
-            v-if="
-              actionScopeType === 'menu' ||
-              dataScopeForm.dataScope === SystemDataScopeEnum.DEPT_CUSTOM
-            "
-            style="display: flex"
-          >
-            <el-card class="card" shadow="never">
-              <template #header>
-                父子联动(选中父节点,自动选择子节点):
-                <el-switch
-                  v-model="checkStrictly"
-                  inline-prompt
-                  active-text="是"
-                  inactive-text="否"
-                />
-                全选/全不选:
-                <el-switch
-                  v-model="treeNodeAll"
-                  inline-prompt
-                  active-text="是"
-                  inactive-text="否"
-                  @change="handleCheckedTreeNodeAll()"
-                />
-              </template>
-              <el-tree
-                ref="treeRef"
-                node-key="id"
-                show-checkbox
-                :check-strictly="!checkStrictly"
-                :props="defaultProps"
-                :data="treeOptions"
-                empty-text="加载中,请稍后"
-              />
-            </el-card>
-          </el-form-item> </el-col
-      ></el-row>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button type="primary" @click="openModal('create')" v-hasPermi="['system:role:create']">
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['system:role:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
     </el-form>
-    <!-- 操作按钮 -->
-    <template #footer>
-      <XButton
-        type="primary"
-        :title="t('action.save')"
-        :loading="actionLoading"
-        @click="submitScope()"
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" align="center">
+      <el-table-column label="角色编号" align="center" prop="id" />
+      <el-table-column label="角色名称" align="center" prop="name" />
+      <el-table-column label="角色类型" align="center" prop="type" />
+      <el-table-column label="角色标识" align="center" prop="code" />
+      <el-table-column label="显示顺序" align="center" prop="sort" />
+      <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column label="状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
       />
-      <XButton
-        :loading="actionLoading"
-        :title="t('dialog.close')"
-        @click="dialogScopeVisible = false"
-      />
-    </template>
-  </XModal>
+      <el-table-column :width="300" label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openModal('update', scope.row.id)"
+            v-hasPermi="['system:role:update']"
+          >
+            编辑
+          </el-button>
+          <!-- 操作:菜单权限 -->
+          <el-button
+            link
+            type="primary"
+            preIcon="ep:basketball"
+            title="菜单权限"
+            v-hasPermi="['system:permission:assign-role-menu']"
+            @click="handleScope('menu', scope.row)"
+          >
+            菜单权限
+          </el-button>
+          <!-- 操作:数据权限 -->
+          <el-button
+            link
+            type="primary"
+            preIcon="ep:coin"
+            title="数据权限"
+            v-hasPermi="['system:permission:assign-role-data-scope']"
+            @click="handleScope('data', scope.row)"
+          >
+            数据权限
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['system:role:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <RoleForm ref="formRef" @success="getList" />
+  <!-- 表单弹窗:菜单权限 -->
+  <MenuPermissionForm ref="menuPermissionFormRef" @success="getList" />
 </template>
-<script setup lang="ts" name="Role">
-import type { ElTree } from 'element-plus'
-import type { FormExpose } from '@/components/Form'
-import { handleTree, defaultProps } from '@/utils/tree'
-import { SystemDataScopeEnum } from '@/utils/constants'
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { rules, allSchemas } from './role.data'
+<script setup lang="tsx">
 import * as RoleApi from '@/api/system/role'
-import { listSimpleMenusApi } from '@/api/system/menu'
-import { listSimpleDeptApi } from '@/api/system/dept'
-import * as PermissionApi from '@/api/system/permission'
+import RoleForm from './RoleForm.vue'
+import MenuPermissionForm from './MenuPermissionForm.vue'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
 
-const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
-// 列表相关的变量
-const [registerTable, { reload, deleteData }] = useXTable({
-  allSchemas: allSchemas,
-  getListApi: RoleApi.getRolePageApi,
-  deleteApi: RoleApi.deleteRoleApi
-})
-
-// ========== CRUD 相关 ==========
-const actionLoading = ref(false) // 遮罩层
+const { t } = useI18n() // 国际化
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const dialogTitle = ref('编辑') // 弹出层标题
 const actionType = ref('') // 操作按钮的类型
-const dialogVisible = ref(false) // 是否显示弹出层
-const dialogTitle = ref('edit') // 弹出层标题
-const formRef = ref<FormExpose>() // 表单 Ref
-const detailData = ref() // 详情 Ref
+const modelVisible = ref(false) // 是否显示弹出层
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  code: '',
+  name: '',
+  status: undefined,
+  createTime: []
+})
 
 // 设置标题
 const setDialogTile = (type: string) => {
   dialogTitle.value = t('action.' + type)
   actionType.value = type
-  dialogVisible.value = true
+  modelVisible.value = true
 }
 
-// 新增操作
-const handleCreate = () => {
-  setDialogTile('create')
+/** 查询角色列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await RoleApi.getRolePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
 }
 
-// 修改操作
-const handleUpdate = async (rowId: number) => {
-  setDialogTile('update')
-  // 设置数据
-  const res = await RoleApi.getRoleApi(rowId)
-  unref(formRef)?.setValues(res)
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
 }
 
-// 详情操作
-const handleDetail = async (rowId: number) => {
-  setDialogTile('detail')
-  // 设置数据
-  const res = await RoleApi.getRoleApi(rowId)
-  detailData.value = res
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
 }
 
-// 提交按钮
-const submitForm = async () => {
-  const elForm = unref(formRef)?.getElFormRef()
-  if (!elForm) return
-  elForm.validate(async (valid) => {
-    if (valid) {
-      actionLoading.value = true
-      // 提交请求
-      try {
-        const data = unref(formRef)?.formModel as RoleApi.RoleVO
-        if (actionType.value === 'create') {
-          await RoleApi.createRoleApi(data)
-          message.success(t('common.createSuccess'))
-        } else {
-          await RoleApi.updateRoleApi(data)
-          message.success(t('common.updateSuccess'))
-        }
-        dialogVisible.value = false
-      } finally {
-        actionLoading.value = false
-        // 刷新列表
-        await reload()
-      }
-    }
-  })
+/** 添加/修改操作 */
+const formRef = ref()
+const openModal = (type: string, id?: number) => {
+  setDialogTile('编辑')
+  formRef.value.openModal(type, id)
 }
 
-// ========== 数据权限 ==========
-const dataScopeForm = reactive({
-  id: 0,
-  name: '',
-  code: '',
-  dataScope: 0,
-  checkList: []
-})
-const treeOptions = ref<any[]>([]) // 菜单树形结构
-const treeRef = ref<InstanceType<typeof ElTree>>()
-const dialogScopeVisible = ref(false)
-const dialogScopeTitle = ref('数据权限')
-const actionScopeType = ref('')
-const dataScopeDictDatas = ref()
-// 选项
-const checkStrictly = ref(true)
-const treeNodeAll = ref(false)
-// 全选/全不选
-const handleCheckedTreeNodeAll = () => {
-  treeRef.value!.setCheckedNodes(treeNodeAll.value ? treeOptions.value : [])
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await RoleApi.deleteRole(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
 }
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await RoleApi.exportPostApi(queryParams)
+    download.excel(data, '角色列表.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+/** 数据权限操作 */
+const menuPermissionFormRef = ref()
+
 // 权限操作
 const handleScope = async (type: string, row: RoleApi.RoleVO) => {
-  dataScopeForm.id = row.id
-  dataScopeForm.name = row.name
-  dataScopeForm.code = row.code
-  actionScopeType.value = type
-  dialogScopeVisible.value = true
-  if (type === 'menu') {
-    const menuRes = await listSimpleMenusApi()
-    treeOptions.value = handleTree(menuRes)
-    const role = await PermissionApi.listRoleMenusApi(row.id)
-    if (role) {
-      role?.forEach((item: any) => {
-        unref(treeRef)?.setChecked(item, true, false)
-      })
-    }
-  } else if (type === 'data') {
-    const deptRes = await listSimpleDeptApi()
-    treeOptions.value = handleTree(deptRes)
-    const role = await RoleApi.getRoleApi(row.id)
-    dataScopeForm.dataScope = role.dataScope
-    if (role.dataScopeDeptIds) {
-      role.dataScopeDeptIds?.forEach((item: any) => {
-        unref(treeRef)?.setChecked(item, true, false)
-      })
-    }
-  }
+  menuPermissionFormRef.value.openModal(type, row)
 }
-// 保存权限
-const submitScope = async () => {
-  if ('data' === actionScopeType.value) {
-    const data = ref<PermissionApi.PermissionAssignRoleDataScopeReqVO>({
-      roleId: dataScopeForm.id,
-      dataScope: dataScopeForm.dataScope,
-      dataScopeDeptIds:
-        dataScopeForm.dataScope !== SystemDataScopeEnum.DEPT_CUSTOM
-          ? []
-          : (treeRef.value!.getCheckedKeys(false) as unknown as Array<number>)
-    })
-    await PermissionApi.assignRoleDataScopeApi(data.value)
-  } else if ('menu' === actionScopeType.value) {
-    const data = ref<PermissionApi.PermissionAssignRoleMenuReqVO>({
-      roleId: dataScopeForm.id,
-      menuIds: [
-        ...(treeRef.value!.getCheckedKeys(false) as unknown as Array<number>),
-        ...(treeRef.value!.getHalfCheckedKeys() as unknown as Array<number>)
-      ]
-    })
-    await PermissionApi.assignRoleMenuApi(data.value)
-  }
-  message.success(t('common.updateSuccess'))
-  dialogScopeVisible.value = false
-}
-const init = () => {
-  dataScopeDictDatas.value = getIntDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE)
-}
-// ========== 初始化 ==========
+/** 初始化 **/
 onMounted(() => {
-  init()
+  getList()
 })
 </script>
-<style scoped>
-.card {
-  width: 100%;
-  max-height: 400px;
-  overflow-y: scroll;
-}
-</style>
diff --git a/src/views/system/role/role.data.ts b/src/views/system/role/role.data.ts
deleted file mode 100644
index d55b5e21..00000000
--- a/src/views/system/role/role.data.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
-// 国际化
-const { t } = useI18n()
-// 表单校验
-export const rules = reactive({
-  name: [required],
-  code: [required],
-  sort: [required]
-})
-// CrudSchema
-const crudSchemas = reactive<VxeCrudSchema>({
-  // primaryKey: 'id',
-  // primaryTitle: '角色编号',
-  // primaryType: 'seq',
-  action: true,
-  actionWidth: '400px',
-  columns: [
-    {
-      title: '角色编号',
-      field: 'id',
-      table: {
-        width: 200
-      }
-    },
-    {
-      title: '角色名称',
-      field: 'name',
-      isSearch: true
-    },
-    {
-      title: '角色类型',
-      field: 'type',
-      dictType: DICT_TYPE.SYSTEM_ROLE_TYPE,
-      dictClass: 'number',
-      isForm: false
-    },
-    {
-      title: '角色标识',
-      field: 'code',
-      isSearch: true
-    },
-    {
-      title: '显示顺序',
-      field: 'sort'
-    },
-    {
-      title: t('form.remark'),
-      field: 'remark',
-      isTable: false,
-      form: {
-        component: 'Input',
-        componentProps: {
-          type: 'textarea',
-          rows: 4
-        },
-        colProps: {
-          span: 24
-        }
-      }
-    },
-    {
-      title: t('common.status'),
-      field: 'status',
-      dictType: DICT_TYPE.COMMON_STATUS,
-      dictClass: 'number',
-      isSearch: true
-    },
-    {
-      title: t('common.createTime'),
-      field: 'createTime',
-      formatter: 'formatDate',
-      isForm: false,
-      search: {
-        show: true,
-        itemRender: {
-          name: 'XDataTimePicker'
-        }
-      }
-    }
-  ]
-})
-export const { allSchemas } = useVxeCrudSchemas(crudSchemas)
diff --git a/src/views/system/sensitiveWord/form.vue b/src/views/system/sensitiveWord/SensitiveWordForm.vue
similarity index 87%
rename from src/views/system/sensitiveWord/form.vue
rename to src/views/system/sensitiveWord/SensitiveWordForm.vue
index 0736542e..c069756b 100644
--- a/src/views/system/sensitiveWord/form.vue
+++ b/src/views/system/sensitiveWord/SensitiveWordForm.vue
@@ -33,7 +33,7 @@
           placeholder="请选择文章标签"
           style="width: 380px"
         >
-          <el-option v-for="tag in tags" :key="tag" :label="tag" :value="tag" />
+          <el-option v-for="tag in tagList" :key="tag" :label="tag" :value="tag" />
         </el-select>
       </el-form-item>
     </el-form>
@@ -47,7 +47,6 @@
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import * as SensitiveWordApi from '@/api/system/sensitiveWord'
 import { CommonStatusEnum } from '@/utils/constants'
-
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
@@ -67,10 +66,10 @@ const formRules = reactive({
   tags: [{ required: true, message: '标签不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
-const tags = ref([]) // todo @blue-syd:在 openModal 里加载下
+const tagList = ref([]) // 标签数组
 
 /** 打开弹窗 */
-const openModal = async (type: string, id?: number) => {
+const open = async (type: string, id?: number) => {
   modelVisible.value = true
   modelTitle.value = t('action.' + type)
   formType.value = type
@@ -80,13 +79,14 @@ const openModal = async (type: string, id?: number) => {
     formLoading.value = true
     try {
       formData.value = await SensitiveWordApi.getSensitiveWord(id)
-      console.log(formData.value)
     } finally {
       formLoading.value = false
     }
   }
+  // 获得 Tag 标签列表
+  tagList.value = await SensitiveWordApi.getSensitiveWordTagList()
 }
-defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
 /** 提交表单 */
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
@@ -100,10 +100,10 @@ const submitForm = async () => {
   try {
     const data = formData.value as unknown as SensitiveWordApi.SensitiveWordVO
     if (formType.value === 'create') {
-      await SensitiveWordApi.createSensitiveWord(data) // TODO @blue-syd:去掉 API 后缀
+      await SensitiveWordApi.createSensitiveWord(data)
       message.success(t('common.createSuccess'))
     } else {
-      await SensitiveWordApi.updateSensitiveWord(data) // TODO @blue-syd:去掉 API 后缀
+      await SensitiveWordApi.updateSensitiveWord(data)
       message.success(t('common.updateSuccess'))
     }
     modelVisible.value = false
diff --git a/src/views/system/sensitiveWord/SensitiveWordTestForm.vue b/src/views/system/sensitiveWord/SensitiveWordTestForm.vue
new file mode 100644
index 00000000..881309c8
--- /dev/null
+++ b/src/views/system/sensitiveWord/SensitiveWordTestForm.vue
@@ -0,0 +1,90 @@
+<template>
+  <Dialog title="检测敏感词" v-model="modelVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="文本" prop="text">
+        <el-input type="textarea" v-model="formData.text" placeholder="请输入测试文本" />
+      </el-form-item>
+      <el-form-item label="标签" prop="tags">
+        <el-select
+          v-model="formData.tags"
+          multiple
+          filterable
+          allow-create
+          placeholder="请选择标签"
+          style="width: 380px"
+        >
+          <el-option v-for="tag in tagList" :key="tag" :label="tag" :value="tag" />
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="submitForm" type="primary" :disabled="formLoading">检 测</el-button>
+        <el-button @click="modelVisible = false">取 消</el-button>
+      </div>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import * as SensitiveWordApi from '@/api/system/sensitiveWord'
+const message = useMessage() // 消息弹窗
+
+const modelVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  text: '',
+  tags: []
+})
+const formRules = reactive({
+  text: [{ required: true, message: '测试文本不能为空', trigger: 'blur' }],
+  tags: [{ required: true, message: '标签不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+const tagList = ref([]) // 标签数组
+
+/** 打开弹窗 */
+const open = async () => {
+  modelVisible.value = true
+  resetForm()
+  // 获得 Tag 标签列表
+  tagList.value = await SensitiveWordApi.getSensitiveWordTagList()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const form = formData.value as unknown as SensitiveWordApi.SensitiveWordTestReqVO
+    const data = await SensitiveWordApi.validateText(form)
+    if (data.length === 0) {
+      message.success('不包含敏感词!')
+      return
+    }
+    message.warning('包含敏感词:' + data.join(', '))
+    modelVisible.value = false
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    text: '',
+    tags: []
+  }
+  formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/sensitiveWord/index.vue b/src/views/system/sensitiveWord/index.vue
index 17da6ca3..cf1fdb82 100644
--- a/src/views/system/sensitiveWord/index.vue
+++ b/src/views/system/sensitiveWord/index.vue
@@ -1,13 +1,20 @@
 <template>
-  <!-- 搜索 -->
-  <content-wrap>
-    <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true">
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
       <el-form-item label="敏感词" prop="name">
         <el-input
           v-model="queryParams.name"
           placeholder="请输入敏感词"
           clearable
           @keyup.enter="handleQuery"
+          class="!w-240px"
         />
       </el-form-item>
       <el-form-item label="标签" prop="tag">
@@ -16,17 +23,19 @@
           placeholder="请选择标签"
           clearable
           @keyup.enter="handleQuery"
+          class="!w-240px"
         >
-          <el-option v-for="tag in tags" :key="tag" :label="tag" :value="tag" />
+          <el-option v-for="tag in tagList" :key="tag" :label="tag" :value="tag" />
         </el-select>
       </el-form-item>
       <el-form-item label="状态" prop="status">
         <el-select v-model="queryParams.status" placeholder="请选择启用状态" clearable>
           <el-option
-            v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)"
-            :key="parseInt(dict.value)"
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
             :label="dict.label"
-            :value="parseInt(dict.value)"
+            :value="dict.value"
+            class="!w-240px"
           />
         </el-select>
       </el-form-item>
@@ -38,6 +47,7 @@
           start-placeholder="开始日期"
           end-placeholder="结束日期"
           :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
         />
       </el-form-item>
       <el-form-item>
@@ -45,7 +55,8 @@
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
         <el-button
           type="primary"
-          @click="openModal('create')"
+          plain
+          @click="openForm('create')"
           v-hasPermi="['system:sensitive-word:create']"
         >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
@@ -59,12 +70,15 @@
         >
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
+        <el-button type="warning" plain @click="openTestForm">
+          <Icon icon="ep:document-checked" class="mr-5px" /> 测试
+        </el-button>
       </el-form-item>
     </el-form>
-  </content-wrap>
+  </ContentWrap>
 
   <!-- 列表 -->
-  <content-wrap>
+  <ContentWrap>
     <el-table v-loading="loading" :data="list">
       <el-table-column label="编号" align="center" prop="id" />
       <el-table-column label="敏感词" align="center" prop="name" />
@@ -77,15 +91,13 @@
       <el-table-column label="标签" align="center" prop="tags">
         <template #default="scope">
           <el-tag
-            :disable-transitions="true"
-            :key="index"
-            v-for="(tag, index) in scope.row.tags"
-            :index="index"
             class="mr-5px"
+            v-for="tag in scope.row.tags"
+            :key="tag"
+            :disable-transitions="true"
           >
             {{ tag }}
           </el-tag>
-          &nbsp; &nbsp;
         </template>
       </el-table-column>
       <el-table-column
@@ -100,7 +112,7 @@
           <el-button
             link
             type="primary"
-            @click="openModal('update', scope.row.id)"
+            @click="openForm('update', scope.row.id)"
             v-hasPermi="['infra:config:update']"
           >
             编辑
@@ -123,17 +135,21 @@
       v-model:limit="queryParams.pageSize"
       @pagination="getList"
     />
-  </content-wrap>
+  </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->
-  <SensitiveWordForm ref="modalRef" @success="getList" />
+  <SensitiveWordForm ref="formRef" @success="getList" />
+
+  <!-- 表单弹窗:测试敏感词 -->
+  <SensitiveWordTestForm ref="testFormRef" />
 </template>
 <script setup lang="ts" name="SensitiveWord">
-import { DICT_TYPE, getDictOptions } from '@/utils/dict'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
 import * as SensitiveWordApi from '@/api/system/sensitiveWord'
-import SensitiveWordForm from './form.vue' // TODO @blue-syd:组件名不对
+import SensitiveWordForm from './SensitiveWordForm.vue'
+import SensitiveWordTestForm from './SensitiveWordTestForm.vue'
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
@@ -150,13 +166,13 @@ const queryParams = reactive({
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
-const tags = ref([])
+const tagList = ref([]) // 标签数组
 
 /** 查询参数列表 */
 const getList = async () => {
   loading.value = true
   try {
-    const data = await SensitiveWordApi.getSensitiveWordPage(queryParams) // TODO @blue-syd:去掉 API 后缀哈
+    const data = await SensitiveWordApi.getSensitiveWordPage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -177,12 +193,16 @@ const resetQuery = () => {
 }
 
 /** 添加/修改操作 */
-const modalRef = ref()
-const openModal = (type: string, id?: number) => {
-  modalRef.value.openModal(type, id)
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
 }
 
-// TODO @blue-syd:还少一个【测试】按钮的功能,参见 http://dashboard.yudao.iocoder.cn/system/sensitive-word
+/** 测试敏感词按钮操作 */
+const testFormRef = ref()
+const openTestForm = () => {
+  testFormRef.value.open()
+}
 
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
@@ -204,7 +224,7 @@ const handleExport = async () => {
     await message.exportConfirm()
     // 发起导出
     exportLoading.value = true
-    const data = await SensitiveWordApi.exportSensitiveWord(queryParams) // TODO @blue-syd:去掉 API 后缀哈
+    const data = await SensitiveWordApi.exportSensitiveWord(queryParams)
     download.excel(data, '敏感词.xls')
   } catch {
   } finally {
@@ -212,14 +232,10 @@ const handleExport = async () => {
   }
 }
 
-/** 获得 Tag 标签列表 */
-const getTags = async () => {
-  tags.value = await SensitiveWordApi.getSensitiveWordTags() // TODO @blue-syd:去掉 API 后缀哈
-}
-
 /** 初始化 **/
-onMounted(() => {
-  getTags()
-  getList()
+onMounted(async () => {
+  await getList()
+  // 获得 Tag 标签列表
+  tagList.value = await SensitiveWordApi.getSensitiveWordTagList()
 })
 </script>
diff --git a/src/views/system/tenantPackage/index.vue b/src/views/system/tenantPackage/index.vue
index c8f5756a..07ea39c6 100644
--- a/src/views/system/tenantPackage/index.vue
+++ b/src/views/system/tenantPackage/index.vue
@@ -71,7 +71,7 @@ import type { ElTree } from 'element-plus'
 // 业务相关的 import
 import { rules, allSchemas } from './tenantPackage.data'
 import * as TenantPackageApi from '@/api/system/tenantPackage'
-import { listSimpleMenusApi } from '@/api/system/menu'
+import { getSimpleMenusList } from '@/api/system/menu'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -102,7 +102,7 @@ const validateCategory = (rule: any, value: any, callback: any) => {
 rules.menuIds = [{ required: true, validator: validateCategory, trigger: 'blur' }]
 
 const getTree = async () => {
-  const res = await listSimpleMenusApi()
+  const res = await getSimpleMenusList()
   menuOptions.value = handleTree(res)
 }
 
diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue
index 9bb50930..542ae2f0 100644
--- a/src/views/system/user/index.vue
+++ b/src/views/system/user/index.vue
@@ -271,8 +271,8 @@ import { getAccessToken, getTenantId } from '@/utils/auth'
 import type { FormExpose } from '@/components/Form'
 import { rules, allSchemas } from './user.data'
 import * as UserApi from '@/api/system/user'
-import { listSimpleDeptApi } from '@/api/system/dept'
-import { listSimpleRolesApi } from '@/api/system/role'
+import { getSimpleDeptList } from '@/api/system/dept'
+import { getSimpleRoleList } from '@/api/system/role'
 import { getSimplePostList, PostVO } from '@/api/system/post'
 import {
   aassignUserRoleApi,
@@ -301,7 +301,7 @@ const filterText = ref('')
 const deptOptions = ref<Tree[]>([]) // 树形结构
 const treeRef = ref<InstanceType<typeof ElTree>>()
 const getTree = async () => {
-  const res = await listSimpleDeptApi()
+  const res = await getSimpleDeptList()
   deptOptions.value.push(...handleTree(res))
 }
 const filterNode = (value: string, data: Tree) => {
@@ -477,7 +477,7 @@ const handleRole = async (row: UserApi.UserVO) => {
   const roles = await listUserRolesApi(row.id)
   userRole.roleIds = roles
   // 获取角色列表
-  const roleOpt = await listSimpleRolesApi()
+  const roleOpt = await getSimpleRoleList()
   roleOptions.value = roleOpt
   roleDialogVisible.value = true
 }