From ab52f8b18fe9060a93088851ec794390ddfe85bc Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Thu, 20 Apr 2023 14:17:21 +0800
Subject: [PATCH 1/8] =?UTF-8?q?=E8=8F=9C=E5=8D=95=E7=AE=A1=E7=90=86?=
 =?UTF-8?q?=EF=BC=9A=E6=B7=BB=E5=8A=A0=E5=88=B7=E6=96=B0=E8=8F=9C=E5=8D=95?=
 =?UTF-8?q?=E7=BC=93=E5=AD=98=E6=8C=89=E9=92=AE=EF=BC=8C=E6=8F=90=E9=AB=98?=
 =?UTF-8?q?=E6=98=93=E7=94=A8=E6=80=A7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/views/system/menu/index.vue | 93 +++++++++++++++++++++------------
 1 file changed, 60 insertions(+), 33 deletions(-)

diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue
index 323126c2..95e71b5c 100644
--- a/src/views/system/menu/index.vue
+++ b/src/views/system/menu/index.vue
@@ -5,27 +5,27 @@
   <!-- 搜索工作栏 -->
   <ContentWrap>
     <el-form
-      class="-mb-15px"
-      :model="queryParams"
       ref="queryFormRef"
       :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
       label-width="68px"
     >
       <el-form-item label="菜单名称" prop="name">
         <el-input
           v-model="queryParams.name"
-          placeholder="请输入菜单名称"
-          clearable
-          @keyup.enter="handleQuery"
           class="!w-240px"
+          clearable
+          placeholder="请输入菜单名称"
+          @keyup.enter="handleQuery"
         />
       </el-form-item>
       <el-form-item label="状态" prop="status">
         <el-select
           v-model="queryParams.status"
-          placeholder="请选择菜单状态"
-          clearable
           class="!w-240px"
+          clearable
+          placeholder="请选择菜单状态"
         >
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
@@ -36,18 +36,30 @@
         </el-select>
       </el-form-item>
       <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-        <el-button
-          type="primary"
-          plain
-          @click="openForm('create')"
-          v-hasPermi="['system:menu:create']"
-        >
-          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        <el-button @click="handleQuery">
+          <Icon class="mr-5px" icon="ep:search" />
+          搜索
         </el-button>
-        <el-button type="danger" plain @click="toggleExpandAll">
-          <Icon icon="ep:sort" class="mr-5px" /> 展开/折叠
+        <el-button @click="resetQuery">
+          <Icon class="mr-5px" icon="ep:refresh" />
+          重置
+        </el-button>
+        <el-button
+          v-hasPermi="['system:menu:create']"
+          plain
+          type="primary"
+          @click="openForm('create')"
+        >
+          <Icon class="mr-5px" icon="ep:plus" />
+          新增
+        </el-button>
+        <el-button plain type="danger" @click="toggleExpandAll">
+          <Icon class="mr-5px" icon="ep:sort" />
+          展开/折叠
+        </el-button>
+        <el-button plain @click="refreshMenu">
+          <Icon class="mr-5px" icon="ep:refresh" />
+          刷新菜单缓存
         </el-button>
       </el-form-item>
     </el-form>
@@ -56,50 +68,50 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table
+      v-if="refreshTable"
       v-loading="loading"
       :data="list"
-      row-key="id"
-      v-if="refreshTable"
       :default-expand-all="isExpandAll"
+      row-key="id"
     >
-      <el-table-column prop="name" label="菜单名称" :show-overflow-tooltip="true" width="250" />
-      <el-table-column prop="icon" label="图标" align="center" width="100">
+      <el-table-column :show-overflow-tooltip="true" label="菜单名称" prop="name" width="250" />
+      <el-table-column align="center" label="图标" prop="icon" width="100">
         <template #default="scope">
           <Icon :icon="scope.row.icon" />
         </template>
       </el-table-column>
-      <el-table-column prop="sort" label="排序" width="60" />
-      <el-table-column prop="permission" label="权限标识" :show-overflow-tooltip="true" />
-      <el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true" />
-      <el-table-column prop="componentName" label="组件名称" :show-overflow-tooltip="true" />
-      <el-table-column prop="status" label="状态" width="80">
+      <el-table-column label="排序" prop="sort" width="60" />
+      <el-table-column :show-overflow-tooltip="true" label="权限标识" prop="permission" />
+      <el-table-column :show-overflow-tooltip="true" label="组件路径" prop="component" />
+      <el-table-column :show-overflow-tooltip="true" label="组件名称" prop="componentName" />
+      <el-table-column label="状态" prop="status" width="80">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
         </template>
       </el-table-column>
-      <el-table-column label="操作" align="center">
+      <el-table-column align="center" label="操作">
         <template #default="scope">
           <el-button
+            v-hasPermi="['system:menu:update']"
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['system:menu:update']"
           >
             修改
           </el-button>
           <el-button
+            v-hasPermi="['system:menu:create']"
             link
             type="primary"
             @click="openForm('create', undefined, scope.row.id)"
-            v-hasPermi="['system:menu:create']"
           >
             新增
           </el-button>
           <el-button
+            v-hasPermi="['system:menu:delete']"
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['system:menu:delete']"
           >
             删除
           </el-button>
@@ -111,11 +123,14 @@
   <!-- 表单弹窗:添加/修改 -->
   <MenuForm ref="formRef" @success="getList" />
 </template>
-<script setup lang="ts" name="SystemMenu">
+<script lang="ts" name="SystemMenu" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { handleTree } from '@/utils/tree'
 import * as MenuApi from '@/api/system/menu'
 import MenuForm from './MenuForm.vue'
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+
+const { wsCache } = useCache()
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
@@ -165,7 +180,19 @@ const toggleExpandAll = () => {
     refreshTable.value = true
   })
 }
-
+/** 刷新菜单缓存按钮操作 */
+const refreshMenu = () => {
+  ElMessageBox.confirm('即将更新缓存刷新浏览器!', '刷新菜单缓存', {
+    confirmButtonText: t('common.ok'),
+    cancelButtonText: t('common.cancel'),
+    type: 'warning'
+  }).then(() => {
+    // 清空,从而触发刷新
+    wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
+    // 刷新浏览器
+    location.reload()
+  })
+}
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {

From b45b85984ce55f09e80d0b3a4ba7e788ad1c8742 Mon Sep 17 00:00:00 2001
From: dhb52 <dhb52@126.com>
Date: Fri, 21 Apr 2023 20:22:11 +0800
Subject: [PATCH 2/8] =?UTF-8?q?refactor:=20mp=E6=A8=A1=E5=9D=97=EF=BC=8C?=
 =?UTF-8?q?=E4=B8=80=E4=B8=AA=E5=A4=A7=E5=A4=A7=E7=9A=84=E9=87=8D=E6=9E=84?=
 =?UTF-8?q?+fix?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../mp/autoReply/components/ReplyTable.vue    |   8 +-
 src/views/mp/autoReply/components/types.ts    |  41 -------
 src/views/mp/autoReply/index.vue              |  55 ++++-----
 .../mp/components/wx-account-select/index.ts  |   3 +
 .../mp/components/wx-account-select/main.vue  |  14 ++-
 src/views/mp/components/wx-location/index.ts  |   3 +
 .../mp/components/wx-material-select/index.ts |   6 +
 .../mp/components/wx-material-select/main.vue |  58 +++++-----
 .../mp/components/wx-material-select/types.ts |  11 ++
 src/views/mp/components/wx-msg/index.ts       |   6 +
 src/views/mp/components/wx-msg/main.vue       |  28 ++---
 src/views/mp/components/wx-music/index.ts     |   3 +
 src/views/mp/components/wx-news/index.ts      |   3 +
 src/views/mp/components/wx-news/main.vue      |  36 +++---
 .../wx-reply/components/TabImage.vue          |  50 ++++-----
 .../wx-reply/components/TabMusic.vue          |  41 ++++---
 .../wx-reply/components/TabNews.vue           |  34 +++---
 .../wx-reply/components/TabText.vue           |  11 +-
 .../wx-reply/components/TabVideo.vue          |  52 ++++-----
 .../wx-reply/components/TabVoice.vue          |  54 ++++-----
 .../components/wx-reply/components/types.ts   |  59 +++++++---
 src/views/mp/components/wx-reply/index.ts     |   7 ++
 src/views/mp/components/wx-reply/main.vue     | 105 ++++++++++++++----
 .../mp/components/wx-video-play/index.ts      |   3 +
 .../mp/components/wx-voice-play/index.ts      |   3 +
 .../mp/components/wx-voice-play/main.vue      |   6 +-
 src/views/mp/draft/components/CoverSelect.vue |  36 ++----
 src/views/mp/draft/components/DraftTable.vue  |   2 +-
 src/views/mp/draft/index.vue                  |   9 +-
 src/views/mp/freePublish/index.vue            |  10 +-
 src/views/mp/hooks/useUpload.ts               |  12 +-
 .../mp/material/components/UploadFile.vue     |  15 +--
 .../mp/material/components/UploadVideo.vue    |  28 ++---
 .../mp/material/components/VideoTable.vue     |   2 +-
 .../mp/material/components/VoiceTable.vue     |   2 +-
 src/views/mp/material/components/upload.ts    |  12 +-
 src/views/mp/material/index.vue               |  28 ++---
 src/views/mp/menu/components/MenuEditor.vue   |  14 +--
 src/views/mp/menu/index.vue                   |   8 +-
 src/views/mp/message/MessageTable.vue         |  10 +-
 src/views/mp/message/index.vue                |  20 ++--
 src/views/mp/tag/index.vue                    |  32 ++++--
 src/views/mp/user/index.vue                   |  16 ++-
 43 files changed, 518 insertions(+), 438 deletions(-)
 create mode 100644 src/views/mp/components/wx-account-select/index.ts
 create mode 100644 src/views/mp/components/wx-location/index.ts
 create mode 100644 src/views/mp/components/wx-material-select/index.ts
 create mode 100644 src/views/mp/components/wx-material-select/types.ts
 create mode 100644 src/views/mp/components/wx-msg/index.ts
 create mode 100644 src/views/mp/components/wx-music/index.ts
 create mode 100644 src/views/mp/components/wx-news/index.ts
 create mode 100644 src/views/mp/components/wx-reply/index.ts
 create mode 100644 src/views/mp/components/wx-video-play/index.ts
 create mode 100644 src/views/mp/components/wx-voice-play/index.ts

diff --git a/src/views/mp/autoReply/components/ReplyTable.vue b/src/views/mp/autoReply/components/ReplyTable.vue
index 0b739cef..e3e53905 100644
--- a/src/views/mp/autoReply/components/ReplyTable.vue
+++ b/src/views/mp/autoReply/components/ReplyTable.vue
@@ -94,10 +94,10 @@
   </el-table>
 </template>
 <script setup lang="ts">
-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 WxVideoPlayer from '@/views/mp/components/wx-video-play'
+import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
+import WxMusic from '@/views/mp/components/wx-music'
+import WxNews from '@/views/mp/components/wx-news'
 import { dateFormatter } from '@/utils/formatTime'
 import { DICT_TYPE } from '@/utils/dict'
 import { MsgType } from './types'
diff --git a/src/views/mp/autoReply/components/types.ts b/src/views/mp/autoReply/components/types.ts
index 0d78fd85..68bc5c94 100644
--- a/src/views/mp/autoReply/components/types.ts
+++ b/src/views/mp/autoReply/components/types.ts
@@ -5,44 +5,3 @@ export enum MsgType {
   Message = 2,
   Keyword = 3
 }
-
-type ReplyType = 'text' | 'image' | 'voice' | 'video' | 'shortvideo' | 'location' | 'link'
-
-export 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
-}
-
-// TODO @Dhb52:ObjData 这个类名可以在看看,ObjData 有点通用
-export 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
-}
diff --git a/src/views/mp/autoReply/index.vue b/src/views/mp/autoReply/index.vue
index 2d9b492d..20a1e683 100644
--- a/src/views/mp/autoReply/index.vue
+++ b/src/views/mp/autoReply/index.vue
@@ -82,7 +82,7 @@
           <el-input v-model="replyForm.requestKeyword" placeholder="请输入内容" clearable />
         </el-form-item>
         <el-form-item label="回复消息">
-          <WxReplySelect :objData="objData" />
+          <WxReplySelect v-model="reply" />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -93,14 +93,14 @@
   </ContentWrap>
 </template>
 <script setup lang="ts" name="MpAutoReply">
-import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
-import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
+import WxReplySelect, { type Reply, ReplyType } from '@/views/mp/components/wx-reply'
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
 import * as MpAutoReplyApi from '@/api/mp/autoReply'
 import { DICT_TYPE, getDictOptions, getIntDictOptions } from '@/utils/dict'
 import { ContentWrap } from '@/components/ContentWrap'
-import type { TabPaneName } from 'element-plus'
+import type { FormInstance, TabPaneName } from 'element-plus'
 import ReplyTable from './components/ReplyTable.vue'
-import { MsgType, ReplyForm, ObjData } from './components/types'
+import { MsgType } from './components/types'
 const message = useMessage() // 消息
 
 const msgType = ref<MsgType>(MsgType.Keyword) // 消息类型
@@ -108,26 +108,26 @@ const RequestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'l
 const loading = ref(true) // 遮罩层
 const total = ref(0) // 总条数
 const list = ref<any[]>([]) // 自动回复列表
-const formRef = ref() // 表单 ref
+const formRef = ref<FormInstance | null>(null) // 表单 ref
 // 查询参数
 interface QueryParams {
   pageNo: number
   pageSize: number
-  accountId?: number
+  accountId: number
 }
 const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  accountId: undefined
+  accountId: 0
 })
 
 const dialogTitle = ref('') // 弹出层标题
 const showFormDialog = ref(false) // 是否显示弹出层
-const replyForm = ref<ReplyForm>({}) // 表单参数
+const replyForm = ref<any>({}) // 表单参数
 // 回复消息
