From 21cce9812debdf51d7ca82b0346eddd2453660ae Mon Sep 17 00:00:00 2001
From: syd <syidong@aliyun.com>
Date: Fri, 24 Mar 2023 23:23:14 +0800
Subject: [PATCH 01/12] =?UTF-8?q?update:=20=E6=95=8F=E6=84=9F=E8=AF=8D?=
 =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=8F=90=E4=BA=A4TODO=20=E5=86=85=E5=AE=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/system/sensitiveWord/index.ts       | 12 ++-
 src/types/auto-components.d.ts              | 14 +---
 src/types/auto-imports.d.ts                 |  2 +-
 src/views/system/sensitiveWord/form.vue     |  5 +-
 src/views/system/sensitiveWord/index.vue    | 18 +++-
 src/views/system/sensitiveWord/testForm.vue | 92 +++++++++++++++++++++
 6 files changed, 126 insertions(+), 17 deletions(-)
 create mode 100644 src/views/system/sensitiveWord/testForm.vue

diff --git a/src/api/system/sensitiveWord/index.ts b/src/api/system/sensitiveWord/index.ts
index 7da2c28e..08078ba6 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
@@ -23,6 +24,11 @@ export interface SensitiveWordExportReqVO {
   createTime?: Date[]
 }
 
+export interface SensitiveWordTestReqVO {
+  text: string
+  tag: string[]
+}
+
 // 查询敏感词列表
 export const getSensitiveWordPage = (params: SensitiveWordPageReqVO) => {
   return request.get({ url: '/system/sensitive-word/page', params })
@@ -59,6 +65,8 @@ export const getSensitiveWordTags = () => {
 }
 
 // 获得文本所包含的不合法的敏感词数组
-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/types/auto-components.d.ts b/src/types/auto-components.d.ts
index 5c679fa9..374893bb 100644
--- a/src/types/auto-components.d.ts
+++ b/src/types/auto-components.d.ts
@@ -1,5 +1,7 @@
-// generated by unplugin-vue-components
-// We suggest you to commit this file into source control
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// Generated by unplugin-vue-components
 // Read more: https://github.com/vuejs/core/pull/3399
 import '@vue/runtime-core'
 
@@ -21,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,30 +54,24 @@ declare module '@vue/runtime-core' {
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
     ElInput: typeof import('element-plus/es')['ElInput']
-    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElLink: typeof import('element-plus/es')['ElLink']
     ElOption: typeof import('element-plus/es')['ElOption']
     ElPagination: typeof import('element-plus/es')['ElPagination']
     ElPopover: typeof import('element-plus/es')['ElPopover']
     ElRadio: typeof import('element-plus/es')['ElRadio']
-    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
     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']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
     ElTransfer: typeof import('element-plus/es')['ElTransfer']
-    ElTree: typeof import('element-plus/es')['ElTree']
-    ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
     ElUpload: typeof import('element-plus/es')['ElUpload']
     Error: typeof import('./../components/Error/src/Error.vue')['default']
     FlowCondition: typeof import('./../components/bpmnProcessDesigner/package/penal/flow-condition/FlowCondition.vue')['default']
diff --git a/src/types/auto-imports.d.ts b/src/types/auto-imports.d.ts
index 2c68c6ce..75cf16d9 100644
--- a/src/types/auto-imports.d.ts
+++ b/src/types/auto-imports.d.ts
@@ -72,5 +72,5 @@ declare global {
 // for type re-export
 declare global {
   // @ts-ignore
-  export type { Component,ComponentPublicInstance,ComputedRef,InjectionKey,PropType,Ref,VNode } from 'vue'
+  export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'
 }
diff --git a/src/views/system/sensitiveWord/form.vue b/src/views/system/sensitiveWord/form.vue
index 24bcdaaa..85f751c3 100644
--- a/src/views/system/sensitiveWord/form.vue
+++ b/src/views/system/sensitiveWord/form.vue
@@ -69,10 +69,11 @@ const formRules = reactive({
   tags: [{ required: true, message: '标签不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
-const tags = ref([]) // todo @blue-syd:在 openModal 里加载下
+const tags: Ref<string[]> = ref([]) // todo @blue-syd:在 openModal 里加载下
 
 /** 打开弹窗 */
-const openModal = async (type: string, id?: number) => {
+const openModal = async (type: string, paramTags: string[], id?: number) => {
+  tags.value = paramTags
   modelVisible.value = true
   modelTitle.value = t('action.' + type)
   formType.value = type
diff --git a/src/views/system/sensitiveWord/index.vue b/src/views/system/sensitiveWord/index.vue
index 17da6ca3..93ea3c71 100644
--- a/src/views/system/sensitiveWord/index.vue
+++ b/src/views/system/sensitiveWord/index.vue
@@ -45,13 +45,14 @@
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
         <el-button
           type="primary"
+          plain
           @click="openModal('create')"
           v-hasPermi="['system:sensitive-word:create']"
         >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
         <el-button
-          type="success"
+          type="warning"
           plain
           @click="handleExport"
           :loading="exportLoading"
@@ -59,6 +60,9 @@
         >
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
+        <el-button type="success" plain @click="handleTest">
+          <Icon icon="ep:document-checked" class="mr-5px" /> 测试
+        </el-button>
       </el-form-item>
     </el-form>
   </content-wrap>
@@ -127,6 +131,9 @@
 
   <!-- 表单弹窗:添加/修改 -->
   <SensitiveWordForm ref="modalRef" @success="getList" />
+
+  <!-- 表单弹窗:测试敏感词 -->
+  <SensitiveWordTestForm ref="modalTestRef" />
 </template>
 <script setup lang="ts" name="SensitiveWord">
 import { DICT_TYPE, getDictOptions } from '@/utils/dict'
@@ -134,6 +141,8 @@ 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 SensitiveWordTestForm from './testForm.vue'
+
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
@@ -179,10 +188,15 @@ const resetQuery = () => {
 /** 添加/修改操作 */
 const modalRef = ref()
 const openModal = (type: string, id?: number) => {
-  modalRef.value.openModal(type, id)
+  modalRef.value.openModal(type, tags.value, id)
 }
 
 // TODO @blue-syd:还少一个【测试】按钮的功能,参见 http://dashboard.yudao.iocoder.cn/system/sensitive-word
+/* 测试敏感词按钮操作 */
+const modalTestRef = ref()
+const handleTest = () => {
+  modalTestRef.value.openModal(tags.value)
+}
 
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
diff --git a/src/views/system/sensitiveWord/testForm.vue b/src/views/system/sensitiveWord/testForm.vue
new file mode 100644
index 00000000..766d771f
--- /dev/null
+++ b/src/views/system/sensitiveWord/testForm.vue
@@ -0,0 +1,92 @@
+<template>
+  <!-- 对话框(测试敏感词) -->
+  <Dialog :title="modelTitle" 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 tags" :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 modelTitle = ref('检测敏感词') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const tags: Ref<string[]> = ref([])
+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 openModal = async (paramTags: string[]) => {
+  tags.value = paramTags
+  modelVisible.value = true
+  resetForm()
+}
+defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗
+
+/** 提交表单 */
+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>

From 8535409d021b34b618ee31c7eecff2926e1430bd Mon Sep 17 00:00:00 2001
From: Theo <koutianyu@163.com>
Date: Sat, 25 Mar 2023 16:47:20 +0800
Subject: [PATCH 02/12] =?UTF-8?q?=E5=AE=8C=E6=88=90=E8=8F=9C=E5=8D=95?=
 =?UTF-8?q?=E6=A8=A1=E5=9D=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/views/system/menu/form.vue     | 297 ++++++++++++++++++
 src/views/system/menu/index.vue    | 473 +++++++++--------------------
 src/views/system/menu/menu.data.ts |  76 -----
 3 files changed, 442 insertions(+), 404 deletions(-)
 create mode 100644 src/views/system/menu/form.vue
 delete mode 100644 src/views/system/menu/menu.data.ts

diff --git a/src/views/system/menu/form.vue b/src/views/system/menu/form.vue
new file mode 100644
index 00000000..cf1583ec
--- /dev/null
+++ b/src/views/system/menu/form.vue
@@ -0,0 +1,297 @@
+<template>
+  <Dialog :title="modelTitle" v-model="modelVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="上级菜单">
+        <el-tree-select
+          node-key="id"
+          v-model="formData.parentId"
+          :props="defaultProps"
+          :data="menuOptions"
+          :default-expanded-keys="[0]"
+          check-strictly
+        />
+      </el-form-item>
+      <el-col :span="16">
+        <el-form-item label="菜单名称" prop="name">
+          <el-input v-model="formData.name" placeholder="请输入菜单名称" clearable />
+        </el-form-item>
+      </el-col>
+      <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>
+      <template v-if="formData.type !== 3">
+        <el-form-item label="菜单图标">
+          <IconSelect v-model="formData.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="formData.path" placeholder="请输入路由地址" clearable />
+          </el-form-item>
+        </el-col>
+      </template>
+      <template v-if="formData.type === 2">
+        <el-col :span="16">
+          <el-form-item label="组件地址" prop="component">
+            <el-input
+              v-model="formData.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="formData.componentName" placeholder="例如说:SystemUser" clearable />
+          </el-form-item>
+        </el-col>
+      </template>
+      <template v-if="formData.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="formData.permission" placeholder="请输入权限标识" clearable />
+          </el-form-item>
+        </el-col>
+      </template>
+      <el-col :span="16">
+        <el-form-item label="显示排序" prop="sort">
+          <el-input-number v-model="formData.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="formData.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="formData.type !== 3">
+        <el-col :span="16">
+          <el-form-item label="显示状态" prop="visible">
+            <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-col>
+      </template>
+      <template v-if="formData.type !== 3">
+        <el-col :span="16">
+          <el-form-item label="总是显示" prop="alwaysShow">
+            <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-col>
+      </template>
+      <template v-if="formData.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="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-col>
+      </template>
+    </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, 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: 1,
+  parentId: 0,
+  path: '',
+  icon: '',
+  component: '',
+  componentName: '',
+  status: CommonStatusEnum.ENABLE,
+  visible: true,
+  keepAlive: true,
+  alwaysShow: true,
+  createTime: new Date()
+})
+
+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 openModal = async (type: string, id?: number) => {
+  modelVisible.value = true
+  modelTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  await getTree()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await MenuApi.getMenuApi(id)
+      // TODO 芋艿:这块要优化下,部分字段未重置,无法修改
+      //   formData.value.componentName = res.componentName || ''
+      //   formData.value.alwaysShow = res.alwaysShow !== undefined ? res.alwaysShow : true
+    } 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 {
+    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
+    if (formType.value === 'create') {
+      await MenuApi.createMenuApi(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await MenuApi.updateMenuApi(data)
+      message.success(t('common.updateSuccess'))
+    }
+    modelVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+    wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
+  }
+}
+
+// ========== 下拉框[上级菜单] ==========
+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 resetForm = () => {
+  formData.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()
+  }
+  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..41d1bd67 100644
--- a/src/views/system/menu/index.vue
+++ b/src/views/system/menu/index.vue
@@ -1,351 +1,168 @@
 <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"
-    >
-      <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 :model="queryParams" ref="queryFormRef" :inline="true">
+      <el-form-item label="菜单名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入菜单名称"
+          clearable
+          @keyup.enter="handleQuery"
         />
       </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>
+          <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>
+        <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:menu:create']">
+          <Icon icon="ep:plus" 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>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="info" plain icon="el-icon-sort" @click="toggleExpandAll"
+          >展开/折叠</el-button
+        >
+      </el-col>
+    </el-row>
+
+    <el-table
+      v-loading="loading"
+      :data="list"
+      v-if="refreshTable"
+      row-key="id"
+      :default-expand-all="isExpandAll"
+      :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+    >
+      <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" class-name="small-padding fixed-width">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openModal('update', scope.row.id)"
+            v-hasPermi="['system:menu:update']"
+            >修改</el-button
+          >
+          <el-button
+            link
+            type="primary"
+            @click="openModal('create', scope.row.id)"
+            v-hasPermi="['system:menu:create']"
+            >新增</el-button
+          >
+          <el-button
+            link
+            type="primary"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['system:menu:delete']"
+            >删除</el-button
+          >
+        </template>
+      </el-table-column>
+    </el-table>
+  </ContentWrap>
+  <!-- 表单弹窗:添加/修改 -->
+  <menu-form ref="modalRef" @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 * as MenuApi from '@/api/system/menu'
-import { allSchemas, rules } from './menu.data'
+import { DICT_TYPE, getDictOptions } from '@/utils/dict'
 
+import { handleTree } from '@/utils/tree'
+import * as MenuApi from '@/api/system/menu'
+import MenuForm from './form.vue'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
-const { wsCache } = useCache()
 
-const xGrid = ref<any>(null)
+const loading = ref(true) // 列表的加载中
 
-// 列表相关的变量
-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 list = ref<any>([]) // 列表的数据
+const isExpandAll = ref(false) // 是否展开,默认全部折叠
+const refreshTable = ref(true) // 重新渲染表格状态
+const queryParams = reactive({
+  name: undefined,
+  status: undefined
 })
+const queryFormRef = ref() // 搜索的表单
 
-// ========== 下拉框[上级菜单] ==========
-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.getMenuListApi(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 modalRef = ref()
+const openModal = async (type: string, id?: number) => {
+  modalRef.value.openModal(type, id)
+}
+
+/** 展开/折叠操作 */
+const toggleExpandAll = () => {
+  refreshTable.value = false
+  isExpandAll.value = !isExpandAll.value
+  nextTick(() => {
+    refreshTable.value = true
+  })
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await MenuApi.deleteMenuApi(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)

From 1134921a0ab6364dec2366999f54d67d729cfc04 Mon Sep 17 00:00:00 2001
From: wuxiran <wuxiran@outlook.com>
Date: Sun, 26 Mar 2023 04:25:34 +0800
Subject: [PATCH 03/12] =?UTF-8?q?1=E3=80=81=E5=BE=AE=E4=BF=A1=E7=BB=84?=
 =?UTF-8?q?=E4=BB=B6=E6=9B=B4=E6=96=B0vue3=EF=BC=8C=E9=83=A8=E5=88=86?=
 =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=8F=AF=E8=83=BD=E8=BF=98=E6=9C=89=E9=97=AE?=
 =?UTF-8?q?=E9=A2=98=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/utils/dict.ts                             |  23 +-
 src/utils/formatTime.ts                       |  57 +-
 src/views/mp/components/img.png               | Bin 0 -> 15404 bytes
 src/views/mp/components/wx-location/main.vue  |  72 ++
 src/views/mp/components/wx-msg/card.scss      | 101 +++
 src/views/mp/components/wx-msg/comment.scss   |  88 +++
 src/views/mp/components/wx-msg/main.vue       | 338 ++++++++++
 src/views/mp/components/wx-music/main.vue     |  60 ++
 src/views/mp/components/wx-news/main.vue      | 107 +++
 src/views/mp/components/wx-reply/main.vue     | 634 ++++++++++++++++++
 .../mp/components/wx-video-play/main.vue      | 117 ++++
 .../mp/components/wx-voice-play/main.vue      | 100 +++
 src/views/mp/freePublish/index.vue            | 394 ++++++++++-
 src/views/mp/message/index.vue                | 261 ++++++-
 14 files changed, 2348 insertions(+), 4 deletions(-)
 create mode 100644 src/views/mp/components/img.png
 create mode 100644 src/views/mp/components/wx-location/main.vue
 create mode 100644 src/views/mp/components/wx-msg/card.scss
 create mode 100644 src/views/mp/components/wx-msg/comment.scss
 create mode 100644 src/views/mp/components/wx-msg/main.vue
 create mode 100644 src/views/mp/components/wx-music/main.vue
 create mode 100644 src/views/mp/components/wx-news/main.vue
 create mode 100644 src/views/mp/components/wx-reply/main.vue
 create mode 100644 src/views/mp/components/wx-video-play/main.vue
 create mode 100644 src/views/mp/components/wx-voice-play/main.vue

diff --git a/src/utils/dict.ts b/src/utils/dict.ts
index 15e57ff2..05c70dad 100644
--- a/src/utils/dict.ts
+++ b/src/utils/dict.ts
@@ -70,6 +70,23 @@ export const getDictObj = (dictType: string, value: any) => {
   })
 }
 
+/**
+ * 获得字典数据的文本展示
+ *
+ * @param dictType 字典类型
+ * @param value 字典数据的值
+ */
+export const getDictLabel = (dictType: string, value: any) => {
+  const dictOptions: DictDataType[] = getDictOptions(dictType)
+  const dictLabel = ref('')
+  dictOptions.forEach((dict: DictDataType) => {
+    if (dict.value === value) {
+      dictLabel.value = dict.label
+    }
+  })
+  return dictLabel.value
+}
+
 export enum DICT_TYPE {
   USER_TYPE = 'user_type',
   COMMON_STATUS = 'common_status',
@@ -123,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/utils/formatTime.ts b/src/utils/formatTime.ts
index 2582beee..ec7f3744 100644
--- a/src/utils/formatTime.ts
+++ b/src/utils/formatTime.ts
@@ -11,10 +11,65 @@ import dayjs from 'dayjs'
  * @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ"
  * @returns 返回拼接后的时间字符串
  */
-export function formatDate(date: Date, format: string): string {
+export function formatDate(date: Date, format?: string): string {
+  // 日期不存在,则返回空
+  if (!date) {
+    return ''
+  }
+  // 日期存在,则进行格式化
+  if (format === undefined) {
+    format = 'YYYY-MM-DD HH:mm:ss'
+  }
   return dayjs(date).format(format)
 }
 
+// TODO 芋艿:稍后去掉
+// 日期格式化
+export function parseTime(time: any, pattern?: string) {
+  if (arguments.length === 0 || !time) {
+    return null
+  }
+  const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
+  let date
+  if (typeof time === 'object') {
+    date = time
+  } else {
+    if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
+      time = parseInt(time)
+    } else if (typeof time === 'string') {
+      time = time
+        .replace(new RegExp(/-/gm), '/')
+        .replace('T', ' ')
+        .replace(new RegExp(/\.\d{3}/gm), '')
+    }
+    if (typeof time === 'number' && time.toString().length === 10) {
+      time = time * 1000
+    }
+    date = new Date(time)
+  }
+  const formatObj = {
+    y: date.getFullYear(),
+    m: date.getMonth() + 1,
+    d: date.getDate(),
+    h: date.getHours(),
+    i: date.getMinutes(),
+    s: date.getSeconds(),
+    a: date.getDay()
+  }
+  const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
+    let value = formatObj[key]
+    // Note: getDay() returns 0 on Sunday
+    if (key === 'a') {
+      return ['日', '一', '二', '三', '四', '五', '六'][value]
+    }
+    if (result.length > 0 && value < 10) {
+      value = '0' + value
+    }
+    return value || 0
+  })
+  return time_str
+}
+
 /**
  * 获取当前日期是第几周
  * @param dateTime 当前传入的日期值
diff --git a/src/views/mp/components/img.png b/src/views/mp/components/img.png
new file mode 100644
index 0000000000000000000000000000000000000000..c25a6e762f3a84c5a7f05c6383de843937725fcf
GIT binary patch
literal 15404
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
D*J2vT

literal 0
HcmV?d00001

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..c0d67e29
--- /dev/null
+++ b/src/views/mp/components/wx-location/main.vue
@@ -0,0 +1,72 @@
+<!--
+  【微信消息 - 定位】
+-->
+<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>{{ label }}
+        </el-row>
+      </el-col>
+    </el-link>
+  </div>
+</template>
+
+<script setup lang="ts" name="WxLocation">
+import { Location } from '@element-plus/icons-vue'
+
+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..f98ac681
--- /dev/null
+++ b/src/views/mp/components/wx-voice-play/main.vue
@@ -0,0 +1,100 @@
+<!--
+  - 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
+      ><VideoPlay v-if="playing !== true" />
+      <VideoPause v-if="playing === true" />
+      <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'
+import { VideoPause, VideoPlay } from '@element-plus/icons-vue'
+
+const props = defineProps({
+  url: {
+    // 语音地址,例如说:https://www.iocoder.cn/xxx.amr
+    type: String,
+    required: true
+  },
+  content: {
+    // 语音文本
+    type: String,
+    required: false
+  }
+})
+
+const amr = ref()
+const playing = ref(false)
+const duration = ref()
+
+const playVoice = () => {
+  // 情况一:未初始化,则创建 BenzAMRRecorder
+  debugger
+  console.log('进入' + amr.value)
+  if (amr.value === undefined) {
+    console.log('开始初始化')
+    amrInit()
+    return
+  }
+
+  if (amr.value.isPlaying()) {
+    amrStop()
+  } else {
+    amrPlay()
+  }
+}
+
+const amrInit = () => {
+  amr.value = new BenzAMRRecorder()
+  console.log(amr.value)
+  console.log(props.url)
+  // 设置播放
+  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()
+}
+</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..1d9b331e 100644
--- a/src/views/mp/freePublish/index.vue
+++ b/src/views/mp/freePublish/index.vue
@@ -1,3 +1,395 @@
 <template>
-  <span>开发中</span>
+  <content-wrap>
+    <doc-alert title="公众号图文" url="https://doc.iocoder.cn/mp/article/" />
+
+    <!-- 搜索工作栏 -->
+    <el-form
+      :model="queryParams"
+      ref="queryFormRef"
+      size="small"
+      :inline="true"
+      v-show="showSearch"
+      label-width="68px"
+    >
+      <el-form-item label="公众号" prop="accountId">
+        <el-select v-model="queryParams.accountId" placeholder="请选择公众号">
+          <el-option
+            v-for="item in accounts"
+            :key="parseInt(item.id)"
+            :label="item.name"
+            :value="parseInt(item.id)"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
+        <el-button :icon="Refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 列表 -->
+    <div class="waterfall" v-loading="loading">
+      <div
+        v-show="item.content && item.content.newsItem"
+        class="waterfall-item"
+        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"
+            :icon="Delete"
+            circle
+            @click="handleDelete(item)"
+            v-hasPermi="['mp:free-publish:delete']"
+          />
+        </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 { getSimpleAccounts } from '@/api/mp/account'
+import WxNews from '@/views/mp/components/wx-news/main.vue'
+import { Delete, Search, Refresh } from '@element-plus/icons-vue'
+
+const message = useMessage() // 消息弹窗
+
+const queryParams = reactive({
+  total: 0, // 总页数
+  currentPage: 1, // 当前页数
+  pageNo: 1, // 当前页数
+  accountId: undefined, // 当前页数
+  queryParamsSize: 10 // 每页显示多少条
+})
+const loading = ref(false) // 列表的加载中
+const showSearch = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const accounts = ref([]) // 列表的数据
+const queryFormRef = ref() // 搜索的表单
+/** 查询列表 */
+const getList = async () => {
+  // 如果没有选中公众号账号,则进行提示。
+  if (!queryParams.accountId) {
+    message.error('未选中公众号,无法查询已发表图文')
+    return false
+  }
+  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 = async () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = async () => {
+  queryFormRef.value.resetFields()
+  // 默认选中第一个
+  if (accounts.value.length > 0) {
+    queryParams.accountId = accounts[0].id
+  }
+  handleQuery()
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (item) => {
+  {
+    const articleId = item.articleId
+    const accountId = queryParams.accountId
+    message
+      .confirm('删除后用户将无法访问此页面,确定删除?')
+      .then(function () {
+        return deleteFreePublish(accountId, articleId)
+      })
+      .then(() => {
+        getList()
+        message.success('删除成功')
+      })
+      .catch(() => {})
+  }
+}
+
+onMounted(() => {
+  getSimpleAccounts().then((response) => {
+    accounts.value = response
+    // 默认选中第一个
+    if (accounts.value.length > 0) {
+      queryParams.accountId = accounts.value[0]['id']
+    }
+    // 加载数据
+    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..34e64ebf 100644
--- a/src/views/mp/message/index.vue
+++ b/src/views/mp/message/index.vue
@@ -1,3 +1,262 @@
 <template>
-  <span>开发中</span>
+  <ContentWrap>
+    <doc-alert title="公众号消息" url="https://doc.iocoder.cn/mp/message/" />
+
+    <!-- 搜索工作栏 -->
+    <el-form
+      :model="queryParams"
+      ref="queryFormRef"
+      size="small"
+      :inline="true"
+      v-show="showSearch"
+      label-width="68px"
+    >
+      <el-form-item label="公众号" prop="accountId">
+        <el-select v-model="queryParams.accountId" placeholder="请选择公众号">
+          <el-option
+            v-for="item in accounts"
+            :key="parseInt(item.id)"
+            :label="item.name"
+            :value="parseInt(item.id)"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="消息类型" prop="type">
+        <el-select v-model="queryParams.type" placeholder="请选择消息类型" clearable size="small">
+          <el-option
+            v-for="dict in getDictOptions(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"
+        />
+      </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']"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!--todo 操作工具栏 -->
+    <!--    <el-row :gutter="10" class="mb8">-->
+    <!--      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />-->
+    <!--    </el-row>-->
+
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="发送时间" align="center" prop="createTime" width="180">
+        <template #default="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <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" size="mini">关注</el-tag>
+          </div>
+          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'unsubscribe'">
+            <el-tag type="danger" size="mini">取消关注</el-tag>
+          </div>
+          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'CLICK'">
+            <el-tag size="mini">点击菜单</el-tag>【{{ scope.row.eventKey }}】
+          </div>
+          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'VIEW'">
+            <el-tag size="mini">点击菜单链接</el-tag>【{{ scope.row.eventKey }}】
+          </div>
+          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_waitmsg'">
+            <el-tag size="mini">扫码结果</el-tag>【{{ scope.row.eventKey }}】
+          </div>
+          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_push'">
+            <el-tag size="mini">扫码结果</el-tag>【{{ scope.row.eventKey }}】
+          </div>
+          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_sysphoto'">
+            <el-tag size="mini">系统拍照发图</el-tag>
+          </div>
+          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_photo_or_album'">
+            <el-tag size="mini">拍照或者相册</el-tag>
+          </div>
+          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_weixin'">
+            <el-tag size="mini">微信相册</el-tag>
+          </div>
+          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'location_select'">
+            <el-tag size="mini">选择地理位置</el-tag>
+          </div>
+          <div v-else-if="scope.row.type === 'event'">
+            <el-tag type="danger" size="mini">未知事件类型</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 size="mini">链接</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" size="mini">未知消息类型</el-tag>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template #default="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @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 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 { getMessagePage } from '@/api/mp/message'
+import { getSimpleAccounts } from '@/api/mp/account'
+import { DICT_TYPE, getDictOptions } from '@/utils/dict'
+import { parseTime } from '@/utils/formatTime'
+
+// ========== CRUD 相关 ==========
+const loading = ref(false) // 遮罩层
+const showSearch = ref(true) // 显示搜索条件
+const total = ref(0) // 总条数
+const list = ref([]) // 粉丝消息列表
+const accounts = ref([]) // 公众号账号列表
+const open = ref(false) // 是否显示弹出层
+const userId = ref(0) // 操作的用户编号
+const message = useMessage() // 消息弹窗
+const queryFormRef = ref() // 搜索的表单
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  openid: null,
+  accountId: null,
+  type: null,
+  createTime: []
+}) // 是否显示弹出层
+
+const getList = async () => {
+  // 如果没有选中公众号账号,则进行提示。
+  if (!queryParams.accountId) {
+    message.error('未选中公众号,无法查询消息')
+    return false
+  }
+
+  loading.value = true
+  // 执行查询
+  getMessagePage(queryParams).then((data) => {
+    console.log(data)
+    list.value = data.list
+    total.value = data.total
+    loading.value = false
+  })
+}
+
+const handleQuery = async () => {
+  queryParams.pageNo = 1
+  getList()
+}
+const resetQuery = async () => {
+  queryFormRef.value.resetFields()
+  // 默认选中第一个
+  if (accounts.value.length > 0) {
+    queryParams.accountId = accounts[0].id
+  }
+  handleQuery()
+}
+const handleSend = async (row) => {
+  userId.value = row.userId
+  open.value = true
+}
+onMounted(() => {
+  getSimpleAccounts().then((response) => {
+    accounts.value = response
+    // 默认选中第一个
+    if (accounts.value.length > 0) {
+      queryParams.accountId = accounts.value[0]['id']
+    }
+    // 加载数据
+    getList()
+  })
+})
+</script>

From eead9efc5efd5e75eea1d0c1ea6f1322454e3ae9 Mon Sep 17 00:00:00 2001
From: yj441106 <yj441106@163.com>
Date: Sun, 26 Mar 2023 15:22:51 +0800
Subject: [PATCH 04/12] =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=E9=87=8D?=
 =?UTF-8?q?=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/views/system/oauth2/client/client.data.ts | 197 -------------
 src/views/system/oauth2/client/form.vue       | 259 ++++++++++++++++++
 src/views/system/oauth2/token/form.vue        | 131 +++++++++
 3 files changed, 390 insertions(+), 197 deletions(-)
 delete mode 100644 src/views/system/oauth2/client/client.data.ts
 create mode 100644 src/views/system/oauth2/client/form.vue
 create mode 100644 src/views/system/oauth2/token/form.vue

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/token/form.vue b/src/views/system/oauth2/token/form.vue
new file mode 100644
index 00000000..5372ca7e
--- /dev/null
+++ b/src/views/system/oauth2/token/form.vue
@@ -0,0 +1,131 @@
+<template>
+  <Dialog :title="modelTitle" v-model="modelVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="参数分类" prop="category">
+        <el-input v-model="formData.category" placeholder="请输入参数分类" />
+      </el-form-item>
+      <el-form-item label="参数名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入参数名称" />
+      </el-form-item>
+      <el-form-item label="参数键名" prop="key">
+        <el-input v-model="formData.key" placeholder="请输入参数键名" />
+      </el-form-item>
+      <el-form-item label="参数键值" prop="value">
+        <el-input v-model="formData.value" placeholder="请输入参数键值" />
+      </el-form-item>
+      <el-form-item label="是否可见" prop="visible">
+        <el-radio-group v-model="formData.visible">
+          <el-radio
+            v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </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 { DICT_TYPE, getDictOptions } from '@/utils/dict'
+import * as ConfigApi from '@/api/infra/config'
+
+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,
+  category: '',
+  name: '',
+  key: '',
+  value: '',
+  visible: true,
+  remark: ''
+})
+const formRules = reactive({
+  category: [{ required: true, message: '参数分类不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }],
+  key: [{ required: true, message: '参数键名不能为空', trigger: 'blur' }],
+  value: [{ required: true, message: '参数键值不能为空', trigger: 'blur' }],
+  visible: [{ 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 ConfigApi.getConfig(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 ConfigApi.ConfigVO
+    if (formType.value === 'create') {
+      await ConfigApi.createConfig(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ConfigApi.updateConfig(data)
+      message.success(t('common.updateSuccess'))
+    }
+    modelVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    category: '',
+    name: '',
+    key: '',
+    value: '',
+    visible: true,
+    remark: ''
+  }
+  formRef.value?.resetFields()
+}
+</script>

From f5d900db29abcae8330cc1265481e4acae116800 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 26 Mar 2023 19:32:19 +0800
Subject: [PATCH 05/12] =?UTF-8?q?Vue3=20=E9=87=8D=E6=9E=84=EF=BC=9A?=
 =?UTF-8?q?=E6=B5=81=E7=A8=8B=E5=88=86=E9=85=8D=E8=A7=84=E5=88=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/bpm/userGroup/index.ts                |   2 +-
 src/api/system/dept/index.ts                  |   2 +-
 src/api/system/post/index.ts                  |   2 +-
 src/api/system/role/index.ts                  |   2 +-
 src/api/system/user/index.ts                  |   2 +-
 src/router/modules/remaining.ts               |   2 +-
 .../bpm/taskAssignRule/TaskAssignRuleForm.vue | 247 ++++++++++++
 src/views/bpm/taskAssignRule/index.vue        | 367 ++++--------------
 .../bpm/taskAssignRule/taskAssignRule.data.ts |  54 ---
 src/views/system/dept/DeptForm.vue            |   2 +-
 src/views/system/role/index.vue               |   4 +-
 src/views/system/user/index.vue               |   8 +-
 12 files changed, 331 insertions(+), 363 deletions(-)
 create mode 100644 src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue
 delete mode 100644 src/views/bpm/taskAssignRule/taskAssignRule.data.ts

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/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/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/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/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/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/role/index.vue b/src/views/system/role/index.vue
index da4b8389..d2661abb 100644
--- a/src/views/system/role/index.vue
+++ b/src/views/system/role/index.vue
@@ -165,7 +165,7 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { rules, allSchemas } from './role.data'
 import * as RoleApi from '@/api/system/role'
 import { listSimpleMenusApi } from '@/api/system/menu'
-import { listSimpleDeptApi } from '@/api/system/dept'
+import { getSimpleDeptList } from '@/api/system/dept'
 import * as PermissionApi from '@/api/system/permission'
 
 const { t } = useI18n() // 国际化
@@ -278,7 +278,7 @@ const handleScope = async (type: string, row: RoleApi.RoleVO) => {
       })
     }
   } else if (type === 'data') {
-    const deptRes = await listSimpleDeptApi()
+    const deptRes = await getSimpleDeptList()
     treeOptions.value = handleTree(deptRes)
     const role = await RoleApi.getRoleApi(row.id)
     dataScopeForm.dataScope = role.dataScope
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
 }

From 9c869dee5f2e582bab8ed23287865fd183e6e69b Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 26 Mar 2023 21:16:16 +0800
Subject: [PATCH 06/12] =?UTF-8?q?Vue3=20=E9=87=8D=E6=9E=84=EF=BC=9AREVIEW?=
 =?UTF-8?q?=20=E6=95=8F=E6=84=9F=E8=AF=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/system/sensitiveWord/index.ts         |  20 +--
 src/types/auto-components.d.ts                |   4 +
 src/views/system/oauth2/client/index.vue      |   1 -
 src/views/system/oauth2/token/form.vue        | 131 ------------------
 .../{form.vue => SensitiveWordForm.vue}       |  17 ++-
 ...testForm.vue => SensitiveWordTestForm.vue} |  18 ++-
 src/views/system/sensitiveWord/index.vue      |  90 ++++++------
 7 files changed, 69 insertions(+), 212 deletions(-)
 delete mode 100644 src/views/system/oauth2/token/form.vue
 rename src/views/system/sensitiveWord/{form.vue => SensitiveWordForm.vue} (86%)
 rename src/views/system/sensitiveWord/{testForm.vue => SensitiveWordTestForm.vue} (82%)

diff --git a/src/api/system/sensitiveWord/index.ts b/src/api/system/sensitiveWord/index.ts
index 08078ba6..1116226f 100644
--- a/src/api/system/sensitiveWord/index.ts
+++ b/src/api/system/sensitiveWord/index.ts
@@ -10,27 +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 })
 }
 
@@ -55,12 +41,12 @@ 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' })
 }
 
diff --git a/src/types/auto-components.d.ts b/src/types/auto-components.d.ts
index 374893bb..4edfc6e7 100644
--- a/src/types/auto-components.d.ts
+++ b/src/types/auto-components.d.ts
@@ -54,11 +54,13 @@ declare module '@vue/runtime-core' {
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
     ElInput: typeof import('element-plus/es')['ElInput']
+    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElLink: typeof import('element-plus/es')['ElLink']
     ElOption: typeof import('element-plus/es')['ElOption']
     ElPagination: typeof import('element-plus/es')['ElPagination']
     ElPopover: typeof import('element-plus/es')['ElPopover']
     ElRadio: typeof import('element-plus/es')['ElRadio']
+    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
@@ -72,6 +74,8 @@ declare module '@vue/runtime-core' {
     ElTag: typeof import('element-plus/es')['ElTag']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
     ElTransfer: typeof import('element-plus/es')['ElTransfer']
+    ElTree: typeof import('element-plus/es')['ElTree']
+    ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
     ElUpload: typeof import('element-plus/es')['ElUpload']
     Error: typeof import('./../components/Error/src/Error.vue')['default']
     FlowCondition: typeof import('./../components/bpmnProcessDesigner/package/penal/flow-condition/FlowCondition.vue')['default']
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/oauth2/token/form.vue b/src/views/system/oauth2/token/form.vue
deleted file mode 100644
index 5372ca7e..00000000
--- a/src/views/system/oauth2/token/form.vue
+++ /dev/null
@@ -1,131 +0,0 @@
-<template>
-  <Dialog :title="modelTitle" v-model="modelVisible">
-    <el-form
-      ref="formRef"
-      :model="formData"
-      :rules="formRules"
-      label-width="80px"
-      v-loading="formLoading"
-    >
-      <el-form-item label="参数分类" prop="category">
-        <el-input v-model="formData.category" placeholder="请输入参数分类" />
-      </el-form-item>
-      <el-form-item label="参数名称" prop="name">
-        <el-input v-model="formData.name" placeholder="请输入参数名称" />
-      </el-form-item>
-      <el-form-item label="参数键名" prop="key">
-        <el-input v-model="formData.key" placeholder="请输入参数键名" />
-      </el-form-item>
-      <el-form-item label="参数键值" prop="value">
-        <el-input v-model="formData.value" placeholder="请输入参数键值" />
-      </el-form-item>
-      <el-form-item label="是否可见" prop="visible">
-        <el-radio-group v-model="formData.visible">
-          <el-radio
-            v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-            :key="dict.value"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-radio>
-        </el-radio-group>
-      </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 { DICT_TYPE, getDictOptions } from '@/utils/dict'
-import * as ConfigApi from '@/api/infra/config'
-
-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,
-  category: '',
-  name: '',
-  key: '',
-  value: '',
-  visible: true,
-  remark: ''
-})
-const formRules = reactive({
-  category: [{ required: true, message: '参数分类不能为空', trigger: 'blur' }],
-  name: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }],
-  key: [{ required: true, message: '参数键名不能为空', trigger: 'blur' }],
-  value: [{ required: true, message: '参数键值不能为空', trigger: 'blur' }],
-  visible: [{ 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 ConfigApi.getConfig(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 ConfigApi.ConfigVO
-    if (formType.value === 'create') {
-      await ConfigApi.createConfig(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await ConfigApi.updateConfig(data)
-      message.success(t('common.updateSuccess'))
-    }
-    modelVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    category: '',
-    name: '',
-    key: '',
-    value: '',
-    visible: true,
-    remark: ''
-  }
-  formRef.value?.resetFields()
-}
-</script>
diff --git a/src/views/system/sensitiveWord/form.vue b/src/views/system/sensitiveWord/SensitiveWordForm.vue
similarity index 86%
rename from src/views/system/sensitiveWord/form.vue
rename to src/views/system/sensitiveWord/SensitiveWordForm.vue
index ce5de578..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,11 +66,10 @@ const formRules = reactive({
   tags: [{ required: true, message: '标签不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
-const tags: Ref<string[]> = ref([]) // todo @blue-syd:在 openModal 里加载下
+const tagList = ref([]) // 标签数组
 
 /** 打开弹窗 */
-const openModal = async (type: string, paramTags: string[], id?: number) => {
-  tags.value = paramTags
+const open = async (type: string, id?: number) => {
   modelVisible.value = true
   modelTitle.value = t('action.' + type)
   formType.value = type
@@ -81,13 +79,14 @@ const openModal = async (type: string, paramTags: 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 事件,用于操作成功后的回调
@@ -101,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/testForm.vue b/src/views/system/sensitiveWord/SensitiveWordTestForm.vue
similarity index 82%
rename from src/views/system/sensitiveWord/testForm.vue
rename to src/views/system/sensitiveWord/SensitiveWordTestForm.vue
index 766d771f..881309c8 100644
--- a/src/views/system/sensitiveWord/testForm.vue
+++ b/src/views/system/sensitiveWord/SensitiveWordTestForm.vue
@@ -1,6 +1,5 @@
 <template>
-  <!-- 对话框(测试敏感词) -->
-  <Dialog :title="modelTitle" v-model="modelVisible">
+  <Dialog title="检测敏感词" v-model="modelVisible">
     <el-form
       ref="formRef"
       :model="formData"
@@ -17,10 +16,10 @@
           multiple
           filterable
           allow-create
-          placeholder="请选择文章标签"
+          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>
@@ -34,13 +33,10 @@
 </template>
 <script setup lang="ts">
 import * as SensitiveWordApi from '@/api/system/sensitiveWord'
-
 const message = useMessage() // 消息弹窗
 
 const modelVisible = ref(false) // 弹窗的是否展示
-const modelTitle = ref('检测敏感词') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const tags: Ref<string[]> = ref([])
 const formData = ref({
   text: '',
   tags: []
@@ -50,14 +46,16 @@ const formRules = reactive({
   tags: [{ required: true, message: '标签不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
+const tagList = ref([]) // 标签数组
 
 /** 打开弹窗 */
-const openModal = async (paramTags: string[]) => {
-  tags.value = paramTags
+const open = async () => {
   modelVisible.value = true
   resetForm()
+  // 获得 Tag 标签列表
+  tagList.value = await SensitiveWordApi.getSensitiveWordTagList()
 }
-defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
 /** 提交表单 */
 const submitForm = async () => {
diff --git a/src/views/system/sensitiveWord/index.vue b/src/views/system/sensitiveWord/index.vue
index 93ea3c71..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>
@@ -46,13 +56,13 @@
         <el-button
           type="primary"
           plain
-          @click="openModal('create')"
+          @click="openForm('create')"
           v-hasPermi="['system:sensitive-word:create']"
         >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
         <el-button
-          type="warning"
+          type="success"
           plain
           @click="handleExport"
           :loading="exportLoading"
@@ -60,15 +70,15 @@
         >
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
-        <el-button type="success" plain @click="handleTest">
+        <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" />
@@ -81,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
@@ -104,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']"
           >
             编辑
@@ -127,22 +135,21 @@
       v-model:limit="queryParams.pageSize"
       @pagination="getList"
     />
-  </content-wrap>
+  </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->
-  <SensitiveWordForm ref="modalRef" @success="getList" />
+  <SensitiveWordForm ref="formRef" @success="getList" />
 
   <!-- 表单弹窗:测试敏感词 -->
-  <SensitiveWordTestForm ref="modalTestRef" />
+  <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 SensitiveWordTestForm from './testForm.vue'
-
+import SensitiveWordForm from './SensitiveWordForm.vue'
+import SensitiveWordTestForm from './SensitiveWordTestForm.vue'
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
@@ -159,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 {
@@ -186,16 +193,15 @@ const resetQuery = () => {
 }
 
 /** 添加/修改操作 */
-const modalRef = ref()
-const openModal = (type: string, id?: number) => {
-  modalRef.value.openModal(type, tags.value, 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 modalTestRef = ref()
-const handleTest = () => {
-  modalTestRef.value.openModal(tags.value)
+/** 测试敏感词按钮操作 */
+const testFormRef = ref()
+const openTestForm = () => {
+  testFormRef.value.open()
 }
 
 /** 删除按钮操作 */
@@ -218,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 {
@@ -226,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>

From 56e2f21d1a8f7eef87f94268d1335d723ccaaa20 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 26 Mar 2023 22:15:45 +0800
Subject: [PATCH 07/12] =?UTF-8?q?Vue3=20=E9=87=8D=E6=9E=84=EF=BC=9AREVIEW?=
 =?UTF-8?q?=20=E8=8F=9C=E5=8D=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/system/menu/index.ts                  |  15 +-
 .../codegen/components/BasicInfoForm.vue      |   4 +-
 src/views/system/menu/MenuForm.vue            | 253 +++++++++++++++
 src/views/system/menu/form.vue                | 297 ------------------
 src/views/system/menu/index.vue               |  89 +++---
 src/views/system/role/index.vue               |   4 +-
 src/views/system/tenantPackage/index.vue      |   4 +-
 7 files changed, 316 insertions(+), 350 deletions(-)
 create mode 100644 src/views/system/menu/MenuForm.vue
 delete mode 100644 src/views/system/menu/form.vue

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/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/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/form.vue b/src/views/system/menu/form.vue
deleted file mode 100644
index cf1583ec..00000000
--- a/src/views/system/menu/form.vue
+++ /dev/null
@@ -1,297 +0,0 @@
-<template>
-  <Dialog :title="modelTitle" v-model="modelVisible">
-    <el-form
-      ref="formRef"
-      :model="formData"
-      :rules="formRules"
-      label-width="80px"
-      v-loading="formLoading"
-    >
-      <el-form-item label="上级菜单">
-        <el-tree-select
-          node-key="id"
-          v-model="formData.parentId"
-          :props="defaultProps"
-          :data="menuOptions"
-          :default-expanded-keys="[0]"
-          check-strictly
-        />
-      </el-form-item>
-      <el-col :span="16">
-        <el-form-item label="菜单名称" prop="name">
-          <el-input v-model="formData.name" placeholder="请输入菜单名称" clearable />
-        </el-form-item>
-      </el-col>
-      <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>
-      <template v-if="formData.type !== 3">
-        <el-form-item label="菜单图标">
-          <IconSelect v-model="formData.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="formData.path" placeholder="请输入路由地址" clearable />
-          </el-form-item>
-        </el-col>
-      </template>
-      <template v-if="formData.type === 2">
-        <el-col :span="16">
-          <el-form-item label="组件地址" prop="component">
-            <el-input
-              v-model="formData.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="formData.componentName" placeholder="例如说:SystemUser" clearable />
-          </el-form-item>
-        </el-col>
-      </template>
-      <template v-if="formData.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="formData.permission" placeholder="请输入权限标识" clearable />
-          </el-form-item>
-        </el-col>
-      </template>
-      <el-col :span="16">
-        <el-form-item label="显示排序" prop="sort">
-          <el-input-number v-model="formData.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="formData.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="formData.type !== 3">
-        <el-col :span="16">
-          <el-form-item label="显示状态" prop="visible">
-            <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-col>
-      </template>
-      <template v-if="formData.type !== 3">
-        <el-col :span="16">
-          <el-form-item label="总是显示" prop="alwaysShow">
-            <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-col>
-      </template>
-      <template v-if="formData.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="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-col>
-      </template>
-    </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, 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: 1,
-  parentId: 0,
-  path: '',
-  icon: '',
-  component: '',
-  componentName: '',
-  status: CommonStatusEnum.ENABLE,
-  visible: true,
-  keepAlive: true,
-  alwaysShow: true,
-  createTime: new Date()
-})
-
-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 openModal = async (type: string, id?: number) => {
-  modelVisible.value = true
-  modelTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  await getTree()
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await MenuApi.getMenuApi(id)
-      // TODO 芋艿:这块要优化下,部分字段未重置,无法修改
-      //   formData.value.componentName = res.componentName || ''
-      //   formData.value.alwaysShow = res.alwaysShow !== undefined ? res.alwaysShow : true
-    } 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 {
-    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
-    if (formType.value === 'create') {
-      await MenuApi.createMenuApi(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await MenuApi.updateMenuApi(data)
-      message.success(t('common.updateSuccess'))
-    }
-    modelVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-    wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
-  }
-}
-
-// ========== 下拉框[上级菜单] ==========
-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 resetForm = () => {
-  formData.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()
-  }
-  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 41d1bd67..3baf3148 100644
--- a/src/views/system/menu/index.vue
+++ b/src/views/system/menu/index.vue
@@ -1,49 +1,63 @@
 <template>
+  <!-- 搜索工作栏 -->
   <ContentWrap>
-    <!-- 搜索工作栏 -->
-    <el-form :model="queryParams" ref="queryFormRef" :inline="true">
+    <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="status">
-        <el-select v-model="queryParams.status" placeholder="菜单状态" clearable>
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择菜单状态"
+          clearable
+          class="!w-240px"
+        >
           <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"
           />
         </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" @click="openModal('create')" v-hasPermi="['system:menu:create']">
+        <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>
     </el-form>
+  </ContentWrap>
 
-    <el-row :gutter="10" class="mb8">
-      <el-col :span="1.5">
-        <el-button type="info" plain icon="el-icon-sort" @click="toggleExpandAll"
-          >展开/折叠</el-button
-        >
-      </el-col>
-    </el-row>
-
+  <!-- 列表 -->
+  <ContentWrap>
     <el-table
       v-loading="loading"
       :data="list"
-      v-if="refreshTable"
       row-key="id"
+      v-if="refreshTable"
       :default-expand-all="isExpandAll"
-      :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
     >
       <el-table-column prop="name" label="菜单名称" :show-overflow-tooltip="true" width="250" />
       <el-table-column prop="icon" label="图标" align="center" width="100">
@@ -60,62 +74,63 @@
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
         </template>
       </el-table-column>
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <el-table-column label="操作" align="center">
         <template #default="scope">
           <el-button
             link
             type="primary"
-            @click="openModal('update', scope.row.id)"
+            @click="openForm('update', scope.row.id)"
             v-hasPermi="['system:menu:update']"
-            >修改</el-button
           >
+            修改
+          </el-button>
           <el-button
             link
             type="primary"
-            @click="openModal('create', scope.row.id)"
+            @click="openForm('create', undefined, scope.row.id)"
             v-hasPermi="['system:menu:create']"
-            >新增</el-button
           >
+            新增
+          </el-button>
           <el-button
             link
-            type="primary"
+            type="danger"
             @click="handleDelete(scope.row.id)"
             v-hasPermi="['system:menu:delete']"
-            >删除</el-button
           >
+            删除
+          </el-button>
         </template>
       </el-table-column>
     </el-table>
   </ContentWrap>
+
   <!-- 表单弹窗:添加/修改 -->
-  <menu-form ref="modalRef" @success="getList" />
+  <MenuForm ref="formRef" @success="getList" />
 </template>
 <script setup lang="ts" name="Menu">
-// 业务相关的 import
-import { DICT_TYPE, getDictOptions } from '@/utils/dict'
-
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { handleTree } from '@/utils/tree'
 import * as MenuApi from '@/api/system/menu'
-import MenuForm from './form.vue'
+import MenuForm from './MenuForm.vue'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
 const loading = ref(true) // 列表的加载中
-
 const list = ref<any>([]) // 列表的数据
-const isExpandAll = ref(false) // 是否展开,默认全部折叠
-const refreshTable = ref(true) // 重新渲染表格状态
 const queryParams = reactive({
   name: undefined,
   status: undefined
 })
 const queryFormRef = ref() // 搜索的表单
+const isExpandAll = ref(false) // 是否展开,默认全部折叠
+const refreshTable = ref(true) // 重新渲染表格状态
 
 /** 查询参数列表 */
 const getList = async () => {
   loading.value = true
   try {
-    const data = await MenuApi.getMenuListApi(queryParams)
+    const data = await MenuApi.getMenuList(queryParams)
     list.value = handleTree(data)
   } finally {
     loading.value = false
@@ -134,9 +149,9 @@ const resetQuery = () => {
 }
 
 /** 添加/修改操作 */
-const modalRef = ref()
-const openModal = async (type: string, id?: number) => {
-  modalRef.value.openModal(type, id)
+const formRef = ref()
+const openForm = (type: string, id?: number, parentId?: number) => {
+  formRef.value.open(type, id, parentId)
 }
 
 /** 展开/折叠操作 */
@@ -154,7 +169,7 @@ const handleDelete = async (id: number) => {
     // 删除的二次确认
     await message.delConfirm()
     // 发起删除
-    await MenuApi.deleteMenuApi(id)
+    await MenuApi.deleteMenu(id)
     message.success(t('common.delSuccess'))
     // 刷新列表
     await getList()
diff --git a/src/views/system/role/index.vue b/src/views/system/role/index.vue
index d2661abb..cf10e09e 100644
--- a/src/views/system/role/index.vue
+++ b/src/views/system/role/index.vue
@@ -164,7 +164,7 @@ import { SystemDataScopeEnum } from '@/utils/constants'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { rules, allSchemas } from './role.data'
 import * as RoleApi from '@/api/system/role'
-import { listSimpleMenusApi } from '@/api/system/menu'
+import { getSimpleMenusList } from '@/api/system/menu'
 import { getSimpleDeptList } from '@/api/system/dept'
 import * as PermissionApi from '@/api/system/permission'
 
@@ -269,7 +269,7 @@ const handleScope = async (type: string, row: RoleApi.RoleVO) => {
   actionScopeType.value = type
   dialogScopeVisible.value = true
   if (type === 'menu') {
-    const menuRes = await listSimpleMenusApi()
+    const menuRes = await getSimpleMenusList()
     treeOptions.value = handleTree(menuRes)
     const role = await PermissionApi.listRoleMenusApi(row.id)
     if (role) {
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)
 }
 

From 9ab791867ace0c1e42780faf70d3e90a4f3d8832 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 26 Mar 2023 22:47:53 +0800
Subject: [PATCH 08/12] =?UTF-8?q?Vue3=20=E9=87=8D=E6=9E=84=EF=BC=9AREVIEW?=
 =?UTF-8?q?=20=E5=85=AC=E4=BC=97=E5=8F=B7=E6=B6=88=E6=81=AF=EF=BC=8C?=
 =?UTF-8?q?=E5=85=88=E4=B8=8D=E6=8A=A5=E9=94=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/mp/message/index.ts    |   2 +-
 src/types/auto-components.d.ts |   1 +
 src/views/mp/message/index.vue | 145 ++++++++++++++++-----------------
 3 files changed, 73 insertions(+), 75 deletions(-)

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/types/auto-components.d.ts b/src/types/auto-components.d.ts
index 4edfc6e7..04eb4d9e 100644
--- a/src/types/auto-components.d.ts
+++ b/src/types/auto-components.d.ts
@@ -52,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']
diff --git a/src/views/mp/message/index.vue b/src/views/mp/message/index.vue
index 34e64ebf..a95de2e4 100644
--- a/src/views/mp/message/index.vue
+++ b/src/views/mp/message/index.vue
@@ -1,20 +1,17 @@
 <template>
   <ContentWrap>
-    <doc-alert title="公众号消息" url="https://doc.iocoder.cn/mp/message/" />
-
     <!-- 搜索工作栏 -->
     <el-form
+      class="-mb-15px"
       :model="queryParams"
       ref="queryFormRef"
-      size="small"
       :inline="true"
-      v-show="showSearch"
       label-width="68px"
     >
       <el-form-item label="公众号" prop="accountId">
-        <el-select v-model="queryParams.accountId" placeholder="请选择公众号">
+        <el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px">
           <el-option
-            v-for="item in accounts"
+            v-for="item in accountList"
             :key="parseInt(item.id)"
             :label="item.name"
             :value="parseInt(item.id)"
@@ -22,9 +19,9 @@
         </el-select>
       </el-form-item>
       <el-form-item label="消息类型" prop="type">
-        <el-select v-model="queryParams.type" placeholder="请选择消息类型" clearable size="small">
+        <el-select v-model="queryParams.type" placeholder="请选择消息类型" class="!w-240px">
           <el-option
-            v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)"
+            v-for="dict in getStrDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)"
             :key="dict.value"
             :label="dict.label"
             :value="dict.value"
@@ -37,6 +34,7 @@
           placeholder="请输入用户标识"
           clearable
           :v-on="handleQuery"
+          class="!w-240px"
         />
       </el-form-item>
       <el-form-item label="创建时间" prop="createTime">
@@ -49,20 +47,18 @@
           start-placeholder="开始日期"
           end-placeholder="结束日期"
           :default-time="['00:00:00', '23:59:59']"
+          class="!w-240px"
         />
       </el-form-item>
       <el-form-item>
-        <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
-        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
+        <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>
 
-    <!--todo 操作工具栏 -->
-    <!--    <el-row :gutter="10" class="mb8">-->
-    <!--      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />-->
-    <!--    </el-row>-->
-
-    <!-- 列表 -->
+  <!-- 列表 -->
+  <ContentWrap>
     <el-table v-loading="loading" :data="list">
       <el-table-column label="发送时间" align="center" prop="createTime" width="180">
         <template #default="scope">
@@ -81,37 +77,37 @@
         <template #default="scope">
           <!-- 【事件】区域 -->
           <div v-if="scope.row.type === 'event' && scope.row.event === 'subscribe'">
-            <el-tag type="success" size="mini">关注</el-tag>
+            <el-tag type="success">关注</el-tag>
           </div>
           <div v-else-if="scope.row.type === 'event' && scope.row.event === 'unsubscribe'">
-            <el-tag type="danger" size="mini">取消关注</el-tag>
+            <el-tag type="danger">取消关注</el-tag>
           </div>
           <div v-else-if="scope.row.type === 'event' && scope.row.event === 'CLICK'">
-            <el-tag size="mini">点击菜单</el-tag>【{{ scope.row.eventKey }}】
+            <el-tag>点击菜单</el-tag>【{{ scope.row.eventKey }}】
           </div>
           <div v-else-if="scope.row.type === 'event' && scope.row.event === 'VIEW'">
-            <el-tag size="mini">点击菜单链接</el-tag>【{{ scope.row.eventKey }}】
+            <el-tag>点击菜单链接</el-tag>【{{ scope.row.eventKey }}】
           </div>
           <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_waitmsg'">
-            <el-tag size="mini">扫码结果</el-tag>【{{ scope.row.eventKey }}】
+            <el-tag>扫码结果</el-tag>【{{ scope.row.eventKey }}】
           </div>
           <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_push'">
-            <el-tag size="mini">扫码结果</el-tag>【{{ scope.row.eventKey }}】
+            <el-tag>扫码结果</el-tag>【{{ scope.row.eventKey }}】
           </div>
           <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_sysphoto'">
-            <el-tag size="mini">系统拍照发图</el-tag>
+            <el-tag>系统拍照发图</el-tag>
           </div>
           <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_photo_or_album'">
-            <el-tag size="mini">拍照或者相册</el-tag>
+            <el-tag>拍照或者相册</el-tag>
           </div>
           <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_weixin'">
-            <el-tag size="mini">微信相册</el-tag>
+            <el-tag>微信相册</el-tag>
           </div>
           <div v-else-if="scope.row.type === 'event' && scope.row.event === 'location_select'">
-            <el-tag size="mini">选择地理位置</el-tag>
+            <el-tag>选择地理位置</el-tag>
           </div>
           <div v-else-if="scope.row.type === 'event'">
-            <el-tag type="danger" size="mini">未知事件类型</el-tag>
+            <el-tag type="danger">未知事件类型</el-tag>
           </div>
           <!-- 【消息】区域 -->
           <div v-else-if="scope.row.type === 'text'">{{ scope.row.content }}</div>
@@ -127,7 +123,7 @@
             <wx-video-player :url="scope.row.mediaUrl" style="margin-top: 10px" />
           </div>
           <div v-else-if="scope.row.type === 'link'">
-            <el-tag size="mini">链接</el-tag>:
+            <el-tag>链接</el-tag>:
             <a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
           </div>
           <div v-else-if="scope.row.type === 'location'">
@@ -150,19 +146,19 @@
             <wx-news :articles="scope.row.articles" />
           </div>
           <div v-else>
-            <el-tag type="danger" size="mini">未知消息类型</el-tag>
+            <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
-            size="mini"
-            type="text"
-            icon="el-icon-edit"
+            link
+            type="primary"
             @click="handleSend(scope.row)"
             v-hasPermi="['mp:message:send']"
-            >消息
+          >
+            消息
           </el-button>
         </template>
       </el-table-column>
@@ -182,30 +178,22 @@
     </el-dialog>
   </ContentWrap>
 </template>
-
 <script setup lang="ts" name="MpMessage">
-import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
-import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+// 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 { getMessagePage } from '@/api/mp/message'
-import { getSimpleAccounts } from '@/api/mp/account'
-import { DICT_TYPE, getDictOptions } from '@/utils/dict'
+// 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 { parseTime } from '@/utils/formatTime'
-
-// ========== CRUD 相关 ==========
-const loading = ref(false) // 遮罩层
-const showSearch = ref(true) // 显示搜索条件
-const total = ref(0) // 总条数
-const list = ref([]) // 粉丝消息列表
-const accounts = ref([]) // 公众号账号列表
-const open = ref(false) // 是否显示弹出层
-const userId = ref(0) // 操作的用户编号
+import * as MpAccountApi from '@/api/mp/account'
+import * as MpMessageApi from '@/api/mp/message'
 const message = useMessage() // 消息弹窗
-const queryFormRef = ref() // 搜索的表单
 
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
@@ -213,34 +201,43 @@ const queryParams = reactive({
   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) {
-    message.error('未选中公众号,无法查询消息')
-    return false
+    await message.error('未选中公众号,无法查询消息')
+    return
   }
-
-  loading.value = true
-  // 执行查询
-  getMessagePage(queryParams).then((data) => {
-    console.log(data)
+  try {
+    loading.value = true
+    const data = await MpMessageApi.getMessagePage(queryParams)
     list.value = data.list
     total.value = data.total
+  } finally {
     loading.value = false
-  })
+  }
 }
 
-const handleQuery = async () => {
+/** 搜索按钮操作 */
+const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
+
+/** 重置按钮操作 */
 const resetQuery = async () => {
   queryFormRef.value.resetFields()
   // 默认选中第一个
-  if (accounts.value.length > 0) {
-    queryParams.accountId = accounts[0].id
+  if (accountList.value.length > 0) {
+    // @ts-ignore
+    queryParams.accountId = accountList.value[0].id
   }
   handleQuery()
 }
@@ -248,15 +245,15 @@ const handleSend = async (row) => {
   userId.value = row.userId
   open.value = true
 }
-onMounted(() => {
-  getSimpleAccounts().then((response) => {
-    accounts.value = response
-    // 默认选中第一个
-    if (accounts.value.length > 0) {
-      queryParams.accountId = accounts.value[0]['id']
-    }
-    // 加载数据
-    getList()
-  })
+
+/** 初始化 **/
+onMounted(async () => {
+  accountList.value = await MpAccountApi.getSimpleAccountList()
+  // 选中第一个
+  if (accountList.value.length > 0) {
+    // @ts-ignore
+    queryParams.accountId = accountList.value[0].id
+  }
+  await getList()
 })
 </script>

From 7380165bf07c9c310ee2dd9c531c2dccf8d2845d Mon Sep 17 00:00:00 2001
From: Chika <wbs_2018@sina.com>
Date: Sun, 26 Mar 2023 22:57:18 +0800
Subject: [PATCH 09/12] =?UTF-8?q?=E8=A7=92=E8=89=B2=E7=AE=A1=E7=90=86ep?=
 =?UTF-8?q?=E9=87=8D=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/views/system/role/MenuPermissionForm.vue | 202 ++++++++
 src/views/system/role/RoleForm.vue           | 148 ++++++
 src/views/system/role/index.vue              | 504 ++++++++-----------
 src/views/system/role/role.data.ts           |  82 ---
 4 files changed, 561 insertions(+), 375 deletions(-)
 create mode 100644 src/views/system/role/MenuPermissionForm.vue
 create mode 100644 src/views/system/role/RoleForm.vue
 delete mode 100644 src/views/system/role/role.data.ts

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 cf10e09e..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 { getSimpleMenusList } from '@/api/system/menu'
-import { getSimpleDeptList } 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 getSimpleMenusList()
-    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 getSimpleDeptList()
-    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)

From e00834adf79a92f150f045bf12f89f9c6cd880ad Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 26 Mar 2023 23:03:30 +0800
Subject: [PATCH 10/12] =?UTF-8?q?Vue3=20=E9=87=8D=E6=9E=84=EF=BC=9AREVIEW?=
 =?UTF-8?q?=20=E5=85=AC=E4=BC=97=E5=8F=B7=E6=B6=88=E6=81=AF=EF=BC=8C?=
 =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20WxLocation=20=E7=9A=84=20icon=20=E6=8A=A5?=
 =?UTF-8?q?=E9=94=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/views/mp/components/wx-location/main.vue | 7 +++----
 src/views/mp/message/index.vue               | 2 +-
 2 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/views/mp/components/wx-location/main.vue b/src/views/mp/components/wx-location/main.vue
index c0d67e29..47eab571 100644
--- a/src/views/mp/components/wx-location/main.vue
+++ b/src/views/mp/components/wx-location/main.vue
@@ -31,16 +31,15 @@
           />
         </el-row>
         <el-row>
-          <el-icon><Location /></el-icon>{{ label }}
+          <el-icon><Location /></el-icon>
+          <Icon icon="ep:location" />
+          {{ label }}
         </el-row>
       </el-col>
     </el-link>
   </div>
 </template>
-
 <script setup lang="ts" name="WxLocation">
-import { Location } from '@element-plus/icons-vue'
-
 const props = defineProps({
   locationX: {
     required: true,
diff --git a/src/views/mp/message/index.vue b/src/views/mp/message/index.vue
index a95de2e4..8cd9c773 100644
--- a/src/views/mp/message/index.vue
+++ b/src/views/mp/message/index.vue
@@ -183,7 +183,7 @@ import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 // 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 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 { parseTime } from '@/utils/formatTime'

From 708aef3e10c9b305742d3687cbefb4f09e20fb9f Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 26 Mar 2023 23:15:38 +0800
Subject: [PATCH 11/12] =?UTF-8?q?Vue3=20=E9=87=8D=E6=9E=84=EF=BC=9AREVIEW?=
 =?UTF-8?q?=20=E5=85=AC=E4=BC=97=E5=8F=B7=E6=B6=88=E6=81=AF=EF=BC=8C?=
 =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20WxVoicePlayer=20=E7=9A=84=20icon=20?=
 =?UTF-8?q?=E6=8A=A5=E9=94=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 package.json                                  |  1 +
 .../mp/components/wx-voice-play/main.vue      | 29 +++++++++----------
 src/views/mp/message/index.vue                |  2 +-
 3 files changed, 15 insertions(+), 17 deletions(-)

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/views/mp/components/wx-voice-play/main.vue b/src/views/mp/components/wx-voice-play/main.vue
index f98ac681..3260fc05 100644
--- a/src/views/mp/components/wx-voice-play/main.vue
+++ b/src/views/mp/components/wx-voice-play/main.vue
@@ -11,9 +11,9 @@
 -->
 <template>
   <div class="wx-voice-div" @click="playVoice">
-    <el-icon
-      ><VideoPlay v-if="playing !== true" />
-      <VideoPause v-if="playing === true" />
+    <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">
@@ -25,19 +25,15 @@
 
 <script setup lang="ts" name="WxVoicePlayer">
 // 因为微信语音是 amr 格式,所以需要用到 amr 解码器:https://www.npmjs.com/package/benz-amr-recorder
-
 import BenzAMRRecorder from 'benz-amr-recorder'
-import { VideoPause, VideoPlay } from '@element-plus/icons-vue'
 
 const props = defineProps({
   url: {
-    // 语音地址,例如说:https://www.iocoder.cn/xxx.amr
-    type: String,
+    type: String, // 语音地址,例如说:https://www.iocoder.cn/xxx.amr
     required: true
   },
   content: {
-    // 语音文本
-    type: String,
+    type: String, // 语音文本
     required: false
   }
 })
@@ -46,16 +42,14 @@ const amr = ref()
 const playing = ref(false)
 const duration = ref()
 
+/** 处理点击,播放或暂停 */
 const playVoice = () => {
   // 情况一:未初始化,则创建 BenzAMRRecorder
-  debugger
-  console.log('进入' + amr.value)
   if (amr.value === undefined) {
-    console.log('开始初始化')
     amrInit()
     return
   }
-
+  // 情况二:已经初始化,则根据情况播放或暂时
   if (amr.value.isPlaying()) {
     amrStop()
   } else {
@@ -63,10 +57,9 @@ const playVoice = () => {
   }
 }
 
+/** 音频初始化 */
 const amrInit = () => {
   amr.value = new BenzAMRRecorder()
-  console.log(amr.value)
-  console.log(props.url)
   // 设置播放
   amr.value.initWithUrl(props.url).then(function () {
     amrPlay()
@@ -77,16 +70,20 @@ const amrInit = () => {
     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;
diff --git a/src/views/mp/message/index.vue b/src/views/mp/message/index.vue
index 8cd9c773..de4a4242 100644
--- a/src/views/mp/message/index.vue
+++ b/src/views/mp/message/index.vue
@@ -181,7 +181,7 @@
 <script setup lang="ts" name="MpMessage">
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 // import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
-// import WxVoicePlayer from '@/views/mp/components/wx-voice-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'

From f5177337cbe8a948a0044f5c7abe0aa2185157a1 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 26 Mar 2023 23:31:41 +0800
Subject: [PATCH 12/12] =?UTF-8?q?Vue3=20=E9=87=8D=E6=9E=84=EF=BC=9AREVIEW?=
 =?UTF-8?q?=20=E5=9B=BE=E6=96=87=E5=8F=91=E8=A1=A8=E8=AE=B0=E5=BD=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/views/mp/freePublish/index.vue | 85 ++++++++++++++----------------
 src/views/mp/message/index.vue     | 18 ++++---
 2 files changed, 51 insertions(+), 52 deletions(-)

diff --git a/src/views/mp/freePublish/index.vue b/src/views/mp/freePublish/index.vue
index 1d9b331e..6ef4a303 100644
--- a/src/views/mp/freePublish/index.vue
+++ b/src/views/mp/freePublish/index.vue
@@ -1,37 +1,36 @@
 <template>
+  <!-- 搜索工作栏 -->
   <content-wrap>
-    <doc-alert title="公众号图文" url="https://doc.iocoder.cn/mp/article/" />
-
-    <!-- 搜索工作栏 -->
     <el-form
+      class="-mb-15px"
       :model="queryParams"
       ref="queryFormRef"
-      size="small"
       :inline="true"
-      v-show="showSearch"
       label-width="68px"
     >
       <el-form-item label="公众号" prop="accountId">
-        <el-select v-model="queryParams.accountId" placeholder="请选择公众号">
+        <el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px">
           <el-option
-            v-for="item in accounts"
-            :key="parseInt(item.id)"
+            v-for="item in accountList"
+            :key="item.id"
             :label="item.name"
-            :value="parseInt(item.id)"
+            :value="item.id"
           />
         </el-select>
       </el-form-item>
       <el-form-item>
-        <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
-        <el-button :icon="Refresh" @click="resetQuery">重置</el-button>
+        <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
-        v-show="item.content && item.content.newsItem"
         class="waterfall-item"
+        v-show="item.content && item.content.newsItem"
         v-for="item in list"
         :key="item.articleId"
       >
@@ -40,11 +39,12 @@
         <el-row justify="center" class="ope-row">
           <el-button
             type="danger"
-            :icon="Delete"
             circle
             @click="handleDelete(item)"
             v-hasPermi="['mp:free-publish:delete']"
-          />
+          >
+            <Icon icon="ep:delete" />
+          </el-button>
         </el-row>
       </div>
     </div>
@@ -61,25 +61,21 @@
 
 <script setup lang="ts" name="freePublish">
 import { getFreePublishPage, deleteFreePublish } from '@/api/mp/freePublish'
-import { getSimpleAccounts } from '@/api/mp/account'
+import * as MpAccountApi from '@/api/mp/account'
 import WxNews from '@/views/mp/components/wx-news/main.vue'
-import { Delete, Search, Refresh } from '@element-plus/icons-vue'
-
 const message = useMessage() // 消息弹窗
 
-const queryParams = reactive({
-  total: 0, // 总页数
-  currentPage: 1, // 当前页数
-  pageNo: 1, // 当前页数
-  accountId: undefined, // 当前页数
-  queryParamsSize: 10 // 每页显示多少条
-})
-const loading = ref(false) // 列表的加载中
-const showSearch = ref(true) // 列表的加载中
+const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
-const accounts = ref([]) // 列表的数据
+const queryParams = reactive({
+  currentPage: 1, // 当前页数
+  pageNo: 1, // 当前页数
+  accountId: undefined // 当前页数
+})
 const queryFormRef = ref() // 搜索的表单
+const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表
+
 /** 查询列表 */
 const getList = async () => {
   // 如果没有选中公众号账号,则进行提示。
@@ -87,6 +83,7 @@ const getList = async () => {
     message.error('未选中公众号,无法查询已发表图文')
     return false
   }
+  // TODO 改成 await 形式
   loading.value = true
   getFreePublishPage(queryParams)
     .then((data) => {
@@ -106,18 +103,20 @@ const getList = async () => {
       loading.value = false
     })
 }
+
 /** 搜索按钮操作 */
-const handleQuery = async () => {
+const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
 
 /** 重置按钮操作 */
-const resetQuery = async () => {
+const resetQuery = () => {
   queryFormRef.value.resetFields()
   // 默认选中第一个
-  if (accounts.value.length > 0) {
-    queryParams.accountId = accounts[0].id
+  if (accountList.value.length > 0) {
+    // @ts-ignore
+    queryParams.accountId = accountList.value[0].id
   }
   handleQuery()
 }
@@ -125,6 +124,7 @@ const resetQuery = async () => {
 /** 删除按钮操作 */
 const handleDelete = async (item) => {
   {
+    // TODO 改成 await 形式
     const articleId = item.articleId
     const accountId = queryParams.accountId
     message
@@ -140,19 +140,16 @@ const handleDelete = async (item) => {
   }
 }
 
-onMounted(() => {
-  getSimpleAccounts().then((response) => {
-    accounts.value = response
-    // 默认选中第一个
-    if (accounts.value.length > 0) {
-      queryParams.accountId = accounts.value[0]['id']
-    }
-    // 加载数据
-    getList()
-  })
+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;
@@ -182,7 +179,7 @@ onMounted(() => {
   margin-left: 5px;
 }
 
-/*新增图文*/
+/* 新增图文 */
 .left {
   display: inline-block;
   width: 35%;
diff --git a/src/views/mp/message/index.vue b/src/views/mp/message/index.vue
index de4a4242..10145221 100644
--- a/src/views/mp/message/index.vue
+++ b/src/views/mp/message/index.vue
@@ -12,9 +12,9 @@
         <el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px">
           <el-option
             v-for="item in accountList"
-            :key="parseInt(item.id)"
+            :key="item.id"
             :label="item.name"
-            :value="parseInt(item.id)"
+            :value="item.id"
           />
         </el-select>
       </el-form-item>
@@ -60,11 +60,13 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="发送时间" align="center" prop="createTime" width="180">
-        <template #default="scope">
-          <span>{{ parseTime(scope.row.createTime) }}</span>
-        </template>
-      </el-table-column>
+      <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">
@@ -180,13 +182,13 @@
 </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 { parseTime } from '@/utils/formatTime'
 import * as MpAccountApi from '@/api/mp/account'
 import * as MpMessageApi from '@/api/mp/message'
 const message = useMessage() // 消息弹窗