From df3b381d6fc340489b825906f1f39fe0e0687442 Mon Sep 17 00:00:00 2001
From: dhb52 <dhb52@126.com>
Date: Wed, 12 Apr 2023 13:29:24 +0800
Subject: [PATCH 1/6] =?UTF-8?q?refactor:=20mp=E6=A8=A1=E5=9D=97ts=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/mp/autoReply/index.vue              | 277 ++++++----
 src/views/mp/components/WxMpSelect.vue        |  22 +-
 .../mp/components/wx-editor/WxEditor.vue      | 115 +----
 src/views/mp/draft/index.vue                  | 475 +++++++++---------
 src/views/mp/freePublish/index.vue            |  33 +-
 src/views/mp/material/index.vue               |  92 ++--
 src/views/mp/menu/index.vue                   |  76 +--
 src/views/mp/tag/TagForm.vue                  |  19 +-
 src/views/mp/tag/index.vue                    |  45 +-
 src/views/mp/user/index.vue                   |  86 +++-
 src/views/pay/order/orderForm.vue             | 152 ++++++
 src/views/pay/refund/refundForm.vue           | 154 ++++++
 12 files changed, 943 insertions(+), 603 deletions(-)
 create mode 100644 src/views/pay/order/orderForm.vue
 create mode 100644 src/views/pay/refund/refundForm.vue

diff --git a/src/views/mp/autoReply/index.vue b/src/views/mp/autoReply/index.vue
index 9208d1e7..94fe34b3 100644
--- a/src/views/mp/autoReply/index.vue
+++ b/src/views/mp/autoReply/index.vue
@@ -3,13 +3,16 @@
 
   <!-- 搜索工作栏 -->
   <ContentWrap>
-    <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect  -->
-    <WxAccountSelect @change="accountChanged" />
+    <el-form class="-mb-15px" :model="queryParams" :inline="true" label-width="68px">
+      <el-form-item label="公众号" prop="accountId">
+        <WxMpSelect @change="onAccountChanged" />
+      </el-form-item>
+    </el-form>
   </ContentWrap>
 
   <!-- tab 切换 -->
   <ContentWrap>
-    <el-tabs v-model="type" @tab-change="handleTabChange">
+    <el-tabs v-model="msgType" @tab-change="handleTabChange">
       <!-- 操作工具栏 -->
       <el-row :gutter="10" class="mb8">
         <el-col :span="1.5">
@@ -18,26 +21,26 @@
             plain
             @click="handleAdd"
             v-hasPermi="['mp:auto-reply:create']"
-            v-if="type !== '1' || list.length <= 0"
+            v-if="msgType !== MsgType.Follow || list.length <= 0"
           >
             <Icon icon="ep:plus" />新增
           </el-button>
         </el-col>
       </el-row>
       <!-- tab 项 -->
-      <el-tab-pane name="1">
+      <el-tab-pane :name="MsgType.Follow">
         <template #label>
-          <span><Icon icon="ep:star-off" /> 关注时回复</span>
+          <span><Icon icon="ep:star" /> 关注时回复</span>
         </template>
       </el-tab-pane>
-      <el-tab-pane name="2">
+      <el-tab-pane :name="MsgType.Message">
         <template #label>
           <span><Icon icon="ep:chat-line-round" /> 消息回复</span>
         </template>
       </el-tab-pane>
-      <el-tab-pane name="3">
+      <el-tab-pane :name="MsgType.Keyword">
         <template #label>
-          <span><Icon icon="ep:news" /> 关键词回复</span>
+          <span><Icon icon="fa:newspaper-o" /> 关键词回复</span>
         </template>
       </el-tab-pane>
     </el-tabs>
@@ -47,10 +50,20 @@
         label="请求消息类型"
         align="center"
         prop="requestMessageType"
-        v-if="type === '2'"
+        v-if="msgType === MsgType.Message"
       />
-      <el-table-column label="关键词" align="center" prop="requestKeyword" v-if="type === '3'" />
-      <el-table-column label="匹配类型" align="center" prop="requestMatch" v-if="type === '3'">
+      <el-table-column
+        label="关键词"
+        align="center"
+        prop="requestKeyword"
+        v-if="msgType === MsgType.Keyword"
+      />
+      <el-table-column
+        label="匹配类型"
+        align="center"
+        prop="requestMatch"
+        v-if="msgType === MsgType.Keyword"
+      >
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH" :value="scope.row.requestMatch" />
         </template>
@@ -64,7 +77,7 @@
         <template #default="scope">
           <div v-if="scope.row.responseMessageType === 'text'">{{ scope.row.responseContent }}</div>
           <div v-else-if="scope.row.responseMessageType === 'voice'">
-            <WxVoicePlayer :url="scope.row.responseMediaUrl" />
+            <WxVoicePlayer v-if="scope.row.responseMediaUrl" :url="scope.row.responseMediaUrl" />
           </div>
           <div v-else-if="scope.row.responseMessageType === 'image'">
             <a target="_blank" :href="scope.row.responseMediaUrl">
@@ -77,7 +90,11 @@
               scope.row.responseMessageType === 'shortvideo'
             "
           >
-            <WxVideoPlayer :url="scope.row.responseMediaUrl" style="margin-top: 10px" />
+            <WxVideoPlayer
+              v-if="scope.row.responseMediaUrl"
+              :url="scope.row.responseMediaUrl"
+              style="margin-top: 10px"
+            />
           </div>
           <div v-else-if="scope.row.responseMessageType === 'news'">
             <WxNews :articles="scope.row.responseArticles" />
@@ -123,21 +140,21 @@
     </el-table>
 
     <!-- 添加或修改自动回复的对话框 -->
-    <el-dialog :title="title" v-model="open" width="800px" append-to-body>
-      <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="消息类型" prop="requestMessageType" v-if="type === '2'">
-          <el-select v-model="form.requestMessageType" placeholder="请选择">
+    <el-dialog :title="title" v-model="showReplyFormDialog" width="800px" append-to-body>
+      <el-form ref="formRef" :model="replyForm" :rules="rules" label-width="80px">
+        <el-form-item label="消息类型" prop="requestMessageType" v-if="msgType === MsgType.Message">
+          <el-select v-model="replyForm.requestMessageType" placeholder="请选择">
             <template v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)" :key="dict.value">
               <el-option
-                v-if="requestMessageTypes.includes(dict.value)"
+                v-if="RequestMessageTypes.includes(dict.value)"
                 :label="dict.label"
                 :value="dict.value"
               />
             </template>
           </el-select>
         </el-form-item>
-        <el-form-item label="匹配类型" prop="requestMatch" v-if="type === '3'">
-          <el-select v-model="form.requestMatch" placeholder="请选择匹配类型" clearable>
+        <el-form-item label="匹配类型" prop="requestMatch" v-if="msgType === MsgType.Keyword">
+          <el-select v-model="replyForm.requestMatch" placeholder="请选择匹配类型" clearable>
             <el-option
               v-for="dict in getDictOptions(DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH)"
               :key="dict.value"
@@ -146,8 +163,8 @@
             />
           </el-select>
         </el-form-item>
-        <el-form-item label="关键词" prop="requestKeyword" v-if="type === '3'">
-          <el-input v-model="form.requestKeyword" placeholder="请输入内容" clearable />
+        <el-form-item label="关键词" prop="requestKeyword" v-if="msgType === MsgType.Keyword">
+          <el-input v-model="replyForm.requestKeyword" placeholder="请输入内容" clearable />
         </el-form-item>
         <el-form-item label="回复消息">
           <WxReplySelect :objData="objData" v-if="hackResetWxReplySelect" />
@@ -160,38 +177,47 @@
     </el-dialog>
   </ContentWrap>
 </template>
-<script setup name="MpAutoReply">
+<script setup lang="ts" name="MpAutoReply">
 import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
 import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
 import WxMusic from '@/views/mp/components/wx-music/main.vue'
 import WxNews from '@/views/mp/components/wx-news/main.vue'
 import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
-import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
+import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
 import * as MpAutoReplyApi from '@/api/mp/autoReply'