-const objData = ref<ObjData>({
-  type: 'text',
-  accountId: undefined
+const reply = ref<Reply>({
+  type: ReplyType.Text,
+  accountId: 0
 })
 // 表单校验
 const rules = {
@@ -136,8 +136,9 @@ const rules = {
 }
 
 /** 侦听账号变化 */
-const onAccountChanged = (id?: number) => {
+const onAccountChanged = (id: number) => {
   queryParams.accountId = id
+  reply.value.accountId = id
   getList()
 }
 
@@ -171,8 +172,8 @@ const onTabChange = (tabName: TabPaneName) => {
 const onCreate = () => {
   reset()
   // 打开表单,并设置初始化
-  objData.value = {
-    type: 'text',
+  reply.value = {
+    type: ReplyType.Text,
     accountId: queryParams.accountId
   }
 
@@ -193,7 +194,7 @@ const onUpdate = async (id: number) => {
   delete replyForm.value['responseMediaUrl']
   delete replyForm.value['responseDescription']
   delete replyForm.value['responseArticles']
-  objData.value = {
+  reply.value = {
     type: data.responseMessageType,
     accountId: queryParams.accountId,
     content: data.responseContent,
@@ -227,17 +228,17 @@ const onSubmit = async () => {
 
   // 处理回复消息
   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
+  submitForm.responseMessageType = reply.value.type
+  submitForm.responseContent = reply.value.content
+  submitForm.responseMediaId = reply.value.mediaId
+  submitForm.responseMediaUrl = reply.value.url
+  submitForm.responseTitle = reply.value.title
+  submitForm.responseDescription = reply.value.description
+  submitForm.responseThumbMediaId = reply.value.thumbMediaId
+  submitForm.responseThumbMediaUrl = reply.value.thumbMediaUrl
+  submitForm.responseArticles = reply.value.articles
+  submitForm.responseMusicUrl = reply.value.musicUrl
+  submitForm.responseHqMusicUrl = reply.value.hqMusicUrl
 
   if (replyForm.value.id !== undefined) {
     await MpAutoReplyApi.updateAutoReply(submitForm)
diff --git a/src/views/mp/components/wx-account-select/index.ts b/src/views/mp/components/wx-account-select/index.ts
new file mode 100644
index 00000000..97556b2f
--- /dev/null
+++ b/src/views/mp/components/wx-account-select/index.ts
@@ -0,0 +1,3 @@
+import WxAccountSelect from './main.vue'
+
+export default WxAccountSelect
diff --git a/src/views/mp/components/wx-account-select/main.vue b/src/views/mp/components/wx-account-select/main.vue
index 5359b366..8dbad499 100644
--- a/src/views/mp/components/wx-account-select/main.vue
+++ b/src/views/mp/components/wx-account-select/main.vue
@@ -14,7 +14,7 @@ const account: MpAccountApi.AccountVO = reactive({
 const accountList: Ref<MpAccountApi.AccountVO[]> = ref([])
 
 const emit = defineEmits<{
-  (e: 'change', id?: number, name?: string): void
+  (e: 'change', id: number, name: string): void
 }>()
 
 const handleQuery = async () => {
@@ -22,15 +22,19 @@ const handleQuery = async () => {
   // 默认选中第一个
   if (accountList.value.length > 0) {
     account.id = accountList.value[0].id
-    account.name = accountList.value[0].name
-    emit('change', account.id, account.name)
+    if (account.id) {
+      account.name = accountList.value[0].name
+      emit('change', account.id, account.name)
+    }
   }
 }
 
 const onChanged = (id?: number) => {
   const found = accountList.value.find((v) => v.id === id)
-  account.name = found ? found.name : ''
-  emit('change', account.id, account.name)
+  if (account.id) {
+    account.name = found ? found.name : ''
+    emit('change', account.id, account.name)
+  }
 }
 
 /** 初始化 */
diff --git a/src/views/mp/components/wx-location/index.ts b/src/views/mp/components/wx-location/index.ts
new file mode 100644
index 00000000..14ba8644
--- /dev/null
+++ b/src/views/mp/components/wx-location/index.ts
@@ -0,0 +1,3 @@
+import WxLocation from './main.vue'
+
+export default WxLocation
diff --git a/src/views/mp/components/wx-material-select/index.ts b/src/views/mp/components/wx-material-select/index.ts
new file mode 100644
index 00000000..eeda31d5
--- /dev/null
+++ b/src/views/mp/components/wx-material-select/index.ts
@@ -0,0 +1,6 @@
+import WxMaterialSelect from './main.vue'
+import { NewsType, MaterialType } from './types'
+
+export { NewsType, MaterialType }
+
+export default WxMaterialSelect
diff --git a/src/views/mp/components/wx-material-select/main.vue b/src/views/mp/components/wx-material-select/main.vue
index d67ffb1f..e711c022 100644
--- a/src/views/mp/components/wx-material-select/main.vue
+++ b/src/views/mp/components/wx-material-select/main.vue
@@ -7,7 +7,7 @@
 <template>
   <div class="pb-30px">
     <!-- 类型:image -->
-    <div v-if="objData.type === 'image'">
+    <div v-if="props.type === 'image'">
       <div class="waterfall" v-loading="loading">
         <div class="waterfall-item" v-for="item in list" :key="item.mediaId">
           <img class="material-img" :src="item.url" />
@@ -29,7 +29,7 @@
       />
     </div>
     <!-- 类型:voice -->
-    <div v-else-if="objData.type === 'voice'">
+    <div v-else-if="props.type === 'voice'">
       <!-- 列表 -->
       <el-table v-loading="loading" :data="list">
         <el-table-column label="编号" align="center" prop="mediaId" />
@@ -64,7 +64,7 @@
       />
     </div>
     <!-- 类型:video -->
-    <div v-else-if="objData.type === 'video'">
+    <div v-else-if="props.type === 'video'">
       <!-- 列表 -->
       <el-table v-loading="loading" :data="list">
         <el-table-column label="编号" align="center" prop="mediaId" />
@@ -106,7 +106,7 @@
       />
     </div>
     <!-- 类型:news -->
-    <div v-else-if="objData.type === 'news'">
+    <div v-else-if="props.type === 'news'">
       <div class="waterfall" v-loading="loading">
         <div class="waterfall-item" v-for="item in list" :key="item.mediaId">
           <div v-if="item.content && item.content.newsItem">
@@ -132,25 +132,25 @@
 </template>
 
 <script lang="ts" setup name="WxMaterialSelect">
-import WxNews from '@/views/mp/components/wx-news/main.vue'
-import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
-import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
+import WxNews from '@/views/mp/components/wx-news'
+import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
+import WxVideoPlayer from '@/views/mp/components/wx-video-play'
+import { NewsType } from './types'
 import * as MpMaterialApi from '@/api/mp/material'
 import * as MpFreePublishApi from '@/api/mp/freePublish'
 import * as MpDraftApi from '@/api/mp/draft'
 import { dateFormatter } from '@/utils/formatTime'
 
-const props = defineProps({
-  objData: {
-    type: Object, // type - 类型;accountId - 公众号账号编号
-    required: true
-  },
-  newsType: {
-    // 图文类型:1、已发布图文;2、草稿箱图文
-    type: String as PropType<string>,
-    default: '1'
+const props = withDefaults(
+  defineProps<{
+    type: string
+    accountId: number
+    newsType?: NewsType
+  }>(),
+  {
+    newsType: NewsType.Published
   }
-})
+)
 
 const emit = defineEmits(['select-material'])
 
@@ -159,15 +159,13 @@ const loading = ref(false)
 // 总条数
 const total = ref(0)
 // 数据列表
-const list = ref([])
+const list = ref<any[]>([])
 // 查询参数
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  accountId: props.objData.accountId
+  accountId: props.accountId
 })
-const objDataRef = reactive(props.objData)
-const newsTypeRef = ref(props.newsType)
 
 const selectMaterialFun = (item) => {
   emit('select-material', item)
@@ -176,10 +174,10 @@ const selectMaterialFun = (item) => {
 const getPage = async () => {
   loading.value = true
   try {
-    if (objDataRef.type === 'news' && newsTypeRef.value === '1') {
+    if (props.type === 'news' && props.newsType === NewsType.Published) {
       // 【图文】+ 【已发布】
       await getFreePublishPageFun()
-    } else if (objDataRef.type === 'news' && newsTypeRef.value === '2') {
+    } else if (props.type === 'news' && props.newsType === NewsType.Draft) {
       // 【图文】+ 【草稿】
       await getDraftPageFun()
     } else {
@@ -194,7 +192,7 @@ const getPage = async () => {
 const getMaterialPageFun = async () => {
   const data = await MpMaterialApi.getMaterialPage({
     ...queryParams,
-    type: objDataRef.type
+    type: props.type
   })
   list.value = data.list
   total.value = data.total
@@ -202,9 +200,9 @@ const getMaterialPageFun = async () => {
 
 const getFreePublishPageFun = async () => {
   const data = await MpFreePublishApi.getFreePublishPage(queryParams)
-  data.list.forEach((item) => {
-    const newsItem = item.content.newsItem
-    newsItem.forEach((article) => {
+  data.list.forEach((item: any) => {
+    const articles = item.content.newsItem
+    articles.forEach((article: any) => {
       article.picUrl = article.thumbUrl
     })
   })
@@ -214,9 +212,9 @@ const getFreePublishPageFun = async () => {
 
 const getDraftPageFun = async () => {
   const data = await MpDraftApi.getDraftPage(queryParams)
-  data.list.forEach((item) => {
-    const newsItem = item.content.newsItem
-    newsItem.forEach((article) => {
+  data.list.forEach((draft: any) => {
+    const articles = draft.content.newsItem
+    articles.forEach((article: any) => {
       article.picUrl = article.thumbUrl
     })
   })
diff --git a/src/views/mp/components/wx-material-select/types.ts b/src/views/mp/components/wx-material-select/types.ts
new file mode 100644
index 00000000..d4add1d5
--- /dev/null
+++ b/src/views/mp/components/wx-material-select/types.ts
@@ -0,0 +1,11 @@
+export enum NewsType {
+  Draft = '2',
+  Published = '1'
+}
+
+export enum MaterialType {
+  Image = 'image',
+  Voice = 'voice',
+  Video = 'video',
+  News = 'news'
+}
diff --git a/src/views/mp/components/wx-msg/index.ts b/src/views/mp/components/wx-msg/index.ts
new file mode 100644
index 00000000..fd9eddd7
--- /dev/null
+++ b/src/views/mp/components/wx-msg/index.ts
@@ -0,0 +1,6 @@
+import WxMsg from './main.vue'
+import { MsgType } from './types'
+
+export { MsgType }
+
+export default WxMsg
diff --git a/src/views/mp/components/wx-msg/main.vue b/src/views/mp/components/wx-msg/main.vue
index c7354628..19763245 100644
--- a/src/views/mp/components/wx-msg/main.vue
+++ b/src/views/mp/components/wx-msg/main.vue
@@ -125,19 +125,19 @@
       </div>
     </div>
     <div class="msg-send" v-loading="sendLoading">
-      <WxReplySelect ref="replySelectRef" :objData="objData" />
+      <WxReplySelect ref="replySelectRef" v-model="reply" />
       <el-button type="success" class="send-but" @click="sendMsg">发送(S)</el-button>
     </div>
   </ContentWrap>
 </template>
 
 <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 WxReplySelect from '@/views/mp/components/wx-reply'
+import WxVideoPlayer from '@/views/mp/components/wx-video-play'
+import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
+import WxNews from '@/views/mp/components/wx-news'
+import WxLocation from '@/views/mp/components/wx-location'
+import WxMusic from '@/views/mp/components/wx-music'
 import { getMessagePage, sendMessage } from '@/api/mp/message'
 import { getUser } from '@/api/mp/user'
 import { formatDate } from '@/utils/formatTime'
@@ -187,14 +187,14 @@ const mp: Mp = reactive({
 
 // ========= 消息发送 =========
 const sendLoading = ref(false) // 发送消息是否加载中
-interface ObjData {
+interface Reply {
   type: MsgType
   accountId: number | null
   articles: any[]
 }
 
 // 微信发送消息
-const objData: ObjData = reactive({
+const reply = ref<Reply>({
   type: MsgType.Text,
   accountId: null,
   articles: []
@@ -209,23 +209,23 @@ onMounted(async () => {
   user.avatar = user.avatar?.length > 0 ? data.avatar : user.avatar
   user.accountId = data.accountId
   queryParams.accountId = data.accountId
-  objData.accountId = data.accountId
+  reply.value.accountId = data.accountId
 
   refreshChange()
 })
 
 // 执行发送
 const sendMsg = async () => {
-  if (!objData) {
+  if (!reply) {
     return
   }
   // 公众号限制:客服消息,公众号只允许发送一条
-  if (objData.type === MsgType.News && objData.articles.length > 1) {
-    objData.articles = [objData.articles[0]]
+  if (reply.value.type === MsgType.News && reply.value.articles.length > 1) {
+    reply.value.articles = [reply.value.articles[0]]
     message.success('图文消息条数限制在 1 条以内,已默认发送第一条')
   }
 
-  const data = await sendMessage(Object.assign({ userId: props.userId }, { ...objData }))
+  const data = await sendMessage({ userId: props.userId, ...reply.value })
   sendLoading.value = false
 
   list.value = [...list.value, ...[data]]
diff --git a/src/views/mp/components/wx-music/index.ts b/src/views/mp/components/wx-music/index.ts
new file mode 100644
index 00000000..c4211261
--- /dev/null
+++ b/src/views/mp/components/wx-music/index.ts
@@ -0,0 +1,3 @@
+import WxMusic from './main.vue'
+
+export default WxMusic
diff --git a/src/views/mp/components/wx-news/index.ts b/src/views/mp/components/wx-news/index.ts
new file mode 100644
index 00000000..e68f4d5d
--- /dev/null
+++ b/src/views/mp/components/wx-news/index.ts
@@ -0,0 +1,3 @@
+import WxNews from './main.vue'
+
+export default WxNews
diff --git a/src/views/mp/components/wx-news/main.vue b/src/views/mp/components/wx-news/main.vue
index 326b674c..779a446f 100644
--- a/src/views/mp/components/wx-news/main.vue
+++ b/src/views/mp/components/wx-news/main.vue
@@ -39,12 +39,14 @@
 </template>
 
 <script lang="ts" name="WxNews" setup>
-const props = defineProps({
-  articles: {
-    type: Array,
-    default: () => null
+const props = withDefaults(
+  defineProps<{
+    articles: any[] | null
+  }>(),
+  {
+    articles: null
   }
-})
+)
 
 defineExpose({
   articles: props.articles
@@ -53,9 +55,9 @@ defineExpose({
 
 <style lang="scss" scoped>
 .news-home {
-  background-color: #ffffff;
   width: 100%;
   margin: auto;
+  background-color: #fff;
 }
 
 .news-main {
@@ -64,29 +66,29 @@ defineExpose({
 }
 
 .news-content {
-  background-color: #acadae;
-  width: 100%;
   position: relative;
+  width: 100%;
+  background-color: #acadae;
 }
 
 .news-content-title {
-  display: inline-block;
-  font-size: 12px;
-  color: #ffffff;
   position: absolute;
-  left: 0;
   bottom: 0;
-  background-color: black;
+  left: 0;
+  display: inline-block;
   width: 98%;
   padding: 1%;
-  opacity: 0.65;
+  font-size: 12px;
+  color: #fff;
   white-space: normal;
+  background-color: black;
+  opacity: 0.65;
   box-sizing: unset !important;
 }
 
 .news-main-item {
-  background-color: #ffffff;
   padding: 5px 0;
+  background-color: #fff;
   border-top: 1px solid #eaeaea;
 }
 
@@ -96,17 +98,17 @@ defineExpose({
 
 .news-content-item-title {
   display: inline-block;
-  font-size: 10px;
   width: 70%;
   margin-left: 1%;
+  font-size: 10px;
   white-space: normal;
 }
 
 .news-content-item-img {
   display: inline-block;
   width: 25%;
-  background-color: #acadae;
   margin-right: 1%;
+  background-color: #acadae;
 }
 
 .material-img {
diff --git a/src/views/mp/components/wx-reply/components/TabImage.vue b/src/views/mp/components/wx-reply/components/TabImage.vue
index eecc24cb..1a82c3aa 100644
--- a/src/views/mp/components/wx-reply/components/TabImage.vue
+++ b/src/views/mp/components/wx-reply/components/TabImage.vue
@@ -1,12 +1,9 @@
 <template>
-  <el-tab-pane name="image">
-    <template #label>
-      <el-row align="middle"><Icon icon="ep:picture" class="mr-5px" /> 图片</el-row>
-    </template>
+  <div>
     <!-- 情况一:已经选择好素材、或者上传好图片 -->
-    <div class="select-item" v-if="objData.url">
-      <img class="material-img" :src="objData.url" />
-      <p class="item-name" v-if="objData.name">{{ objData.name }}</p>
+    <div class="select-item" v-if="reply.url">
+      <img class="material-img" :src="reply.url" />
+      <p class="item-name" v-if="reply.name">{{ reply.name }}</p>
       <el-row class="ope-row" justify="center">
         <el-button type="danger" circle @click="onDelete">
           <Icon icon="ep:delete" />
@@ -27,7 +24,11 @@
           append-to-body
           destroy-on-close
         >
-          <WxMaterialSelect :objData="objData" @select-material="selectMaterial" />
+          <WxMaterialSelect
+            type="image"
+            :account-id="reply.accountId"
+            @select-material="selectMaterial"
+          />
         </el-dialog>
       </el-col>
       <!-- 文件上传 -->
@@ -51,27 +52,27 @@
         </el-upload>
       </el-col>
     </el-row>
-  </el-tab-pane>
+  </div>
 </template>
 
 <script setup lang="ts">
-import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
-import { MaterialType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
+import WxMaterialSelect from '@/views/mp/components/wx-material-select'
+import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
 import type { UploadRawFile } from 'element-plus'
 import { getAccessToken } from '@/utils/auth'
-import { ObjData } from './types'
+import { Reply } from './types'
 const message = useMessage()
 
 const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary'
 const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
 
 const props = defineProps<{
-  modelValue: ObjData
+  modelValue: Reply
 }>()
 const emit = defineEmits<{
-  (e: 'update:modelValue', v: ObjData)
+  (e: 'update:modelValue', v: Reply)
 }>()
-const objData = computed<ObjData>({
+const reply = computed<Reply>({
   get: () => props.modelValue,
   set: (val) => emit('update:modelValue', val)
 })
@@ -79,14 +80,13 @@ const objData = computed<ObjData>({
 const showDialog = ref(false)
 const fileList = ref([])
 const uploadData = reactive({
-  accountId: objData.value.accountId,
+  accountId: reply.value.accountId,
   type: 'image',
   title: '',
   introduction: ''
 })
 
-const beforeImageUpload = (rawFile: UploadRawFile) =>
-  useBeforeUpload(MaterialType.Image, 2)(rawFile)
+const beforeImageUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Image, 2)(rawFile)
 
 const onUploadSuccess = (res: any) => {
   if (res.code !== 0) {
@@ -104,18 +104,18 @@ const onUploadSuccess = (res: any) => {
 }
 
 const onDelete = () => {
-  objData.value.mediaId = null
-  objData.value.url = null
-  objData.value.name = null
+  reply.value.mediaId = null
+  reply.value.url = null
+  reply.value.name = null
 }
 
 const selectMaterial = (item) => {
   showDialog.value = false
 
-  objData.value.type = 'image'
-  objData.value.mediaId = item.mediaId
-  objData.value.url = item.url
-  objData.value.name = item.name
+  // reply.value.type = 'image'
+  reply.value.mediaId = item.mediaId
+  reply.value.url = item.url
+  reply.value.name = item.name
 }
 </script>
 
diff --git a/src/views/mp/components/wx-reply/components/TabMusic.vue b/src/views/mp/components/wx-reply/components/TabMusic.vue
index 2c3b04e5..b19fb592 100644
--- a/src/views/mp/components/wx-reply/components/TabMusic.vue
+++ b/src/views/mp/components/wx-reply/components/TabMusic.vue
@@ -1,14 +1,11 @@
 <template>
-  <el-tab-pane name="music">
-    <template #label>
-      <el-row align="middle"><Icon icon="ep:service" />音乐</el-row>
-    </template>
+  <div>
     <el-row align="middle" justify="center">
       <el-col :span="6">
         <el-row align="middle" justify="center" class="thumb-div">
           <el-col :span="24">
             <el-row align="middle" justify="center">
-              <img style="width: 100px" v-if="objData.thumbMediaUrl" :src="objData.thumbMediaUrl" />
+              <img style="width: 100px" v-if="reply.thumbMediaUrl" :src="reply.thumbMediaUrl" />
               <icon v-else icon="ep:plus" />
             </el-row>
             <el-row align="middle" justify="center" style="margin-top: 2%">
@@ -42,30 +39,31 @@
           destroy-on-close
         >
           <WxMaterialSelect
-            :objData="{ type: 'image', accountId: objData.accountId }"
+            type="image"
+            :account-id="reply.accountId"
             @select-material="selectMaterial"
           />
         </el-dialog>
       </el-col>
       <el-col :span="18">
-        <el-input v-model="objData.title" placeholder="请输入标题" />
+        <el-input v-model="reply.title" placeholder="请输入标题" />
         <div style="margin: 20px 0"></div>
-        <el-input v-model="objData.description" placeholder="请输入描述" />
+        <el-input v-model="reply.description" placeholder="请输入描述" />
       </el-col>
     </el-row>
     <div style="margin: 20px 0"></div>
-    <el-input v-model="objData.musicUrl" placeholder="请输入音乐链接" />
+    <el-input v-model="reply.musicUrl" placeholder="请输入音乐链接" />
     <div style="margin: 20px 0"></div>
-    <el-input v-model="objData.hqMusicUrl" placeholder="请输入高质量音乐链接" />
-  </el-tab-pane>
+    <el-input v-model="reply.hqMusicUrl" placeholder="请输入高质量音乐链接" />
+  </div>
 </template>
 
 <script setup lang="ts">
-import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
+import WxMaterialSelect from '@/views/mp/components/wx-material-select'
 import type { UploadRawFile } from 'element-plus'
-import { MaterialType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
+import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
 import { getAccessToken } from '@/utils/auth'
-import { ObjData } from './types'
+import { Reply } from './types'
 
 const message = useMessage()
 
@@ -73,12 +71,12 @@ const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/u
 const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
 
 const props = defineProps<{
-  modelValue: ObjData
+  modelValue: Reply
 }>()
 const emit = defineEmits<{
-  (e: 'update:modelValue', v: ObjData)
+  (e: 'update:modelValue', v: Reply)
 }>()
-const objData = computed<ObjData>({
+const reply = computed<Reply>({
   get: () => props.modelValue,
   set: (val) => emit('update:modelValue', val)
 })
@@ -86,14 +84,13 @@ const objData = computed<ObjData>({
 const showDialog = ref(false)
 const fileList = ref([])
 const uploadData = reactive({
-  accountId: objData.value.accountId,
+  accountId: reply.value.accountId,
   type: 'thumb', // 音乐类型为thumb
   title: '',
   introduction: ''
 })
 
-const beforeImageUpload = (rawFile: UploadRawFile) =>
-  useBeforeUpload(MaterialType.Image, 2)(rawFile)
+const beforeImageUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Image, 2)(rawFile)
 
 const onUploadSuccess = (res: any) => {
   if (res.code !== 0) {
@@ -113,7 +110,7 @@ const onUploadSuccess = (res: any) => {
 const selectMaterial = (item: any) => {
   showDialog.value = false
 
-  objData.value.thumbMediaId = item.mediaId
-  objData.value.thumbMediaUrl = item.url
+  reply.value.thumbMediaId = item.mediaId
+  reply.value.thumbMediaUrl = item.url
 }
 </script>
diff --git a/src/views/mp/components/wx-reply/components/TabNews.vue b/src/views/mp/components/wx-reply/components/TabNews.vue
index bb9272e7..88a82a53 100644
--- a/src/views/mp/components/wx-reply/components/TabNews.vue
+++ b/src/views/mp/components/wx-reply/components/TabNews.vue
@@ -1,11 +1,8 @@
 <template>
-  <el-tab-pane name="news">
-    <template #label>
-      <el-row align="middle"><Icon icon="ep:reading" /> 图文</el-row>
-    </template>
+  <div>
     <el-row>
-      <div class="select-item" v-if="objData.articles?.length > 0">
-        <WxNews :articles="objData.articles" />
+      <div class="select-item" v-if="reply.articles && reply.articles.length > 0">
+        <WxNews :articles="reply.articles" />
         <el-col class="ope-row">
           <el-button type="danger" circle @click="onDelete">
             <Icon icon="ep:delete" />
@@ -13,7 +10,7 @@
         </el-col>
       </div>
       <!-- 选择素材 -->
-      <el-col :span="24" v-if="!objData.content">
+      <el-col :span="24" v-if="!reply.content">
         <el-row style="text-align: center" align="middle">
           <el-col :span="24">
             <el-button type="success" @click="showDialog = true">
@@ -25,28 +22,29 @@
       </el-col>
       <el-dialog title="选择图文" v-model="showDialog" width="90%" append-to-body destroy-on-close>
         <WxMaterialSelect
-          :objData="objData"
-          @select-material="selectMaterial"
+          type="news"
+          :account-id="reply.accountId"
           :newsType="newsType"
+          @select-material="selectMaterial"
         />
       </el-dialog>
     </el-row>
-  </el-tab-pane>
+  </div>
 </template>
 
 <script setup lang="ts">
-import WxNews from '@/views/mp/components/wx-news/main.vue'
-import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
-import { ObjData, NewsType } from './types'
+import WxNews from '@/views/mp/components/wx-news'
+import WxMaterialSelect from '@/views/mp/components/wx-material-select'
+import { Reply, NewsType } from './types'
 
 const props = defineProps<{
-  modelValue: ObjData
+  modelValue: Reply
   newsType: NewsType
 }>()
 const emit = defineEmits<{
-  (e: 'update:modelValue', v: ObjData)
+  (e: 'update:modelValue', v: Reply)
 }>()
-const objData = computed<ObjData>({
+const reply = computed<Reply>({
   get: () => props.modelValue,
   set: (val) => emit('update:modelValue', val)
 })
@@ -55,11 +53,11 @@ const showDialog = ref(false)
 
 const selectMaterial = (item: any) => {
   showDialog.value = false
-  objData.value.articles = item.content.newsItem
+  reply.value.articles = item.content.newsItem
 }
 
 const onDelete = () => {
-  objData.value.articles = []
+  reply.value.articles = []
 }
 </script>
 
diff --git a/src/views/mp/components/wx-reply/components/TabText.vue b/src/views/mp/components/wx-reply/components/TabText.vue
index bd7b0187..aac698e8 100644
--- a/src/views/mp/components/wx-reply/components/TabText.vue
+++ b/src/views/mp/components/wx-reply/components/TabText.vue
@@ -1,15 +1,10 @@
 <template>
-  <el-tab-pane name="text">
-    <template #label>
-      <el-row align="middle"><Icon icon="ep:document" /> 文本</el-row>
-    </template>
-    <el-input type="textarea" :rows="5" placeholder="请输入内容" v-model="content" />
-  </el-tab-pane>
+  <el-input type="textarea" :rows="5" placeholder="请输入内容" v-model="content" />
 </template>
 
 <script setup lang="ts">
 const props = defineProps<{
-  modelValue: string | null
+  modelValue?: string | null
 }>()
 
 const emit = defineEmits<{
@@ -17,7 +12,7 @@ const emit = defineEmits<{
   (e: 'input', v: string | null)
 }>()
 
-const content = computed<string | null>({
+const content = computed<string | null | undefined>({
   get: () => props.modelValue,
   set: (val: string | null) => {
     emit('update:modelValue', val)
diff --git a/src/views/mp/components/wx-reply/components/TabVideo.vue b/src/views/mp/components/wx-reply/components/TabVideo.vue
index c924bc2a..52553521 100644
--- a/src/views/mp/components/wx-reply/components/TabVideo.vue
+++ b/src/views/mp/components/wx-reply/components/TabVideo.vue
@@ -1,17 +1,10 @@
 <template>
-  <el-tab-pane name="video">
-    <template #label>
-      <el-row align="middle"><Icon icon="ep:share" /> 视频</el-row>
-    </template>
+  <div>
     <el-row>
-      <el-input v-model="objData.title" class="input-margin-bottom" placeholder="请输入标题" />
-      <el-input
-        class="input-margin-bottom"
-        v-model="objData.description"
-        placeholder="请输入描述"
-      />
+      <el-input v-model="reply.title" class="input-margin-bottom" placeholder="请输入标题" />
+      <el-input class="input-margin-bottom" v-model="reply.description" placeholder="请输入描述" />
       <el-row class="ope-row" justify="center">
-        <WxVideoPlayer v-if="objData.url" :url="objData.url" />
+        <WxVideoPlayer v-if="reply.url" :url="reply.url" />
       </el-row>
       <el-col>
         <el-row style="text-align: center" align="middle">
@@ -27,7 +20,11 @@
               append-to-body
               destroy-on-close
             >
-              <WxMaterialSelect :objData="objData" @select-material="selectMaterial" />
+              <WxMaterialSelect
+                type="video"
+                :account-id="reply.accountId"
+                @select-material="selectMaterial"
+              />
             </el-dialog>
           </el-col>
           <!-- 文件上传 -->
@@ -48,16 +45,16 @@
         </el-row>
       </el-col>
     </el-row>
-  </el-tab-pane>
+  </div>
 </template>
 
 <script setup lang="ts">
-import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
-import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
+import WxVideoPlayer from '@/views/mp/components/wx-video-play'
+import WxMaterialSelect from '@/views/mp/components/wx-material-select'
 import type { UploadRawFile } from 'element-plus'
-import { MaterialType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
+import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
 import { getAccessToken } from '@/utils/auth'
-import { ObjData } from './types'
+import { Reply } from './types'
 
 const message = useMessage()
 
@@ -65,12 +62,12 @@ const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/u
 const HEADERS = { Authorization: 'Bearer ' + getAccessToken() }
 
 const props = defineProps<{
-  modelValue: ObjData
+  modelValue: Reply
 }>()
 const emit = defineEmits<{
-  (e: 'update:modelValue', v: ObjData)
+  (e: 'update:modelValue', v: Reply)
 }>()
-const objData = computed<ObjData>({
+const reply = computed<Reply>({
   get: () => props.modelValue,
   set: (val) => emit('update:modelValue', val)
 })
@@ -78,14 +75,13 @@ const objData = computed<ObjData>({
 const showDialog = ref(false)
 const fileList = ref([])
 const uploadData = reactive({
-  accountId: objData.value.accountId,
+  accountId: reply.value.accountId,
   type: 'video',
   title: '',
   introduction: ''
 })
 
-const beforeVideoUpload = (rawFile: UploadRawFile) =>
-  useBeforeUpload(MaterialType.Video, 10)(rawFile)
+const beforeVideoUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Video, 10)(rawFile)
 
 const onUploadSuccess = (res: any) => {
   if (res.code !== 0) {
@@ -105,16 +101,16 @@ const onUploadSuccess = (res: any) => {
 const selectMaterial = (item: any) => {
   showDialog.value = false
 
-  objData.value.mediaId = item.mediaId
-  objData.value.url = item.url
-  objData.value.name = item.name
+  reply.value.mediaId = item.mediaId
+  reply.value.url = item.url
+  reply.value.name = item.name
 
   // title、introduction:从 item 到 tempObjItem,因为素材里有 title、introduction
   if (item.title) {
-    objData.value.title = item.title || ''
+    reply.value.title = item.title || ''
   }
   if (item.introduction) {
-    objData.value.description = item.introduction || ''
+    reply.value.description = item.introduction || ''
   }
 }
 </script>
diff --git a/src/views/mp/components/wx-reply/components/TabVoice.vue b/src/views/mp/components/wx-reply/components/TabVoice.vue
index 6d40a052..c4868cf8 100644
--- a/src/views/mp/components/wx-reply/components/TabVoice.vue
+++ b/src/views/mp/components/wx-reply/components/TabVoice.vue
@@ -1,12 +1,9 @@
 <template>
-  <el-tab-pane name="voice">
-    <template #label>
-      <el-row align="middle"><Icon icon="ep:phone" /> 语音</el-row>
-    </template>
-    <div class="select-item2" v-if="objData.url">
-      <p class="item-name">{{ objData.name }}</p>
+  <div>
+    <div class="select-item2" v-if="reply.url">
+      <p class="item-name">{{ reply.name }}</p>
       <el-row class="ope-row" justify="center">
-        <WxVoicePlayer :url="objData.url" />
+        <WxVoicePlayer :url="reply.url" />
       </el-row>
       <el-row class="ope-row" justify="center">
         <el-button type="danger" circle @click="onDelete"><Icon icon="ep:delete" /></el-button>
@@ -25,7 +22,11 @@
           append-to-body
           destroy-on-close
         >
-          <WxMaterialSelect :objData="objData" @select-material="selectMaterial" />
+          <WxMaterialSelect
+            type="voice"
+            :account-id="reply.accountId"
+            @select-material="selectMaterial"
+          />
         </el-dialog>
       </el-col>
       <!-- 文件上传 -->
@@ -49,27 +50,27 @@
         </el-upload>
       </el-col>
     </el-row>
-  </el-tab-pane>
+  </div>
 </template>
 <script setup lang="ts">
-import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
-import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
-import { MaterialType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
+import WxMaterialSelect from '@/views/mp/components/wx-material-select'
+import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
+import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
 import type { UploadRawFile } from 'element-plus'
 import { getAccessToken } from '@/utils/auth'
-import { ObjData } from './types'
+import { Reply } from './types'
 const message = useMessage()
 
 const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary'
 const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
 
 const props = defineProps<{
-  modelValue: ObjData
+  modelValue: Reply
 }>()
 const emit = defineEmits<{
-  (e: 'update:modelValue', v: ObjData)
+  (e: 'update:modelValue', v: Reply)
 }>()
-const objData = computed<ObjData>({
+const reply = computed<Reply>({
   get: () => props.modelValue,
   set: (val) => emit('update:modelValue', val)
 })
@@ -77,14 +78,13 @@ const objData = computed<ObjData>({
 const showDialog = ref(false)
 const fileList = ref([])
 const uploadData = reactive({
-  accountId: objData.value.accountId,
+  accountId: reply.value.accountId,
   type: 'voice',
   title: '',
   introduction: ''
 })
 
-const beforeVoiceUpload = (rawFile: UploadRawFile) =>
-  useBeforeUpload(MaterialType.Voice, 10)(rawFile)
+const beforeVoiceUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Voice, 10)(rawFile)
 
 const onUploadSuccess = (res: any) => {
   if (res.code !== 0) {
@@ -102,18 +102,18 @@ const onUploadSuccess = (res: any) => {
 }
 
 const onDelete = () => {
-  objData.value.mediaId = null
-  objData.value.url = null
-  objData.value.name = null
+  reply.value.mediaId = null
+  reply.value.url = null
+  reply.value.name = null
 }
 
-const selectMaterial = (item: ObjData) => {
+const selectMaterial = (item: Reply) => {
   showDialog.value = false
 
-  objData.value.type = 'voice'
-  objData.value.mediaId = item.mediaId
-  objData.value.url = item.url
-  objData.value.name = item.name
+  // reply.value.type = ReplyType.Voice
+  reply.value.mediaId = item.mediaId
+  reply.value.url = item.url
+  reply.value.name = item.name
 }
 </script>
 
diff --git a/src/views/mp/components/wx-reply/components/types.ts b/src/views/mp/components/wx-reply/components/types.ts
index d5273334..3e07d6e5 100644
--- a/src/views/mp/components/wx-reply/components/types.ts
+++ b/src/views/mp/components/wx-reply/components/types.ts
@@ -1,25 +1,54 @@
-type ReplyType = '' | 'news' | 'image' | 'voice' | 'video' | 'music' | 'text'
+enum ReplyType {
+  News = 'news',
+  Image = 'image',
+  Voice = 'voice',
+  Video = 'video',
+  Music = 'music',
+  Text = 'text'
+}
 
-interface ObjData {
+interface _Reply {
   accountId: number
   type: ReplyType
-  name: string | null
-  content: string | null
-  mediaId: string | null
-  url: string | null
-  title: string | null
-  description: string | null
-  thumbMediaId: string | null
-  thumbMediaUrl: string | null
-  musicUrl: string | null
-  hqMusicUrl: string | null
-  introduction: string | null
-  articles: any[]
+  name?: string | null
+  content?: string | null
+  mediaId?: string | null
+  url?: string | null
+  title?: string | null
+  description?: string | null
+  thumbMediaId?: string | null
+  thumbMediaUrl?: string | null
+  musicUrl?: string | null
+  hqMusicUrl?: string | null
+  introduction?: string | null
+  articles?: any[]
 }
 
+type Reply = _Reply //Partial<_Reply>
+
 enum NewsType {
   Published = '1',
   Draft = '2'
 }
 
-export { ObjData, NewsType }
+/** 利用旧的reply[accountId, type]初始化新的Reply */
+const createEmptyReply = (old: Reply | Ref<Reply>): Reply => {
+  return {
+    accountId: unref(old).accountId,
+    type: unref(old).type,
+    name: null,
+    content: null,
+    mediaId: null,
+    url: null,
+    title: null,
+    description: null,
+    thumbMediaId: null,
+    thumbMediaUrl: null,
+    musicUrl: null,
+    hqMusicUrl: null,
+    introduction: null,
+    articles: []
+  }
+}
+
+export { Reply, NewsType, ReplyType, createEmptyReply }
diff --git a/src/views/mp/components/wx-reply/index.ts b/src/views/mp/components/wx-reply/index.ts
new file mode 100644
index 00000000..d1da217e
--- /dev/null
+++ b/src/views/mp/components/wx-reply/index.ts
@@ -0,0 +1,7 @@
+import { Reply, NewsType, ReplyType, createEmptyReply } from './components/types'
+
+import WxReplySelect from './main.vue'
+
+export type { Reply }
+export { createEmptyReply, NewsType, ReplyType }
+export default WxReplySelect
diff --git a/src/views/mp/components/wx-reply/main.vue b/src/views/mp/components/wx-reply/main.vue
index b00e4345..32a31222 100644
--- a/src/views/mp/components/wx-reply/main.vue
+++ b/src/views/mp/components/wx-reply/main.vue
@@ -8,24 +8,59 @@
   ④ 支持发送【视频】消息时,支持新建视频
 -->
 <template>
-  <el-tabs type="border-card" v-model="objData.type" @tab-click="onTabClick">
+  <el-tabs type="border-card" v-model="currentTab">
     <!-- 类型 1:文本 -->
-    <TabText v-model="objData.content" />
+    <el-tab-pane :name="ReplyType.Text">
+      <template #label>
+        <el-row align="middle"><Icon icon="ep:document" /> 文本</el-row>
+      </template>
+      <TabText v-model="reply.content" />
+    </el-tab-pane>
+
     <!-- 类型 2:图片 -->
-    <TabImage v-model="objData" />
+    <el-tab-pane :name="ReplyType.Image">
+      <template #label>
+        <el-row align="middle"><Icon icon="ep:picture" class="mr-5px" /> 图片</el-row>
+      </template>
+      <TabImage v-model="reply" />
+    </el-tab-pane>
+
     <!-- 类型 3:语音 -->
-    <TabVoice v-model="objData" />
+    <el-tab-pane :name="ReplyType.Voice">
+      <template #label>
+        <el-row align="middle"><Icon icon="ep:phone" /> 语音</el-row>
+      </template>
+      <TabVoice v-model="reply" />
+    </el-tab-pane>
+
     <!-- 类型 4:视频 -->
-    <TabVideo v-model="objData" />
+    <el-tab-pane :name="ReplyType.Video">
+      <template #label>
+        <el-row align="middle"><Icon icon="ep:share" /> 视频</el-row>
+      </template>
+      <TabVideo v-model="reply" />
+    </el-tab-pane>
+
     <!-- 类型 5:图文 -->
-    <TabNews v-model="objData" :news-type="newsType" />
+    <el-tab-pane :name="ReplyType.News">
+      <template #label>
+        <el-row align="middle"><Icon icon="ep:reading" /> 图文</el-row>
+      </template>
+      <TabNews v-model="reply" :news-type="newsType" />
+    </el-tab-pane>
+
     <!-- 类型 6:音乐 -->
-    <TabMusic v-model="objData" />
+    <el-tab-pane :name="ReplyType.Music">
+      <template #label>
+        <el-row align="middle"><Icon icon="ep:service" />音乐</el-row>
+      </template>
+      <TabMusic v-model="reply" />
+    </el-tab-pane>
   </el-tabs>
 </template>
 
 <script setup lang="ts" name="WxReplySelect">
-import { ObjData, NewsType } from './components/types'
+import { Reply, NewsType, ReplyType, createEmptyReply } from './components/types'
 import TabText from './components/TabText.vue'
 import TabImage from './components/TabImage.vue'
 import TabVoice from './components/TabVoice.vue'
@@ -34,30 +69,54 @@ import TabNews from './components/TabNews.vue'
 import TabMusic from './components/TabMusic.vue'
 
 interface Props {
-  objData: ObjData
+  modelValue: Reply
   newsType?: NewsType
 }
 const props = withDefaults(defineProps<Props>(), {
   newsType: () => NewsType.Published
 })
+const emit = defineEmits<{
+  (e: 'update:modelValue', v: Reply)
+}>()
 
-const objData = reactive(props.objData)
-// TODO @Dhb52:Tab 切换的时候,应该表单还保留着;清除只有两个时机:1)发送成功后;2)关闭窗口后;我捉摸,是不是每个 TabXXX 组件,是个独立的 Form,然后有自己的对象,不粘在 objData 一起。这样最终就是 MusicMessageForm、ImageMessageForm
-// const tempObj = new Map().set(objData.type, Object.assign({}, objData))
+const reply = computed<Reply>({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+})
+// 作为多个标签保存各自Reply的缓存
+const objCache = new Map<ReplyType, Reply>()
+// 采用独立的ref来保存当前tab,避免在watch标签变化,对reply进行赋值会产生了循环调用
+const currentTab = ref<ReplyType>(props.modelValue.type || ReplyType.Text)
 
-/** 切换消息类型的 tab */
-const onTabClick = () => {
-  clear()
-}
+watch(
+  currentTab,
+  (newTab, oldTab) => {
+    // 第一次进入:oldTab 为 undefined
+    // 判断 newTab 是因为 Reply 为 Partial
+    if (oldTab === undefined || newTab === undefined) {
+      return
+    }
 
-/** 清除除了`type`的字段 */
+    objCache.set(oldTab, unref(reply))
+
+    // 从缓存里面取出新tab内容,有则覆盖Reply,没有则创建空Reply
+    const temp = objCache.get(newTab)
+    if (temp) {
+      reply.value = temp
+    } else {
+      let newData = createEmptyReply(reply)
+      newData.type = newTab
+      reply.value = newData
+    }
+  },
+  {
+    immediate: true
+  }
+)
+
+/** 清除除了`type`, `accountId`的字段 */
 const clear = () => {
-  objData.content = ''
-  objData.mediaId = ''
-  objData.url = ''
-  objData.title = ''
-  objData.description = ''
-  objData.articles = []
+  reply.value = createEmptyReply(reply)
 }
 
 defineExpose({
diff --git a/src/views/mp/components/wx-video-play/index.ts b/src/views/mp/components/wx-video-play/index.ts
new file mode 100644
index 00000000..91e00efa
--- /dev/null
+++ b/src/views/mp/components/wx-video-play/index.ts
@@ -0,0 +1,3 @@
+import WxVideoPlayer from './main.vue'
+
+export default WxVideoPlayer
diff --git a/src/views/mp/components/wx-voice-play/index.ts b/src/views/mp/components/wx-voice-play/index.ts
new file mode 100644
index 00000000..9eb78e02
--- /dev/null
+++ b/src/views/mp/components/wx-voice-play/index.ts
@@ -0,0 +1,3 @@
+import WxVoicePlayer from './main.vue'
+
+export default WxVoicePlayer
diff --git a/src/views/mp/components/wx-voice-play/main.vue b/src/views/mp/components/wx-voice-play/main.vue
index 3ce970c1..169593d8 100644
--- a/src/views/mp/components/wx-voice-play/main.vue
+++ b/src/views/mp/components/wx-voice-play/main.vue
@@ -7,7 +7,7 @@
     1)joolun 的做法:使用 mediaId 从微信公众号,下载对应的 mp4 素材,从而播放内容;
       存在的问题:mediaId 有效期是 3 天,超过时间后无法播放
     2)重构后的做法:后端接收到微信公众号的视频消息后,将视频消息的 media_id 的文件内容保存到文件服务器中,这样前端可以直接使用 URL 播放。
-  ② 代码优化:将 props 中的 objData 调成为 data 中对应的属性,并补充相关注释
+  ② 代码优化:将 props 中的 reply 调成为 data 中对应的属性,并补充相关注释
 -->
 <template>
   <div class="wx-voice-div" @click="playVoice">
@@ -89,8 +89,8 @@ const amrStop = () => {
   padding: 5px;
   background-color: #eaeaea;
   border-radius: 10px;
-  width: 40px;
-  height: 40px;
+  width: 120px;
+  height: 50px;
 
   display: flex;
   justify-content: center;
diff --git a/src/views/mp/draft/components/CoverSelect.vue b/src/views/mp/draft/components/CoverSelect.vue
index ae2b6591..944b7d96 100644
--- a/src/views/mp/draft/components/CoverSelect.vue
+++ b/src/views/mp/draft/components/CoverSelect.vue
@@ -27,9 +27,7 @@
           :on-success="onUploadSuccess"
         >
           <template #trigger>
-            <el-button size="small" type="primary" :loading="isUploading" disabled="isUploading">
-              {{ isUploading ? '正在上传' : '本地上传' }}
-            </el-button>
+            <el-button size="small" type="primary">本地上传</el-button>
           </template>
           <el-button
             size="small"
@@ -52,7 +50,8 @@
         destroy-on-close
       >
         <WxMaterialSelect
-          :objData="{ type: 'image', accountId: accountId }"
+          type="image"
+          :account-id="accountId"
           @select-material="onMaterialSelected"
         />
       </el-dialog>
@@ -61,13 +60,13 @@
 </template>
 
 <script setup lang="ts">
-import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
+import WxMaterialSelect from '@/views/mp/components/wx-material-select'
 import { getAccessToken } from '@/utils/auth'
 import type { UploadFiles, UploadProps, UploadRawFile } from 'element-plus'
+import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
 import { NewsItem } from './types'
 const message = useMessage()
 
-// const UPLOAD_URL = 'http://localhost:8000/upload/' // 上传永久素材的地址
 const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传永久素材的地址
 const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
 
@@ -93,14 +92,13 @@ const showImageDialog = ref(false)
 
 const fileList = ref<UploadFiles>([])
 interface UploadData {
-  type: 'image' | 'video' | 'audio'
-  accountId?: number
+  type: UploadType
+  accountId: number | undefined
 }
 const uploadData: UploadData = reactive({
-  type: 'image',
+  type: UploadType.Image,
   accountId: accountId
 })
-const isUploading = ref(false)
 
 /** 素材选择完成事件*/
 const onMaterialSelected = (item: any) => {
@@ -109,22 +107,8 @@ const onMaterialSelected = (item: any) => {
   newsItem.value.thumbUrl = item.url
 }
 
-const onBeforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
-  const isType = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'].includes(
-    rawFile.type
-  )
-  if (!isType) {
-    message.error('上传图片格式不对!')
-    return false
-  }
-
-  if (rawFile.size / 1024 / 1024 > 2) {
-    message.error('上传图片大小不能超过 2M!')
-    return false
-  }
-  // 校验通过
-  return true
-}
+const onBeforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
+  useBeforeUpload(UploadType.Image, 2)(rawFile)
 
 const onUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
   if (res.code !== 0) {
diff --git a/src/views/mp/draft/components/DraftTable.vue b/src/views/mp/draft/components/DraftTable.vue
index 63cee31f..62a18528 100644
--- a/src/views/mp/draft/components/DraftTable.vue
+++ b/src/views/mp/draft/components/DraftTable.vue
@@ -36,7 +36,7 @@
 </template>
 
 <script setup lang="ts">
-import WxNews from '@/views/mp/components/wx-news/main.vue'
+import WxNews from '@/views/mp/components/wx-news'
 
 import { Article } from './types'
 
diff --git a/src/views/mp/draft/index.vue b/src/views/mp/draft/index.vue
index 7de992cd..d8e771a0 100644
--- a/src/views/mp/draft/index.vue
+++ b/src/views/mp/draft/index.vue
@@ -46,7 +46,6 @@
   </ContentWrap>
 
   <!-- 添加或修改草稿对话框 -->
-  <!-- TODO @Dhb52:是不是整个做成一个组件 -->
   <el-dialog
     :title="isCreating ? '新建图文' : '修改图文'"
     width="80%"
@@ -63,7 +62,7 @@
 </template>
 
 <script setup lang="ts" name="MpDraft">
-import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
 import * as MpDraftApi from '@/api/mp/draft'
 import * as MpFreePublishApi from '@/api/mp/freePublish'
 import {
@@ -77,7 +76,7 @@ import {
 
 const message = useMessage() // 消息
 
-const accountId = ref(0)
+const accountId = ref<number>(0)
 provide('accountId', accountId)
 
 const loading = ref(true) // 列表的加载中
@@ -91,7 +90,7 @@ interface QueryParams {
 const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  accountId: accountId.value
+  accountId: 0
 })
 
 interface UploadData {
@@ -100,7 +99,7 @@ interface UploadData {
 }
 const uploadData: UploadData = reactive({
   type: 'image',
-  accountId: accountId.value
+  accountId: 0
 })
 
 // ========== 草稿新建 or 修改 ==========
diff --git a/src/views/mp/freePublish/index.vue b/src/views/mp/freePublish/index.vue
index 04edb705..2fda5100 100644
--- a/src/views/mp/freePublish/index.vue
+++ b/src/views/mp/freePublish/index.vue
@@ -50,8 +50,8 @@
 
 <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 WxNews from '@/views/mp/components/wx-news'
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
@@ -62,16 +62,16 @@ const list = ref<any[]>([]) // 列表的数据
 interface QueryParams {
   pageNo: number
   pageSize: number
-  accountId?: number
+  accountId: number
 }
 const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  accountId: undefined
+  accountId: 0
 })
 
 /** 侦听公众号变化 **/
-const onAccountChanged = (id: number | undefined) => {
+const onAccountChanged = (id: number) => {
   queryParams.accountId = id
   getList()
 }
diff --git a/src/views/mp/hooks/useUpload.ts b/src/views/mp/hooks/useUpload.ts
index b2d32d71..b0e70531 100644
--- a/src/views/mp/hooks/useUpload.ts
+++ b/src/views/mp/hooks/useUpload.ts
@@ -2,29 +2,29 @@ import type { UploadRawFile } from 'element-plus'
 
 const message = useMessage() // 消息
 
-enum MaterialType {
+enum UploadType {
   Image = 'image',
   Voice = 'voice',
   Video = 'video'
 }
 
-const useBeforeUpload = (type: MaterialType, maxSizeMB: number) => {
+const useBeforeUpload = (type: UploadType, maxSizeMB: number) => {
   const fn = (rawFile: UploadRawFile): boolean => {
     let allowTypes: string[] = []
     let name = ''
 
     switch (type) {
-      case MaterialType.Image:
+      case UploadType.Image:
         allowTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg']
         maxSizeMB = 2
         name = '图片'
         break
-      case MaterialType.Voice:
+      case UploadType.Voice:
         allowTypes = ['audio/mp3', 'audio/mpeg', 'audio/wma', 'audio/wav', 'audio/amr']
         maxSizeMB = 2
         name = '语音'
         break
-      case MaterialType.Video:
+      case UploadType.Video:
         allowTypes = ['video/mp4']
         maxSizeMB = 10
         name = '视频'
@@ -47,4 +47,4 @@ const useBeforeUpload = (type: MaterialType, maxSizeMB: number) => {
   return fn
 }
 
-export { MaterialType, useBeforeUpload }
+export { UploadType, useBeforeUpload }
diff --git a/src/views/mp/material/components/UploadFile.vue b/src/views/mp/material/components/UploadFile.vue
index be7e323b..f58084bb 100644
--- a/src/views/mp/material/components/UploadFile.vue
+++ b/src/views/mp/material/components/UploadFile.vue
@@ -6,14 +6,11 @@
     :limit="1"
     :file-list="fileList"
     :data="uploadData"
-    :on-progress="(isUploading = true)"
     :on-error="onUploadError"
     :before-upload="onBeforeUpload"
     :on-success="onUploadSuccess"
   >
-    <el-button type="primary" plain :loading="isUploading" :disabled="isUploading">
-      {{ isUploading ? '正在上传' : '点击上传' }}
-    </el-button>
+    <el-button type="primary" plain> 点击上传 </el-button>
     <template #tip>
       <span class="el-upload__tip" style="margin-left: 5px">
         <slot></slot>
@@ -27,14 +24,14 @@ import {
   HEADERS,
   UPLOAD_URL,
   UploadData,
-  MaterialType,
+  UploadType,
   beforeImageUpload,
   beforeVoiceUpload
 } from './upload'
 
 const message = useMessage()
 
-const props = defineProps<{ type: MaterialType }>()
+const props = defineProps<{ type: UploadType }>()
 
 const fileList = ref<UploadUserFile[]>([])
 const emit = defineEmits<{
@@ -42,14 +39,13 @@ const emit = defineEmits<{
 }>()
 
 const uploadData: UploadData = reactive({
-  type: MaterialType.Image,
+  type: UploadType.Image,
   title: '',
   introduction: ''
 })
-const isUploading = ref(false)
 
 /** 上传前检查 */
-const onBeforeUpload = props.type === MaterialType.Image ? beforeImageUpload : beforeVoiceUpload
+const onBeforeUpload = props.type === UploadType.Image ? beforeImageUpload : beforeVoiceUpload
 
 /** 上传成功处理 */
 const onUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
@@ -64,7 +60,6 @@ const onUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
   uploadData.introduction = ''
 
   message.notifySuccess('上传成功')
-  isUploading.value = false
   emit('uploaded')
 }
 
diff --git a/src/views/mp/material/components/UploadVideo.vue b/src/views/mp/material/components/UploadVideo.vue
index 8e374767..9d2fd861 100644
--- a/src/views/mp/material/components/UploadVideo.vue
+++ b/src/views/mp/material/components/UploadVideo.vue
@@ -1,5 +1,5 @@
 <template>
-  <el-dialog title="新建视频" v-model="showDialog" width="600px" destroy-on-close>
+  <el-dialog title="新建视频" v-model="showDialog" width="600px">
     <el-upload
       :action="UPLOAD_URL"
       :headers="HEADERS"
@@ -8,7 +8,6 @@
       :file-list="fileList"
       :data="uploadData"
       :before-upload="beforeVideoUpload"
-      :on-progress="(isUploading = true)"
       :on-error="onUploadError"
       :on-success="onUploadSuccess"
       ref="uploadVideoRef"
@@ -18,12 +17,14 @@
       <template #trigger>
         <el-button type="primary" plain>选择视频</el-button>
       </template>
-      <span class="el-upload__tip" style="margin-left: 10px"
-        >格式支持 MP4,文件大小不超过 10MB</span
-      >
+      <template #tip>
+        <span class="el-upload__tip" style="margin-left: 10px"
+          >格式支持 MP4,文件大小不超过 10MB</span
+        >
+      </template>
     </el-upload>
     <el-divider />
-    <el-form :model="uploadData" :rules="uploadRules" ref="uploadFormRef" v-loading="isUploading">
+    <el-form :model="uploadData" :rules="uploadRules" ref="uploadFormRef">
       <el-form-item label="标题" prop="title">
         <el-input
           v-model="uploadData.title"
@@ -41,9 +42,7 @@
     </el-form>
     <template #footer>
       <el-button @click="showDialog = false">取 消</el-button>
-      <el-button type="primary" @click="submitVideo" :loading="isUploading" :disabled="isUploading"
-        >提 交</el-button
-      >
+      <el-button type="primary" @click="submitVideo">提 交</el-button>
     </template>
   </el-dialog>
 </template>
@@ -56,7 +55,7 @@ import type {
   UploadProps,
   UploadUserFile
 } from 'element-plus'
-import { HEADERS, UploadData, UPLOAD_URL, beforeVideoUpload, MaterialType } from './upload'
+import { HEADERS, UploadData, UPLOAD_URL, UploadType, beforeVideoUpload } from './upload'
 
 const message = useMessage()
 
@@ -85,18 +84,16 @@ const showDialog = computed<boolean>({
   }
 })
 
-const isUploading = ref(false)
-
 const fileList = ref<UploadUserFile[]>([])
 
 const uploadData: UploadData = reactive({
-  type: MaterialType.Video,
+  type: UploadType.Video,
   title: '',
   introduction: ''
 })
 
-const uploadFormRef = ref<FormInstance>()
-const uploadVideoRef = ref<UploadInstance>()
+const uploadFormRef = ref<FormInstance | null>(null)
+const uploadVideoRef = ref<UploadInstance | null>(null)
 
 const submitVideo = () => {
   uploadFormRef.value?.validate((valid) => {
@@ -109,7 +106,6 @@ const submitVideo = () => {
 
 /** 上传成功处理 */
 const onUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
-  isUploading.value = false
   if (res.code !== 0) {
     message.error('上传出错:' + res.msg)
     return false
diff --git a/src/views/mp/material/components/VideoTable.vue b/src/views/mp/material/components/VideoTable.vue
index 81472959..b1e14dd5 100644
--- a/src/views/mp/material/components/VideoTable.vue
+++ b/src/views/mp/material/components/VideoTable.vue
@@ -39,7 +39,7 @@
 </template>
 
 <script setup lang="ts">
-import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
+import WxVideoPlayer from '@/views/mp/components/wx-video-play'
 import { dateFormatter } from '@/utils/formatTime'
 
 const props = defineProps<{
diff --git a/src/views/mp/material/components/VoiceTable.vue b/src/views/mp/material/components/VoiceTable.vue
index 6f37e1a0..4ae5174b 100644
--- a/src/views/mp/material/components/VoiceTable.vue
+++ b/src/views/mp/material/components/VoiceTable.vue
@@ -37,7 +37,7 @@
 </template>
 
 <script setup lang="ts">
-import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
+import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
 import { dateFormatter } from '@/utils/formatTime'
 
 const props = defineProps<{
diff --git a/src/views/mp/material/components/upload.ts b/src/views/mp/material/components/upload.ts
index dc52a9eb..7158ab12 100644
--- a/src/views/mp/material/components/upload.ts
+++ b/src/views/mp/material/components/upload.ts
@@ -1,29 +1,29 @@
 import type { UploadProps, UploadRawFile } from 'element-plus'
 import { getAccessToken } from '@/utils/auth'
-import { MaterialType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
+import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
 
 const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 请求头
 const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传地址
 
 interface UploadData {
-  type: MaterialType
+  type: UploadType
   title: string
   introduction: string
 }
 
 const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
-  useBeforeUpload(MaterialType.Image, 2)(rawFile)
+  useBeforeUpload(UploadType.Image, 2)(rawFile)
 
 const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
-  useBeforeUpload(MaterialType.Voice, 2)(rawFile)
+  useBeforeUpload(UploadType.Voice, 2)(rawFile)
 
 const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
-  useBeforeUpload(MaterialType.Video, 10)(rawFile)
+  useBeforeUpload(UploadType.Video, 10)(rawFile)
 
 export {
   HEADERS,
   UPLOAD_URL,
-  MaterialType,
+  UploadType,
   UploadData,
   beforeImageUpload,
   beforeVoiceUpload,
diff --git a/src/views/mp/material/index.vue b/src/views/mp/material/index.vue
index 4d8d3707..0e2a87d6 100644
--- a/src/views/mp/material/index.vue
+++ b/src/views/mp/material/index.vue
@@ -12,13 +12,13 @@
   <ContentWrap>
     <el-tabs v-model="type" @tab-change="onTabChange">
       <!-- tab 1:图片  -->
-      <el-tab-pane :name="MaterialType.Image">
+      <el-tab-pane :name="UploadType.Image">
         <template #label>
-          <span> <Icon icon="ep:picture" />图片 </span>
+          <el-row align="middle"> <Icon icon="ep:picture" />图片 </el-row>
         </template>
         <UploadFile
           v-hasPermi="['mp:material:upload-permanent']"
-          :type="MaterialType.Image"
+          :type="UploadType.Image"
           @uploaded="getList"
         >
           支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M
@@ -35,13 +35,13 @@
       </el-tab-pane>
 
       <!-- tab 2:语音  -->
-      <el-tab-pane :name="MaterialType.Voice">
+      <el-tab-pane :name="UploadType.Voice">
         <template #label>
-          <span> <Icon icon="ep:microphone" />语音 </span>
+          <el-row align="middle"> <Icon icon="ep:microphone" />语音 </el-row>
         </template>
         <UploadFile
           v-hasPermi="['mp:material:upload-permanent']"
-          :type="MaterialType.Voice"
+          :type="UploadType.Voice"
           @uploaded="getList"
         >
           格式支持 mp3/wma/wav/amr,文件大小不超过 2M,播放长度不超过 60s
@@ -58,9 +58,9 @@
       </el-tab-pane>
 
       <!-- tab 3:视频 -->
-      <el-tab-pane :name="MaterialType.Video">
+      <el-tab-pane :name="UploadType.Video">
         <template #label>
-          <span> <Icon icon="ep:video-play" /> 视频 </span>
+          <el-row align="middle"> <Icon icon="ep:video-play" /> 视频 </el-row>
         </template>
         <el-button
           v-hasPermi="['mp:material:upload-permanent']"
@@ -85,17 +85,17 @@
   </ContentWrap>
 </template>
 <script lang="ts" setup name="MpMaterial">
-import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
 import ImageTable from './components/ImageTable.vue'
 import VoiceTable from './components/VoiceTable.vue'
 import VideoTable from './components/VideoTable.vue'
 import UploadFile from './components/UploadFile.vue'
 import UploadVideo from './components/UploadVideo.vue'
-import { MaterialType } from './components/upload'
+import { UploadType } from './components/upload'
 import * as MpMaterialApi from '@/api/mp/material'
 const message = useMessage() // 消息
 
-const type = ref<MaterialType>(MaterialType.Image) // 素材类型
+const type = ref<UploadType>(UploadType.Image) // 素材类型
 const loading = ref(false) // 遮罩层
 const list = ref<any[]>([]) // 总条数
 const total = ref(0) // 数据列表
@@ -103,19 +103,19 @@ const total = ref(0) // 数据列表
 interface QueryParams {
   pageNo: number
   pageSize: number
-  accountId?: number
+  accountId: number
   permanent: boolean
 }
 const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  accountId: undefined,
+  accountId: 0,
   permanent: true
 })
 const showCreateVideo = ref(false) // 是否新建视频的弹窗
 
 /** 侦听公众号变化 **/
-const onAccountChanged = (id?: number) => {
+const onAccountChanged = (id: number) => {
   queryParams.accountId = id
   getList()
 }
diff --git a/src/views/mp/menu/components/MenuEditor.vue b/src/views/mp/menu/components/MenuEditor.vue
index 0bd27f19..684d66f6 100644
--- a/src/views/mp/menu/components/MenuEditor.vue
+++ b/src/views/mp/menu/components/MenuEditor.vue
@@ -94,7 +94,8 @@
             </div>
             <el-dialog title="选择图文" v-model="showNewsDialog" width="80%" destroy-on-close>
               <WxMaterialSelect
-                :objData="{ type: 'news', accountId: props.accountId }"
+                type="news"
+                :account-id="props.accountId"
                 @select-material="selectMaterial"
               />
             </el-dialog>
@@ -104,7 +105,7 @@
           class="configur_content"
           v-if="menu.type === 'click' || menu.type === 'scancode_waitmsg'"
         >
-          <WxReplySelect v-if="hackResetWxReplySelect" :objData="menu.reply" />
+          <WxReplySelect v-if="hackResetWxReplySelect" v-model="menu.reply" />
         </div>
       </div>
     </div>
@@ -112,15 +113,15 @@
 </template>
 
 <script setup lang="ts">
-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 WxReplySelect from '@/views/mp/components/wx-reply'
+import WxNews from '@/views/mp/components/wx-news'
+import WxMaterialSelect from '@/views/mp/components/wx-material-select'
 import menuOptions from './menuOptions'
 
 const message = useMessage()
 
 const props = defineProps<{
-  accountId?: number
+  accountId: number
   modelValue: any
   isParent: boolean
 }>()
@@ -130,7 +131,6 @@ const emit = defineEmits<{
   (e: 'update:modelValue', v: any)
 }>()
 
-// TODO @Dhb52 输入的 table 切换时,表单应该保留
 const menu = computed({
   get() {
     return props.modelValue
diff --git a/src/views/mp/menu/index.vue b/src/views/mp/menu/index.vue
index c053a47c..c8ed0835 100644
--- a/src/views/mp/menu/index.vue
+++ b/src/views/mp/menu/index.vue
@@ -53,7 +53,7 @@
 </template>
 
 <script lang="ts" setup name="MpMenu">
-import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
 import MenuEditor from './components/MenuEditor.vue'
 import MenuPreviewer from './components/MenuPreviewer.vue'
 import * as MpMenuApi from '@/api/mp/menu'
@@ -65,8 +65,8 @@ const MENU_NOT_SELECTED = '__MENU_NOT_SELECTED__'
 
 // ======================== 列表查询 ========================
 const loading = ref(false) // 遮罩层
-const accountId = ref<number | undefined>()
-const accountName = ref<string | undefined>('')
+const accountId = ref<number>(0)
+const accountName = ref<string>('')
 const menuList = ref<Menu[]>([])
 
 // ======================== 菜单操作 ========================
@@ -103,7 +103,7 @@ const tempSelfObj = ref<{
 const dialogNewsVisible = ref(false) // 跳转图文时的素材选择弹窗
 
 /** 侦听公众号变化 **/
-const onAccountChanged = (id?: number, name?: string) => {
+const onAccountChanged = (id: number, name: string) => {
   accountId.value = id
   accountName.value = name
   getList()
diff --git a/src/views/mp/message/MessageTable.vue b/src/views/mp/message/MessageTable.vue
index 23eb9aae..fc5e55ff 100644
--- a/src/views/mp/message/MessageTable.vue
+++ b/src/views/mp/message/MessageTable.vue
@@ -122,11 +122,11 @@
 </template>
 
 <script setup lang="ts">
-import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
-import WxVoicePlayer from '@/views/mp/components/wx-voice-play/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 WxVideoPlayer from '@/views/mp/components/wx-video-play'
+import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
+import WxLocation from '@/views/mp/components/wx-location'
+import WxMusic from '@/views/mp/components/wx-music'
+import WxNews from '@/views/mp/components/wx-news'
 import { dateFormatter } from '@/utils/formatTime'
 import { MsgType } from '@/views/mp/components/wx-msg/types'
 
diff --git a/src/views/mp/message/index.vue b/src/views/mp/message/index.vue
index c115813c..85048f38 100644
--- a/src/views/mp/message/index.vue
+++ b/src/views/mp/message/index.vue
@@ -81,8 +81,8 @@
 </template>
 <script setup lang="ts" name="MpMessage">
 import * as MpMessageApi from '@/api/mp/message'
-import WxMsg from '@/views/mp/components/wx-msg/main.vue'
-import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
+import WxMsg from '@/views/mp/components/wx-msg'
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
 import MessageTable from './MessageTable.vue'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import { MsgType } from '@/views/mp/components/wx-msg/types'
@@ -96,17 +96,17 @@ const list = ref<any[]>([]) // 当前页的列表数据
 interface QueryParams {
   pageNo: number
   pageSize: number
-  openid: string | null
-  accountId: number | null
-  type: MsgType | null
+  openid: string | undefined
+  accountId: number
+  type: MsgType | undefined
   createTime: string[] | []
 }
 const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  openid: null,
-  accountId: null,
-  type: null,
+  openid: undefined,
+  accountId: 0,
+  type: undefined,
   createTime: []
 })
 const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单
@@ -118,8 +118,8 @@ const messageBox = reactive({
 })
 
 /** 侦听accountId */
-const onAccountChanged = (id?: number) => {
-  queryParams.accountId = id as number
+const onAccountChanged = (id: number) => {
+  queryParams.accountId = id
   handleQuery()
 }
 
diff --git a/src/views/mp/tag/index.vue b/src/views/mp/tag/index.vue
index 6689529d..a92d9127 100644
--- a/src/views/mp/tag/index.vue
+++ b/src/views/mp/tag/index.vue
@@ -14,10 +14,22 @@
         <WxAccountSelect @change="onAccountChanged" />
       </el-form-item>
       <el-form-item>
-        <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['mp:tag:create']">
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['mp:tag:create']"
+          :disabled="queryParams.accountId === 0"
+        >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
-        <el-button type="success" plain @click="handleSync" v-hasPermi="['mp:tag:sync']">
+        <el-button
+          type="success"
+          plain
+          @click="handleSync"
+          v-hasPermi="['mp:tag:sync']"
+          :disabled="queryParams.accountId === 0"
+        >
           <Icon icon="ep:refresh" class="mr-5px" /> 同步
         </el-button>
       </el-form-item>
@@ -74,28 +86,30 @@
 import { dateFormatter } from '@/utils/formatTime'
 import * as MpTagApi from '@/api/mp/tag'
 import TagForm from './TagForm.vue'
-import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
+
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
-const list = ref<any>([]) // 列表的数据
+const list = ref<any[]>([]) // 列表的数据
 
 interface QueryParams {
   pageNo: number
   pageSize: number
-  accountId?: number
+  accountId: number
 }
 const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  accountId: undefined
+  accountId: 0
 })
+
 const formRef = ref<InstanceType<typeof TagForm> | null>(null)
 
 /** 侦听公众号变化 **/
-const onAccountChanged = (id?: number) => {
+const onAccountChanged = (id: number) => {
   queryParams.pageNo = 1
   queryParams.accountId = id
   getList()
@@ -114,8 +128,8 @@ const getList = async () => {
 }
 
 /** 添加/修改操作 */
-const openForm = (type: string, id?: number) => {
-  formRef.value?.open(type, queryParams.accountId as number, id)
+const openForm = (type: 'create' | 'update', id?: number) => {
+  formRef.value?.open(type, queryParams.accountId, id)
 }
 
 /** 删除按钮操作 */
diff --git a/src/views/mp/user/index.vue b/src/views/mp/user/index.vue
index 8a54d38e..03e58a7f 100644
--- a/src/views/mp/user/index.vue
+++ b/src/views/mp/user/index.vue
@@ -34,7 +34,13 @@
       <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']">
+        <el-button
+          type="success"
+          plain
+          @click="handleSync"
+          v-hasPermi="['mp:user:sync']"
+          :disabled="queryParams.accountId === 0"
+        >
           <Icon icon="ep:refresh" class="mr-5px" /> 同步
         </el-button>
       </el-form-item>
@@ -97,7 +103,7 @@
 import { dateFormatter } from '@/utils/formatTime'
 import * as MpUserApi from '@/api/mp/user'
 import * as MpTagApi from '@/api/mp/tag'
-import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
 import type { FormInstance } from 'element-plus'
 import UserForm from './UserForm.vue'
 
@@ -110,14 +116,14 @@ const list = ref<any[]>([]) // 列表的数据
 interface QueryParams {
   pageNo: number
   pageSize: number
-  accountId?: number
+  accountId: number
   openid: string | null
   nickname: string | null
 }
 const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  accountId: undefined,
+  accountId: 0,
   openid: null,
   nickname: null
 })
@@ -125,7 +131,7 @@ const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单
 const tagList = ref<any[]>([]) // 公众号标签列表
 
 /** 侦听公众号变化 **/
-const onAccountChanged = (id?: number) => {
+const onAccountChanged = (id: number) => {
   queryParams.pageNo = 1
   queryParams.accountId = id
   getList()

From 4925a66cc5becb1877ee46a9a64e4913513feeed Mon Sep 17 00:00:00 2001
From: dhb52 <dhb52@126.com>
Date: Fri, 21 Apr 2023 23:19:04 +0800
Subject: [PATCH 3/8] =?UTF-8?q?style:=20mp=E6=A8=A1=E5=9D=97stylelint?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/views/mp/account/index.vue                |   2 +-
 .../mp/components/wx-material-select/main.vue |  45 +++----
 src/views/mp/components/wx-msg/card.scss      |  79 +++++++-----
 src/views/mp/components/wx-msg/comment.scss   |  58 ++++++---
 src/views/mp/components/wx-music/main.vue     |   2 +-
 .../wx-reply/components/TabImage.vue          |  20 +--
 .../wx-reply/components/TabNews.vue           |   2 +-
 .../wx-reply/components/TabVoice.vue          |  20 +--
 src/views/mp/components/wx-reply/main.vue     |  32 ++---
 .../mp/components/wx-voice-play/main.vue      |  10 +-
 src/views/mp/freePublish/index.vue            | 121 +++++++++---------
 .../mp/material/components/ImageTable.vue     |  48 +++----
 src/views/mp/menu/index.vue                   |   1 -
 13 files changed, 236 insertions(+), 204 deletions(-)

diff --git a/src/views/mp/account/index.vue b/src/views/mp/account/index.vue
index d890f44f..f993b8e3 100644
--- a/src/views/mp/account/index.vue
+++ b/src/views/mp/account/index.vue
@@ -46,7 +46,7 @@
             v-if="scope.row.qrCodeUrl"
             :src="scope.row.qrCodeUrl"
             alt="二维码"
-            style="height: 100px; display: inline-block"
+            style="display: inline-block; height: 100px"
           />
           <el-button
             link
diff --git a/src/views/mp/components/wx-material-select/main.vue b/src/views/mp/components/wx-material-select/main.vue
index e711c022..8f2792dd 100644
--- a/src/views/mp/components/wx-material-select/main.vue
+++ b/src/views/mp/components/wx-material-select/main.vue
@@ -227,29 +227,6 @@ onMounted(async () => {
 })
 </script>
 <style lang="scss" scoped>
-/*瀑布流样式*/
-.waterfall {
-  width: 100%;
-  column-gap: 10px;
-  column-count: 5;
-  margin: 0 auto;
-}
-
-.waterfall-item {
-  padding: 10px;
-  margin-bottom: 10px;
-  break-inside: avoid;
-  border: 1px solid #eaeaea;
-}
-
-.material-img {
-  width: 100%;
-}
-
-p {
-  line-height: 30px;
-}
-
 @media (min-width: 992px) and (max-width: 1300px) {
   .waterfall {
     column-count: 3;
@@ -276,5 +253,25 @@ p {
   }
 }
 
-/*瀑布流样式*/
+.waterfall {
+  width: 100%;
+  column-gap: 10px;
+  column-count: 5;
+  margin: 0 auto;
+}
+
+.waterfall-item {
+  padding: 10px;
+  margin-bottom: 10px;
+  break-inside: avoid;
+  border: 1px solid #eaeaea;
+}
+
+.material-img {
+  width: 100%;
+}
+
+p {
+  line-height: 30px;
+}
 </style>
diff --git a/src/views/mp/components/wx-msg/card.scss b/src/views/mp/components/wx-msg/card.scss
index 67ac9219..7fbbe802 100644
--- a/src/views/mp/components/wx-msg/card.scss
+++ b/src/views/mp/components/wx-msg/card.scss
@@ -1,25 +1,27 @@
-.avue-card{
-  &__item{
+.avue-card {
+  &__item {
     margin-bottom: 16px;
     border: 1px solid #e8e8e8;
     background-color: #fff;
     box-sizing: border-box;
-    color: rgba(0,0,0,.65);
+    color: rgba(0, 0, 0, 0.65);
     font-size: 14px;
     font-variant: tabular-nums;
     line-height: 1.5;
     list-style: none;
-    font-feature-settings: "tnum";
+    font-feature-settings: 'tnum';
     cursor: pointer;
-    height:200px;
-    &:hover{
-      border-color: rgba(0,0,0,.09);
-      box-shadow: 0 2px 8px rgba(0,0,0,.09);
+    height: 200px;
+
+    &:hover {
+      border-color: rgba(0, 0, 0, 0.09);
+      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
     }
-    &--add{
-      border:1px dashed #000;
+
+    &--add {
+      border: 1px dashed #000;
       width: 100%;
-      color: rgba(0,0,0,.45);
+      color: rgba(0, 0, 0, 0.45);
       background-color: #fff;
       border-color: #d9d9d9;
       border-radius: 2px;
@@ -27,74 +29,87 @@
       align-items: center;
       justify-content: center;
       font-size: 16px;
-      i{
+
+      i {
         margin-right: 10px;
       }
-      &:hover{
+
+      &:hover {
         color: #40a9ff;
         background-color: #fff;
         border-color: #40a9ff;
       }
     }
   }
-  &__body{
+
+  &__body {
     display: flex;
     padding: 24px;
   }
-  &__detail{
-    flex:1
+
+  &__detail {
+    flex: 1;
   }
-  &__avatar{
+
+  &__avatar {
     width: 48px;
     height: 48px;
     border-radius: 48px;
     overflow: hidden;
     margin-right: 12px;
-    img{
+
+    img {
       width: 100%;
       height: 100%;
     }
   }
-  &__title{
-    color: rgba(0,0,0,.85);
+
+  &__title {
+    color: rgba(0, 0, 0, 0.85);
     margin-bottom: 12px;
     font-size: 16px;
-    &:hover{
-      color:#1890ff;
+
+    &:hover {
+      color: #1890ff;
     }
   }
-  &__info{
-    color: rgba(0,0,0,.45);
+
+  &__info {
+    color: rgba(0, 0, 0, 0.45);
     display: -webkit-box;
     -webkit-box-orient: vertical;
     -webkit-line-clamp: 3;
     overflow: hidden;
     height: 64px;
   }
-  &__menu{
+
+  &__menu {
     display: flex;
-    justify-content:space-around;
+    justify-content: space-around;
     height: 50px;
     background: #f7f9fa;
-    color: rgba(0,0,0,.45);
+    color: rgba(0, 0, 0, 0.45);
     text-align: center;
     line-height: 50px;
-    &:hover{
-      color:#1890ff;
+
+    &:hover {
+      color: #1890ff;
     }
   }
 }
 
 /** joolun 额外加的 */
 .avue-comment__main {
-  flex: unset!important;
-  border-radius: 5px!important;
-  margin: 0 8px!important;
+  flex: unset !important;
+  border-radius: 5px !important;
+  margin: 0 8px !important;
 }
+
 .avue-comment__header {
   border-top-left-radius: 5px;
   border-top-right-radius: 5px;
 }
+
 .avue-comment__body {
   border-bottom-right-radius: 5px;
   border-bottom-left-radius: 5px;
diff --git a/src/views/mp/components/wx-msg/comment.scss b/src/views/mp/components/wx-msg/comment.scss
index 3f1341b2..aaeaccaf 100644
--- a/src/views/mp/components/wx-msg/comment.scss
+++ b/src/views/mp/components/wx-msg/comment.scss
@@ -1,27 +1,33 @@
 /* 来自 https://github.com/nmxiaowei/avue/blob/master/styles/src/element-ui/comment.scss  */
-.avue-comment{
+.avue-comment {
   margin-bottom: 30px;
   display: flex;
   align-items: flex-start;
-  &--reverse{
-    flex-direction:row-reverse;
-    .avue-comment__main{
-      &:before,&:after{
+
+  &--reverse {
+    flex-direction: row-reverse;
+
+    .avue-comment__main {
+      &:before,
+      &:after {
         left: auto;
         right: -8px;
         border-width: 8px 0 8px 8px;
       }
-      &:before{
+
+      &:before {
         border-left-color: #dedede;
       }
-      &:after{
+
+      &:after {
         border-left-color: #f8f8f8;
         margin-right: 1px;
         margin-left: auto;
       }
     }
   }
-  &__avatar{
+
+  &__avatar {
     width: 48px;
     height: 48px;
     border-radius: 50%;
@@ -29,7 +35,8 @@
     box-sizing: border-box;
     vertical-align: middle;
   }
-  &__header{
+
+  &__header {
     padding: 5px 15px;
     background: #f8f8f8;
     border-bottom: 1px solid #eee;
@@ -37,18 +44,22 @@
     align-items: center;
     justify-content: space-between;
   }
-  &__author{
+
+  &__author {
     font-weight: 700;
     font-size: 14px;
     color: #999;
   }
-  &__main{
-    flex:1;
+
+  &__main {
+    flex: 1;
     margin: 0 20px;
     position: relative;
     border: 1px solid #dedede;
     border-radius: 2px;
-    &:before,&:after{
+
+    &:before,
+    &:after {
       position: absolute;
       top: 10px;
       left: -8px;
@@ -56,32 +67,39 @@
       width: 0;
       height: 0;
       display: block;
-      content: " ";
+      content: ' ';
       border-color: transparent;
       border-style: solid solid outset;
       border-width: 8px 8px 8px 0;
       pointer-events: none;
     }
+
     &:before {
       border-right-color: #dedede;
       z-index: 1;
     }
-    &:after{
+
+    &:after {
       border-right-color: #f8f8f8;
       margin-left: 1px;
       z-index: 2;
     }
   }
-  &__body{
+
+  &__body {
     padding: 15px;
     overflow: hidden;
     background: #fff;
-    font-family: Segoe UI,Lucida Grande,Helvetica,Arial,Microsoft YaHei,FreeSans,Arimo,Droid Sans,wenquanyi micro hei,Hiragino Sans GB,Hiragino Sans GB W3,FontAwesome,sans-serif;color: #333;
+    font-family: Segoe UI, Lucida Grande, Helvetica, Arial, Microsoft YaHei, FreeSans, Arimo,
+      Droid Sans, wenquanyi micro hei, Hiragino Sans GB, Hiragino Sans GB W3, FontAwesome,
+      sans-serif;
+    color: #333;
     font-size: 14px;
   }
-  blockquote{
-    margin:0;
-    font-family: Georgia,Times New Roman,Times,Kai,Kaiti SC,KaiTi,BiauKai,FontAwesome,serif;
+
+  blockquote {
+    margin: 0;
+    font-family: Georgia, Times New Roman, Times, Kai, Kaiti SC, KaiTi, BiauKai, FontAwesome, serif;
     padding: 1px 0 1px 15px;
     border-left: 4px solid #ddd;
   }
diff --git a/src/views/mp/components/wx-music/main.vue b/src/views/mp/components/wx-music/main.vue
index 52555f15..70f4c58b 100644
--- a/src/views/mp/components/wx-music/main.vue
+++ b/src/views/mp/components/wx-music/main.vue
@@ -56,5 +56,5 @@ defineExpose({
 
 <style lang="scss" scoped>
 /* 因为 joolun 实现依赖 avue 组件,该页面使用了 card.scc  */
-@import '../wx-msg/card.scss';
+@import url('../wx-msg/card.scss');
 </style>
diff --git a/src/views/mp/components/wx-reply/components/TabImage.vue b/src/views/mp/components/wx-reply/components/TabImage.vue
index 1a82c3aa..71950e04 100644
--- a/src/views/mp/components/wx-reply/components/TabImage.vue
+++ b/src/views/mp/components/wx-reply/components/TabImage.vue
@@ -123,7 +123,7 @@ const selectMaterial = (item) => {
 .select-item {
   width: 280px;
   padding: 10px;
-  margin: 0 auto 10px auto;
+  margin: 0 auto 10px;
   border: 1px solid #eaeaea;
 
   .material-img {
@@ -131,11 +131,11 @@ const selectMaterial = (item) => {
   }
 
   .item-name {
-    font-size: 12px;
     overflow: hidden;
+    font-size: 12px;
+    text-align: center;
     text-overflow: ellipsis;
     white-space: nowrap;
-    text-align: center;
 
     .item-infos {
       width: 30%;
@@ -149,18 +149,18 @@ const selectMaterial = (item) => {
   }
 
   .col-select {
-    border: 1px solid rgb(234, 234, 234);
-    padding: 50px 0px;
-    height: 160px;
     width: 49.5%;
+    height: 160px;
+    padding: 50px 0;
+    border: 1px solid rgb(234 234 234);
   }
 
   .col-add {
-    border: 1px solid rgb(234, 234, 234);
-    padding: 50px 0px;
-    height: 160px;
-    width: 49.5%;
     float: right;
+    width: 49.5%;
+    height: 160px;
+    padding: 50px 0;
+    border: 1px solid rgb(234 234 234);
 
     .el-upload__tip {
       line-height: 18px;
diff --git a/src/views/mp/components/wx-reply/components/TabNews.vue b/src/views/mp/components/wx-reply/components/TabNews.vue
index 88a82a53..ecb5026d 100644
--- a/src/views/mp/components/wx-reply/components/TabNews.vue
+++ b/src/views/mp/components/wx-reply/components/TabNews.vue
@@ -65,7 +65,7 @@ const onDelete = () => {
 .select-item {
   width: 280px;
   padding: 10px;
-  margin: 0 auto 10px auto;
+  margin: 0 auto 10px;
   border: 1px solid #eaeaea;
 
   .ope-row {
diff --git a/src/views/mp/components/wx-reply/components/TabVoice.vue b/src/views/mp/components/wx-reply/components/TabVoice.vue
index c4868cf8..5c9f64a7 100644
--- a/src/views/mp/components/wx-reply/components/TabVoice.vue
+++ b/src/views/mp/components/wx-reply/components/TabVoice.vue
@@ -120,15 +120,15 @@ const selectMaterial = (item: Reply) => {
 <style lang="scss" scoped>
 .select-item2 {
   padding: 10px;
-  margin: 0 auto 10px auto;
+  margin: 0 auto 10px;
   border: 1px solid #eaeaea;
 
   .item-name {
-    font-size: 12px;
     overflow: hidden;
+    font-size: 12px;
+    text-align: center;
     text-overflow: ellipsis;
     white-space: nowrap;
-    text-align: center;
 
     .ope-row {
       width: 100%;
@@ -138,18 +138,18 @@ const selectMaterial = (item: Reply) => {
   }
 
   .col-select {
-    border: 1px solid rgb(234, 234, 234);
-    padding: 50px 0px;
-    height: 160px;
     width: 49.5%;
+    height: 160px;
+    padding: 50px 0;
+    border: 1px solid rgb(234 234 234);
   }
 
   .col-add {
-    border: 1px solid rgb(234, 234, 234);
-    padding: 50px 0px;
-    height: 160px;
-    width: 49.5%;
     float: right;
+    width: 49.5%;
+    height: 160px;
+    padding: 50px 0;
+    border: 1px solid rgb(234 234 234);
 
     .el-upload__tip {
       line-height: 18px;
diff --git a/src/views/mp/components/wx-reply/main.vue b/src/views/mp/components/wx-reply/main.vue
index 32a31222..1ef9dde0 100644
--- a/src/views/mp/components/wx-reply/main.vue
+++ b/src/views/mp/components/wx-reply/main.vue
@@ -128,13 +128,13 @@ defineExpose({
 .select-item {
   width: 280px;
   padding: 10px;
-  margin: 0 auto 10px auto;
+  margin: 0 auto 10px;
   border: 1px solid #eaeaea;
 }
 
 .select-item2 {
   padding: 10px;
-  margin: 0 auto 10px auto;
+  margin: 0 auto 10px;
   border: 1px solid #eaeaea;
 }
 
@@ -148,11 +148,11 @@ defineExpose({
 }
 
 .item-name {
-  font-size: 12px;
   overflow: hidden;
+  font-size: 12px;
+  text-align: center;
   text-overflow: ellipsis;
   white-space: nowrap;
-  text-align: center;
 }
 
 .el-form-item__content {
@@ -160,34 +160,34 @@ defineExpose({
 }
 
 .col-select {
-  border: 1px solid rgb(234, 234, 234);
-  padding: 50px 0px;
-  height: 160px;
   width: 49.5%;
+  height: 160px;
+  padding: 50px 0;
+  border: 1px solid rgb(234 234 234);
 }
 
 .col-select2 {
-  border: 1px solid rgb(234, 234, 234);
-  padding: 50px 0px;
   height: 160px;
+  padding: 50px 0;
+  border: 1px solid rgb(234 234 234);
 }
 
 .col-add {
-  border: 1px solid rgb(234, 234, 234);
-  padding: 50px 0px;
-  height: 160px;
-  width: 49.5%;
   float: right;
+  width: 49.5%;
+  height: 160px;
+  padding: 50px 0;
+  border: 1px solid rgb(234 234 234);
 }
 
 .avatar-uploader-icon {
-  border: 1px solid #d9d9d9;
-  font-size: 28px;
-  color: #8c939d;
   width: 100px !important;
   height: 100px !important;
+  font-size: 28px;
   line-height: 100px !important;
+  color: #8c939d;
   text-align: center;
+  border: 1px solid #d9d9d9;
 }
 
 .material-img {
diff --git a/src/views/mp/components/wx-voice-play/main.vue b/src/views/mp/components/wx-voice-play/main.vue
index 169593d8..3cfec418 100644
--- a/src/views/mp/components/wx-voice-play/main.vue
+++ b/src/views/mp/components/wx-voice-play/main.vue
@@ -86,18 +86,18 @@ const amrStop = () => {
 </script>
 <style lang="scss" scoped>
 .wx-voice-div {
+  display: flex;
+  width: 120px;
+  height: 50px;
   padding: 5px;
   background-color: #eaeaea;
   border-radius: 10px;
-  width: 120px;
-  height: 50px;
-
-  display: flex;
   justify-content: center;
   align-items: center;
 }
+
 .amr-duration {
-  font-size: 11px;
   margin-left: 5px;
+  font-size: 11px;
 }
 </style>
diff --git a/src/views/mp/freePublish/index.vue b/src/views/mp/freePublish/index.vue
index 2fda5100..08a202c2 100644
--- a/src/views/mp/freePublish/index.vue
+++ b/src/views/mp/freePublish/index.vue
@@ -102,19 +102,45 @@ const handleDelete = async (item: any) => {
 }
 </script>
 <style lang="scss" scoped>
+@media (min-width: 992px) and (max-width: 1300px) {
+  .waterfall {
+    column-count: 3;
+  }
+
+  p {
+    color: red;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .waterfall {
+    column-count: 2;
+  }
+
+  p {
+    color: orange;
+  }
+}
+
+@media (max-width: 767px) {
+  .waterfall {
+    column-count: 1;
+  }
+}
+
 .ope-row {
+  padding-top: 5px;
   margin-top: 5px;
   text-align: center;
   border-top: 1px solid #eaeaea;
-  padding-top: 5px;
 }
 
 .item-name {
-  font-size: 12px;
   overflow: hidden;
+  font-size: 12px;
+  text-align: center;
   text-overflow: ellipsis;
   white-space: nowrap;
-  text-align: center;
 }
 
 .el-upload__tip {
@@ -125,8 +151,8 @@ const handleDelete = async (item: any) => {
 .left {
   display: inline-block;
   width: 35%;
-  vertical-align: top;
   margin-top: 200px;
+  vertical-align: top;
 }
 
 .right {
@@ -136,16 +162,16 @@ const handleDelete = async (item: any) => {
 }
 
 .avatar-uploader {
-  width: 20%;
   display: inline-block;
+  width: 20%;
 }
 
 .avatar-uploader .el-upload {
-  border-radius: 6px;
-  cursor: pointer;
   position: relative;
   overflow: hidden;
   text-align: unset !important;
+  cursor: pointer;
+  border-radius: 6px;
 }
 
 .avatar-uploader .el-upload:hover {
@@ -153,13 +179,13 @@ const handleDelete = async (item: any) => {
 }
 
 .avatar-uploader-icon {
-  border: 1px solid #d9d9d9;
-  font-size: 28px;
-  color: #8c939d;
   width: 120px;
   height: 120px;
+  font-size: 28px;
   line-height: 120px;
+  color: #8c939d;
   text-align: center;
+  border: 1px solid #d9d9d9;
 }
 
 .avatar {
@@ -173,13 +199,14 @@ const handleDelete = async (item: any) => {
 }
 
 .digest {
-  width: 60%;
   display: inline-block;
+  width: 60%;
   vertical-align: top;
 }
 
-/*新增图文*/
-/*瀑布流样式*/
+/* 新增图文 */
+
+/* 瀑布流样式 */
 .waterfall {
   width: 100%;
   column-gap: 10px;
@@ -198,68 +225,44 @@ p {
   line-height: 30px;
 }
 
-@media (min-width: 992px) and (max-width: 1300px) {
-  .waterfall {
-    column-count: 3;
-  }
-  p {
-    color: red;
-  }
-}
-
-@media (min-width: 768px) and (max-width: 991px) {
-  .waterfall {
-    column-count: 2;
-  }
-  p {
-    color: orange;
-  }
-}
-
-@media (max-width: 767px) {
-  .waterfall {
-    column-count: 1;
-  }
-}
-
-/*瀑布流样式*/
+/* 瀑布流样式 */
 .news-main {
-  background-color: #ffffff;
   width: 100%;
-  margin: auto;
   height: 120px;
+  margin: auto;
+  background-color: #fff;
 }
 
 .news-content {
-  background-color: #acadae;
+  position: relative;
   width: 100%;
   height: 120px;
-  position: relative;
+  background-color: #acadae;
 }
 
 .news-content-title {
-  display: inline-block;
-  font-size: 15px;
-  color: #ffffff;
   position: absolute;
-  left: 0px;
-  bottom: 0px;
-  background-color: black;
+  bottom: 0;
+  left: 0;
+  display: inline-block;
   width: 98%;
+  height: 25px;
   padding: 1%;
-  opacity: 0.65;
   overflow: hidden;
+  font-size: 15px;
+  color: #fff;
   text-overflow: ellipsis;
   white-space: nowrap;
-  height: 25px;
+  background-color: black;
+  opacity: 0.65;
 }
 
 .news-main-item {
-  background-color: #ffffff;
-  padding: 5px 0px;
-  border-top: 1px solid #eaeaea;
   width: 100%;
+  padding: 5px 0;
   margin: auto;
+  background-color: #fff;
+  border-top: 1px solid #eaeaea;
 }
 
 .news-content-item {
@@ -269,8 +272,8 @@ p {
 
 .news-content-item-title {
   display: inline-block;
-  font-size: 12px;
   width: 70%;
+  font-size: 12px;
 }
 
 .news-content-item-img {
@@ -289,9 +292,9 @@ p {
 
 .news-main-plus {
   width: 280px;
-  text-align: center;
-  margin: auto;
   height: 50px;
+  margin: auto;
+  text-align: center;
 }
 
 .icon-plus {
@@ -302,15 +305,15 @@ p {
 .select-item {
   width: 60%;
   padding: 10px;
-  margin: 0 auto 10px auto;
+  margin: 0 auto 10px;
   border: 1px solid #eaeaea;
 }
 
 .father .child {
-  display: none;
-  text-align: center;
   position: relative;
   bottom: 25px;
+  display: none;
+  text-align: center;
 }
 
 .father:hover .child {
diff --git a/src/views/mp/material/components/ImageTable.vue b/src/views/mp/material/components/ImageTable.vue
index 118fc7bd..cecbb6c2 100644
--- a/src/views/mp/material/components/ImageTable.vue
+++ b/src/views/mp/material/components/ImageTable.vue
@@ -31,30 +31,6 @@ const emit = defineEmits<{
 </script>
 
 <style lang="scss" scoped>
-/*瀑布流样式*/
-.waterfall {
-  width: 100%;
-  column-gap: 10px;
-  column-count: 5;
-  margin-top: 10px;
-  /* 芋道源码:增加 10px,避免顶着上面 */
-}
-
-.waterfall-item {
-  padding: 10px;
-  margin-bottom: 10px;
-  break-inside: avoid;
-  border: 1px solid #eaeaea;
-}
-
-.material-img {
-  width: 100%;
-}
-
-p {
-  line-height: 30px;
-}
-
 @media (min-width: 992px) and (max-width: 1300px) {
   .waterfall {
     column-count: 3;
@@ -80,4 +56,28 @@ p {
     column-count: 1;
   }
 }
+
+.waterfall {
+  width: 100%;
+  column-gap: 10px;
+  column-count: 5;
+  margin-top: 10px;
+
+  /* 芋道源码:增加 10px,避免顶着上面 */
+}
+
+.waterfall-item {
+  padding: 10px;
+  margin-bottom: 10px;
+  break-inside: avoid;
+  border: 1px solid #eaeaea;
+}
+
+.material-img {
+  width: 100%;
+}
+
+p {
+  line-height: 30px;
+}
 </style>
diff --git a/src/views/mp/menu/index.vue b/src/views/mp/menu/index.vue
index c8ed0835..442e33b5 100644
--- a/src/views/mp/menu/index.vue
+++ b/src/views/mp/menu/index.vue
@@ -367,7 +367,6 @@ div {
     margin-left: 20px;
     background-color: #e8e7e7;
     box-sizing: border-box;
-    box-sizing: border-box;
   }
 }
 </style>

From d99f9e98fbfccba07f0582d2fa6eb4266c7da791 Mon Sep 17 00:00:00 2001
From: dhb52 <dhb52@126.com>
Date: Fri, 21 Apr 2023 23:35:31 +0800
Subject: [PATCH 4/8] =?UTF-8?q?feat:=20mp/menu=20=E6=8B=96=E5=8A=A8?=
 =?UTF-8?q?=E6=8E=92=E5=BA=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../mp/menu/components/MenuPreviewer.vue      | 46 ++++++++++++++++++-
 1 file changed, 45 insertions(+), 1 deletion(-)

diff --git a/src/views/mp/menu/components/MenuPreviewer.vue b/src/views/mp/menu/components/MenuPreviewer.vue
index aca1c2ae..637c8805 100644
--- a/src/views/mp/menu/components/MenuPreviewer.vue
+++ b/src/views/mp/menu/components/MenuPreviewer.vue
@@ -4,6 +4,9 @@
     <div
       @click="menuClicked(parent, x)"
       class="menu_item"
+      draggable="true"
+      @dragstart="onDragStart(DragType.Parent, x)"
+      @dragenter.prevent="onDragEnter(DragType.Parent, x)"
       :class="{ active: props.activeIndex === `${x}` }"
     >
       <Icon icon="ep:fold" color="black" />{{ parent.name }}
@@ -13,6 +16,9 @@
       <div class="subtitle menu_bottom" v-for="(child, y) in parent.children" :key="y">
         <div
           class="menu_subItem"
+          draggable="true"
+          @dragstart="onDragStart(DragType.Child, y)"
+          @dragenter.prevent="onDragEnter(DragType.Child, x, y)"
           v-if="parent.children"
           :class="{ active: props.activeIndex === `${x}-${y}` }"
           @click="subMenuClicked(child, x, y)"
@@ -43,7 +49,7 @@ const props = defineProps<{
   modelValue: Menu[]
   activeIndex: string
   parentIndex: number
-  accountId?: number
+  accountId: number
 }>()
 
 const emit = defineEmits<{
@@ -94,6 +100,44 @@ const menuClicked = (parent: Menu, x: number) => {
 const subMenuClicked = (child: Menu, x: number, y: number) => {
   emit('submenu-clicked', child, x, y)
 }
+
+// ======================== 菜单排序 ========================
+const dragIndex = ref<number>(0)
+enum DragType {
+  Parent = 'parent',
+  Child = 'child'
+}
+const dragType = ref<DragType>()
+
+/**
+ * 菜单开始拖动回调,记录被拖动菜单的信息(类型,下标)
+ *
+ * @param type DragType, 拖动类型,父节点、子节点
+ * @param index number, 被拖动的菜单下标
+ */
+const onDragStart = (type: DragType, index: number) => {
+  dragIndex.value = index
+  dragType.value = type
+}
+
+/**
+ * 拖动其他菜单位置回调, 判断【被拖动】及【被替换位置】的两个菜单是否同个类型,同类型才会进行插入
+ *
+ * @param type: DragType, 拖动类型,父节点、子节点
+ * @param x number, 准备替换父节点位置的下标
+ * @param y number, 准备替换子节点位置的下标, 父节点拖动时可选
+ */
+const onDragEnter = (type: DragType, x: number, y = -1) => {
+  if (dragIndex.value !== x && dragType.value === type) {
+    if (type === DragType.Parent) {
+      const source = menuList.value.splice(dragIndex.value, 1)
+      menuList.value.splice(x, 0, ...source)
+    } else {
+      const source = menuList.value[x].children?.splice(dragIndex.value, 1)
+      menuList.value[x].children?.splice(y, 0, ...(source as any))
+    }
+  }
+}
 </script>
 
 <style lang="scss" scoped>

From 08eb72e56868a8fbf3bfb849c93d6c69f7a5b6c0 Mon Sep 17 00:00:00 2001
From: dhb52 <dhb52@126.com>
Date: Sat, 22 Apr 2023 01:23:52 +0800
Subject: [PATCH 5/8] style: mp/WxReply objCache => tabCache

---
 src/views/mp/components/wx-reply/main.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/views/mp/components/wx-reply/main.vue b/src/views/mp/components/wx-reply/main.vue
index 1ef9dde0..9a83b2fe 100644
--- a/src/views/mp/components/wx-reply/main.vue
+++ b/src/views/mp/components/wx-reply/main.vue
@@ -84,7 +84,7 @@ const reply = computed<Reply>({
   set: (val) => emit('update:modelValue', val)
 })
 // 作为多个标签保存各自Reply的缓存
-const objCache = new Map<ReplyType, Reply>()
+const tabCache = new Map<ReplyType, Reply>()
 // 采用独立的ref来保存当前tab,避免在watch标签变化,对reply进行赋值会产生了循环调用
 const currentTab = ref<ReplyType>(props.modelValue.type || ReplyType.Text)
 
@@ -97,10 +97,10 @@ watch(
       return
     }
 
-    objCache.set(oldTab, unref(reply))
+    tabCache.set(oldTab, unref(reply))
 
     // 从缓存里面取出新tab内容,有则覆盖Reply,没有则创建空Reply
-    const temp = objCache.get(newTab)
+    const temp = tabCache.get(newTab)
     if (temp) {
       reply.value = temp
     } else {

From ebacbbb9cf69bd4358367bef3d9de9bcdfaad754 Mon Sep 17 00:00:00 2001
From: dhb52 <dhb52@126.com>
Date: Sat, 22 Apr 2023 09:04:21 +0800
Subject: [PATCH 6/8] =?UTF-8?q?perf:=20mp/menu=E4=BD=BF=E7=94=A8vuedraggab?=
 =?UTF-8?q?le=E6=9B=BF=E6=8D=A2=E6=8B=96=E5=8A=A8=E7=9A=84=E5=8E=9F?=
 =?UTF-8?q?=E7=94=9F=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../mp/menu/components/MenuPreviewer.vue      | 146 ++++++++++--------
 1 file changed, 81 insertions(+), 65 deletions(-)

diff --git a/src/views/mp/menu/components/MenuPreviewer.vue b/src/views/mp/menu/components/MenuPreviewer.vue
index 637c8805..3066e590 100644
--- a/src/views/mp/menu/components/MenuPreviewer.vue
+++ b/src/views/mp/menu/components/MenuPreviewer.vue
@@ -1,41 +1,55 @@
 <template>
-  <div class="menu_bottom" v-for="(parent, x) of menuList" :key="x">
-    <!-- 一级菜单 -->
-    <div
-      @click="menuClicked(parent, x)"
-      class="menu_item"
-      draggable="true"
-      @dragstart="onDragStart(DragType.Parent, x)"
-      @dragenter.prevent="onDragEnter(DragType.Parent, x)"
-      :class="{ active: props.activeIndex === `${x}` }"
-    >
-      <Icon icon="ep:fold" color="black" />{{ parent.name }}
-    </div>
-    <!-- 以下为二级菜单-->
-    <div class="submenu" v-if="parentIndex === x && parent.children">
-      <div class="subtitle menu_bottom" v-for="(child, y) in parent.children" :key="y">
+  <draggable
+    v-model="menuList"
+    item-key="id"
+    ghost-class="draggable-ghost"
+    :animation="400"
+    @end="onDragEnd"
+  >
+    <template #item="{ element: parent, index: x }">
+      <div class="menu_bottom">
+        <!-- 一级菜单 -->
         <div
-          class="menu_subItem"
-          draggable="true"
-          @dragstart="onDragStart(DragType.Child, y)"
-          @dragenter.prevent="onDragEnter(DragType.Child, x, y)"
-          v-if="parent.children"
-          :class="{ active: props.activeIndex === `${x}-${y}` }"
-          @click="subMenuClicked(child, x, y)"
+          @click="menuClicked(parent, x)"
+          class="menu_item"
+          :class="{ active: props.activeIndex === `${x}` }"
         >
-          {{ child.name }}
+          <Icon icon="ep:fold" color="black" />{{ parent.name }}
+        </div>
+        <!-- 以下为二级菜单-->
+        <div class="submenu" v-if="props.parentIndex === x && parent.children">
+          <draggable
+            v-model="parent.children"
+            item-key="id"
+            ghost-class="draggable-ghost"
+            :animation="400"
+          >
+            <template #item="{ element: child, index: y }">
+              <div class="subtitle menu_bottom">
+                <div
+                  class="menu_subItem"
+                  v-if="parent.children"
+                  :class="{ active: props.activeIndex === `${x}-${y}` }"
+                  @click="subMenuClicked(child, x, y)"
+                >
+                  {{ child.name }}
+                </div>
+              </div>
+            </template>
+          </draggable>
+          <!-- 二级菜单加号, 当长度 小于 5 才显示二级菜单的加号  -->
+          <div
+            class="menu_bottom menu_addicon"
+            v-if="!parent.children || parent.children.length < 5"
+            @click="addSubMenu(x, parent)"
+          >
+            <Icon icon="ep:plus" class="plus" />
+          </div>
         </div>
       </div>
-      <!-- 二级菜单加号, 当长度 小于 5 才显示二级菜单的加号  -->
-      <div
-        class="menu_bottom menu_addicon"
-        v-if="!parent.children || parent.children.length < 5"
-        @click="addSubMenu(x, parent)"
-      >
-        <Icon icon="ep:plus" class="plus" />
-      </div>
-    </div>
-  </div>
+    </template>
+  </draggable>
+
   <!-- 一级菜单加号 -->
   <div class="menu_bottom menu_addicon" v-if="menuList.length < 3" @click="addMenu">
     <Icon icon="ep:plus" class="plus" />
@@ -44,6 +58,7 @@
 
 <script setup lang="ts">
 import { Menu } from './types'
+import draggable from 'vuedraggable'
 
 const props = defineProps<{
   modelValue: Menu[]
@@ -97,46 +112,41 @@ const addSubMenu = (i: number, parent: any) => {
 const menuClicked = (parent: Menu, x: number) => {
   emit('menu-clicked', parent, x)
 }
+
 const subMenuClicked = (child: Menu, x: number, y: number) => {
   emit('submenu-clicked', child, x, y)
 }
 
-// ======================== 菜单排序 ========================
-const dragIndex = ref<number>(0)
-enum DragType {
-  Parent = 'parent',
-  Child = 'child'
-}
-const dragType = ref<DragType>()
-
 /**
- * 菜单开始拖动回调,记录被拖动菜单的信息(类型,下标)
+ * 处理一级菜单展开后被拖动
  *
- * @param type DragType, 拖动类型,父节点、子节点
- * @param index number, 被拖动的菜单下标
+ * @param oldIndex: 一级菜单拖动前的位置
+ * @param newIndex: 一级菜单拖动后的位置
  */
-const onDragStart = (type: DragType, index: number) => {
-  dragIndex.value = index
-  dragType.value = type
-}
-
-/**
- * 拖动其他菜单位置回调, 判断【被拖动】及【被替换位置】的两个菜单是否同个类型,同类型才会进行插入
- *
- * @param type: DragType, 拖动类型,父节点、子节点
- * @param x number, 准备替换父节点位置的下标
- * @param y number, 准备替换子节点位置的下标, 父节点拖动时可选
- */
-const onDragEnter = (type: DragType, x: number, y = -1) => {
-  if (dragIndex.value !== x && dragType.value === type) {
-    if (type === DragType.Parent) {
-      const source = menuList.value.splice(dragIndex.value, 1)
-      menuList.value.splice(x, 0, ...source)
-    } else {
-      const source = menuList.value[x].children?.splice(dragIndex.value, 1)
-      menuList.value[x].children?.splice(y, 0, ...(source as any))
-    }
+const onDragEnd = ({ oldIndex, newIndex }) => {
+  // 二级菜单没有展开,直接返回
+  if (props.activeIndex === '__MENU_NOT_SELECTED__') {
+    return
   }
+
+  let newParent = props.parentIndex
+  if (props.parentIndex === oldIndex) {
+    newParent = newIndex
+  } else if (props.parentIndex === newIndex) {
+    newParent = oldIndex
+  } else {
+    // 如果展开的二级菜单下标`props.parentIndex`不是被移动的菜单的前后下标。
+    // 那么使用一个辅助素组来模拟菜单移动,然后找到展开的二级菜单的新下标`newParent`
+    let positions = new Array<boolean>(menuList.value.length).fill(false)
+    positions[props.parentIndex] = true
+    positions.splice(oldIndex, 1)
+    positions.splice(newIndex, 0, true)
+    newParent = positions.indexOf(true)
+  }
+
+  // 找到菜单元素,触发一级菜单点击
+  const parent = menuList.value[newParent]
+  emit('menu-clicked', parent, newParent)
 }
 </script>
 
@@ -199,4 +209,10 @@ const onDragEnter = (type: DragType, x: number, y = -1) => {
     box-sizing: border-box;
   }
 }
+
+.draggable-ghost {
+  opacity: 0.5;
+  background: #f7fafc;
+  border: 1px solid #4299e1;
+}
 </style>

From f27b93db5953d1ce499613f7151393412f668ebf Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 22 Apr 2023 20:44:32 +0800
Subject: [PATCH 7/8] =?UTF-8?q?=E8=8F=9C=E5=8D=95=E7=AE=A1=E7=90=86?=
 =?UTF-8?q?=EF=BC=9A=E6=B7=BB=E5=8A=A0=E5=88=B7=E6=96=B0=E8=8F=9C=E5=8D=95?=
 =?UTF-8?q?=E7=BC=93=E5=AD=98=E6=8C=89=E9=92=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/views/system/menu/index.vue | 14 ++++++--------
 1 file changed, 6 insertions(+), 8 deletions(-)

diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue
index 95e71b5c..6be82506 100644
--- a/src/views/system/menu/index.vue
+++ b/src/views/system/menu/index.vue
@@ -129,7 +129,6 @@ import { handleTree } from '@/utils/tree'
 import * as MenuApi from '@/api/system/menu'
 import MenuForm from './MenuForm.vue'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-
 const { wsCache } = useCache()
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -180,19 +179,18 @@ const toggleExpandAll = () => {
     refreshTable.value = true
   })
 }
+
 /** 刷新菜单缓存按钮操作 */
-const refreshMenu = () => {
-  ElMessageBox.confirm('即将更新缓存刷新浏览器!', '刷新菜单缓存', {
-    confirmButtonText: t('common.ok'),
-    cancelButtonText: t('common.cancel'),
-    type: 'warning'
-  }).then(() => {
+const refreshMenu = async () => {
+  try {
+    await message.confirm('即将更新缓存刷新浏览器!', '刷新菜单缓存')
     // 清空,从而触发刷新
     wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
     // 刷新浏览器
     location.reload()
-  })
+  } catch {}
 }
+
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {

From c14b5c5bff083f7289ed8e955cd74e1e9a961120 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 22 Apr 2023 21:21:45 +0800
Subject: [PATCH 8/8] =?UTF-8?q?=E7=99=BB=E5=BD=95=E7=95=8C=E9=9D=A2?=
 =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E2=80=9C=E8=90=8C=E6=96=B0=E5=BF=85?=
 =?UTF-8?q?=E8=AF=BB=E2=80=9D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/views/Login/components/LoginForm.vue | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/src/views/Login/components/LoginForm.vue b/src/views/Login/components/LoginForm.vue
index 4cd09cc6..e988ce3b 100644
--- a/src/views/Login/components/LoginForm.vue
+++ b/src/views/Login/components/LoginForm.vue
@@ -125,6 +125,21 @@
           </div>
         </el-form-item>
       </el-col>
+      <el-divider content-position="center">萌新必读</el-divider>
+      <el-col :span="24" style="padding-left: 10px; padding-right: 10px">
+        <el-form-item>
+          <div class="flex justify-between w-[100%]">
+            <el-link href="https://doc.iocoder.cn/" target="_blank">📚开发指南</el-link>
+            <el-link href="https://doc.iocoder.cn/video/" target="_blank">🔥视频教程</el-link>
+            <el-link href="https://www.iocoder.cn/Interview/good-collection/" target="_blank">
+              ⚡面试手册
+            </el-link>
+            <el-link href="http://static.yudao.iocoder.cn/mp/Aix9975.jpeg" target="_blank">
+              🤝外包咨询
+            </el-link>
+          </div>
+        </el-form-item>
+      </el-col>
     </el-row>
   </el-form>
 </template>