Quellcode durchsuchen

refactor: mp模块,一个大大的重构+fix

dhb52 vor 2 Jahren
Ursprung
Commit
b45b85984c
43 geänderte Dateien mit 520 neuen und 440 gelöschten Zeilen
  1. 4 4
      src/views/mp/autoReply/components/ReplyTable.vue
  2. 0 41
      src/views/mp/autoReply/components/types.ts
  3. 28 27
      src/views/mp/autoReply/index.vue
  4. 3 0
      src/views/mp/components/wx-account-select/index.ts
  5. 9 5
      src/views/mp/components/wx-account-select/main.vue
  6. 3 0
      src/views/mp/components/wx-location/index.ts
  7. 6 0
      src/views/mp/components/wx-material-select/index.ts
  8. 28 30
      src/views/mp/components/wx-material-select/main.vue
  9. 11 0
      src/views/mp/components/wx-material-select/types.ts
  10. 6 0
      src/views/mp/components/wx-msg/index.ts
  11. 14 14
      src/views/mp/components/wx-msg/main.vue
  12. 3 0
      src/views/mp/components/wx-music/index.ts
  13. 3 0
      src/views/mp/components/wx-news/index.ts
  14. 19 17
      src/views/mp/components/wx-news/main.vue
  15. 25 25
      src/views/mp/components/wx-reply/components/TabImage.vue
  16. 19 22
      src/views/mp/components/wx-reply/components/TabMusic.vue
  17. 16 18
      src/views/mp/components/wx-reply/components/TabNews.vue
  18. 3 8
      src/views/mp/components/wx-reply/components/TabText.vue
  19. 24 28
      src/views/mp/components/wx-reply/components/TabVideo.vue
  20. 27 27
      src/views/mp/components/wx-reply/components/TabVoice.vue
  21. 44 15
      src/views/mp/components/wx-reply/components/types.ts
  22. 7 0
      src/views/mp/components/wx-reply/index.ts
  23. 84 25
      src/views/mp/components/wx-reply/main.vue
  24. 3 0
      src/views/mp/components/wx-video-play/index.ts
  25. 3 0
      src/views/mp/components/wx-voice-play/index.ts
  26. 3 3
      src/views/mp/components/wx-voice-play/main.vue
  27. 10 26
      src/views/mp/draft/components/CoverSelect.vue
  28. 1 1
      src/views/mp/draft/components/DraftTable.vue
  29. 4 5
      src/views/mp/draft/index.vue
  30. 5 5
      src/views/mp/freePublish/index.vue
  31. 6 6
      src/views/mp/hooks/useUpload.ts
  32. 5 10
      src/views/mp/material/components/UploadFile.vue
  33. 12 16
      src/views/mp/material/components/UploadVideo.vue
  34. 1 1
      src/views/mp/material/components/VideoTable.vue
  35. 1 1
      src/views/mp/material/components/VoiceTable.vue
  36. 6 6
      src/views/mp/material/components/upload.ts
  37. 14 14
      src/views/mp/material/index.vue
  38. 7 7
      src/views/mp/menu/components/MenuEditor.vue
  39. 4 4
      src/views/mp/menu/index.vue
  40. 5 5
      src/views/mp/message/MessageTable.vue
  41. 10 10
      src/views/mp/message/index.vue
  42. 23 9
      src/views/mp/tag/index.vue
  43. 11 5
      src/views/mp/user/index.vue

+ 4 - 4
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'

+ 0 - 41
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
-}

+ 28 - 27
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)

+ 3 - 0
src/views/mp/components/wx-account-select/index.ts

@@ -0,0 +1,3 @@
+import WxAccountSelect from './main.vue'
+
+export default WxAccountSelect

+ 9 - 5
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)
+  }
 }
 
 /** 初始化 */

+ 3 - 0
src/views/mp/components/wx-location/index.ts

@@ -0,0 +1,3 @@
+import WxLocation from './main.vue'
+
+export default WxLocation

+ 6 - 0
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

+ 28 - 30
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
     })
   })

+ 11 - 0
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'
+}

+ 6 - 0
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

+ 14 - 14
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]]

+ 3 - 0
src/views/mp/components/wx-music/index.ts

@@ -0,0 +1,3 @@
+import WxMusic from './main.vue'
+
+export default WxMusic

+ 3 - 0
src/views/mp/components/wx-news/index.ts

@@ -0,0 +1,3 @@
+import WxNews from './main.vue'
+
+export default WxNews

+ 19 - 17
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 {

+ 25 - 25
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>
 

+ 19 - 22
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>

+ 16 - 18
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>
 

+ 3 - 8
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)

+ 24 - 28
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>

+ 27 - 27
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>
 

+ 44 - 15
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 }

+ 7 - 0
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

+ 84 - 25
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))
-
-/** 切换消息类型的 tab */
-const onTabClick = () => {
-  clear()
-}
-
-/** 清除除了`type`的字段 */
+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)
+
+watch(
+  currentTab,
+  (newTab, oldTab) => {
+    // 第一次进入:oldTab 为 undefined
+    // 判断 newTab 是因为 Reply 为 Partial
+    if (oldTab === undefined || newTab === undefined) {
+      return
+    }
+
+    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({

+ 3 - 0
src/views/mp/components/wx-video-play/index.ts

@@ -0,0 +1,3 @@
+import WxVideoPlayer from './main.vue'
+
+export default WxVideoPlayer

+ 3 - 0
src/views/mp/components/wx-voice-play/index.ts

@@ -0,0 +1,3 @@
+import WxVoicePlayer from './main.vue'
+
+export default WxVoicePlayer

+ 3 - 3
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;

+ 10 - 26
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) {

+ 1 - 1
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'
 

+ 4 - 5
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 修改 ==========

+ 5 - 5
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()
 }

+ 6 - 6
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 }

+ 5 - 10
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')
 }
 

+ 12 - 16
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

+ 1 - 1
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<{

+ 1 - 1
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<{

+ 6 - 6
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,

+ 14 - 14
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()
 }

+ 7 - 7
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

+ 4 - 4
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()

+ 5 - 5
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'
 

+ 10 - 10
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()
 }
 

+ 23 - 9
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)
 }
 
 /** 删除按钮操作 */

+ 11 - 5
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()