-
 import { DICT_TYPE, getDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import { ContentWrap } from '@/components/ContentWrap'
+import { TabPaneName } from 'element-plus'
 
 const message = useMessage()
 
-// const queryFormRef = ref()
 const formRef = ref()
 
-// tab 类型(1、关注时回复;2、消息回复;3、关键词回复)
-const type = ref('3')
+// 消息类型(Follow: 关注时回复;Message: 消息回复;Keyword: 关键词回复)
+// 作为tab.name
+enum MsgType {
+  Follow = 1,
+  Message = 2,
+  Keyword = 3
+}
+const msgType = ref<MsgType>(MsgType.Keyword)
 // 允许选择的请求消息类型
-const requestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link']
+const RequestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link']
 // 遮罩层
 const loading = ref(true)
-// 显示搜索条件
-// const showSearch = ref(true)
 // 总条数
 const total = ref(0)
 // 自动回复列表
-const list = ref([])
+const list = ref<any[]>([])
+
 // 查询参数
-const queryParams = reactive({
+interface QueryParams {
+  pageNo: number
+  pageSize: number
+  accountId?: number
+}
+const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   accountId: undefined
@@ -200,12 +226,50 @@ const queryParams = reactive({
 // 弹出层标题
 const title = ref('')
 // 是否显示弹出层
-const open = ref(false)
+const showReplyFormDialog = ref(false)
 // 表单参数
-const form = ref({})
+type ReplyType = 'text' | 'image' | 'voice' | 'video' | 'shortvideo' | 'location' | 'link'
+interface ReplyForm {
+  // relation:
+  id?: number
+  accountId?: number
+  type?: MsgType
+  // request:
+  requestMessageType?: ReplyType
+  requestMatch?: number
+  requestKeyword?: string
+  // response:
+  responseMessageType?: ReplyType
+  responseContent?: string
+  responseMediaId?: number
+  responseMediaUrl?: string
+  responseTitle?: string
+  responseDescription?: number
+  responseThumbMediaId?: string
+  responseThumbMediaUrl?: string
+  responseArticles?: any[]
+  responseMusicUrl?: string
+  responseHqMusicUrl?: string
+}
+interface ObjData {
+  type: ReplyType
+  accountId?: number
+  content?: string
+  mediaId?: number
+  url?: string
+  title?: string
+  description?: string
+  thumbMediaId?: number
+  thumbMediaUrl?: string
+  articles?: any[]
+  musicUrl?: string
+  hqMusicUrl?: string
+}
+const replyForm = ref<ReplyForm>({})
 // 回复消息
-const objData = ref({
-  type: 'text'
+const objData = ref<ObjData>({
+  type: 'text',
+  accountId: undefined
 })
 // 表单校验
 const rules = {
@@ -216,8 +280,8 @@ const rules = {
 // 重置 WxReplySelect 组件,解决无法清除的问题
 const hackResetWxReplySelect = ref(false)
 
-const accountChanged = (accountId) => {
-  queryParams.accountId = accountId
+const onAccountChanged = (id?: number) => {
+  queryParams.accountId = id
   getList()
 }
 
@@ -227,7 +291,7 @@ const getList = async () => {
   try {
     const data = await MpAutoReplyApi.getAutoReplyPage({
       ...queryParams,
-      type: type.value
+      type: msgType.value
     })
     list.value = data.list
     total.value = data.total
@@ -242,8 +306,8 @@ const handleQuery = () => {
   getList()
 }
 
-const handleTabChange = (tabName) => {
-  type.value = tabName
+const handleTabChange = (tabName: TabPaneName) => {
+  msgType.value = tabName as MsgType
   handleQuery()
 }
 
@@ -252,94 +316,87 @@ const handleAdd = () => {
   reset()
   resetEditor()
   // 打开表单,并设置初始化
-  open.value = true
-  title.value = '新增自动回复'
   objData.value = {
     type: 'text',
     accountId: queryParams.accountId
   }
+
+  title.value = '新增自动回复'
+  showReplyFormDialog.value = true
 }
 
 /** 修改按钮操作 */
-const handleUpdate = (row) => {
+const handleUpdate = async (row: any) => {
   reset()
   resetEditor()
-  console.log(row)
 
-  MpAutoReplyApi.getAutoReply(row.id).then((data) => {
-    // 设置属性
-    form.value = { ...data }
-    delete form.value['responseMessageType']
-    delete form.value['responseContent']
-    delete form.value['responseMediaId']
-    delete form.value['responseMediaUrl']
-    delete form.value['responseDescription']
-    delete form.value['responseArticles']
-    objData.value = {
-      type: data.responseMessageType,
-      accountId: queryParams.accountId,
-      content: data.responseContent,
-      mediaId: data.responseMediaId,
-      url: data.responseMediaUrl,
-      title: data.responseTitle,
-      description: data.responseDescription,
-      thumbMediaId: data.responseThumbMediaId,
-      thumbMediaUrl: data.responseThumbMediaUrl,
-      articles: data.responseArticles,
-      musicUrl: data.responseMusicUrl,
-      hqMusicUrl: data.responseHqMusicUrl
-    }
+  const data = await MpAutoReplyApi.getAutoReply(row.id)
+  // 设置属性
+  replyForm.value = { ...data }
+  delete replyForm.value['responseMessageType']
+  delete replyForm.value['responseContent']
+  delete replyForm.value['responseMediaId']
+  delete replyForm.value['responseMediaUrl']
+  delete replyForm.value['responseDescription']
+  delete replyForm.value['responseArticles']
+  objData.value = {
+    type: data.responseMessageType,
+    accountId: queryParams.accountId,
+    content: data.responseContent,
+    mediaId: data.responseMediaId,
+    url: data.responseMediaUrl,
+    title: data.responseTitle,
+    description: data.responseDescription,
+    thumbMediaId: data.responseThumbMediaId,
+    thumbMediaUrl: data.responseThumbMediaUrl,
+    articles: data.responseArticles,
+    musicUrl: data.responseMusicUrl,
+    hqMusicUrl: data.responseHqMusicUrl
+  }
 
-    // 打开表单
-    open.value = true
-    title.value = '修改自动回复'
-  })
+  // 打开表单
+  title.value = '修改自动回复'
+  showReplyFormDialog.value = true
 }
 
-const handleSubmit = () => {
-  formRef.value?.validate((valid) => {
-    if (!valid) {
-      return
-    }
+const handleSubmit = async () => {
+  const valid = await formRef.value?.validate()
+  if (!valid) return
 
-    // 处理回复消息
-    const form = { ...form.value }
-    form.responseMessageType = objData.value.type
-    form.responseContent = objData.value.content
-    form.responseMediaId = objData.value.mediaId
-    form.responseMediaUrl = objData.value.url
-    form.responseTitle = objData.value.title
-    form.responseDescription = objData.value.description
-    form.responseThumbMediaId = objData.value.thumbMediaId
-    form.responseThumbMediaUrl = objData.value.thumbMediaUrl
-    form.responseArticles = objData.value.articles
-    form.responseMusicUrl = objData.value.musicUrl
-    form.responseHqMusicUrl = objData.value.hqMusicUrl
+  // 处理回复消息
+  const submitForm: any = { ...replyForm.value }
+  submitForm.responseMessageType = objData.value.type
+  submitForm.responseContent = objData.value.content
+  submitForm.responseMediaId = objData.value.mediaId
+  submitForm.responseMediaUrl = objData.value.url
+  submitForm.responseTitle = objData.value.title
+  submitForm.responseDescription = objData.value.description
+  submitForm.responseThumbMediaId = objData.value.thumbMediaId
+  submitForm.responseThumbMediaUrl = objData.value.thumbMediaUrl
+  submitForm.responseArticles = objData.value.articles
+  submitForm.responseMusicUrl = objData.value.musicUrl
+  submitForm.responseHqMusicUrl = objData.value.hqMusicUrl
 
-    if (form.value.id !== undefined) {
-      MpAutoReplyApi.updateAutoReply(form).then(() => {
-        message.success('修改成功')
-        open.value = false
-        getList()
-      })
-    } else {
-      MpAutoReplyApi.createAutoReply(form).then(() => {
-        message.success('新增成功')
-        open.value = false
-        getList()
-      })
-    }
-  })
+  if (replyForm.value.id !== undefined) {
+    await MpAutoReplyApi.updateAutoReply(submitForm)
+    message.success('修改成功')
+  } else {
+    await MpAutoReplyApi.createAutoReply(submitForm)
+    message.success('新增成功')
+  }
+
+  showReplyFormDialog.value = false
+  getList()
 }
 
 // 表单重置
 const reset = () => {
-  form.value = {
+  replyForm.value = {
     id: undefined,
     accountId: queryParams.accountId,
-    type: type.value,
+    type: msgType.value,
     requestKeyword: undefined,
-    requestMatch: type.value === '3' ? 1 : undefined,
+    requestMatch: msgType.value === MsgType.Keyword ? 1 : undefined,
     requestMessageType: undefined
   }
   formRef.value?.resetFields()
@@ -347,7 +404,7 @@ const reset = () => {
 
 // 取消按钮
 const cancel = () => {
-  open.value = false
+  showReplyFormDialog.value = false
   reset()
 }
 
diff --git a/src/views/mp/components/WxMpSelect.vue b/src/views/mp/components/WxMpSelect.vue
index e39bbf96..14f347d6 100644
--- a/src/views/mp/components/WxMpSelect.vue
+++ b/src/views/mp/components/WxMpSelect.vue
@@ -1,10 +1,5 @@
 <template>
-  <el-select
-    v-model="accountId"
-    placeholder="请选择公众号"
-    class="!w-240px"
-    @change="accountChanged"
-  >
+  <el-select v-model="account.id" placeholder="请选择公众号" class="!w-240px" @change="onChanged">
     <el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" />
   </el-select>
 </template>
@@ -12,11 +7,14 @@
 <script lang="ts" setup name="WxMpSelect">
 import * as MpAccountApi from '@/api/mp/account'
 
-const accountId: Ref<number | undefined> = ref()
+const account: MpAccountApi.AccountVO = reactive({
+  id: undefined,
+  name: ''
+})
 const accountList: Ref<MpAccountApi.AccountVO[]> = ref([])
 
 const emit = defineEmits<{
-  (e: 'change', id: number | undefined): void
+  (e: 'change', id?: number, name?: string): void
 }>()
 
 onMounted(() => {
@@ -27,12 +25,12 @@ const handleQuery = async () => {
   accountList.value = await MpAccountApi.getSimpleAccountList()
   // 默认选中第一个
   if (accountList.value.length > 0) {
-    accountId.value = accountList.value[0].id
-    emit('change', accountId.value)
+    account.id = accountList.value[0].id
+    emit('change', account.id, account.name)
   }
 }
 
-const accountChanged = () => {
-  emit('change', accountId.value)
+const onChanged = () => {
+  emit('change', account.id, account.name)
 }
 </script>
diff --git a/src/views/mp/components/wx-editor/WxEditor.vue b/src/views/mp/components/wx-editor/WxEditor.vue
index 50c65c6e..cf384113 100644
--- a/src/views/mp/components/wx-editor/WxEditor.vue
+++ b/src/views/mp/components/wx-editor/WxEditor.vue
@@ -1,11 +1,11 @@
 <script setup>
 import { ref, reactive } from 'vue'
-import { QuillEditor } from '@vueup/vue-quill'
-import '@vueup/vue-quill/dist/vue-quill.snow.css'
 import { getAccessToken } from '@/utils/auth'
-import editorOptions from './quill-options'
+import { Editor } from '@/components/Editor'
 
 const BASE_URL = import.meta.env.VITE_BASE_URL
+const actionUrl = BASE_URL + '/admin-api/mp/material/upload-news-image' // 这里写你要上传的图片服务器地址
+const headers = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
 
 const message = useMessage()
 
@@ -30,21 +30,16 @@ const props = defineProps({
 const emit = defineEmits(['input'])
 
 const myQuillEditorRef = ref()
-
 const content = ref(props.value.replace(/data-src/g, 'src'))
-
 const loading = ref(false) // 根据图片上传状态来确定是否显示loading动画,刚开始是false,不显示
-
-const actionUrl = ref(BASE_URL + '/admin-api/mp/material/upload-news-image') // 这里写你要上传的图片服务器地址
-const headers = ref({ Authorization: 'Bearer ' + getAccessToken() }) // 设置上传的请求头部
 const uploadData = reactive({
   type: 'image', // TODO 芋艿:试试要不要换成 thumb
   accountId: props.accountId
 })
 
-const onEditorChange = () => {
+const onEditorChange = (text) => {
   //内容改变事件
-  emit('input', content.value)
+  emit('input', text)
 }
 
 // 富文本图片上传前
@@ -98,104 +93,14 @@ const uploadError = () => {
         :on-error="uploadError"
         :before-upload="beforeUpload"
       />
-      <QuillEditor
-        class="editor"
-        v-model="content"
+      <Editor
+        editor-id="wxEditor"
         ref="quillEditorRef"
-        :options="editorOptions"
-        @change="onEditorChange($event)"
+        :modelValue="content"
+        @change="(editor) => onEditorChange(editor.getText())"
       />
     </div>
   </div>
 </template>
 
-<style>
-.editor {
-  line-height: normal !important;
-  height: 500px;
-}
-
-.ql-snow .ql-tooltip[data-mode='link']::before {
-  content: '请输入链接地址:';
-}
-
-.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
-  border-right: 0;
-  content: '保存';
-  padding-right: 0;
-}
-
-.ql-snow .ql-tooltip[data-mode='video']::before {
-  content: '请输入视频地址:';
-}
-
-.ql-snow .ql-picker.ql-size .ql-picker-label::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item::before {
-  content: '14px';
-}
-
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before {
-  content: '10px';
-}
-
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before {
-  content: '18px';
-}
-
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
-  content: '32px';
-}
-
-.ql-snow .ql-picker.ql-header .ql-picker-label::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item::before {
-  content: '文本';
-}
-
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before {
-  content: '标题1';
-}
-
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before {
-  content: '标题2';
-}
-
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before {
-  content: '标题3';
-}
-
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before {
-  content: '标题4';
-}
-
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before {
-  content: '标题5';
-}
-
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before {
-  content: '标题6';
-}
-
-.ql-snow .ql-picker.ql-font .ql-picker-label::before,
-.ql-snow .ql-picker.ql-font .ql-picker-item::before {
-  content: '标准字体';
-}
-
-.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before,
-.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before {
-  content: '衬线字体';
-}
-
-.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before,
-.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
-  content: '等宽字体';
-}
-</style>
+<style></style>
diff --git a/src/views/mp/draft/index.vue b/src/views/mp/draft/index.vue
index 299ec68a..5df7687a 100644
--- a/src/views/mp/draft/index.vue
+++ b/src/views/mp/draft/index.vue
@@ -3,14 +3,22 @@
 
   <!-- 搜索工作栏 -->
   <ContentWrap>
-    <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect  -->
-    <WxAccountSelect @change="accountChanged">
-      <template #actions>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="公众号" prop="accountId">
+        <WxMpSelect @change="onAccountChanged" />
+      </el-form-item>
+      <el-form-item>
         <el-button type="primary" plain @click="handleAdd" v-hasPermi="['mp:draft:create']">
           <Icon icon="ep:plus" />新增
         </el-button>
-      </template>
-    </WxAccountSelect>
+      </el-form-item>
+    </el-form>
   </ContentWrap>
 
   <!-- 列表 -->
@@ -58,214 +66,221 @@
     />
   </ContentWrap>
 
-  <!-- TODO @Dhb52:迁移成独立路由 -->
   <div class="app-container">
     <!-- 添加或修改草稿对话框 -->
-    <Teleport to="body">
-      <el-dialog
-        :title="operateMaterial === 'add' ? '新建图文' : '修改图文'"
-        width="80%"
-        top="20px"
-        v-model="dialogNewsVisible"
-        :before-close="dialogNewsClose"
-        :close-on-click-modal="false"
-      >
-        <div class="left">
-          <div class="select-item">
-            <div v-for="(news, index) in articlesAdd" :key="news.id">
-              <div
-                class="news-main father"
-                v-if="index === 0"
-                :class="{ activeAddNews: isActiveAddNews === index }"
-                @click="activeNews(index)"
-              >
-                <div class="news-content">
-                  <img class="material-img" v-if="news.thumbUrl" :src="news.thumbUrl" />
-                  <div class="news-content-title">{{ news.title }}</div>
-                </div>
-                <div class="child" v-if="articlesAdd.length > 1">
-                  <el-button size="small" @click="downNews(index)"
-                    ><Icon icon="ep:sort-down" />下移</el-button
-                  >
-                  <el-button v-if="operateMaterial === 'add'" size="small" @click="minusNews(index)"
-                    ><Icon icon="ep:delete" />删除
-                  </el-button>
-                </div>
+    <el-dialog
+      :title="operateMaterial === 'add' ? '新建图文' : '修改图文'"
+      width="80%"
+      top="20px"
+      v-model="dialogNewsVisible"
+      :before-close="dialogNewsClose"
+      :close-on-click-modal="false"
+    >
+      <div class="left">
+        <div class="select-item">
+          <div v-for="(news, index) in articlesAdd" :key="news.id">
+            <div
+              class="news-main father"
+              v-if="index === 0"
+              :class="{ activeAddNews: isActiveAddNews === index }"
+              @click="activeNews(index)"
+            >
+              <div class="news-content">
+                <img class="material-img" v-if="news.thumbUrl" :src="news.thumbUrl" />
+                <div class="news-content-title">{{ news.title }}</div>
               </div>
-              <div
-                class="news-main-item father"
-                v-if="index > 0"
-                :class="{ activeAddNews: isActiveAddNews === index }"
-                @click="activeNews(index)"
-              >
-                <div class="news-content-item">
-                  <div class="news-content-item-title">{{ news.title }}</div>
-                  <div class="news-content-item-img">
-                    <img
-                      class="material-img"
-                      v-if="news.thumbUrl"
-                      :src="news.thumbUrl"
-                      height="100%"
-                    />
-                  </div>
-                </div>
-                <div class="child">
-                  <el-button
-                    v-if="articlesAdd.length > index + 1"
-                    size="small"
-                    @click="downNews(index)"
-                    ><Icon icon="ep:sort-down" />下移
-                  </el-button>
-                  <el-button size="small" @click="upNews(index)"
-                    ><Icon icon="ep:sort-up" />上移</el-button
-                  >
-                  <el-button
-                    v-if="operateMaterial === 'add'"
-                    type="danger"
-                    size="small"
-                    @click="minusNews(index)"
-                    ><Icon icon="ep:delete" />删除
-                  </el-button>
-                </div>
-              </div>
-            </div>
-            <el-row justify="center" class="ope-row">
-              <el-button
-                type="primary"
-                circle
-                @click="plusNews(item)"
-                v-if="articlesAdd.length < 8 && operateMaterial === 'add'"
-              >
-                <Icon icon="ep:plus" />
-              </el-button>
-            </el-row>
-          </div>
-        </div>
-        <div class="right" v-loading="addMaterialLoading" v-if="articlesAdd.length > 0">
-          <br />
-          <br />
-          <br />
-          <br />
-          <!-- 标题、作者、原文地址 -->
-          <el-input v-model="articlesAdd[isActiveAddNews].title" placeholder="请输入标题(必填)" />
-          <el-input
-            v-model="articlesAdd[isActiveAddNews].author"
-            placeholder="请输入作者"
-            style="margin-top: 5px"
-          />
-          <el-input
-            v-model="articlesAdd[isActiveAddNews].contentSourceUrl"
-            placeholder="请输入原文地址"
-            style="margin-top: 5px"
-          />
-          <!-- 封面和摘要 -->
-          <div class="input-tt">封面和摘要:</div>
-          <div>
-            <div class="thumb-div">
-              <img
-                class="material-img"
-                v-if="articlesAdd[isActiveAddNews].thumbUrl"
-                :src="articlesAdd[isActiveAddNews].thumbUrl"
-                :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'"
-              />
-              <Icon
-                v-else
-                icon="ep:plus"
-                class="avatar-uploader-icon"
-                :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'"
-              />
-              <div class="thumb-but">
-                <el-upload
-                  :action="actionUrl"
-                  :headers="headers"
-                  multiple
-                  :limit="1"
-                  :file-list="fileList"
-                  :data="uploadData"
-                  :before-upload="beforeThumbImageUpload"
-                  :on-success="handleUploadSuccess"
+              <div class="child" v-if="articlesAdd.length > 1">
+                <el-button size="small" @click="downNews(index)"
+                  ><Icon icon="ep:sort-down" />下移</el-button
                 >
-                  <template #trigger>
-                    <el-button size="small" type="primary">本地上传</el-button>
-                  </template>
-                  <el-button
-                    size="small"
-                    type="primary"
-                    @click="openMaterial"
-                    style="margin-left: 5px"
-                    >素材库选择</el-button
-                  >
-                  <template #tip>
-                    <div class="el-upload__tip">支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div>
-                  </template>
-                </el-upload>
+                <el-button v-if="operateMaterial === 'add'" size="small" @click="minusNews(index)"
+                  ><Icon icon="ep:delete" />删除
+                </el-button>
+              </div>
+            </div>
+            <div
+              class="news-main-item father"
+              v-if="index > 0"
+              :class="{ activeAddNews: isActiveAddNews === index }"
+              @click="activeNews(index)"
+            >
+              <div class="news-content-item">
+                <div class="news-content-item-title">{{ news.title }}</div>
+                <div class="news-content-item-img">
+                  <img
+                    class="material-img"
+                    v-if="news.thumbUrl"
+                    :src="news.thumbUrl"
+                    height="100%"
+                  />
+                </div>
+              </div>
+              <div class="child">
+                <el-button
+                  v-if="articlesAdd.length > index + 1"
+                  size="small"
+                  @click="downNews(index)"
+                  ><Icon icon="ep:sort-down" />下移
+                </el-button>
+                <el-button size="small" @click="upNews(index)"
+                  ><Icon icon="ep:sort-up" />上移</el-button
+                >
+                <el-button
+                  v-if="operateMaterial === 'add'"
+                  type="danger"
+                  size="small"
+                  @click="minusNews(index)"
+                  ><Icon icon="ep:delete" />删除
+                </el-button>
               </div>
-              <Teleport to="body">
-                <el-dialog title="选择图片" v-model="dialogImageVisible" width="80%">
-                  <WxMaterialSelect
-                    ref="materialSelectRef"
-                    :objData="{ type: 'image', accountId: queryParams.accountId }"
-                    @select-material="selectMaterial"
-                  />
-                </el-dialog>
-              </Teleport>
             </div>
-            <el-input
-              :rows="8"
-              type="textarea"
-              v-model="articlesAdd[isActiveAddNews].digest"
-              placeholder="请输入摘要"
-              class="digest"
-              maxlength="120"
-              style="float: right"
-            />
           </div>
-          <!--富文本编辑器组件-->
-          <el-row>
-            <WxEditor
-              v-model="articlesAdd[isActiveAddNews].content"
-              :account-id="uploadData.accountId"
-              v-if="hackResetEditor"
-            />
+          <el-row justify="center" class="ope-row">
+            <el-button
+              type="primary"
+              circle
+              @click="plusNews"
+              v-if="articlesAdd.length < 8 && operateMaterial === 'add'"
+            >
+              <Icon icon="ep:plus" />
+            </el-button>
           </el-row>
         </div>
-        <template #footer>
-          <el-button @click="dialogNewsVisible = false">取 消</el-button>
-          <el-button type="primary" @click="submitForm">提 交</el-button>
-        </template>
-      </el-dialog>
-    </Teleport>
+      </div>
+      <div class="right" v-loading="addMaterialLoading" v-if="articlesAdd.length > 0">
+        <br />
+        <br />
+        <br />
+        <br />
+        <!-- 标题、作者、原文地址 -->
+        <el-input v-model="articlesAdd[isActiveAddNews].title" placeholder="请输入标题(必填)" />
+        <el-input
+          v-model="articlesAdd[isActiveAddNews].author"
+          placeholder="请输入作者"
+          style="margin-top: 5px"
+        />
+        <el-input
+          v-model="articlesAdd[isActiveAddNews].contentSourceUrl"
+          placeholder="请输入原文地址"
+          style="margin-top: 5px"
+        />
+        <!-- 封面和摘要 -->
+        <div class="input-tt">封面和摘要:</div>
+        <div>
+          <div class="thumb-div">
+            <img
+              class="material-img"
+              v-if="articlesAdd[isActiveAddNews].thumbUrl"
+              :src="articlesAdd[isActiveAddNews].thumbUrl"
+              :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'"
+            />
+            <Icon
+              v-else
+              icon="ep:plus"
+              class="avatar-uploader-icon"
+              :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'"
+            />
+            <div class="thumb-but">
+              <el-upload
+                :action="uploadUrl"
+                :headers="headers"
+                multiple
+                :limit="1"
+                :file-list="fileList"
+                :data="uploadData"
+                :before-upload="beforeThumbImageUpload"
+                :on-success="handleUploadSuccess"
+              >
+                <template #trigger>
+                  <el-button size="small" type="primary">本地上传</el-button>
+                </template>
+                <el-button
+                  size="small"
+                  type="primary"
+                  @click="openMaterial"
+                  style="margin-left: 5px"
+                  >素材库选择</el-button
+                >
+                <template #tip>
+                  <div class="el-upload__tip">支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div>
+                </template>
+              </el-upload>
+            </div>
+            <el-dialog title="选择图片" v-model="dialogImageVisible" width="80%" append-to-body>
+              <WxMaterialSelect
+                ref="materialSelectRef"
+                :objData="{ type: 'image', accountId: queryParams.accountId }"
+                @select-material="selectMaterial"
+              />
+            </el-dialog>
+          </div>
+          <el-input
+            :rows="8"
+            type="textarea"
+            v-model="articlesAdd[isActiveAddNews].digest"
+            placeholder="请输入摘要"
+            class="digest"
+            maxlength="120"
+            style="float: right"
+          />
+        </div>
+        <!--富文本编辑器组件-->
+        <el-row>
+          <WxEditor
+            v-model="articlesAdd[isActiveAddNews].content"
+            :account-id="uploadData.accountId"
+            v-if="hackResetEditor"
+          />
+        </el-row>
+      </div>
+      <template #footer>
+        <el-button @click="dialogNewsVisible = false">取 消</el-button>
+        <el-button type="primary" @click="submitForm">提 交</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
-<script setup name="MpDraft">
+
+<script setup lang="ts" name="MpDraft">
 import WxEditor from '@/views/mp/components/wx-editor/WxEditor.vue'
 import WxNews from '@/views/mp/components/wx-news/main.vue'
 import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
-import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
+import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
 import { getAccessToken } from '@/utils/auth'
 import * as MpDraftApi from '@/api/mp/draft'
 import * as MpFreePublishApi from '@/api/mp/freePublish'
+import { UploadFiles, UploadProps, UploadRawFile } from 'element-plus'
 // 可以用改本地数据模拟,避免API调用超限
 // import drafts from './mock'
 
 const message = useMessage() // 消息
 
 const loading = ref(true) // 列表的加载中
+const list = ref<any[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
+interface QueryParams {
+  pageNo: number
+  pageSize: number
+  accountId?: number
+}
+const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   accountId: undefined
 })
 
 // ========== 文件上传 ==========
-const materialSelectRef = ref()
 const BASE_URL = import.meta.env.VITE_BASE_URL
-const actionUrl = ref(BASE_URL + '/admin-api/mp/material/upload-permanent') // 上传永久素材的地址
-const headers = ref({ Authorization: 'Bearer ' + getAccessToken() }) // 设置上传的请求头部
-const fileList = ref([])
-const uploadData = reactive({
+const uploadUrl = BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传永久素材的地址
+const headers = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
+
+const materialSelectRef = ref<InstanceType<typeof WxMaterialSelect> | null>(null)
+const fileList = ref<UploadFiles>([])
+interface UploadData {
+  type: 'image' | 'video' | 'audio'
+  accountId?: number
+}
+const uploadData: UploadData = reactive({
   type: 'image',
   accountId: 1
 })
@@ -273,34 +288,28 @@ const uploadData = reactive({
 // ========== 草稿新建 or 修改 ==========
 const dialogNewsVisible = ref(false)
 const addMaterialLoading = ref(false) // 添加草稿的 loading 标识
-const articlesAdd = ref([])
+const articlesAdd = ref<any[]>([])
 const isActiveAddNews = ref(0)
 const dialogImageVisible = ref(false)
-const operateMaterial = ref('add')
+const operateMaterial = ref<'add' | 'edit'>('add')
 const articlesMediaId = ref('')
 const hackResetEditor = ref(false)
 
 /** 侦听公众号变化 **/
-const accountChanged = (accountId) => {
-  setAccountId(accountId)
+const onAccountChanged = (id?: number) => {
+  setAccountId(id)
   getList()
 }
 
 // ======================== 列表查询 ========================
 /** 设置账号编号 */
-const setAccountId = (accountId) => {
-  queryParams.accountId = accountId
-  uploadData.accountId = accountId
+const setAccountId = (id?: number) => {
+  queryParams.accountId = id
+  uploadData.accountId = id
 }
 
 /** 查询列表 */
 const getList = async () => {
-  // 如果没有选中公众号账号,则进行提示。
-  if (!queryParams.accountId) {
-    message.error('未选中公众号,无法查询草稿箱')
-    return false
-  }
-
   loading.value = true
   try {
     const drafts = await MpDraftApi.getDraftPage(queryParams)
@@ -329,7 +338,7 @@ const handleAdd = () => {
 }
 
 /** 更新按钮操作 */
-const handleUpdate = (item) => {
+const handleUpdate = (item: any) => {
   resetEditor()
   reset()
   articlesMediaId.value = item.mediaId
@@ -340,39 +349,30 @@ const handleUpdate = (item) => {
 }
 
 /** 提交按钮 */
-const submitForm = () => {
-  // TODO @Dhb52: 参考别的模块写法,改成 await 方式
+const submitForm = async () => {
   addMaterialLoading.value = true
-  if (operateMaterial.value === 'add') {
-    MpDraftApi.createDraft(queryParams.accountId, articlesAdd.value)
-      .then(() => {
-        message.notifySuccess('新增成功')
-        dialogNewsVisible.value = false
-        getList()
-      })
-      .finally(() => {
-        addMaterialLoading.value = false
-      })
-  } else {
-    MpDraftApi.updateDraft(queryParams.accountId, articlesMediaId.value, articlesAdd.value)
-      .then(() => {
-        message.notifySuccess('更新成功')
-        dialogNewsVisible.value = false
-        getList()
-      })
-      .finally(() => {
-        addMaterialLoading.value = false
-      })
+  try {
+    if (operateMaterial.value === 'add') {
+      await MpDraftApi.createDraft(queryParams.accountId, articlesAdd.value)
+      message.notifySuccess('新增成功')
+    } else {
+      await MpDraftApi.updateDraft(queryParams.accountId, articlesMediaId.value, articlesAdd.value)
+      message.notifySuccess('更新成功')
+    }
+  } finally {
+    dialogNewsVisible.value = false
+    addMaterialLoading.value = false
+    getList()
   }
 }
 
 // 关闭弹窗
-const dialogNewsClose = async (done) => {
+const dialogNewsClose = async (onDone: () => {}) => {
   try {
     await message.confirm('修改内容可能还未保存,确定关闭吗?')
     reset()
     resetEditor()
-    done()
+    onDone()
   } catch {}
 }
 
@@ -391,7 +391,7 @@ const resetEditor = () => {
 }
 
 // 将图文向下移动
-const downNews = (index) => {
+const downNews = (index: number) => {
   let temp = articlesAdd.value[index]
   articlesAdd.value[index] = articlesAdd.value[index + 1]
   articlesAdd.value[index + 1] = temp
@@ -407,13 +407,13 @@ const upNews = (index) => {
 }
 
 // 选中指定 index 的图文
-const activeNews = (index) => {
+const activeNews = (index: number) => {
   resetEditor()
   isActiveAddNews.value = index
 }
 
 // 删除指定 index 的图文
-const minusNews = async (index) => {
+const minusNews = async (index: number) => {
   try {
     await message.confirm('确定删除该图文吗?')
     articlesAdd.value.splice(index, 1)
@@ -446,20 +446,17 @@ const buildEmptyArticle = () => {
 }
 
 // ======================== 文件上传 ========================
-const beforeThumbImageUpload = (file) => {
+const beforeThumbImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
   addMaterialLoading.value = true
-  const isType =
-    file.type === 'image/jpeg' ||
-    file.type === 'image/png' ||
-    file.type === 'image/gif' ||
-    file.type === 'image/bmp' ||
-    file.type === 'image/jpg'
+  const isType = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'].includes(
+    rawFile.type
+  )
   if (!isType) {
     message.error('上传图片格式不对!')
     addMaterialLoading.value = false
     return false
   }
-  const isLt = file.size / 1024 / 1024 < 2
+  const isLt = rawFile.size / 1024 / 1024 < 2
   if (!isLt) {
     message.error('上传图片大小不能超过 2M!')
     addMaterialLoading.value = false
@@ -469,7 +466,7 @@ const beforeThumbImageUpload = (file) => {
   return true
 }
 
-const handleUploadSuccess = (response, file, fileList) => {
+const handleUploadSuccess: UploadProps['onSuccess'] = (response: any) => {
   addMaterialLoading.value = false
   if (response.code !== 0) {
     message.error('上传出错:' + response.msg)
@@ -485,7 +482,7 @@ const handleUploadSuccess = (response, file, fileList) => {
 }
 
 // 选择 or 上传完素材,设置回草稿
-const selectMaterial = (item) => {
+const selectMaterial = (item: any) => {
   dialogImageVisible.value = false
   articlesAdd.value[isActiveAddNews.value].thumbMediaId = item.mediaId
   articlesAdd.value[isActiveAddNews.value].thumbUrl = item.url
@@ -494,14 +491,10 @@ const selectMaterial = (item) => {
 // 打开素材选择
 const openMaterial = () => {
   dialogImageVisible.value = true
-  try {
-    materialSelectRef.value.queryParams.accountId = queryParams.accountId // 强制设置下 accountId,避免二次查询不对
-    materialSelectRef.value.handleQuery() // 刷新列表,失败也无所谓
-  } catch (e) {}
 }
 
 // ======================== 草稿箱发布 ========================
-const handlePublish = async (item) => {
+const handlePublish = async (item: any) => {
   const accountId = queryParams.accountId
   const mediaId = item.mediaId
   const content =
@@ -510,19 +503,19 @@ const handlePublish = async (item) => {
     await message.confirm(content)
     await MpFreePublishApi.submitFreePublish(accountId, mediaId)
     message.notifySuccess('发布成功')
-    await getList()
+    getList()
   } catch {}
 }
 
 /** 删除按钮操作 */
-const handleDelete = async (item) => {
+const handleDelete = async (item: any) => {
   const accountId = queryParams.accountId
   const mediaId = item.mediaId
   try {
     await message.confirm('此操作将永久删除该草稿, 是否继续?')
     await MpDraftApi.deleteDraft(accountId, mediaId)
     message.notifySuccess('删除成功')
-    await getList()
+    getList()
   } catch {}
 }
 </script>
diff --git a/src/views/mp/freePublish/index.vue b/src/views/mp/freePublish/index.vue
index 0d370f4a..7a15b31f 100644
--- a/src/views/mp/freePublish/index.vue
+++ b/src/views/mp/freePublish/index.vue
@@ -3,8 +3,17 @@
 
   <!-- 搜索工作栏 -->
   <ContentWrap>
-    <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect  -->
-    <WxAccountSelect @change="(accountId) => accountChanged(accountId)" />
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="公众号" prop="accountId">
+        <WxMpSelect @change="onAccountChanged" />
+      </el-form-item>
+    </el-form>
   </ContentWrap>
 
   <!-- 列表 -->
@@ -39,26 +48,32 @@
   </ContentWrap>
 </template>
 
-<script setup name="MpFreePublish">
+<script lang="ts" setup name="MpFreePublish">
 import * as FreePublishApi from '@/api/mp/freePublish'
 import WxNews from '@/views/mp/components/wx-news/main.vue'
-import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
+import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
+const list = ref<any[]>([]) // 列表的数据
+
+interface QueryParams {
+  pageNo: number
+  pageSize: number
+  accountId?: number
+}
+const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   accountId: undefined
 })
 
 /** 侦听公众号变化 **/
-const accountChanged = (accountId) => {
-  queryParams.accountId = accountId
+const onAccountChanged = (id: number | undefined) => {
+  queryParams.accountId = id
   getList()
 }
 
@@ -75,7 +90,7 @@ const getList = async () => {
 }
 
 /** 删除按钮操作 */
-const handleDelete = async (item) => {
+const handleDelete = async (item: any) => {
   try {
     // 删除的二次确认
     await message.delConfirm('删除后用户将无法访问此页面,确定删除?')
diff --git a/src/views/mp/material/index.vue b/src/views/mp/material/index.vue
index e4382b69..4d4017d4 100644
--- a/src/views/mp/material/index.vue
+++ b/src/views/mp/material/index.vue
@@ -4,13 +4,13 @@
   <ContentWrap>
     <el-form class="-mb-15px" :inline="true" label-width="68px">
       <el-form-item label="公众号" prop="accountId">
-        <WxMpSelect @change="accountChange" />
+        <WxMpSelect @change="onAccountChanged" />
       </el-form-item>
     </el-form>
   </ContentWrap>
 
   <ContentWrap>
-    <el-tabs v-model="type" @tab-change="handleTabChange">
+    <el-tabs v-model="type" @tab-change="onTabChange">
       <!-- tab 1:图片  -->
       <el-tab-pane name="image">
         <template #label>
@@ -93,7 +93,7 @@
           <el-table-column label="文件名" align="center" prop="name" />
           <el-table-column label="语音" align="center">
             <template #default="scope">
-              <WxVoicePlayer :url="scope.row.url" />
+              <WxVoicePlayer v-if="scope.row.url" :url="scope.row.url" />
             </template>
           </el-table-column>
           <el-table-column
@@ -188,10 +188,8 @@
             </el-row>
           </el-form>
           <template #footer>
-            <!-- <span class="dialog-footer"> -->
             <el-button @click="cancelVideo">取 消</el-button>
             <el-button type="primary" @click="submitVideo">提 交</el-button>
-            <!-- </span> -->
           </template>
         </el-dialog>
 
@@ -203,7 +201,7 @@
           <el-table-column label="介绍" align="center" prop="introduction" />
           <el-table-column label="视频" align="center">
             <template #default="scope">
-              <WxVideoPlayer :url="scope.row.url" />
+              <WxVideoPlayer v-if="scope.row.url" :url="scope.row.url" />
             </template>
           </el-table-column>
           <el-table-column
@@ -284,9 +282,9 @@ const type = ref<MatertialType>('image')
 // 遮罩层
 const loading = ref(false)
 // 总条数
-const total = ref(0)
 // 数据列表
-const list = ref([])
+const list = ref<any[]>([])
+const total = ref(0)
 // 查询参数
 interface QueryParams {
   pageNo: number
@@ -319,8 +317,8 @@ const dialogVideoVisible = ref(false)
 const addMaterialLoading = ref(false)
 
 /** 侦听公众号变化 **/
-const accountChange = (accountId: number | undefined) => {
-  queryParams.accountId = accountId
+const onAccountChanged = (id?: number) => {
+  queryParams.accountId = id
   getList()
 }
 
@@ -346,7 +344,7 @@ const handleQuery = () => {
   getList()
 }
 
-const handleTabChange = (tabName: TabPaneName) => {
+const onTabChange = (tabName: TabPaneName) => {
   // 设置 type
   uploadData.type = tabName as MatertialType
 
@@ -359,54 +357,49 @@ const handleTabChange = (tabName: TabPaneName) => {
 }
 
 // ======================== 文件上传 ========================
-const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
-  const isType = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'].includes(
-    rawFile.type
-  )
-  if (!isType) {
-    message.error('上传图片格式不对!')
+const beforeUpload = (rawFile: UploadRawFile, category: 'image' | 'audio' | 'video'): boolean => {
+  let allowTypes: string[] = []
+  let maxSizeMB = 0
+  let name = ''
+
+  switch (category) {
+    case 'image':
+      allowTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg']
+      maxSizeMB = 2
+      name = '图片'
+      break
+    case 'audio':
+      allowTypes = ['audio/mp3', 'audio/mpeg', 'audio/wma', 'audio/wav', 'audio/amr']
+      maxSizeMB = 2
+      name = '图片'
+      break
+    case 'video':
+      allowTypes = ['video/mp4']
+      maxSizeMB = 10
+      name = '视频'
+  }
+
+  if (!allowTypes.includes(rawFile.type)) {
+    message.error(`上传${name}格式不对!`)
     return false
   }
-  const isLt = rawFile.size / 1024 / 1024 < 2
-  if (!isLt) {
-    message.error('上传图片大小不能超过 2M!')
+  // 校验大小
+  if (rawFile.size / 1024 / 1024 > maxSizeMB) {
+    message.error(`上传${name}大小不能超过${maxSizeMB}M!`)
     return false
   }
   loading.value = true
   return true
 }
 
-const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
-  const isType = ['audio/mp3', 'audio/wma', 'audio/wav', 'audio/amr'].includes(file.type)
-  const isLt = rawFile.size / 1024 / 1024 < 2
-  if (!isType) {
-    message.error('上传语音格式不对!')
-    return false
-  }
-  if (!isLt) {
-    message.error('上传语音大小不能超过 2M!')
-    return false
-  }
-  loading.value = true
-  return true
-}
+const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
+  beforeUpload(rawFile, 'image')
 
-const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
-  const isType = rawFile.type === 'video/mp4'
-  if (!isType) {
-    message.error('上传视频格式不对!')
-    return false
-  }
+const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
+  beforeUpload(rawFile, 'audio')
 
-  const isLt = rawFile.size / 1024 / 1024 < 10
-  if (!isLt) {
-    message.error('上传视频大小不能超过 10M!')
-    return false
-  }
-
-  addMaterialLoading.value = true
-  return true
-}
+const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
+  beforeUpload(rawFile, 'video')
 
 const handleUploadSuccess: UploadProps['onSuccess'] = (response: any) => {
   loading.value = false
@@ -441,6 +434,7 @@ const submitVideo = () => {
   })
 }
 
+// 弹出 video 新建的表单
 const handleAddVideo = () => {
   resetVideo()
   dialogVideoVisible.value = true
diff --git a/src/views/mp/menu/index.vue b/src/views/mp/menu/index.vue
index a6a08c97..fea08614 100644
--- a/src/views/mp/menu/index.vue
+++ b/src/views/mp/menu/index.vue
@@ -2,8 +2,11 @@
   <doc-alert title="公众号菜单" url="https://doc.iocoder.cn/mp/menu/" />
   <!-- 搜索工作栏 -->
   <ContentWrap>
-    <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect  -->
-    <WxAccountSelect @change="accountChanged" />
+    <el-form class="-mb-15px" ref="queryFormRef" :inline="true" label-width="68px">
+      <el-form-item label="公众号" prop="accountId">
+        <WxMpSelect @change="onAccountChanged" />
+      </el-form-item>
+    </el-form>
   </ContentWrap>
 
   <!-- 列表 -->
@@ -12,7 +15,7 @@
       <!--左边配置菜单-->
       <div class="left">
         <div class="weixin-hd">
-          <div class="weixin-title">{{ name }}</div>
+          <div class="weixin-title">{{ accountName }}</div>
         </div>
         <div class="weixin-menu menu_main clearfix">
           <div class="menu_bottom" v-for="(item, i) of menuList" :key="i">
@@ -68,7 +71,7 @@
       <div v-if="showRightFlag" class="right">
         <div class="configure_page">
           <div class="delete_btn">
-            <el-button size="small" type="danger" @click="handleDeleteMenu(tempObj)">
+            <el-button size="small" type="danger" @click="handleDeleteMenu">
               删除当前菜单<Icon icon="ep:delete" />
             </el-button>
           </div>
@@ -155,7 +158,7 @@
                 <div v-else>
                   <el-row justify="center">
                     <el-col :span="24" style="text-align: center">
-                      <el-button type="success" @click="openMaterial">
+                      <el-button type="success" @click="dialogNewsVisible = true">
                         素材库选择<Icon icon="ep:circle-check" />
                       </el-button>
                     </el-col>
@@ -185,24 +188,26 @@
     </div>
   </ContentWrap>
 </template>
-<script setup name="MpMenu">
-import { handleTree } from '@/utils/tree'
+<script lang="ts" setup name="MpMenu">
 import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
 import WxNews from '@/views/mp/components/wx-news/main.vue'
 import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
-import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
+import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
 import * as MpMenuApi from '@/api/mp/menu'
+import { handleTree } from '@/utils/tree'
 import menuOptions from './menuOptions'
+
 const message = useMessage() // 消息
 
 // ======================== 列表查询 ========================
-const loading = ref(true) // 遮罩层
-const accountId = ref(undefined) // 公众号Id
-const menuList = ref({ children: [] })
+const loading = ref(false) // 遮罩层
+const accountId = ref<number | undefined>()
+const accountName = ref<string | undefined>('')
+const menuList = ref<any>({ children: [] })
 
 // ======================== 菜单操作 ========================
 const isActive = ref(-1) // 一级菜单点中样式
-const isSubMenuActive = ref(-1) // 一级菜单点中样式
+const isSubMenuActive = ref<string | number>(-1) // 一级菜单点中样式
 const isSubMenuFlag = ref(-1) // 二级菜单显示标志
 
 // ======================== 菜单编辑 ========================
@@ -210,15 +215,16 @@ const showRightFlag = ref(false) // 右边配置显示默认详情还是配置
 const nameMaxLength = ref(0) // 菜单名称最大长度;1 级是 4 字符;2 级是 7 字符;
 const showConfigureContent = ref(true) // 是否展示配置内容;如果有子菜单,就不显示配置内容
 const hackResetWxReplySelect = ref(false) // 重置 WxReplySelect 组件
-const tempObj = ref({}) // 右边临时变量,作为中间值牵引关系
+const tempObj = ref<any>({}) // 右边临时变量,作为中间值牵引关系
 
 // 一些临时值放在这里进行判断,如果放在 tempObj,由于引用关系,menu 也会多了多余的参数
-const tempSelfObj = ref({})
+const tempSelfObj = ref<any>({})
 const dialogNewsVisible = ref(false) // 跳转图文时的素材选择弹窗
 
 /** 侦听公众号变化 **/
-const accountChanged = (id) => {
+const onAccountChanged = (id?: number, name?: string) => {
   accountId.value = id
+  accountName.value = name
   getList()
 }
 
@@ -241,10 +247,10 @@ const handleQuery = () => {
 }
 
 // 将后端返回的 menuList,转换成前端的 menuList
-const convertMenuList = (list) => {
+const convertMenuList = (list: any[]) => {
   if (!list) return []
 
-  const menuList = []
+  const result: any[] = []
   list.forEach((item) => {
     const menu = {
       ...item
@@ -271,9 +277,9 @@ const convertMenuList = (list) => {
         hqMusicUrl: item.replyHqMusicUrl
       }
     }
-    menuList.push(menu)
+    result.push(menu)
   })
-  return menuList
+  return result
 }
 
 // 重置表单,清空表单数据
@@ -286,7 +292,7 @@ const resetForm = () => {
   // 菜单编辑
   showRightFlag.value = false
   nameMaxLength.value = 0
-  showConfigureContent.value = 0
+  showConfigureContent.value = false
   hackResetWxReplySelect.value = false
   tempObj.value = {}
   tempSelfObj.value = {}
@@ -295,7 +301,7 @@ const resetForm = () => {
 
 // ======================== 菜单操作 ========================
 // 一级菜单点击事件
-const menuClick = (i, item) => {
+const menuClick = (i: number, item: any) => {
   // 右侧的表单相关
   resetEditor()
   showRightFlag.value = true // 右边菜单
@@ -312,11 +318,10 @@ const menuClick = (i, item) => {
 }
 
 // 二级菜单点击事件
-const subMenuClick = (subItem, index, k) => {
+const subMenuClick = (subItem: any, index: number, k: number) => {
   // 右侧的表单相关
   resetEditor()
   showRightFlag.value = true // 右边菜单
-  console.log(subItem)
   tempObj.value = subItem // 将点击的数据放到临时变量,对象有引用作用
   tempSelfObj.value.grand = '2' // 表示二级菜单
   tempSelfObj.value.index = index // 表示一级菜单索引
@@ -331,7 +336,7 @@ const subMenuClick = (subItem, index, k) => {
 
 // 添加横向一级菜单
 const addMenu = () => {
-  const menuKeyLength = menuList.value.length
+  const menuKeyLength: number = menuList.value.length
   const addButton = {
     name: '菜单名称',
     children: [],
@@ -342,10 +347,10 @@ const addMenu = () => {
     }
   }
   menuList.value[menuKeyLength] = addButton
-  menuClick(menuKeyLength.value - 1, addButton)
+  menuClick(menuKeyLength - 1, addButton)
 }
 // 添加横向二级菜单;item 表示要操作的父菜单
-const addSubMenu = (i, item) => {
+const addSubMenu = (i: number, item: any) => {
   // 清空父菜单的属性,因为它只需要 name 属性即可
   if (!item.children || item.children.length <= 0) {
     item.children = []
@@ -361,8 +366,8 @@ const addSubMenu = (i, item) => {
     showConfigureContent.value = false
   }
 
-  let subMenuKeyLength = item.children.length // 获取二级菜单key长度
-  let addButton = {
+  const subMenuKeyLength = item.children.length // 获取二级菜单key长度
+  const addButton = {
     name: '子菜单名称',
     reply: {
       // 用于存储回复内容
@@ -399,7 +404,7 @@ const handleDeleteMenu = async () => {
 // ======================== 菜单编辑 ========================
 const handleSave = async () => {
   try {
-    await message.confirm('确定要删除吗?')
+    await message.confirm('确定要保存吗?')
     loading.value = true
     await MpMenuApi.saveMenu(accountId.value, convertMenuFormList())
     getList()
@@ -413,7 +418,6 @@ const handleSave = async () => {
 const resetEditor = () => {
   hackResetWxReplySelect.value = false // 销毁组件
   nextTick(() => {
-    console.log('nextTick')
     hackResetWxReplySelect.value = true // 重建组件
   })
 }
@@ -432,9 +436,9 @@ const handleDelete = async () => {
 
 // 将前端的 menuList,转换成后端接收的 menuList
 const convertMenuFormList = () => {
-  const result = []
+  const result: any[] = []
   menuList.value.forEach((item) => {
-    let menu = convertMenuForm(item)
+    const menu = convertMenuForm(item)
     result.push(menu)
 
     // 处理子菜单
@@ -450,7 +454,7 @@ const convertMenuFormList = () => {
 }
 
 // 将前端的 menu,转换成后端接收的 menu
-const convertMenuForm = (menu) => {
+const convertMenuForm = (menu: any) => {
   let result = {
     ...menu,
     children: undefined, // 不处理子节点
@@ -473,11 +477,7 @@ const convertMenuForm = (menu) => {
 }
 
 // ======================== 菜单编辑(素材选择) ========================
-const openMaterial = () => {
-  dialogNewsVisible.value = true
-}
-
-const selectMaterial = (item) => {
+const selectMaterial = (item: any) => {
   const articleId = item.articleId
   const articles = item.content.newsItem
   // 提示,针对多图文
diff --git a/src/views/mp/tag/TagForm.vue b/src/views/mp/tag/TagForm.vue
index e190af89..81f19988 100644
--- a/src/views/mp/tag/TagForm.vue
+++ b/src/views/mp/tag/TagForm.vue
@@ -19,24 +19,30 @@
 </template>
 <script setup lang="ts">
 import * as MpTagApi from '@/api/mp/tag'
+import { FormInstance, FormRules } from 'element-plus'
+
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formType = ref<'create' | 'update' | ''>('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
   accountId: -1,
   name: ''
 })
-const formRules = reactive({
+const formRules: FormRules = {
   name: [{ required: true, message: '请输入标签名称', trigger: 'blur' }]
-})
-const formRef = ref() // 表单 Ref
+}
+const formRef = ref<FormInstance | null>(null) // 表单 Ref
+
+const emit = defineEmits<{
+  (e: 'success'): void
+}>()
 
 /** 打开弹窗 */
-const open = async (type: string, accountId: number, id?: number) => {
+const open = async (type: 'create' | 'update', accountId: number, id?: number) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
@@ -55,11 +61,10 @@ const open = async (type: string, accountId: number, id?: number) => {
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
 /** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const submitForm = async () => {
   // 校验表单
   if (!formRef) return
-  const valid = await formRef.value.validate()
+  const valid = await formRef.value?.validate()
   if (!valid) return
   // 提交请求
   formLoading.value = true
diff --git a/src/views/mp/tag/index.vue b/src/views/mp/tag/index.vue
index 44578519..6dc0d11a 100644
--- a/src/views/mp/tag/index.vue
+++ b/src/views/mp/tag/index.vue
@@ -3,17 +3,25 @@
 
   <!-- 搜索工作栏 -->
   <ContentWrap>
-    <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect  -->
-    <WxAccountSelect @change="accountChanged">
-      <template #actions>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="公众号" prop="accountId">
+        <WxMpSelect @change="onAccountChanged" />
+      </el-form-item>
+      <el-form-item>
         <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['mp:tag:create']">
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
         <el-button type="success" plain @click="handleSync" v-hasPermi="['mp:tag:sync']">
           <Icon icon="ep:refresh" class="mr-5px" /> 同步
         </el-button>
-      </template>
-    </WxAccountSelect>
+      </el-form-item>
+    </el-form>
   </ContentWrap>
 
   <!-- 列表 -->
@@ -63,26 +71,35 @@
   <TagForm ref="formRef" @success="getList" />
 </template>
 <script setup lang="ts" name="MpTag">
-import { dateFormatter } from '@/utils/formatTime'
-import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
 import * as MpTagApi from '@/api/mp/tag'
 import TagForm from './TagForm.vue'
+import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
+import { dateFormatter } from '@/utils/formatTime'
+
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
+const list = ref<any>([]) // 列表的数据
+
+interface QueryParams {
+  pageNo: number
+  pageSize: number
+  accountId?: number
+}
+const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   accountId: undefined
 })
 
+const formRef = ref<InstanceType<typeof TagForm> | null>(null)
+
 /** 侦听公众号变化 **/
-const accountChanged = (accountId) => {
+const onAccountChanged = (id?: number) => {
   queryParams.pageNo = 1
-  queryParams.accountId = accountId
+  queryParams.accountId = id
   getList()
 }
 
@@ -99,9 +116,8 @@ const getList = async () => {
 }
 
 /** 添加/修改操作 */
-const formRef = ref()
 const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, queryParams.accountId, id)
+  formRef.value?.open(type, queryParams.accountId as number, id)
 }
 
 /** 删除按钮操作 */
@@ -121,8 +137,7 @@ const handleDelete = async (id: number) => {
 const handleSync = async () => {
   try {
     await message.confirm('是否确认同步标签?')
-    // @ts-ignore
-    await MpTagApi.syncTag(queryParams.accountId)
+    await MpTagApi.syncTag(queryParams.accountId as number)
     message.success('同步标签成功')
     await getList()
   } catch {}
diff --git a/src/views/mp/user/index.vue b/src/views/mp/user/index.vue
index 91fd7dff..e8e23aaa 100644
--- a/src/views/mp/user/index.vue
+++ b/src/views/mp/user/index.vue
@@ -3,14 +3,42 @@
 
   <!-- 搜索工作栏 -->
   <ContentWrap>
-    <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect  -->
-    <WxAccountSelect @change="(accountId) => accountChanged(accountId)">
-      <template #actions>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="公众号" prop="accountId">
+        <WxMpSelect @change="onAccountChanged" />
+      </el-form-item>
+      <el-form-item label="用户标识" prop="openid">
+        <el-input
+          v-model="queryParams.openid"
+          placeholder="请输入用户标识"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="昵称" prop="nickname">
+        <el-input
+          v-model="queryParams.nickname"
+          placeholder="请输入昵称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"> <Icon icon="ep:search" />搜索 </el-button>
+        <el-button @click="resetQuery"> <Icon icon="ep:refresh" />重置 </el-button>
         <el-button type="success" plain @click="handleSync" v-hasPermi="['mp:user:sync']">
           <Icon icon="ep:refresh" class="mr-5px" /> 同步
         </el-button>
-      </template>
-    </WxAccountSelect>
+      </el-form-item>
+    </el-form>
   </ContentWrap>
 
   <!-- 列表 -->
@@ -66,25 +94,37 @@
   <UserForm ref="formRef" @success="getList" />
 </template>
 <script lang="ts" setup name="MpUser">
-import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
 import { dateFormatter } from '@/utils/formatTime'
 import * as MpUserApi from '@/api/mp/user'
 import * as MpTagApi from '@/api/mp/tag'
+import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
+import { FormInstance } from 'element-plus'
 import UserForm from './UserForm.vue'
 
 const message = useMessage() // 消息
 
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
+const list = ref<any[]>([]) // 列表的数据
+
+interface QueryParams {
+  pageNo: number
+  pageSize: number
+  accountId?: number
+  openid: string | null
+  nickname: string | null
+}
+const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  accountId: null,
+  accountId: undefined,
   openid: null,
   nickname: null
 })
-const tagList = ref([]) // 公众号标签列表
+
+const tagList = ref<any[]>([]) // 公众号标签列表
+const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单
+const formRef = ref<InstanceType<typeof UserForm> | null>(null)
 
 /** 初始化 */
 onMounted(async () => {
@@ -92,9 +132,9 @@ onMounted(async () => {
 })
 
 /** 侦听公众号变化 **/
-const accountChanged = (accountId) => {
+const onAccountChanged = (id?: number) => {
   queryParams.pageNo = 1
-  queryParams.accountId = accountId
+  queryParams.accountId = id
   getList()
 }
 
@@ -110,20 +150,32 @@ const getList = async () => {
   }
 }
 
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  const accountId = queryParams.accountId
+  queryFormRef.value?.resetFields()
+  queryParams.accountId = accountId
+  handleQuery()
+}
+
 /** 添加/修改操作 */
-const formRef = ref()
 const openForm = (id: number) => {
-  formRef.value.open(id)
+  formRef.value?.open(id)
 }
 
 /** 同步标签 */
 const handleSync = async () => {
-  const accountId = queryParams.accountId
   try {
     await message.confirm('是否确认同步粉丝?')
-    await MpUserApi.syncUser(accountId)
+    await MpUserApi.syncUser(queryParams.accountId)
     message.success('开始从微信公众号同步粉丝信息,同步需要一段时间,建议稍后再查询')
-    await getList()
+    getList()
   } catch {}
 }
 </script>
diff --git a/src/views/pay/order/orderForm.vue b/src/views/pay/order/orderForm.vue
new file mode 100644
index 00000000..f9625085
--- /dev/null
+++ b/src/views/pay/order/orderForm.vue
@@ -0,0 +1,152 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="50%">
+    <el-descriptions :column="2" label-class-name="desc-label">
+      <el-descriptions-item label="商户名称">{{ orderDetail.merchantName }}</el-descriptions-item>
+      <el-descriptions-item label="应用名称">{{ orderDetail.appName }}</el-descriptions-item>
+      <el-descriptions-item label="商品名称">{{ orderDetail.subject }}</el-descriptions-item>
+    </el-descriptions>
+    <el-divider />
+    <el-descriptions :column="2" label-class-name="desc-label">
+      <el-descriptions-item label="商户订单号">
+        <el-tag size="small">{{ orderDetail.merchantOrderId }}</el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="渠道订单号">
+        <el-tag class="tag-purple" size="small">{{ orderDetail.channelOrderNo }}</el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="支付订单号">
+        <el-tag v-if="orderDetail.payOrderExtension.no !== ''" class="tag-pink" size="small">
+          {{ orderDetail.payOrderExtension.no }}
+        </el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="金额">
+        <el-tag type="success" size="small">{{ parseFloat(orderDetail.amount / 100, 2) }}</el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="手续费">
+        <el-tag type="warning" size="small"
+          >{{ parseFloat(orderDetail.channelFeeAmount / 100, 2) }}
+        </el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="手续费比例">
+        {{ parseFloat(orderDetail.channelFeeRate / 100, 2) }}%
+      </el-descriptions-item>
+      <el-descriptions-item label="支付状态">
+        <dict-tag :type="DICT_TYPE.PAY_ORDER_STATUS" :value="orderDetail.status" />
+      </el-descriptions-item>
+      <el-descriptions-item label="回调状态">
+        <dict-tag :type="DICT_TYPE.PAY_ORDER_NOTIFY_STATUS" :value="orderDetail.notifyStatus" />
+      </el-descriptions-item>
+      <el-descriptions-item label="回调地址">{{ orderDetail.notifyUrl }}</el-descriptions-item>
+      <el-descriptions-item label="创建时间" :formatter="dateFormatter">
+        {{ formatDate(orderDetail.createTime) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="支付时间" :formatter="dateFormatter">
+        {{ formatDate(orderDetail.successTime) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="失效时间" :formatter="dateFormatter">
+        {{ formatDate(orderDetail.expireTime) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="通知时间" :formatter="dateFormatter">
+        {{ formatDate(orderDetail.notifyTime) }}
+      </el-descriptions-item>
+    </el-descriptions>
+    <el-divider />
+    <el-descriptions :column="2" label-class-name="desc-label">
+      <el-descriptions-item label="支付渠道"
+        >{{ orderDetail.channelCodeName }}
+      </el-descriptions-item>
+      <el-descriptions-item label="支付IP">{{ orderDetail.userIp }}</el-descriptions-item>
+      <el-descriptions-item label="退款状态">
+        <dict-tag :type="DICT_TYPE.PAY_ORDER_REFUND_STATUS" :value="orderDetail.refundStatus" />
+      </el-descriptions-item>
+      <el-descriptions-item label="退款次数">{{ orderDetail.refundTimes }}</el-descriptions-item>
+      <el-descriptions-item label="退款金额">
+        <el-tag type="warning">
+          {{ parseFloat(orderDetail.refundAmount / 100, 2) }}
+        </el-tag>
+      </el-descriptions-item>
+    </el-descriptions>
+    <el-divider />
+    <el-descriptions :column="1" label-class-name="desc-label" direction="vertical" border>
+      <el-descriptions-item label="商品描述">
+        {{ orderDetail.body }}
+      </el-descriptions-item>
+      <el-descriptions-item label="支付通道异步回调内容">
+        {{ orderDetail.payOrderExtension.channelNotifyData }}
+      </el-descriptions-item>
+    </el-descriptions>
+  </Dialog>
+</template>
+<script setup lang="ts" name="orderForm">
+import { DICT_TYPE } from '@/utils/dict'
+import * as OrderApi from '@/api/pay/order'
+import { dateFormatter, formatDate } from '@/utils/formatTime'
+
+const { t } = useI18n() // 国际化
+// const message = useMessage() // 消息弹窗
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('订单详情') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const defaultOrderDetail = {
+  merchantName: '',
+  appName: '',
+  channelCodeName: '',
+  subject: '',
+  merchantOrderId: null,
+  channelOrderNo: '',
+  body: '',
+  amount: null,
+  channelFeeRate: null,
+  channelFeeAmount: null,
+  userIp: '',
+  status: null,
+  notifyUrl: '',
+  notifyStatus: null,
+  refundStatus: null,
+  refundTimes: '',
+  refundAmount: null,
+  createTime: '',
+  successTime: '',
+  notifyTime: '',
+  expireTime: '',
+  payOrderExtension: {
+    channelNotifyData: '',
+    no: ''
+  }
+}
+const orderDetail = ref(JSON.parse(JSON.stringify(defaultOrderDetail)))
+
+/** 打开弹窗 */
+const open = async (id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.preview')
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      orderDetail.value = await OrderApi.getOrderDetail(id)
+      if (orderDetail.value.payOrderExtension === null) {
+        orderDetail.value.payOrderExtension = Object.assign(
+          defaultOrderDetail.payOrderExtension,
+          {}
+        )
+      }
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+</script>
+<style>
+.tag-purple {
+  color: #722ed1;
+  background: #f9f0ff;
+  border-color: #d3adf7;
+}
+
+.tag-pink {
+  color: #eb2f96;
+  background: #fff0f6;
+  border-color: #ffadd2;
+}
+</style>
diff --git a/src/views/pay/refund/refundForm.vue b/src/views/pay/refund/refundForm.vue
new file mode 100644
index 00000000..cc9d8726
--- /dev/null
+++ b/src/views/pay/refund/refundForm.vue
@@ -0,0 +1,154 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="50%">
+    <el-descriptions :column="2" label-class-name="desc-label">
+      <el-descriptions-item label="商户名称">{{ refundDetail.merchantName }}</el-descriptions-item>
+      <el-descriptions-item label="应用名称">{{ refundDetail.appName }}</el-descriptions-item>
+      <el-descriptions-item label="商品名称">{{ refundDetail.subject }}</el-descriptions-item>
+    </el-descriptions>
+    <el-divider />
+    <el-descriptions :column="2" label-class-name="desc-label">
+      <el-descriptions-item label="商户退款单号">
+        <el-tag size="small">{{ refundDetail.merchantRefundNo }}</el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="商户订单号"
+        >{{ refundDetail.merchantOrderId }}
+      </el-descriptions-item>
+      <el-descriptions-item label="交易订单号">{{ refundDetail.tradeNo }}</el-descriptions-item>
+    </el-descriptions>
+    <el-divider />
+    <el-descriptions :column="2" label-class-name="desc-label">
+      <el-descriptions-item label="支付金额">
+        {{ parseFloat(refundDetail.payAmount / 100).toFixed(2) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="退款金额" size="small">
+        <el-tag class="tag-purple" size="small">
+          {{ parseFloat(refundDetail.refundAmount / 100).toFixed(2) }}
+        </el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="退款类型">
+        <dict-tag :type="DICT_TYPE.PAY_REFUND_ORDER_TYPE" :value="refundDetail.type" />
+      </el-descriptions-item>
+      <el-descriptions-item label="退款状态">
+        <dict-tag :type="DICT_TYPE.PAY_REFUND_ORDER_STATUS" :value="refundDetail.status" />
+      </el-descriptions-item>
+      <el-descriptions-item label="创建时间"
+        >{{ formatDate(refundDetail.createTime) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="退款成功时间"
+        >{{ formatDate(refundDetail.successTime) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="退款失效时间"
+        >{{ formatDate(refundDetail.expireTime) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="更新时间"
+        >{{ formatDate(refundDetail.updateTime) }}
+      </el-descriptions-item>
+    </el-descriptions>
+    <el-divider />
+    <el-descriptions :column="2" label-class-name="desc-label">
+      <el-descriptions-item label="支付渠道">
+        {{ refundDetail.channelCodeName }}
+      </el-descriptions-item>
+      <el-descriptions-item label="支付IP" size="small">
+        {{ refundDetail.userIp }}
+      </el-descriptions-item>
+      <el-descriptions-item label="回调地址">{{ refundDetail.notifyUrl }}</el-descriptions-item>
+      <el-descriptions-item label="回调状态">
+        <dict-tag :type="DICT_TYPE.PAY_ORDER_NOTIFY_STATUS" :value="refundDetail.notifyStatus" />
+      </el-descriptions-item>
+      <el-descriptions-item label="回调时间"
+        >{{ formatDate(refundDetail.notifyTime) }}
+      </el-descriptions-item>
+    </el-descriptions>
+    <el-divider />
+    <el-descriptions :column="2" label-class-name="desc-label">
+      <el-descriptions-item label="渠道订单号"
+        >{{ refundDetail.channelOrderNo }}
+      </el-descriptions-item>
+      <el-descriptions-item label="渠道退款单号"
+        >{{ refundDetail.channelRefundNo }}
+      </el-descriptions-item>
+      <el-descriptions-item label="渠道错误码"
+        >{{ refundDetail.channelErrorCode }}
+      </el-descriptions-item>
+      <el-descriptions-item label="渠道错误码描述"
+        >{{ refundDetail.channelErrorMsg }}
+      </el-descriptions-item>
+    </el-descriptions>
+    <br />
+    <el-descriptions :column="1" label-class-name="desc-label" direction="vertical" border>
+      <el-descriptions-item label="渠道额外参数"
+        >{{ refundDetail.channelExtras }}
+      </el-descriptions-item>
+      <el-descriptions-item label="退款原因">{{ refundDetail.reason }}</el-descriptions-item>
+    </el-descriptions>
+  </Dialog>
+</template>
+<script setup lang="ts" name="refundForm">
+import { DICT_TYPE } from '@/utils/dict'
+import * as RefundApi from '@/api/pay/refund'
+import { formatDate } from '@/utils/formatTime'
+
+const { t } = useI18n() // 国际化
+// const message = useMessage() // 消息弹窗
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('退款订单详情') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const defaultrefundDetail = {
+  id: null,
+  appId: null,
+  appName: '',
+  channelCode: '',
+  channelCodeName: '',
+  channelErrorCode: '',
+  channelErrorMsg: '',
+  channelExtras: '',
+  channelId: null,
+  channelOrderNo: '',
+  channelRefundNo: '',
+  createTime: null,
+  expireTime: null,
+  merchantId: null,
+  merchantName: '',
+  merchantOrderId: '',
+  merchantRefundNo: '',
+  notifyStatus: null,
+  notifyTime: null,
+  notifyUrl: '',
+  orderId: null,
+  payAmount: null,
+  reason: '',
+  refundAmount: null,
+  status: null,
+  subject: '',
+  successTime: null,
+  tradeNo: '',
+  type: null,
+  userIp: ''
+}
+const refundDetail = ref(JSON.parse(JSON.stringify(defaultrefundDetail)))
+
+/** 打开弹窗 */
+const open = async (id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.preview')
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      refundDetail.value = await RefundApi.getRefundApi(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+</script>
+
+<style>
+.tag-purple {
+  color: #722ed1;
+  background: #f9f0ff;
+  border-color: #d3adf7;
+}
+</style>

From fbbf4131f67706d77042744813a865b0c16e29e7 Mon Sep 17 00:00:00 2001
From: dhb52 <dhb52@126.com>
Date: Wed, 12 Apr 2023 14:05:21 +0800
Subject: [PATCH 2/6] =?UTF-8?q?fix:=20=E5=8E=BB=E9=99=A4WxLocation?=
 =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=A4=9A=E4=BD=99Location=E5=9B=BE=E6=A0=87?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/views/mp/components/wx-location/main.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/views/mp/components/wx-location/main.vue b/src/views/mp/components/wx-location/main.vue
index 47eab571..17598136 100644
--- a/src/views/mp/components/wx-location/main.vue
+++ b/src/views/mp/components/wx-location/main.vue
@@ -31,7 +31,6 @@
           />
         </el-row>
         <el-row>
-          <el-icon><Location /></el-icon>
           <Icon icon="ep:location" />
           {{ label }}
         </el-row>
@@ -39,6 +38,7 @@
     </el-link>
   </div>
 </template>
+
 <script setup lang="ts" name="WxLocation">
 const props = defineProps({
   locationX: {

From 357a4789f4bf737a175a9b4657c7890c7775bf32 Mon Sep 17 00:00:00 2001
From: dhb52 <dhb52@126.com>
Date: Wed, 12 Apr 2023 14:19:31 +0800
Subject: [PATCH 3/6] fix: import type { FormInstance ...} from 'element-plus'

---
 src/types/auto-imports.d.ts      | 2 ++
 src/views/mp/autoReply/index.vue | 2 +-
 src/views/mp/draft/index.vue     | 2 +-
 src/views/mp/tag/TagForm.vue     | 2 +-
 src/views/mp/user/index.vue      | 2 +-
 5 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/src/types/auto-imports.d.ts b/src/types/auto-imports.d.ts
index cfda3e66..75cf16d9 100644
--- a/src/types/auto-imports.d.ts
+++ b/src/types/auto-imports.d.ts
@@ -62,6 +62,8 @@ declare global {
   const useRouter: typeof import('vue-router')['useRouter']
   const useSlots: typeof import('vue')['useSlots']
   const useTable: typeof import('@/hooks/web/useTable')['useTable']
+  const useVxeCrudSchemas: typeof import('@/hooks/web/useVxeCrudSchemas')['useVxeCrudSchemas']
+  const useXTable: typeof import('@/hooks/web/useXTable')['useXTable']
   const watch: typeof import('vue')['watch']
   const watchEffect: typeof import('vue')['watchEffect']
   const watchPostEffect: typeof import('vue')['watchPostEffect']
diff --git a/src/views/mp/autoReply/index.vue b/src/views/mp/autoReply/index.vue
index 94fe34b3..4c8eb388 100644
--- a/src/views/mp/autoReply/index.vue
+++ b/src/views/mp/autoReply/index.vue
@@ -188,7 +188,7 @@ import * as MpAutoReplyApi from '@/api/mp/autoReply'
 import { DICT_TYPE, getDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import { ContentWrap } from '@/components/ContentWrap'
-import { TabPaneName } from 'element-plus'
+import type { TabPaneName } from 'element-plus'
 
 const message = useMessage()
 
diff --git a/src/views/mp/draft/index.vue b/src/views/mp/draft/index.vue
index 5df7687a..844d01b2 100644
--- a/src/views/mp/draft/index.vue
+++ b/src/views/mp/draft/index.vue
@@ -249,7 +249,7 @@ import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
 import { getAccessToken } from '@/utils/auth'
 import * as MpDraftApi from '@/api/mp/draft'
 import * as MpFreePublishApi from '@/api/mp/freePublish'
-import { UploadFiles, UploadProps, UploadRawFile } from 'element-plus'
+import type { UploadFiles, UploadProps, UploadRawFile } from 'element-plus'
 // 可以用改本地数据模拟,避免API调用超限
 // import drafts from './mock'
 
diff --git a/src/views/mp/tag/TagForm.vue b/src/views/mp/tag/TagForm.vue
index 81f19988..73b29baa 100644
--- a/src/views/mp/tag/TagForm.vue
+++ b/src/views/mp/tag/TagForm.vue
@@ -19,7 +19,7 @@
 </template>
 <script setup lang="ts">
 import * as MpTagApi from '@/api/mp/tag'
-import { FormInstance, FormRules } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
diff --git a/src/views/mp/user/index.vue b/src/views/mp/user/index.vue
index e8e23aaa..6fc355e0 100644
--- a/src/views/mp/user/index.vue
+++ b/src/views/mp/user/index.vue
@@ -98,7 +98,7 @@ import { dateFormatter } from '@/utils/formatTime'
 import * as MpUserApi from '@/api/mp/user'
 import * as MpTagApi from '@/api/mp/tag'
 import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
-import { FormInstance } from 'element-plus'
+import type { FormInstance } from 'element-plus'
 import UserForm from './UserForm.vue'
 
 const message = useMessage() // 消息

From 3536077a34f12d5ddb61927c07005a39a5f67d12 Mon Sep 17 00:00:00 2001
From: dhb52 <dhb52@126.com>
Date: Wed, 12 Apr 2023 14:37:59 +0800
Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20MP=E6=B6=88=E6=81=AF=E7=AE=A1?=
 =?UTF-8?q?=E7=90=86=20ts=E9=87=8D=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/views/mp/components/wx-msg/main.vue | 365 ++++++++++++------------
 src/views/mp/components/wx-msg/types.ts |  11 +
 src/views/mp/message/index.vue          | 140 +++++----
 3 files changed, 262 insertions(+), 254 deletions(-)
 create mode 100644 src/views/mp/components/wx-msg/types.ts

diff --git a/src/views/mp/components/wx-msg/main.vue b/src/views/mp/components/wx-msg/main.vue
index 60c0a5a2..ad37c1f1 100644
--- a/src/views/mp/components/wx-msg/main.vue
+++ b/src/views/mp/components/wx-msg/main.vue
@@ -39,79 +39,79 @@
               :style="item.sendFrom === 2 ? 'background: #6BED72;' : ''"
             >
               <!-- 【事件】区域 -->
-              <div v-if="item.type === 'event' && item.event === 'subscribe'">
+              <div v-if="item.type === MsgType.Event && item.event === 'subscribe'">
                 <el-tag type="success">关注</el-tag>
               </div>
-              <div v-else-if="item.type === 'event' && item.event === 'unsubscribe'">
+              <div v-else-if="item.type === MsgType.Event && item.event === 'unsubscribe'">
                 <el-tag type="danger">取消关注</el-tag>
               </div>
-              <div v-else-if="item.type === 'event' && item.event === 'CLICK'">
+              <div v-else-if="item.type === MsgType.Event && item.event === 'CLICK'">
                 <el-tag>点击菜单</el-tag>
                 【{{ item.eventKey }}】
               </div>
-              <div v-else-if="item.type === 'event' && item.event === 'VIEW'">
+              <div v-else-if="item.type === MsgType.Event && item.event === 'VIEW'">
                 <el-tag>点击菜单链接</el-tag>
                 【{{ item.eventKey }}】
               </div>
-              <div v-else-if="item.type === 'event' && item.event === 'scancode_waitmsg'">
+              <div v-else-if="item.type === MsgType.Event && item.event === 'scancode_waitmsg'">
                 <el-tag>扫码结果</el-tag>
                 【{{ item.eventKey }}】
               </div>
-              <div v-else-if="item.type === 'event' && item.event === 'scancode_push'">
+              <div v-else-if="item.type === MsgType.Event && item.event === 'scancode_push'">
                 <el-tag>扫码结果</el-tag>
                 【{{ item.eventKey }}】
               </div>
-              <div v-else-if="item.type === 'event' && item.event === 'pic_sysphoto'">
+              <div v-else-if="item.type === MsgType.Event && item.event === 'pic_sysphoto'">
                 <el-tag>系统拍照发图</el-tag>
               </div>
-              <div v-else-if="item.type === 'event' && item.event === 'pic_photo_or_album'">
+              <div v-else-if="item.type === MsgType.Event && item.event === 'pic_photo_or_album'">
                 <el-tag>拍照或者相册</el-tag>
               </div>
-              <div v-else-if="item.type === 'event' && item.event === 'pic_weixin'">
+              <div v-else-if="item.type === MsgType.Event && item.event === 'pic_weixin'">
                 <el-tag>微信相册</el-tag>
               </div>
-              <div v-else-if="item.type === 'event' && item.event === 'location_select'">
+              <div v-else-if="item.type === MsgType.Event && item.event === 'location_select'">
                 <el-tag>选择地理位置</el-tag>
               </div>
-              <div v-else-if="item.type === 'event'">
+              <div v-else-if="item.type === MsgType.Event">
                 <el-tag type="danger">未知事件类型</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 v-else-if="item.type === MsgType.Text">{{ item.content }}</div>
+              <div v-else-if="item.type === MsgType.Voice">
+                <WxVoicePlayer :url="item.mediaUrl" :content="item.recognition" />
               </div>
-              <div v-else-if="item.type === 'image'">
+              <div v-else-if="item.type === MsgType.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'"
+                v-else-if="item.type === MsgType.Video || item.type === 'shortvideo'"
                 style="text-align: center"
               >
-                <wx-video-player :url="item.mediaUrl" />
+                <WxVideoPlayer :url="item.mediaUrl" />
               </div>
-              <div v-else-if="item.type === 'link'" class="avue-card__detail">
+              <div v-else-if="item.type === MsgType.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
+              <div v-else-if="item.type === MsgType.Location">
+                <WxLocation
                   :label="item.label"
                   :location-y="item.locationY"
                   :location-x="item.locationX"
                 />
               </div>
-              <div v-else-if="item.type === 'news'" style="width: 300px">
+              <div v-else-if="item.type === MsgType.News" style="width: 300px">
                 <!-- TODO 芋艿:待测试;详情页也存在类似的情况 -->
-                <wx-news :articles="item.articles" />
+                <WxNews :articles="item.articles" />
               </div>
-              <div v-else-if="item.type === 'music'">
-                <wx-music
+              <div v-else-if="item.type === MsgType.Music">
+                <WxMusic
                   :title="item.title"
                   :description="item.description"
                   :thumb-media-url="item.thumbMediaUrl"
@@ -125,182 +125,185 @@
       </div>
     </div>
     <div class="msg-send" v-loading="sendLoading">
-      <wx-reply-select ref="replySelect" :objData="objData" />
+      <WxReplySelect ref="replySelectRef" :objData="objData" />
       <el-button type="success" size="small" class="send-but" @click="sendMsg">发送(S)</el-button>
     </div>
   </ContentWrap>
 </template>
 
-<script lang="ts" name="WxMsg">
-import { getMessagePage, sendMessage } from '@/api/mp/message'
+<script setup lang="ts" name="WxMsg">
 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 { getMessagePage, sendMessage } from '@/api/mp/message'
 import { getUser } from '@/api/mp/user'
-import { defineComponent } from 'vue'
-
-const message = useMessage() // 消息弹窗
+import { formatDate } from '@/utils/formatTime'
 import profile from '@/assets/imgs/profile.jpg'
 import wechat from '@/assets/imgs/wechat.png'
-import { formatDate } from '@/utils/formatTime'
+import { MsgType } from './types'
 
-export default defineComponent({
-  components: {
-    WxReplySelect,
-    WxVideoPlayer,
-    WxVoicePlayer,
-    WxNews,
-    WxLocation,
-    WxMusic
-  },
-  props: {
-    userId: {
-      type: Number,
-      required: true
-    }
-  },
-  setup(props) {
-    const nowStr = ref(new Date().getTime()) // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处
-    const loading = ref(false) // 消息列表是否正在加载中
-    const loadMore = ref(true) // 是否可以加载更多
-    const list = ref<any[]>([]) // 消息列表
-    const queryParams = reactive({
-      pageNo: 1, // 当前页数
-      pageSize: 14, // 每页显示多少条
-      accountId: undefined
-    })
-    const user = reactive({
-      // 由于微信不再提供昵称,直接使用“用户”展示
-      nickname: '用户',
-      avatar: profile,
-      accountId: 0 // 公众号账号编号
-    })
-    const mp = reactive({
-      nickname: '公众号',
-      avatar: wechat
-    })
+const message = useMessage() // 消息弹窗
 
-    // ========= 消息发送 =========
-    const sendLoading = ref(false) // 发送消息是否加载中
-    const objData = reactive({
-      // 微信发送消息
-      type: 'text',
-      accountId: null,
-      articles: []
-    })
-
-    const replySelect = ref(null)
-    // 执行发送
-    const sendMsg = async () => {
-      if (!objData) {
-        return
-      }
-      //     // 公众号限制:客服消息,公众号只允许发送一条
-      if (objData.type === 'news' && objData.articles.length > 1) {
-        objData.articles = [objData.articles[0]]
-        message.success('图文消息条数限制在 1 条以内,已默认发送第一条')
-      }
-      let data = await sendMessage(Object.assign({ userId: props.userId }, { ...objData }))
-      sendLoading.value = false
-      list.value = [...list.value, ...[data]]
-      scrollToBottom()
-      //ts檢查的時候會判斷這個組件可能是空的,所以需要進行斷言。
-      //避免 tab 的数据未清理
-      const deleteObj = (replySelect.value as any).deleteObj
-      if (deleteObj) {
-        deleteObj()
-      }
-    }
-    const loadingMore = () => {
-      queryParams.pageNo++
-      getPage(queryParams, null)
-    }
-    const getPage = async (page, params) => {
-      loading.value = true
-      let dataTemp = await getMessagePage(
-        Object.assign(
-          {
-            pageNo: page.pageNo,
-            pageSize: page.pageSize,
-            userId: props.userId,
-            accountId: page.accountId
-          },
-          params
-        )
-      )
-      const msgDiv = document.getElementById('msg-div' + nowStr.value)
-      let scrollHeight = 0
-      if (msgDiv) {
-        scrollHeight = msgDiv.scrollHeight
-      }
-      // 处理数据
-      let data = dataTemp.list.reverse()
-      list.value = [...data, ...list.value]
-      loading.value = false
-      if (data.length < queryParams.pageSize || data.length === 0) {
-        loadMore.value = false
-      }
-      queryParams.pageNo = page.pageNo
-      queryParams.pageSize = page.pageSize
-      // 滚动到原来的位置
-      if (queryParams.pageNo === 1) {
-        // 定位到消息底部
-        scrollToBottom()
-      } else if (data.length !== 0) {
-        // 定位滚动条
-        await nextTick(() => {
-          if (scrollHeight !== 0) {
-            let div = document.getElementById('msg-div' + nowStr.value)
-            if (div && msgDiv) {
-              msgDiv.scrollTop = div.scrollHeight - scrollHeight - 100
-            }
-          }
-        })
-      }
-    }
-    const refreshChange = () => {
-      getPage(queryParams, null)
-    }
-    /** 定位到消息底部 */
-    const scrollToBottom = () => {
-      nextTick(() => {
-        let div = document.getElementById('msg-div' + nowStr.value)
-        if (div) {
-          div.scrollTop = div.scrollHeight
-        }
-      })
-    }
-
-    onMounted(async () => {
-      let data = await getUser(props.userId)
-      user.nickname = data.nickname && data.nickname.length > 0 ? data.nickname : user.nickname
-      user.avatar = data.avatar && user.avatar.length > 0 ? data.avatar : user.avatar
-      user.accountId = data.accountId
-      queryParams.accountId = data.accountId
-      objData.accountId = data.accountId
-      refreshChange()
-    })
-    return {
-      sendMsg,
-      loadingMore,
-      formatDate,
-      scrollToBottom,
-      objData,
-      mp,
-      user,
-      queryParams,
-      list,
-      loadMore,
-      loading,
-      nowStr,
-      sendLoading
-    }
+const props = defineProps({
+  userId: {
+    type: Number,
+    required: true
   }
 })
+
+const nowStr = ref(new Date().getTime()) // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处
+const loading = ref(false) // 消息列表是否正在加载中
+const loadMore = ref(true) // 是否可以加载更多
+const list = ref<any[]>([]) // 消息列表
+const queryParams = reactive({
+  pageNo: 1, // 当前页数
+  pageSize: 14, // 每页显示多少条
+  accountId: undefined
+})
+
+interface User {
+  nickname: string
+  avatar: string
+  accountId: number
+}
+// 由于微信不再提供昵称,直接使用“用户”展示
+const user: User = reactive({
+  nickname: '用户',
+  avatar: profile,
+  accountId: 0 // 公众号账号编号
+})
+
+interface Mp {
+  nickname: string
+  avatar: string
+}
+const mp: Mp = reactive({
+  nickname: '公众号',
+  avatar: wechat
+})
+
+// ========= 消息发送 =========
+const sendLoading = ref(false) // 发送消息是否加载中
+interface ObjData {
+  type: MsgType
+  accountId: number | null
+  articles: any[]
+}
+
+// 微信发送消息
+const objData: ObjData = reactive({
+  type: MsgType.Text,
+  accountId: null,
+  articles: []
+})
+
+const replySelectRef = ref<InstanceType<typeof WxReplySelect> | null>(null)
+
+/** 完成加载 */
+onMounted(async () => {
+  const data = await getUser(props.userId)
+  user.nickname = data.nickname?.length > 0 ? data.nickname : user.nickname
+  user.avatar = user.avatar?.length > 0 ? data.avatar : user.avatar
+  user.accountId = data.accountId
+  queryParams.accountId = data.accountId
+  objData.accountId = data.accountId
+
+  refreshChange()
+})
+
+// 执行发送
+const sendMsg = async () => {
+  if (!objData) {
+    return
+  }
+  // 公众号限制:客服消息,公众号只允许发送一条
+  if (objData.type === MsgType.News && objData.articles.length > 1) {
+    objData.articles = [objData.articles[0]]
+    message.success('图文消息条数限制在 1 条以内,已默认发送第一条')
+  }
+
+  const data = await sendMessage(Object.assign({ userId: props.userId }, { ...objData }))
+  sendLoading.value = false
+
+  list.value = [...list.value, ...[data]]
+  scrollToBottom()
+
+  //ts检查的時候会判断这个组件可能是空的,所以需要进行断言。
+  //避免 tab 的数据未清理
+  const deleteObj = replySelectRef.value?.deleteObj
+  if (deleteObj) {
+    deleteObj()
+  }
+}
+
+const loadingMore = () => {
+  queryParams.pageNo++
+  getPage(queryParams, null)
+}
+
+const getPage = async (page, params) => {
+  loading.value = true
+  let dataTemp = await getMessagePage(
+    Object.assign(
+      {
+        pageNo: page.pageNo,
+        pageSize: page.pageSize,
+        userId: props.userId,
+        accountId: page.accountId
+      },
+      params
+    )
+  )
+
+  const msgDiv = document.getElementById('msg-div' + nowStr.value)
+  let scrollHeight = 0
+  if (msgDiv) {
+    scrollHeight = msgDiv.scrollHeight
+  }
+  // 处理数据
+  const data = dataTemp.list.reverse()
+  list.value = [...data, ...list.value]
+  loading.value = false
+  if (data.length < queryParams.pageSize || data.length === 0) {
+    loadMore.value = false
+  }
+  queryParams.pageNo = page.pageNo
+  queryParams.pageSize = page.pageSize
+  // 滚动到原来的位置
+  if (queryParams.pageNo === 1) {
+    // 定位到消息底部
+    scrollToBottom()
+  } else if (data.length !== 0) {
+    // 定位滚动条
+    await nextTick(() => {
+      if (scrollHeight !== 0) {
+        let div = document.getElementById('msg-div' + nowStr.value)
+        if (div && msgDiv) {
+          msgDiv.scrollTop = div.scrollHeight - scrollHeight - 100
+        }
+      }
+    })
+  }
+}
+
+const refreshChange = () => {
+  getPage(queryParams, null)
+}
+
+/** 定位到消息底部 */
+const scrollToBottom = () => {
+  nextTick(() => {
+    let div = document.getElementById('msg-div' + nowStr.value)
+    if (div) {
+      div.scrollTop = div.scrollHeight
+    }
+  })
+}
 </script>
+
 <style lang="scss" scoped>
 /* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc  */
 @import './comment.scss';
diff --git a/src/views/mp/components/wx-msg/types.ts b/src/views/mp/components/wx-msg/types.ts
new file mode 100644
index 00000000..b1989ea7
--- /dev/null
+++ b/src/views/mp/components/wx-msg/types.ts
@@ -0,0 +1,11 @@
+export enum MsgType {
+  Event = 'event',
+  Text = 'text',
+  Voice = 'voice',
+  Image = 'image',
+  Video = 'video',
+  Link = 'link',
+  Location = 'location',
+  Music = 'music',
+  News = 'news'
+}
diff --git a/src/views/mp/message/index.vue b/src/views/mp/message/index.vue
index 90db931a..d13acf81 100644
--- a/src/views/mp/message/index.vue
+++ b/src/views/mp/message/index.vue
@@ -9,14 +9,7 @@
       label-width="68px"
     >
       <el-form-item label="公众号" prop="accountId">
-        <el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px">
-          <el-option
-            v-for="item in accountList"
-            :key="item.id"
-            :label="item.name"
-            :value="item.id"
-          />
-        </el-select>
+        <WxMpSelect @change="onAccountChanged" />
       </el-form-item>
       <el-form-item label="消息类型" prop="type">
         <el-select v-model="queryParams.type" placeholder="请选择消息类型" class="!w-240px">
@@ -84,70 +77,76 @@
       <el-table-column label="内容" prop="content">
         <template #default="scope">
           <!-- 【事件】区域 -->
-          <div v-if="scope.row.type === 'event' && scope.row.event === 'subscribe'">
+          <div v-if="scope.row.type === MsgType.Event && scope.row.event === 'subscribe'">
             <el-tag type="success">关注</el-tag>
           </div>
-          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'unsubscribe'">
+          <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'unsubscribe'">
             <el-tag type="danger">取消关注</el-tag>
           </div>
-          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'CLICK'">
+          <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'CLICK'">
             <el-tag>点击菜单</el-tag>
             【{{ scope.row.eventKey }}】
           </div>
-          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'VIEW'">
+          <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'VIEW'">
             <el-tag>点击菜单链接</el-tag>
             【{{ scope.row.eventKey }}】
           </div>
-          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_waitmsg'">
+          <div
+            v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'scancode_waitmsg'"
+          >
             <el-tag>扫码结果</el-tag>
             【{{ scope.row.eventKey }}】
           </div>
-          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_push'">
+          <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'scancode_push'">
             <el-tag>扫码结果</el-tag>
             【{{ scope.row.eventKey }}】
           </div>
-          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_sysphoto'">
+          <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_sysphoto'">
             <el-tag>系统拍照发图</el-tag>
           </div>
-          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_photo_or_album'">
+          <div
+            v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_photo_or_album'"
+          >
             <el-tag>拍照或者相册</el-tag>
           </div>
-          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_weixin'">
+          <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_weixin'">
             <el-tag>微信相册</el-tag>
           </div>
-          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'location_select'">
+          <div
+            v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'location_select'"
+          >
             <el-tag>选择地理位置</el-tag>
           </div>
-          <div v-else-if="scope.row.type === 'event'">
+          <div v-else-if="scope.row.type === MsgType.Event">
             <el-tag type="danger">未知事件类型</el-tag>
           </div>
           <!-- 【消息】区域 -->
-          <div v-else-if="scope.row.type === 'text'">{{ scope.row.content }}</div>
-          <div v-else-if="scope.row.type === 'voice'">
+          <div v-else-if="scope.row.type === MsgType.Text">{{ scope.row.content }}</div>
+          <div v-else-if="scope.row.type === MsgType.Voice">
             <wx-voice-player :url="scope.row.mediaUrl" :content="scope.row.recognition" />
           </div>
-          <div v-else-if="scope.row.type === 'image'">
+          <div v-else-if="scope.row.type === MsgType.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'">
+          <div v-else-if="scope.row.type === MsgType.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'">
+          <div v-else-if="scope.row.type === MsgType.Link">
             <el-tag>链接</el-tag>
             :
             <a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
           </div>
-          <div v-else-if="scope.row.type === 'location'">
-            <wx-location
+          <div v-else-if="scope.row.type === MsgType.Location">
+            <WxLocation
               :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
+          <div v-else-if="scope.row.type === MsgType.Music">
+            <WxMusic
               :title="scope.row.title"
               :description="scope.row.description"
               :thumb-media-url="scope.row.thumbMediaUrl"
@@ -155,8 +154,8 @@
               :hq-music-url="scope.row.hqMusicUrl"
             />
           </div>
-          <div v-else-if="scope.row.type === 'news'">
-            <wx-news :articles="scope.row.articles" />
+          <div v-else-if="scope.row.type === MsgType.News">
+            <WxNews :articles="scope.row.articles" />
           </div>
           <div v-else>
             <el-tag type="danger">未知消息类型</el-tag>
@@ -177,7 +176,7 @@
       </el-table-column>
     </el-table>
     <!-- 分页组件 -->
-    <pagination
+    <Pagination
       v-show="total > 0"
       :total="total"
       v-model:page="queryParams.pageNo"
@@ -186,9 +185,14 @@
     />
 
     <!-- 发送消息的弹窗 -->
-    <el-dialog title="粉丝消息列表" v-model="open" @click="openDialog()" width="50%">
+    <el-dialog
+      title="粉丝消息列表"
+      v-model="showMessageBox"
+      @click="showMessageBox = true"
+      width="50%"
+    >
       <template #footer>
-        <wx-msg :user-id="userId" v-if="open" />
+        <WxMsg :user-id="userId" v-if="showMessageBox" />
       </template>
     </el-dialog>
   </ContentWrap>
@@ -200,17 +204,27 @@ import WxMsg from '@/views/mp/components/wx-msg/main.vue'
 import WxLocation from '@/views/mp/components/wx-location/main.vue'
 import WxMusic from '@/views/mp/components/wx-music/main.vue'
 import WxNews from '@/views/mp/components/wx-news/main.vue'
-import * as MpAccountApi from '@/api/mp/account'
+import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
 import * as MpMessageApi from '@/api/mp/message'
-
-const message = useMessage() // 消息弹窗
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
+import { MsgType } from '@/views/mp/components/wx-msg/types'
+import type { FormInstance } from 'element-plus'
 
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
+const list = ref<any[]>([]) // 列表的数据
+
+// 搜索参数
+interface QueryParams {
+  pageNo: number
+  pageSize: number
+  openid: string | null
+  accountId: number | null
+  type: MsgType | null
+  createTime: string[] | []
+}
+const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   openid: null,
@@ -218,19 +232,18 @@ const queryParams = reactive({
   type: null,
   createTime: []
 })
-const queryFormRef = ref() // 搜索的表单
-// TODO 芋艿:下面应该移除
-const open = ref(false) // 是否显示弹出层
+const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单
+const showMessageBox = ref(false) // 是否显示弹出层
 const userId = ref(0) // 操作的用户编号
-const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表
+
+/** 侦听accountId */
+const onAccountChanged = (id?: number) => {
+  queryParams.accountId = id as number
+  handleQuery()
+}
 
 /** 查询列表 */
 const getList = async () => {
-  // 如果没有选中公众号账号,则进行提示。
-  if (!queryParams.accountId) {
-    await message.error('未选中公众号,无法查询消息')
-    return
-  }
   try {
     loading.value = true
     const data = await MpMessageApi.getMessagePage(queryParams)
@@ -249,34 +262,15 @@ const handleQuery = () => {
 
 /** 重置按钮操作 */
 const resetQuery = async () => {
-  queryFormRef.value.resetFields()
-  // 默认选中第一个
-  if (accountList.value.length > 0) {
-    // @ts-ignore
-    queryParams.accountId = accountList.value[0].id
-  }
+  const accountId = queryParams.accountId
+  queryFormRef.value?.resetFields()
+  queryParams.accountId = accountId
   handleQuery()
 }
-const handleSend = async (row) => {
+
+/** 打开消息发送窗口 */
+const handleSend = async (row: any) => {
   userId.value = row.userId
-  open.value = true
+  showMessageBox.value = true
 }
-
-const openDialog = () => {
-  open.value = true
-}
-// const closeDiaLog = () => {
-//   open.value = false
-// }
-
-/** 初始化 **/
-onMounted(async () => {
-  accountList.value = await MpAccountApi.getSimpleAccountList()
-  // 选中第一个
-  if (accountList.value.length > 0) {
-    // @ts-ignore
-    queryParams.accountId = accountList.value[0].id
-  }
-  await getList()
-})
 </script>

From f436490165b7adacf96fc4a793498ac4d334f77c Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 13 Apr 2023 21:11:55 +0800
Subject: [PATCH 5/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20IconSelect=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/components/Icon/src/IconSelect.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/Icon/src/IconSelect.vue b/src/components/Icon/src/IconSelect.vue
index 1a124ca6..9b77fb2b 100644
--- a/src/components/Icon/src/IconSelect.vue
+++ b/src/components/Icon/src/IconSelect.vue
@@ -95,7 +95,7 @@ watch(
     return props.modelValue
   },
   () => {
-    if (props.modelValue && props.modelValue.contains(':')) {
+    if (props.modelValue && props.modelValue.indexOf(':') >= 0) {
       currentActiveType.value = props.modelValue.substring(0, props.modelValue.indexOf(':') + 1)
       icon.value = props.modelValue.substring(props.modelValue.indexOf(':') + 1)
     }

From 1a1bfe0ebbaeae9fab99929d3d8bf9fd7a2d3cb1 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 13 Apr 2023 22:25:35 +0800
Subject: [PATCH 6/6] =?UTF-8?q?REVIEW=20=E5=85=AC=E4=BC=97=E5=8F=B7?=
 =?UTF-8?q?=E7=9A=84=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 build/vite/optimize.ts                        |   3 +-
 src/views/mp/components/WxMpSelect.vue        |   2 +-
 .../mp/components/wx-editor/WxEditor.vue      |   2 -
 src/views/mp/draft/index.vue                  |   3 +-
 src/views/mp/material/index.vue               |  17 +-
 src/views/mp/tag/index.vue                    |   4 +-
 src/views/mp/user/index.vue                   |  17 +-
 src/views/pay/order/orderForm.vue             | 152 -----------------
 src/views/pay/refund/refundForm.vue           | 154 ------------------
 9 files changed, 20 insertions(+), 334 deletions(-)
 delete mode 100644 src/views/pay/order/orderForm.vue
 delete mode 100644 src/views/pay/refund/refundForm.vue

diff --git a/build/vite/optimize.ts b/build/vite/optimize.ts
index f7a10eb9..416ea02e 100644
--- a/build/vite/optimize.ts
+++ b/build/vite/optimize.ts
@@ -75,7 +75,8 @@ const include = [
   'element-plus/es/components/dropdown-item/style/css',
   'element-plus/es/components/badge/style/css',
   'element-plus/es/components/breadcrumb/style/css',
-  'element-plus/es/components/breadcrumb-item/style/css'
+  'element-plus/es/components/breadcrumb-item/style/css',
+  'element-plus/es/components/image/style/css'
 ]
 
 const exclude = ['@iconify/json']
diff --git a/src/views/mp/components/WxMpSelect.vue b/src/views/mp/components/WxMpSelect.vue
index 14f347d6..615745de 100644
--- a/src/views/mp/components/WxMpSelect.vue
+++ b/src/views/mp/components/WxMpSelect.vue
@@ -3,7 +3,7 @@
     <el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" />
   </el-select>
 </template>
-
+<!-- TODO @芋艿:WxMpSelect 改成 WxAccountSelect,然后挪到现有的 wx-account-select 包下 -->
 <script lang="ts" setup name="WxMpSelect">
 import * as MpAccountApi from '@/api/mp/account'
 
diff --git a/src/views/mp/components/wx-editor/WxEditor.vue b/src/views/mp/components/wx-editor/WxEditor.vue
index cf384113..d2220e10 100644
--- a/src/views/mp/components/wx-editor/WxEditor.vue
+++ b/src/views/mp/components/wx-editor/WxEditor.vue
@@ -102,5 +102,3 @@ const uploadError = () => {
     </div>
   </div>
 </template>
-
-<style></style>
diff --git a/src/views/mp/draft/index.vue b/src/views/mp/draft/index.vue
index 844d01b2..d9a44076 100644
--- a/src/views/mp/draft/index.vue
+++ b/src/views/mp/draft/index.vue
@@ -252,7 +252,6 @@ import * as MpFreePublishApi from '@/api/mp/freePublish'
 import type { UploadFiles, UploadProps, UploadRawFile } from 'element-plus'
 // 可以用改本地数据模拟,避免API调用超限
 // import drafts from './mock'
-
 const message = useMessage() // 消息
 
 const loading = ref(true) // 列表的加载中
@@ -362,7 +361,7 @@ const submitForm = async () => {
   } finally {
     dialogNewsVisible.value = false
     addMaterialLoading.value = false
-    getList()
+    await getList()
   }
 }
 
diff --git a/src/views/mp/material/index.vue b/src/views/mp/material/index.vue
index 4d4017d4..81d8fb08 100644
--- a/src/views/mp/material/index.vue
+++ b/src/views/mp/material/index.vue
@@ -277,14 +277,11 @@ const uploadRules: FormRules = {
 }
 
 // 素材类型
-type MatertialType = 'image' | 'voice' | 'video'
-const type = ref<MatertialType>('image')
-// 遮罩层
-const loading = ref(false)
-// 总条数
-// 数据列表
-const list = ref<any[]>([])
-const total = ref(0)
+type MaterialType = 'image' | 'voice' | 'video'
+const type = ref<MaterialType>('image')
+const loading = ref(false) // 遮罩层
+const list = ref<any[]>([]) // 总条数
+const total = ref(0) // 数据列表
 // 查询参数
 interface QueryParams {
   pageNo: number
@@ -302,7 +299,7 @@ const queryParams: QueryParams = reactive({
 const fileList = ref<UploadUserFile[]>([])
 
 interface UploadData {
-  type: MatertialType
+  type: MaterialType
   title: string
   introduction: string
 }
@@ -346,7 +343,7 @@ const handleQuery = () => {
 
 const onTabChange = (tabName: TabPaneName) => {
   // 设置 type
-  uploadData.type = tabName as MatertialType
+  uploadData.type = tabName as MaterialType
 
   // 提前情况数据,避免tab切换后显示垃圾数据
   list.value = []
diff --git a/src/views/mp/tag/index.vue b/src/views/mp/tag/index.vue
index 6dc0d11a..125c8702 100644
--- a/src/views/mp/tag/index.vue
+++ b/src/views/mp/tag/index.vue
@@ -71,11 +71,10 @@
   <TagForm ref="formRef" @success="getList" />
 </template>
 <script setup lang="ts" name="MpTag">
+import { dateFormatter } from '@/utils/formatTime'
 import * as MpTagApi from '@/api/mp/tag'
 import TagForm from './TagForm.vue'
 import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
-import { dateFormatter } from '@/utils/formatTime'
-
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
@@ -93,7 +92,6 @@ const queryParams: QueryParams = reactive({
   pageSize: 10,
   accountId: undefined
 })
-
 const formRef = ref<InstanceType<typeof TagForm> | null>(null)
 
 /** 侦听公众号变化 **/
diff --git a/src/views/mp/user/index.vue b/src/views/mp/user/index.vue
index 6fc355e0..5b2ec3bd 100644
--- a/src/views/mp/user/index.vue
+++ b/src/views/mp/user/index.vue
@@ -121,15 +121,8 @@ const queryParams: QueryParams = reactive({
   openid: null,
   nickname: null
 })
-
-const tagList = ref<any[]>([]) // 公众号标签列表
 const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单
-const formRef = ref<InstanceType<typeof UserForm> | null>(null)
-
-/** 初始化 */
-onMounted(async () => {
-  tagList.value = await MpTagApi.getSimpleTagList()
-})
+const tagList = ref<any[]>([]) // 公众号标签列表
 
 /** 侦听公众号变化 **/
 const onAccountChanged = (id?: number) => {
@@ -165,6 +158,7 @@ const resetQuery = () => {
 }
 
 /** 添加/修改操作 */
+const formRef = ref<InstanceType<typeof UserForm> | null>(null)
 const openForm = (id: number) => {
   formRef.value?.open(id)
 }
@@ -175,7 +169,12 @@ const handleSync = async () => {
     await message.confirm('是否确认同步粉丝?')
     await MpUserApi.syncUser(queryParams.accountId)
     message.success('开始从微信公众号同步粉丝信息,同步需要一段时间,建议稍后再查询')
-    getList()
+    await getList()
   } catch {}
 }
+
+/** 初始化 */
+onMounted(async () => {
+  tagList.value = await MpTagApi.getSimpleTagList()
+})
 </script>
diff --git a/src/views/pay/order/orderForm.vue b/src/views/pay/order/orderForm.vue
deleted file mode 100644
index f9625085..00000000
--- a/src/views/pay/order/orderForm.vue
+++ /dev/null
@@ -1,152 +0,0 @@
-<template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible" width="50%">
-    <el-descriptions :column="2" label-class-name="desc-label">
-      <el-descriptions-item label="商户名称">{{ orderDetail.merchantName }}</el-descriptions-item>
-      <el-descriptions-item label="应用名称">{{ orderDetail.appName }}</el-descriptions-item>
-      <el-descriptions-item label="商品名称">{{ orderDetail.subject }}</el-descriptions-item>
-    </el-descriptions>
-    <el-divider />
-    <el-descriptions :column="2" label-class-name="desc-label">
-      <el-descriptions-item label="商户订单号">
-        <el-tag size="small">{{ orderDetail.merchantOrderId }}</el-tag>
-      </el-descriptions-item>
-      <el-descriptions-item label="渠道订单号">
-        <el-tag class="tag-purple" size="small">{{ orderDetail.channelOrderNo }}</el-tag>
-      </el-descriptions-item>
-      <el-descriptions-item label="支付订单号">
-        <el-tag v-if="orderDetail.payOrderExtension.no !== ''" class="tag-pink" size="small">
-          {{ orderDetail.payOrderExtension.no }}
-        </el-tag>
-      </el-descriptions-item>
-      <el-descriptions-item label="金额">
-        <el-tag type="success" size="small">{{ parseFloat(orderDetail.amount / 100, 2) }}</el-tag>
-      </el-descriptions-item>
-      <el-descriptions-item label="手续费">
-        <el-tag type="warning" size="small"
-          >{{ parseFloat(orderDetail.channelFeeAmount / 100, 2) }}
-        </el-tag>
-      </el-descriptions-item>
-      <el-descriptions-item label="手续费比例">
-        {{ parseFloat(orderDetail.channelFeeRate / 100, 2) }}%
-      </el-descriptions-item>
-      <el-descriptions-item label="支付状态">
-        <dict-tag :type="DICT_TYPE.PAY_ORDER_STATUS" :value="orderDetail.status" />
-      </el-descriptions-item>
-      <el-descriptions-item label="回调状态">
-        <dict-tag :type="DICT_TYPE.PAY_ORDER_NOTIFY_STATUS" :value="orderDetail.notifyStatus" />
-      </el-descriptions-item>
-      <el-descriptions-item label="回调地址">{{ orderDetail.notifyUrl }}</el-descriptions-item>
-      <el-descriptions-item label="创建时间" :formatter="dateFormatter">
-        {{ formatDate(orderDetail.createTime) }}
-      </el-descriptions-item>
-      <el-descriptions-item label="支付时间" :formatter="dateFormatter">
-        {{ formatDate(orderDetail.successTime) }}
-      </el-descriptions-item>
-      <el-descriptions-item label="失效时间" :formatter="dateFormatter">
-        {{ formatDate(orderDetail.expireTime) }}
-      </el-descriptions-item>
-      <el-descriptions-item label="通知时间" :formatter="dateFormatter">
-        {{ formatDate(orderDetail.notifyTime) }}
-      </el-descriptions-item>
-    </el-descriptions>
-    <el-divider />
-    <el-descriptions :column="2" label-class-name="desc-label">
-      <el-descriptions-item label="支付渠道"
-        >{{ orderDetail.channelCodeName }}
-      </el-descriptions-item>
-      <el-descriptions-item label="支付IP">{{ orderDetail.userIp }}</el-descriptions-item>
-      <el-descriptions-item label="退款状态">
-        <dict-tag :type="DICT_TYPE.PAY_ORDER_REFUND_STATUS" :value="orderDetail.refundStatus" />
-      </el-descriptions-item>
-      <el-descriptions-item label="退款次数">{{ orderDetail.refundTimes }}</el-descriptions-item>
-      <el-descriptions-item label="退款金额">
-        <el-tag type="warning">
-          {{ parseFloat(orderDetail.refundAmount / 100, 2) }}
-        </el-tag>
-      </el-descriptions-item>
-    </el-descriptions>
-    <el-divider />
-    <el-descriptions :column="1" label-class-name="desc-label" direction="vertical" border>
-      <el-descriptions-item label="商品描述">
-        {{ orderDetail.body }}
-      </el-descriptions-item>
-      <el-descriptions-item label="支付通道异步回调内容">
-        {{ orderDetail.payOrderExtension.channelNotifyData }}
-      </el-descriptions-item>
-    </el-descriptions>
-  </Dialog>
-</template>
-<script setup lang="ts" name="orderForm">
-import { DICT_TYPE } from '@/utils/dict'
-import * as OrderApi from '@/api/pay/order'
-import { dateFormatter, formatDate } from '@/utils/formatTime'
-
-const { t } = useI18n() // 国际化
-// const message = useMessage() // 消息弹窗
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('订单详情') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const defaultOrderDetail = {
-  merchantName: '',
-  appName: '',
-  channelCodeName: '',
-  subject: '',
-  merchantOrderId: null,
-  channelOrderNo: '',
-  body: '',
-  amount: null,
-  channelFeeRate: null,
-  channelFeeAmount: null,
-  userIp: '',
-  status: null,
-  notifyUrl: '',
-  notifyStatus: null,
-  refundStatus: null,
-  refundTimes: '',
-  refundAmount: null,
-  createTime: '',
-  successTime: '',
-  notifyTime: '',
-  expireTime: '',
-  payOrderExtension: {
-    channelNotifyData: '',
-    no: ''
-  }
-}
-const orderDetail = ref(JSON.parse(JSON.stringify(defaultOrderDetail)))
-
-/** 打开弹窗 */
-const open = async (id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.preview')
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      orderDetail.value = await OrderApi.getOrderDetail(id)
-      if (orderDetail.value.payOrderExtension === null) {
-        orderDetail.value.payOrderExtension = Object.assign(
-          defaultOrderDetail.payOrderExtension,
-          {}
-        )
-      }
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-</script>
-<style>
-.tag-purple {
-  color: #722ed1;
-  background: #f9f0ff;
-  border-color: #d3adf7;
-}
-
-.tag-pink {
-  color: #eb2f96;
-  background: #fff0f6;
-  border-color: #ffadd2;
-}
-</style>
diff --git a/src/views/pay/refund/refundForm.vue b/src/views/pay/refund/refundForm.vue
deleted file mode 100644
index cc9d8726..00000000
--- a/src/views/pay/refund/refundForm.vue
+++ /dev/null
@@ -1,154 +0,0 @@
-<template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible" width="50%">
-    <el-descriptions :column="2" label-class-name="desc-label">
-      <el-descriptions-item label="商户名称">{{ refundDetail.merchantName }}</el-descriptions-item>
-      <el-descriptions-item label="应用名称">{{ refundDetail.appName }}</el-descriptions-item>
-      <el-descriptions-item label="商品名称">{{ refundDetail.subject }}</el-descriptions-item>
-    </el-descriptions>
-    <el-divider />
-    <el-descriptions :column="2" label-class-name="desc-label">
-      <el-descriptions-item label="商户退款单号">
-        <el-tag size="small">{{ refundDetail.merchantRefundNo }}</el-tag>
-      </el-descriptions-item>
-      <el-descriptions-item label="商户订单号"
-        >{{ refundDetail.merchantOrderId }}
-      </el-descriptions-item>
-      <el-descriptions-item label="交易订单号">{{ refundDetail.tradeNo }}</el-descriptions-item>
-    </el-descriptions>
-    <el-divider />
-    <el-descriptions :column="2" label-class-name="desc-label">
-      <el-descriptions-item label="支付金额">
-        {{ parseFloat(refundDetail.payAmount / 100).toFixed(2) }}
-      </el-descriptions-item>
-      <el-descriptions-item label="退款金额" size="small">
-        <el-tag class="tag-purple" size="small">
-          {{ parseFloat(refundDetail.refundAmount / 100).toFixed(2) }}
-        </el-tag>
-      </el-descriptions-item>
-      <el-descriptions-item label="退款类型">
-        <dict-tag :type="DICT_TYPE.PAY_REFUND_ORDER_TYPE" :value="refundDetail.type" />
-      </el-descriptions-item>
-      <el-descriptions-item label="退款状态">
-        <dict-tag :type="DICT_TYPE.PAY_REFUND_ORDER_STATUS" :value="refundDetail.status" />
-      </el-descriptions-item>
-      <el-descriptions-item label="创建时间"
-        >{{ formatDate(refundDetail.createTime) }}
-      </el-descriptions-item>
-      <el-descriptions-item label="退款成功时间"
-        >{{ formatDate(refundDetail.successTime) }}
-      </el-descriptions-item>
-      <el-descriptions-item label="退款失效时间"
-        >{{ formatDate(refundDetail.expireTime) }}
-      </el-descriptions-item>
-      <el-descriptions-item label="更新时间"
-        >{{ formatDate(refundDetail.updateTime) }}
-      </el-descriptions-item>
-    </el-descriptions>
-    <el-divider />
-    <el-descriptions :column="2" label-class-name="desc-label">
-      <el-descriptions-item label="支付渠道">
-        {{ refundDetail.channelCodeName }}
-      </el-descriptions-item>
-      <el-descriptions-item label="支付IP" size="small">
-        {{ refundDetail.userIp }}
-      </el-descriptions-item>
-      <el-descriptions-item label="回调地址">{{ refundDetail.notifyUrl }}</el-descriptions-item>
-      <el-descriptions-item label="回调状态">
-        <dict-tag :type="DICT_TYPE.PAY_ORDER_NOTIFY_STATUS" :value="refundDetail.notifyStatus" />
-      </el-descriptions-item>
-      <el-descriptions-item label="回调时间"
-        >{{ formatDate(refundDetail.notifyTime) }}
-      </el-descriptions-item>
-    </el-descriptions>
-    <el-divider />
-    <el-descriptions :column="2" label-class-name="desc-label">
-      <el-descriptions-item label="渠道订单号"
-        >{{ refundDetail.channelOrderNo }}
-      </el-descriptions-item>
-      <el-descriptions-item label="渠道退款单号"
-        >{{ refundDetail.channelRefundNo }}
-      </el-descriptions-item>
-      <el-descriptions-item label="渠道错误码"
-        >{{ refundDetail.channelErrorCode }}
-      </el-descriptions-item>
-      <el-descriptions-item label="渠道错误码描述"
-        >{{ refundDetail.channelErrorMsg }}
-      </el-descriptions-item>
-    </el-descriptions>
-    <br />
-    <el-descriptions :column="1" label-class-name="desc-label" direction="vertical" border>
-      <el-descriptions-item label="渠道额外参数"
-        >{{ refundDetail.channelExtras }}
-      </el-descriptions-item>
-      <el-descriptions-item label="退款原因">{{ refundDetail.reason }}</el-descriptions-item>
-    </el-descriptions>
-  </Dialog>
-</template>
-<script setup lang="ts" name="refundForm">
-import { DICT_TYPE } from '@/utils/dict'
-import * as RefundApi from '@/api/pay/refund'
-import { formatDate } from '@/utils/formatTime'
-
-const { t } = useI18n() // 国际化
-// const message = useMessage() // 消息弹窗
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('退款订单详情') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const defaultrefundDetail = {
-  id: null,
-  appId: null,
-  appName: '',
-  channelCode: '',
-  channelCodeName: '',
-  channelErrorCode: '',
-  channelErrorMsg: '',
-  channelExtras: '',
-  channelId: null,
-  channelOrderNo: '',
-  channelRefundNo: '',
-  createTime: null,
-  expireTime: null,
-  merchantId: null,
-  merchantName: '',
-  merchantOrderId: '',
-  merchantRefundNo: '',
-  notifyStatus: null,
-  notifyTime: null,
-  notifyUrl: '',
-  orderId: null,
-  payAmount: null,
-  reason: '',
-  refundAmount: null,
-  status: null,
-  subject: '',
-  successTime: null,
-  tradeNo: '',
-  type: null,
-  userIp: ''
-}
-const refundDetail = ref(JSON.parse(JSON.stringify(defaultrefundDetail)))
-
-/** 打开弹窗 */
-const open = async (id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.preview')
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      refundDetail.value = await RefundApi.getRefundApi(id)
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-</script>
-
-<style>
-.tag-purple {
-  color: #722ed1;
-  background: #f9f0ff;
-  border-color: #d3adf7;
-}
-</style>