Merge remote-tracking branch 'yudao/dev' into dev
# Conflicts: # src/views/mp/components/wx-editor/WxEditor.vue # src/views/mp/tag/TagForm.vuepull/125/head
						commit
						02d98f2ce7
					
				|  | @ -75,7 +75,8 @@ const include = [ | |||
|   'element-plus/es/components/dropdown-item/style/css', | ||||
|   'element-plus/es/components/badge/style/css', | ||||
|   'element-plus/es/components/breadcrumb/style/css', | ||||
|   'element-plus/es/components/breadcrumb-item/style/css' | ||||
|   'element-plus/es/components/breadcrumb-item/style/css', | ||||
|   'element-plus/es/components/image/style/css' | ||||
| ] | ||||
| 
 | ||||
| const exclude = ['@iconify/json'] | ||||
|  |  | |||
|  | @ -95,7 +95,7 @@ watch( | |||
|     return props.modelValue | ||||
|   }, | ||||
|   () => { | ||||
|     if (props.modelValue && props.modelValue.contains(':')) { | ||||
|     if (props.modelValue && props.modelValue.indexOf(':') >= 0) { | ||||
|       currentActiveType.value = props.modelValue.substring(0, props.modelValue.indexOf(':') + 1) | ||||
|       icon.value = props.modelValue.substring(props.modelValue.indexOf(':') + 1) | ||||
|     } | ||||
|  |  | |||
|  | @ -62,6 +62,8 @@ declare global { | |||
|   const useRouter: typeof import('vue-router')['useRouter'] | ||||
|   const useSlots: typeof import('vue')['useSlots'] | ||||
|   const useTable: typeof import('@/hooks/web/useTable')['useTable'] | ||||
|   const useVxeCrudSchemas: typeof import('@/hooks/web/useVxeCrudSchemas')['useVxeCrudSchemas'] | ||||
|   const useXTable: typeof import('@/hooks/web/useXTable')['useXTable'] | ||||
|   const watch: typeof import('vue')['watch'] | ||||
|   const watchEffect: typeof import('vue')['watchEffect'] | ||||
|   const watchPostEffect: typeof import('vue')['watchPostEffect'] | ||||
|  |  | |||
|  | @ -3,13 +3,16 @@ | |||
| 
 | ||||
|   <!-- 搜索工作栏 --> | ||||
|   <ContentWrap> | ||||
|     <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect  --> | ||||
|     <WxAccountSelect @change="accountChanged" /> | ||||
|     <el-form class="-mb-15px" :model="queryParams" :inline="true" label-width="68px"> | ||||
|       <el-form-item label="公众号" prop="accountId"> | ||||
|         <WxMpSelect @change="onAccountChanged" /> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- tab 切换 --> | ||||
|   <ContentWrap> | ||||
|     <el-tabs v-model="type" @tab-change="handleTabChange"> | ||||
|     <el-tabs v-model="msgType" @tab-change="handleTabChange"> | ||||
|       <!-- 操作工具栏 --> | ||||
|       <el-row :gutter="10" class="mb8"> | ||||
|         <el-col :span="1.5"> | ||||
|  | @ -18,26 +21,26 @@ | |||
|             plain | ||||
|             @click="handleAdd" | ||||
|             v-hasPermi="['mp:auto-reply:create']" | ||||
|             v-if="type !== '1' || list.length <= 0" | ||||
|             v-if="msgType !== MsgType.Follow || list.length <= 0" | ||||
|           > | ||||
|             <Icon icon="ep:plus" />新增 | ||||
|           </el-button> | ||||
|         </el-col> | ||||
|       </el-row> | ||||
|       <!-- tab 项 --> | ||||
|       <el-tab-pane name="1"> | ||||
|       <el-tab-pane :name="MsgType.Follow"> | ||||
|         <template #label> | ||||
|           <span><Icon icon="ep:star-off" /> 关注时回复</span> | ||||
|           <span><Icon icon="ep:star" /> 关注时回复</span> | ||||
|         </template> | ||||
|       </el-tab-pane> | ||||
|       <el-tab-pane name="2"> | ||||
|       <el-tab-pane :name="MsgType.Message"> | ||||
|         <template #label> | ||||
|           <span><Icon icon="ep:chat-line-round" /> 消息回复</span> | ||||
|         </template> | ||||
|       </el-tab-pane> | ||||
|       <el-tab-pane name="3"> | ||||
|       <el-tab-pane :name="MsgType.Keyword"> | ||||
|         <template #label> | ||||
|           <span><Icon icon="ep:news" /> 关键词回复</span> | ||||
|           <span><Icon icon="fa:newspaper-o" /> 关键词回复</span> | ||||
|         </template> | ||||
|       </el-tab-pane> | ||||
|     </el-tabs> | ||||
|  | @ -47,10 +50,20 @@ | |||
|         label="请求消息类型" | ||||
|         align="center" | ||||
|         prop="requestMessageType" | ||||
|         v-if="type === '2'" | ||||
|         v-if="msgType === MsgType.Message" | ||||
|       /> | ||||
|       <el-table-column label="关键词" align="center" prop="requestKeyword" v-if="type === '3'" /> | ||||
|       <el-table-column label="匹配类型" align="center" prop="requestMatch" v-if="type === '3'"> | ||||
|       <el-table-column | ||||
|         label="关键词" | ||||
|         align="center" | ||||
|         prop="requestKeyword" | ||||
|         v-if="msgType === MsgType.Keyword" | ||||
|       /> | ||||
|       <el-table-column | ||||
|         label="匹配类型" | ||||
|         align="center" | ||||
|         prop="requestMatch" | ||||
|         v-if="msgType === MsgType.Keyword" | ||||
|       > | ||||
|         <template #default="scope"> | ||||
|           <dict-tag :type="DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH" :value="scope.row.requestMatch" /> | ||||
|         </template> | ||||
|  | @ -64,7 +77,7 @@ | |||
|         <template #default="scope"> | ||||
|           <div v-if="scope.row.responseMessageType === 'text'">{{ scope.row.responseContent }}</div> | ||||
|           <div v-else-if="scope.row.responseMessageType === 'voice'"> | ||||
|             <WxVoicePlayer :url="scope.row.responseMediaUrl" /> | ||||
|             <WxVoicePlayer v-if="scope.row.responseMediaUrl" :url="scope.row.responseMediaUrl" /> | ||||
|           </div> | ||||
|           <div v-else-if="scope.row.responseMessageType === 'image'"> | ||||
|             <a target="_blank" :href="scope.row.responseMediaUrl"> | ||||
|  | @ -77,7 +90,11 @@ | |||
|               scope.row.responseMessageType === 'shortvideo' | ||||
|             " | ||||
|           > | ||||
|             <WxVideoPlayer :url="scope.row.responseMediaUrl" style="margin-top: 10px" /> | ||||
|             <WxVideoPlayer | ||||
|               v-if="scope.row.responseMediaUrl" | ||||
|               :url="scope.row.responseMediaUrl" | ||||
|               style="margin-top: 10px" | ||||
|             /> | ||||
|           </div> | ||||
|           <div v-else-if="scope.row.responseMessageType === 'news'"> | ||||
|             <WxNews :articles="scope.row.responseArticles" /> | ||||
|  | @ -123,21 +140,21 @@ | |||
|     </el-table> | ||||
| 
 | ||||
|     <!-- 添加或修改自动回复的对话框 --> | ||||
|     <el-dialog :title="title" v-model="open" width="800px" append-to-body> | ||||
|       <el-form ref="formRef" :model="form" :rules="rules" label-width="80px"> | ||||
|         <el-form-item label="消息类型" prop="requestMessageType" v-if="type === '2'"> | ||||
|           <el-select v-model="form.requestMessageType" placeholder="请选择"> | ||||
|     <el-dialog :title="title" v-model="showReplyFormDialog" width="800px" append-to-body> | ||||
|       <el-form ref="formRef" :model="replyForm" :rules="rules" label-width="80px"> | ||||
|         <el-form-item label="消息类型" prop="requestMessageType" v-if="msgType === MsgType.Message"> | ||||
|           <el-select v-model="replyForm.requestMessageType" placeholder="请选择"> | ||||
|             <template v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)" :key="dict.value"> | ||||
|               <el-option | ||||
|                 v-if="requestMessageTypes.includes(dict.value)" | ||||
|                 v-if="RequestMessageTypes.includes(dict.value)" | ||||
|                 :label="dict.label" | ||||
|                 :value="dict.value" | ||||
|               /> | ||||
|             </template> | ||||
|           </el-select> | ||||
|         </el-form-item> | ||||
|         <el-form-item label="匹配类型" prop="requestMatch" v-if="type === '3'"> | ||||
|           <el-select v-model="form.requestMatch" placeholder="请选择匹配类型" clearable> | ||||
|         <el-form-item label="匹配类型" prop="requestMatch" v-if="msgType === MsgType.Keyword"> | ||||
|           <el-select v-model="replyForm.requestMatch" placeholder="请选择匹配类型" clearable> | ||||
|             <el-option | ||||
|               v-for="dict in getDictOptions(DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH)" | ||||
|               :key="dict.value" | ||||
|  | @ -146,8 +163,8 @@ | |||
|             /> | ||||
|           </el-select> | ||||
|         </el-form-item> | ||||
|         <el-form-item label="关键词" prop="requestKeyword" v-if="type === '3'"> | ||||
|           <el-input v-model="form.requestKeyword" placeholder="请输入内容" clearable /> | ||||
|         <el-form-item label="关键词" prop="requestKeyword" v-if="msgType === MsgType.Keyword"> | ||||
|           <el-input v-model="replyForm.requestKeyword" placeholder="请输入内容" clearable /> | ||||
|         </el-form-item> | ||||
|         <el-form-item label="回复消息"> | ||||
|           <WxReplySelect :objData="objData" v-if="hackResetWxReplySelect" /> | ||||
|  | @ -160,38 +177,47 @@ | |||
|     </el-dialog> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| <script setup name="MpAutoReply"> | ||||
| <script setup lang="ts" name="MpAutoReply"> | ||||
| import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue' | ||||
| import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue' | ||||
| import WxMusic from '@/views/mp/components/wx-music/main.vue' | ||||
| import WxNews from '@/views/mp/components/wx-news/main.vue' | ||||
| import WxReplySelect from '@/views/mp/components/wx-reply/main.vue' | ||||
| import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' | ||||
| import WxMpSelect from '@/views/mp/components/WxMpSelect.vue' | ||||
| import * as MpAutoReplyApi from '@/api/mp/autoReply' | ||||
| 
 | ||||
| import { DICT_TYPE, getDictOptions } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import { ContentWrap } from '@/components/ContentWrap' | ||||
| import type { TabPaneName } from 'element-plus' | ||||
| 
 | ||||
| const message = useMessage() | ||||
| 
 | ||||
| // const queryFormRef = ref() | ||||
| const formRef = ref() | ||||
| 
 | ||||
| // tab 类型(1、关注时回复;2、消息回复;3、关键词回复) | ||||
| const type = ref('3') | ||||
| // 消息类型(Follow: 关注时回复;Message: 消息回复;Keyword: 关键词回复) | ||||
| // 作为tab.name | ||||
| enum MsgType { | ||||
|   Follow = 1, | ||||
|   Message = 2, | ||||
|   Keyword = 3 | ||||
| } | ||||
| const msgType = ref<MsgType>(MsgType.Keyword) | ||||
| // 允许选择的请求消息类型 | ||||
| const requestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link'] | ||||
| const RequestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link'] | ||||
| // 遮罩层 | ||||
| const loading = ref(true) | ||||
| // 显示搜索条件 | ||||
| // const showSearch = ref(true) | ||||
| // 总条数 | ||||
| const total = ref(0) | ||||
| // 自动回复列表 | ||||
| const list = ref([]) | ||||
| const list = ref<any[]>([]) | ||||
| 
 | ||||
| // 查询参数 | ||||
| const queryParams = reactive({ | ||||
| interface QueryParams { | ||||
|   pageNo: number | ||||
|   pageSize: number | ||||
|   accountId?: number | ||||
| } | ||||
| const queryParams: QueryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   accountId: undefined | ||||
|  | @ -200,12 +226,50 @@ const queryParams = reactive({ | |||
| // 弹出层标题 | ||||
| const title = ref('') | ||||
| // 是否显示弹出层 | ||||
| const open = ref(false) | ||||
| const showReplyFormDialog = ref(false) | ||||
| // 表单参数 | ||||
| const form = ref({}) | ||||
| type ReplyType = 'text' | 'image' | 'voice' | 'video' | 'shortvideo' | 'location' | 'link' | ||||
| interface ReplyForm { | ||||
|   // relation: | ||||
|   id?: number | ||||
|   accountId?: number | ||||
|   type?: MsgType | ||||
|   // request: | ||||
|   requestMessageType?: ReplyType | ||||
|   requestMatch?: number | ||||
|   requestKeyword?: string | ||||
|   // response: | ||||
|   responseMessageType?: ReplyType | ||||
|   responseContent?: string | ||||
|   responseMediaId?: number | ||||
|   responseMediaUrl?: string | ||||
|   responseTitle?: string | ||||
|   responseDescription?: number | ||||
|   responseThumbMediaId?: string | ||||
|   responseThumbMediaUrl?: string | ||||
|   responseArticles?: any[] | ||||
|   responseMusicUrl?: string | ||||
|   responseHqMusicUrl?: string | ||||
| } | ||||
| interface ObjData { | ||||
|   type: ReplyType | ||||
|   accountId?: number | ||||
|   content?: string | ||||
|   mediaId?: number | ||||
|   url?: string | ||||
|   title?: string | ||||
|   description?: string | ||||
|   thumbMediaId?: number | ||||
|   thumbMediaUrl?: string | ||||
|   articles?: any[] | ||||
|   musicUrl?: string | ||||
|   hqMusicUrl?: string | ||||
| } | ||||
| const replyForm = ref<ReplyForm>({}) | ||||
| // 回复消息 | ||||
| const objData = ref({ | ||||
|   type: 'text' | ||||
| const objData = ref<ObjData>({ | ||||
|   type: 'text', | ||||
|   accountId: undefined | ||||
| }) | ||||
| // 表单校验 | ||||
| const rules = { | ||||
|  | @ -216,8 +280,8 @@ const rules = { | |||
| // 重置 WxReplySelect 组件,解决无法清除的问题 | ||||
| const hackResetWxReplySelect = ref(false) | ||||
| 
 | ||||
| const accountChanged = (accountId) => { | ||||
|   queryParams.accountId = accountId | ||||
| const onAccountChanged = (id?: number) => { | ||||
|   queryParams.accountId = id | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
|  | @ -227,7 +291,7 @@ const getList = async () => { | |||
|   try { | ||||
|     const data = await MpAutoReplyApi.getAutoReplyPage({ | ||||
|       ...queryParams, | ||||
|       type: type.value | ||||
|       type: msgType.value | ||||
|     }) | ||||
|     list.value = data.list | ||||
|     total.value = data.total | ||||
|  | @ -242,8 +306,8 @@ const handleQuery = () => { | |||
|   getList() | ||||
| } | ||||
| 
 | ||||
| const handleTabChange = (tabName) => { | ||||
|   type.value = tabName | ||||
| const handleTabChange = (tabName: TabPaneName) => { | ||||
|   msgType.value = tabName as MsgType | ||||
|   handleQuery() | ||||
| } | ||||
| 
 | ||||
|  | @ -252,29 +316,29 @@ const handleAdd = () => { | |||
|   reset() | ||||
|   resetEditor() | ||||
|   // 打开表单,并设置初始化 | ||||
|   open.value = true | ||||
|   title.value = '新增自动回复' | ||||
|   objData.value = { | ||||
|     type: 'text', | ||||
|     accountId: queryParams.accountId | ||||
|   } | ||||
| 
 | ||||
|   title.value = '新增自动回复' | ||||
|   showReplyFormDialog.value = true | ||||
| } | ||||
| 
 | ||||
| /** 修改按钮操作 */ | ||||
| const handleUpdate = (row) => { | ||||
| const handleUpdate = async (row: any) => { | ||||
|   reset() | ||||
|   resetEditor() | ||||
|   console.log(row) | ||||
| 
 | ||||
|   MpAutoReplyApi.getAutoReply(row.id).then((data) => { | ||||
|   const data = await MpAutoReplyApi.getAutoReply(row.id) | ||||
|   // 设置属性 | ||||
|     form.value = { ...data } | ||||
|     delete form.value['responseMessageType'] | ||||
|     delete form.value['responseContent'] | ||||
|     delete form.value['responseMediaId'] | ||||
|     delete form.value['responseMediaUrl'] | ||||
|     delete form.value['responseDescription'] | ||||
|     delete form.value['responseArticles'] | ||||
|   replyForm.value = { ...data } | ||||
|   delete replyForm.value['responseMessageType'] | ||||
|   delete replyForm.value['responseContent'] | ||||
|   delete replyForm.value['responseMediaId'] | ||||
|   delete replyForm.value['responseMediaUrl'] | ||||
|   delete replyForm.value['responseDescription'] | ||||
|   delete replyForm.value['responseArticles'] | ||||
|   objData.value = { | ||||
|     type: data.responseMessageType, | ||||
|     accountId: queryParams.accountId, | ||||
|  | @ -291,55 +355,48 @@ const handleUpdate = (row) => { | |||
|   } | ||||
| 
 | ||||
|   // 打开表单 | ||||
|     open.value = true | ||||
|   title.value = '修改自动回复' | ||||
|   }) | ||||
|   showReplyFormDialog.value = true | ||||
| } | ||||
| 
 | ||||
| const handleSubmit = () => { | ||||
|   formRef.value?.validate((valid) => { | ||||
|     if (!valid) { | ||||
|       return | ||||
|     } | ||||
| const handleSubmit = async () => { | ||||
|   const valid = await formRef.value?.validate() | ||||
|   if (!valid) return | ||||
| 
 | ||||
|   // 处理回复消息 | ||||
|     const form = { ...form.value } | ||||
|     form.responseMessageType = objData.value.type | ||||
|     form.responseContent = objData.value.content | ||||
|     form.responseMediaId = objData.value.mediaId | ||||
|     form.responseMediaUrl = objData.value.url | ||||
|     form.responseTitle = objData.value.title | ||||
|     form.responseDescription = objData.value.description | ||||
|     form.responseThumbMediaId = objData.value.thumbMediaId | ||||
|     form.responseThumbMediaUrl = objData.value.thumbMediaUrl | ||||
|     form.responseArticles = objData.value.articles | ||||
|     form.responseMusicUrl = objData.value.musicUrl | ||||
|     form.responseHqMusicUrl = objData.value.hqMusicUrl | ||||
|   const submitForm: any = { ...replyForm.value } | ||||
|   submitForm.responseMessageType = objData.value.type | ||||
|   submitForm.responseContent = objData.value.content | ||||
|   submitForm.responseMediaId = objData.value.mediaId | ||||
|   submitForm.responseMediaUrl = objData.value.url | ||||
|   submitForm.responseTitle = objData.value.title | ||||
|   submitForm.responseDescription = objData.value.description | ||||
|   submitForm.responseThumbMediaId = objData.value.thumbMediaId | ||||
|   submitForm.responseThumbMediaUrl = objData.value.thumbMediaUrl | ||||
|   submitForm.responseArticles = objData.value.articles | ||||
|   submitForm.responseMusicUrl = objData.value.musicUrl | ||||
|   submitForm.responseHqMusicUrl = objData.value.hqMusicUrl | ||||
| 
 | ||||
|     if (form.value.id !== undefined) { | ||||
|       MpAutoReplyApi.updateAutoReply(form).then(() => { | ||||
|   if (replyForm.value.id !== undefined) { | ||||
|     await MpAutoReplyApi.updateAutoReply(submitForm) | ||||
|     message.success('修改成功') | ||||
|         open.value = false | ||||
|         getList() | ||||
|       }) | ||||
|   } else { | ||||
|       MpAutoReplyApi.createAutoReply(form).then(() => { | ||||
|     await MpAutoReplyApi.createAutoReply(submitForm) | ||||
|     message.success('新增成功') | ||||
|         open.value = false | ||||
|         getList() | ||||
|       }) | ||||
|   } | ||||
|   }) | ||||
| 
 | ||||
|   showReplyFormDialog.value = false | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
| // 表单重置 | ||||
| const reset = () => { | ||||
|   form.value = { | ||||
|   replyForm.value = { | ||||
|     id: undefined, | ||||
|     accountId: queryParams.accountId, | ||||
|     type: type.value, | ||||
|     type: msgType.value, | ||||
|     requestKeyword: undefined, | ||||
|     requestMatch: type.value === '3' ? 1 : undefined, | ||||
|     requestMatch: msgType.value === MsgType.Keyword ? 1 : undefined, | ||||
|     requestMessageType: undefined | ||||
|   } | ||||
|   formRef.value?.resetFields() | ||||
|  | @ -347,7 +404,7 @@ const reset = () => { | |||
| 
 | ||||
| // 取消按钮 | ||||
| const cancel = () => { | ||||
|   open.value = false | ||||
|   showReplyFormDialog.value = false | ||||
|   reset() | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,22 +1,20 @@ | |||
| <template> | ||||
|   <el-select | ||||
|     v-model="accountId" | ||||
|     placeholder="请选择公众号" | ||||
|     class="!w-240px" | ||||
|     @change="accountChanged" | ||||
|   > | ||||
|   <el-select v-model="account.id" placeholder="请选择公众号" class="!w-240px" @change="onChanged"> | ||||
|     <el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" /> | ||||
|   </el-select> | ||||
| </template> | ||||
| 
 | ||||
| <!-- TODO @芋艿:WxMpSelect 改成 WxAccountSelect,然后挪到现有的 wx-account-select 包下 --> | ||||
| <script lang="ts" setup name="WxMpSelect"> | ||||
| import * as MpAccountApi from '@/api/mp/account' | ||||
| 
 | ||||
| const accountId: Ref<number | undefined> = ref() | ||||
| const account: MpAccountApi.AccountVO = reactive({ | ||||
|   id: undefined, | ||||
|   name: '' | ||||
| }) | ||||
| const accountList: Ref<MpAccountApi.AccountVO[]> = ref([]) | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
|   (e: 'change', id: number | undefined): void | ||||
|   (e: 'change', id?: number, name?: string): void | ||||
| }>() | ||||
| 
 | ||||
| onMounted(() => { | ||||
|  | @ -27,12 +25,12 @@ const handleQuery = async () => { | |||
|   accountList.value = await MpAccountApi.getSimpleAccountList() | ||||
|   // 默认选中第一个 | ||||
|   if (accountList.value.length > 0) { | ||||
|     accountId.value = accountList.value[0].id | ||||
|     emit('change', accountId.value) | ||||
|     account.id = accountList.value[0].id | ||||
|     emit('change', account.id, account.name) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const accountChanged = () => { | ||||
|   emit('change', accountId.value) | ||||
| const onChanged = () => { | ||||
|   emit('change', account.id, account.name) | ||||
| } | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,11 +1,11 @@ | |||
| <script name="WxEditor" setup> | ||||
| import { reactive, ref } from 'vue' | ||||
| import { QuillEditor } from '@vueup/vue-quill' | ||||
| import '@vueup/vue-quill/dist/vue-quill.snow.css' | ||||
| import { ref, reactive } from 'vue' | ||||
| import { getAccessToken } from '@/utils/auth' | ||||
| import editorOptions from './quill-options' | ||||
| import { Editor } from '@/components/Editor' | ||||
| 
 | ||||
| const BASE_URL = import.meta.env.VITE_BASE_URL | ||||
| const actionUrl = BASE_URL + '/admin-api/mp/material/upload-news-image' // 这里写你要上传的图片服务器地址 | ||||
| const headers = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部 | ||||
| 
 | ||||
| const message = useMessage() | ||||
| 
 | ||||
|  | @ -30,21 +30,16 @@ const props = defineProps({ | |||
| const emit = defineEmits(['input']) | ||||
| 
 | ||||
| const myQuillEditorRef = ref() | ||||
| 
 | ||||
| const content = ref(props.value.replace(/data-src/g, 'src')) | ||||
| 
 | ||||
| const loading = ref(false) // 根据图片上传状态来确定是否显示loading动画,刚开始是false,不显示 | ||||
| 
 | ||||
| const actionUrl = ref(BASE_URL + '/admin-api/mp/material/upload-news-image') // 这里写你要上传的图片服务器地址 | ||||
| const headers = ref({ Authorization: 'Bearer ' + getAccessToken() }) // 设置上传的请求头部 | ||||
| const uploadData = reactive({ | ||||
|   type: 'image', // TODO 芋艿:试试要不要换成 thumb | ||||
|   accountId: props.accountId | ||||
| }) | ||||
| 
 | ||||
| const onEditorChange = () => { | ||||
| const onEditorChange = (text) => { | ||||
|   //内容改变事件 | ||||
|   emit('input', content.value) | ||||
|   emit('input', text) | ||||
| } | ||||
| 
 | ||||
| // 富文本图片上传前 | ||||
|  | @ -88,114 +83,22 @@ const uploadError = () => { | |||
|     <div v-loading="loading" element-loading-text="请稍等,图片上传中"> | ||||
|       <!-- 图片上传组件辅助--> | ||||
|       <el-upload | ||||
|         :action="actionUrl" | ||||
|         :before-upload="beforeUpload" | ||||
|         :data="uploadData" | ||||
|         :headers="headers" | ||||
|         :on-error="uploadError" | ||||
|         :on-success="uploadSuccess" | ||||
|         :show-file-list="false" | ||||
|         class="avatar-uploader" | ||||
|         name="file" | ||||
|         :action="actionUrl" | ||||
|         :headers="headers" | ||||
|         :show-file-list="false" | ||||
|         :data="uploadData" | ||||
|         :on-success="uploadSuccess" | ||||
|         :on-error="uploadError" | ||||
|         :before-upload="beforeUpload" | ||||
|       /> | ||||
|       <QuillEditor | ||||
|       <Editor | ||||
|         editor-id="wxEditor" | ||||
|         ref="quillEditorRef" | ||||
|         v-model="content" | ||||
|         :options="editorOptions" | ||||
|         class="editor" | ||||
|         @change="onEditorChange($event)" | ||||
|         :modelValue="content" | ||||
|         @change="(editor) => onEditorChange(editor.getText())" | ||||
|       /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <style> | ||||
| .editor { | ||||
|   line-height: normal !important; | ||||
|   height: 500px; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-tooltip[data-mode='link']::before { | ||||
|   content: '请输入链接地址:'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-tooltip.ql-editing a.ql-action::after { | ||||
|   border-right: 0; | ||||
|   content: '保存'; | ||||
|   padding-right: 0; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-tooltip[data-mode='video']::before { | ||||
|   content: '请输入视频地址:'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-size .ql-picker-label::before, | ||||
| .ql-snow .ql-picker.ql-size .ql-picker-item::before { | ||||
|   content: '14px'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before, | ||||
| .ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before { | ||||
|   content: '10px'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before, | ||||
| .ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before { | ||||
|   content: '18px'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before, | ||||
| .ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before { | ||||
|   content: '32px'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-label::before, | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-item::before { | ||||
|   content: '文本'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before, | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before { | ||||
|   content: '标题1'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before, | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before { | ||||
|   content: '标题2'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before, | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before { | ||||
|   content: '标题3'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before, | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before { | ||||
|   content: '标题4'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before, | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before { | ||||
|   content: '标题5'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before, | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before { | ||||
|   content: '标题6'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-font .ql-picker-label::before, | ||||
| .ql-snow .ql-picker.ql-font .ql-picker-item::before { | ||||
|   content: '标准字体'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before, | ||||
| .ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before { | ||||
|   content: '衬线字体'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before, | ||||
| .ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before { | ||||
|   content: '等宽字体'; | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -31,7 +31,6 @@ | |||
|           /> | ||||
|         </el-row> | ||||
|         <el-row> | ||||
|           <el-icon><Location /></el-icon> | ||||
|           <Icon icon="ep:location" /> | ||||
|           {{ label }} | ||||
|         </el-row> | ||||
|  | @ -39,6 +38,7 @@ | |||
|     </el-link> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts" name="WxLocation"> | ||||
| const props = defineProps({ | ||||
|   locationX: { | ||||
|  |  | |||
|  | @ -39,79 +39,79 @@ | |||
|               :style="item.sendFrom === 2 ? 'background: #6BED72;' : ''" | ||||
|             > | ||||
|               <!-- 【事件】区域 --> | ||||
|               <div v-if="item.type === 'event' && item.event === 'subscribe'"> | ||||
|               <div v-if="item.type === MsgType.Event && item.event === 'subscribe'"> | ||||
|                 <el-tag type="success">关注</el-tag> | ||||
|               </div> | ||||
|               <div v-else-if="item.type === 'event' && item.event === 'unsubscribe'"> | ||||
|               <div v-else-if="item.type === MsgType.Event && item.event === 'unsubscribe'"> | ||||
|                 <el-tag type="danger">取消关注</el-tag> | ||||
|               </div> | ||||
|               <div v-else-if="item.type === 'event' && item.event === 'CLICK'"> | ||||
|               <div v-else-if="item.type === MsgType.Event && item.event === 'CLICK'"> | ||||
|                 <el-tag>点击菜单</el-tag> | ||||
|                 【{{ item.eventKey }}】 | ||||
|               </div> | ||||
|               <div v-else-if="item.type === 'event' && item.event === 'VIEW'"> | ||||
|               <div v-else-if="item.type === MsgType.Event && item.event === 'VIEW'"> | ||||
|                 <el-tag>点击菜单链接</el-tag> | ||||
|                 【{{ item.eventKey }}】 | ||||
|               </div> | ||||
|               <div v-else-if="item.type === 'event' && item.event === 'scancode_waitmsg'"> | ||||
|               <div v-else-if="item.type === MsgType.Event && item.event === 'scancode_waitmsg'"> | ||||
|                 <el-tag>扫码结果</el-tag> | ||||
|                 【{{ item.eventKey }}】 | ||||
|               </div> | ||||
|               <div v-else-if="item.type === 'event' && item.event === 'scancode_push'"> | ||||
|               <div v-else-if="item.type === MsgType.Event && item.event === 'scancode_push'"> | ||||
|                 <el-tag>扫码结果</el-tag> | ||||
|                 【{{ item.eventKey }}】 | ||||
|               </div> | ||||
|               <div v-else-if="item.type === 'event' && item.event === 'pic_sysphoto'"> | ||||
|               <div v-else-if="item.type === MsgType.Event && item.event === 'pic_sysphoto'"> | ||||
|                 <el-tag>系统拍照发图</el-tag> | ||||
|               </div> | ||||
|               <div v-else-if="item.type === 'event' && item.event === 'pic_photo_or_album'"> | ||||
|               <div v-else-if="item.type === MsgType.Event && item.event === 'pic_photo_or_album'"> | ||||
|                 <el-tag>拍照或者相册</el-tag> | ||||
|               </div> | ||||
|               <div v-else-if="item.type === 'event' && item.event === 'pic_weixin'"> | ||||
|               <div v-else-if="item.type === MsgType.Event && item.event === 'pic_weixin'"> | ||||
|                 <el-tag>微信相册</el-tag> | ||||
|               </div> | ||||
|               <div v-else-if="item.type === 'event' && item.event === 'location_select'"> | ||||
|               <div v-else-if="item.type === MsgType.Event && item.event === 'location_select'"> | ||||
|                 <el-tag>选择地理位置</el-tag> | ||||
|               </div> | ||||
|               <div v-else-if="item.type === 'event'"> | ||||
|               <div v-else-if="item.type === MsgType.Event"> | ||||
|                 <el-tag type="danger">未知事件类型</el-tag> | ||||
|               </div> | ||||
|               <!-- 【消息】区域 --> | ||||
|               <div v-else-if="item.type === 'text'">{{ item.content }}</div> | ||||
|               <div v-else-if="item.type === 'voice'"> | ||||
|                 <wx-voice-player :url="item.mediaUrl" :content="item.recognition" /> | ||||
|               <div v-else-if="item.type === MsgType.Text">{{ item.content }}</div> | ||||
|               <div v-else-if="item.type === MsgType.Voice"> | ||||
|                 <WxVoicePlayer :url="item.mediaUrl" :content="item.recognition" /> | ||||
|               </div> | ||||
|               <div v-else-if="item.type === 'image'"> | ||||
|               <div v-else-if="item.type === MsgType.Image"> | ||||
|                 <a target="_blank" :href="item.mediaUrl"> | ||||
|                   <img :src="item.mediaUrl" style="width: 100px" /> | ||||
|                 </a> | ||||
|               </div> | ||||
|               <div | ||||
|                 v-else-if="item.type === 'video' || item.type === 'shortvideo'" | ||||
|                 v-else-if="item.type === MsgType.Video || item.type === 'shortvideo'" | ||||
|                 style="text-align: center" | ||||
|               > | ||||
|                 <wx-video-player :url="item.mediaUrl" /> | ||||
|                 <WxVideoPlayer :url="item.mediaUrl" /> | ||||
|               </div> | ||||
|               <div v-else-if="item.type === 'link'" class="avue-card__detail"> | ||||
|               <div v-else-if="item.type === MsgType.Link" class="avue-card__detail"> | ||||
|                 <el-link type="success" :underline="false" target="_blank" :href="item.url"> | ||||
|                   <div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div> | ||||
|                 </el-link> | ||||
|                 <div class="avue-card__info" style="height: unset">{{ item.description }}</div> | ||||
|               </div> | ||||
|               <!-- TODO 芋艿:待完善 --> | ||||
|               <div v-else-if="item.type === 'location'"> | ||||
|                 <wx-location | ||||
|               <div v-else-if="item.type === MsgType.Location"> | ||||
|                 <WxLocation | ||||
|                   :label="item.label" | ||||
|                   :location-y="item.locationY" | ||||
|                   :location-x="item.locationX" | ||||
|                 /> | ||||
|               </div> | ||||
|               <div v-else-if="item.type === 'news'" style="width: 300px"> | ||||
|               <div v-else-if="item.type === MsgType.News" style="width: 300px"> | ||||
|                 <!-- TODO 芋艿:待测试;详情页也存在类似的情况 --> | ||||
|                 <wx-news :articles="item.articles" /> | ||||
|                 <WxNews :articles="item.articles" /> | ||||
|               </div> | ||||
|               <div v-else-if="item.type === 'music'"> | ||||
|                 <wx-music | ||||
|               <div v-else-if="item.type === MsgType.Music"> | ||||
|                 <WxMusic | ||||
|                   :title="item.title" | ||||
|                   :description="item.description" | ||||
|                   :thumb-media-url="item.thumbMediaUrl" | ||||
|  | @ -125,100 +125,126 @@ | |||
|       </div> | ||||
|     </div> | ||||
|     <div class="msg-send" v-loading="sendLoading"> | ||||
|       <wx-reply-select ref="replySelect" :objData="objData" /> | ||||
|       <WxReplySelect ref="replySelectRef" :objData="objData" /> | ||||
|       <el-button type="success" size="small" class="send-but" @click="sendMsg">发送(S)</el-button> | ||||
|     </div> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" name="WxMsg"> | ||||
| import { getMessagePage, sendMessage } from '@/api/mp/message' | ||||
| <script setup lang="ts" name="WxMsg"> | ||||
| import WxReplySelect from '@/views/mp/components/wx-reply/main.vue' | ||||
| import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue' | ||||
| import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue' | ||||
| import WxNews from '@/views/mp/components/wx-news/main.vue' | ||||
| import WxLocation from '@/views/mp/components/wx-location/main.vue' | ||||
| import WxMusic from '@/views/mp/components/wx-music/main.vue' | ||||
| import { getMessagePage, sendMessage } from '@/api/mp/message' | ||||
| import { getUser } from '@/api/mp/user' | ||||
| import { defineComponent } from 'vue' | ||||
| 
 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| import { formatDate } from '@/utils/formatTime' | ||||
| import profile from '@/assets/imgs/profile.jpg' | ||||
| import wechat from '@/assets/imgs/wechat.png' | ||||
| import { formatDate } from '@/utils/formatTime' | ||||
| import { MsgType } from './types' | ||||
| 
 | ||||
| export default defineComponent({ | ||||
|   components: { | ||||
|     WxReplySelect, | ||||
|     WxVideoPlayer, | ||||
|     WxVoicePlayer, | ||||
|     WxNews, | ||||
|     WxLocation, | ||||
|     WxMusic | ||||
|   }, | ||||
|   props: { | ||||
| const message = useMessage() // 消息弹窗 | ||||
| 
 | ||||
| const props = defineProps({ | ||||
|   userId: { | ||||
|     type: Number, | ||||
|     required: true | ||||
|   } | ||||
|   }, | ||||
|   setup(props) { | ||||
|     const nowStr = ref(new Date().getTime()) // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处 | ||||
|     const loading = ref(false) // 消息列表是否正在加载中 | ||||
|     const loadMore = ref(true) // 是否可以加载更多 | ||||
|     const list = ref<any[]>([]) // 消息列表 | ||||
|     const queryParams = reactive({ | ||||
| }) | ||||
| 
 | ||||
| const nowStr = ref(new Date().getTime()) // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处 | ||||
| const loading = ref(false) // 消息列表是否正在加载中 | ||||
| const loadMore = ref(true) // 是否可以加载更多 | ||||
| const list = ref<any[]>([]) // 消息列表 | ||||
| const queryParams = reactive({ | ||||
|   pageNo: 1, // 当前页数 | ||||
|   pageSize: 14, // 每页显示多少条 | ||||
|   accountId: undefined | ||||
|     }) | ||||
|     const user = reactive({ | ||||
|       // 由于微信不再提供昵称,直接使用“用户”展示 | ||||
| }) | ||||
| 
 | ||||
| interface User { | ||||
|   nickname: string | ||||
|   avatar: string | ||||
|   accountId: number | ||||
| } | ||||
| // 由于微信不再提供昵称,直接使用“用户”展示 | ||||
| const user: User = reactive({ | ||||
|   nickname: '用户', | ||||
|   avatar: profile, | ||||
|   accountId: 0 // 公众号账号编号 | ||||
|     }) | ||||
|     const mp = reactive({ | ||||
| }) | ||||
| 
 | ||||
| interface Mp { | ||||
|   nickname: string | ||||
|   avatar: string | ||||
| } | ||||
| const mp: Mp = reactive({ | ||||
|   nickname: '公众号', | ||||
|   avatar: wechat | ||||
|     }) | ||||
| }) | ||||
| 
 | ||||
|     // ========= 消息发送 ========= | ||||
|     const sendLoading = ref(false) // 发送消息是否加载中 | ||||
|     const objData = reactive({ | ||||
|       // 微信发送消息 | ||||
|       type: 'text', | ||||
| // ========= 消息发送 ========= | ||||
| const sendLoading = ref(false) // 发送消息是否加载中 | ||||
| interface ObjData { | ||||
|   type: MsgType | ||||
|   accountId: number | null | ||||
|   articles: any[] | ||||
| } | ||||
| 
 | ||||
| // 微信发送消息 | ||||
| const objData: ObjData = reactive({ | ||||
|   type: MsgType.Text, | ||||
|   accountId: null, | ||||
|   articles: [] | ||||
|     }) | ||||
| }) | ||||
| 
 | ||||
|     const replySelect = ref(null) | ||||
|     // 执行发送 | ||||
|     const sendMsg = async () => { | ||||
| const replySelectRef = ref<InstanceType<typeof WxReplySelect> | null>(null) | ||||
| 
 | ||||
| /** 完成加载 */ | ||||
| onMounted(async () => { | ||||
|   const data = await getUser(props.userId) | ||||
|   user.nickname = data.nickname?.length > 0 ? data.nickname : user.nickname | ||||
|   user.avatar = user.avatar?.length > 0 ? data.avatar : user.avatar | ||||
|   user.accountId = data.accountId | ||||
|   queryParams.accountId = data.accountId | ||||
|   objData.accountId = data.accountId | ||||
| 
 | ||||
|   refreshChange() | ||||
| }) | ||||
| 
 | ||||
| // 执行发送 | ||||
| const sendMsg = async () => { | ||||
|   if (!objData) { | ||||
|     return | ||||
|   } | ||||
|       //     // 公众号限制:客服消息,公众号只允许发送一条 | ||||
|       if (objData.type === 'news' && objData.articles.length > 1) { | ||||
|   // 公众号限制:客服消息,公众号只允许发送一条 | ||||
|   if (objData.type === MsgType.News && objData.articles.length > 1) { | ||||
|     objData.articles = [objData.articles[0]] | ||||
|     message.success('图文消息条数限制在 1 条以内,已默认发送第一条') | ||||
|   } | ||||
|       let data = await sendMessage(Object.assign({ userId: props.userId }, { ...objData })) | ||||
| 
 | ||||
|   const data = await sendMessage(Object.assign({ userId: props.userId }, { ...objData })) | ||||
|   sendLoading.value = false | ||||
| 
 | ||||
|   list.value = [...list.value, ...[data]] | ||||
|   scrollToBottom() | ||||
|       //ts檢查的時候會判斷這個組件可能是空的,所以需要進行斷言。 | ||||
| 
 | ||||
|   //ts检查的時候会判断这个组件可能是空的,所以需要进行断言。 | ||||
|   //避免 tab 的数据未清理 | ||||
|       const deleteObj = (replySelect.value as any).deleteObj | ||||
|   const deleteObj = replySelectRef.value?.deleteObj | ||||
|   if (deleteObj) { | ||||
|     deleteObj() | ||||
|   } | ||||
|     } | ||||
|     const loadingMore = () => { | ||||
| } | ||||
| 
 | ||||
| const loadingMore = () => { | ||||
|   queryParams.pageNo++ | ||||
|   getPage(queryParams, null) | ||||
|     } | ||||
|     const getPage = async (page, params) => { | ||||
| } | ||||
| 
 | ||||
| const getPage = async (page, params) => { | ||||
|   loading.value = true | ||||
|   let dataTemp = await getMessagePage( | ||||
|     Object.assign( | ||||
|  | @ -231,13 +257,14 @@ export default defineComponent({ | |||
|       params | ||||
|     ) | ||||
|   ) | ||||
| 
 | ||||
|   const msgDiv = document.getElementById('msg-div' + nowStr.value) | ||||
|   let scrollHeight = 0 | ||||
|   if (msgDiv) { | ||||
|     scrollHeight = msgDiv.scrollHeight | ||||
|   } | ||||
|   // 处理数据 | ||||
|       let data = dataTemp.list.reverse() | ||||
|   const data = dataTemp.list.reverse() | ||||
|   list.value = [...data, ...list.value] | ||||
|   loading.value = false | ||||
|   if (data.length < queryParams.pageSize || data.length === 0) { | ||||
|  | @ -260,47 +287,23 @@ export default defineComponent({ | |||
|       } | ||||
|     }) | ||||
|   } | ||||
|     } | ||||
|     const refreshChange = () => { | ||||
| } | ||||
| 
 | ||||
| const refreshChange = () => { | ||||
|   getPage(queryParams, null) | ||||
|     } | ||||
|     /** 定位到消息底部 */ | ||||
|     const scrollToBottom = () => { | ||||
| } | ||||
| 
 | ||||
| /** 定位到消息底部 */ | ||||
| const scrollToBottom = () => { | ||||
|   nextTick(() => { | ||||
|     let div = document.getElementById('msg-div' + nowStr.value) | ||||
|     if (div) { | ||||
|       div.scrollTop = div.scrollHeight | ||||
|     } | ||||
|   }) | ||||
|     } | ||||
| 
 | ||||
|     onMounted(async () => { | ||||
|       let data = await getUser(props.userId) | ||||
|       user.nickname = data.nickname && data.nickname.length > 0 ? data.nickname : user.nickname | ||||
|       user.avatar = data.avatar && user.avatar.length > 0 ? data.avatar : user.avatar | ||||
|       user.accountId = data.accountId | ||||
|       queryParams.accountId = data.accountId | ||||
|       objData.accountId = data.accountId | ||||
|       refreshChange() | ||||
|     }) | ||||
|     return { | ||||
|       sendMsg, | ||||
|       loadingMore, | ||||
|       formatDate, | ||||
|       scrollToBottom, | ||||
|       objData, | ||||
|       mp, | ||||
|       user, | ||||
|       queryParams, | ||||
|       list, | ||||
|       loadMore, | ||||
|       loading, | ||||
|       nowStr, | ||||
|       sendLoading | ||||
|     } | ||||
|   } | ||||
| }) | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| /* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc  */ | ||||
| @import './comment.scss'; | ||||
|  |  | |||
|  | @ -0,0 +1,11 @@ | |||
| export enum MsgType { | ||||
|   Event = 'event', | ||||
|   Text = 'text', | ||||
|   Voice = 'voice', | ||||
|   Image = 'image', | ||||
|   Video = 'video', | ||||
|   Link = 'link', | ||||
|   Location = 'location', | ||||
|   Music = 'music', | ||||
|   News = 'news' | ||||
| } | ||||
|  | @ -3,14 +3,22 @@ | |||
| 
 | ||||
|   <!-- 搜索工作栏 --> | ||||
|   <ContentWrap> | ||||
|     <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect  --> | ||||
|     <WxAccountSelect @change="accountChanged"> | ||||
|       <template #actions> | ||||
|     <el-form | ||||
|       class="-mb-15px" | ||||
|       :model="queryParams" | ||||
|       ref="queryFormRef" | ||||
|       :inline="true" | ||||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item label="公众号" prop="accountId"> | ||||
|         <WxMpSelect @change="onAccountChanged" /> | ||||
|       </el-form-item> | ||||
|       <el-form-item> | ||||
|         <el-button type="primary" plain @click="handleAdd" v-hasPermi="['mp:draft:create']"> | ||||
|           <Icon icon="ep:plus" />新增 | ||||
|         </el-button> | ||||
|       </template> | ||||
|     </WxAccountSelect> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 列表 --> | ||||
|  | @ -58,10 +66,8 @@ | |||
|     /> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- TODO @Dhb52:迁移成独立路由 --> | ||||
|   <div class="app-container"> | ||||
|     <!-- 添加或修改草稿对话框 --> | ||||
|     <Teleport to="body"> | ||||
|     <el-dialog | ||||
|       :title="operateMaterial === 'add' ? '新建图文' : '修改图文'" | ||||
|       width="80%" | ||||
|  | @ -133,7 +139,7 @@ | |||
|             <el-button | ||||
|               type="primary" | ||||
|               circle | ||||
|                 @click="plusNews(item)" | ||||
|               @click="plusNews" | ||||
|               v-if="articlesAdd.length < 8 && operateMaterial === 'add'" | ||||
|             > | ||||
|               <Icon icon="ep:plus" /> | ||||
|  | @ -176,7 +182,7 @@ | |||
|             /> | ||||
|             <div class="thumb-but"> | ||||
|               <el-upload | ||||
|                   :action="actionUrl" | ||||
|                 :action="uploadUrl" | ||||
|                 :headers="headers" | ||||
|                 multiple | ||||
|                 :limit="1" | ||||
|  | @ -200,15 +206,13 @@ | |||
|                 </template> | ||||
|               </el-upload> | ||||
|             </div> | ||||
|               <Teleport to="body"> | ||||
|                 <el-dialog title="选择图片" v-model="dialogImageVisible" width="80%"> | ||||
|             <el-dialog title="选择图片" v-model="dialogImageVisible" width="80%" append-to-body> | ||||
|               <WxMaterialSelect | ||||
|                 ref="materialSelectRef" | ||||
|                 :objData="{ type: 'image', accountId: queryParams.accountId }" | ||||
|                 @select-material="selectMaterial" | ||||
|               /> | ||||
|             </el-dialog> | ||||
|               </Teleport> | ||||
|           </div> | ||||
|           <el-input | ||||
|             :rows="8" | ||||
|  | @ -234,38 +238,48 @@ | |||
|         <el-button type="primary" @click="submitForm">提 交</el-button> | ||||
|       </template> | ||||
|     </el-dialog> | ||||
|     </Teleport> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup name="MpDraft"> | ||||
| 
 | ||||
| <script setup lang="ts" name="MpDraft"> | ||||
| import WxEditor from '@/views/mp/components/wx-editor/WxEditor.vue' | ||||
| import WxNews from '@/views/mp/components/wx-news/main.vue' | ||||
| import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue' | ||||
| import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' | ||||
| import WxMpSelect from '@/views/mp/components/WxMpSelect.vue' | ||||
| import { getAccessToken } from '@/utils/auth' | ||||
| import * as MpDraftApi from '@/api/mp/draft' | ||||
| import * as MpFreePublishApi from '@/api/mp/freePublish' | ||||
| import type { UploadFiles, UploadProps, UploadRawFile } from 'element-plus' | ||||
| // 可以用改本地数据模拟,避免API调用超限 | ||||
| // import drafts from './mock' | ||||
| 
 | ||||
| const message = useMessage() // 消息 | ||||
| 
 | ||||
| const loading = ref(true) // 列表的加载中 | ||||
| const list = ref<any[]>([]) // 列表的数据 | ||||
| const total = ref(0) // 列表的总页数 | ||||
| const list = ref([]) // 列表的数据 | ||||
| const queryParams = reactive({ | ||||
| interface QueryParams { | ||||
|   pageNo: number | ||||
|   pageSize: number | ||||
|   accountId?: number | ||||
| } | ||||
| const queryParams: QueryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   accountId: undefined | ||||
| }) | ||||
| 
 | ||||
| // ========== 文件上传 ========== | ||||
| const materialSelectRef = ref() | ||||
| const BASE_URL = import.meta.env.VITE_BASE_URL | ||||
| const actionUrl = ref(BASE_URL + '/admin-api/mp/material/upload-permanent') // 上传永久素材的地址 | ||||
| const headers = ref({ Authorization: 'Bearer ' + getAccessToken() }) // 设置上传的请求头部 | ||||
| const fileList = ref([]) | ||||
| const uploadData = reactive({ | ||||
| const uploadUrl = BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传永久素材的地址 | ||||
| const headers = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部 | ||||
| 
 | ||||
| const materialSelectRef = ref<InstanceType<typeof WxMaterialSelect> | null>(null) | ||||
| const fileList = ref<UploadFiles>([]) | ||||
| interface UploadData { | ||||
|   type: 'image' | 'video' | 'audio' | ||||
|   accountId?: number | ||||
| } | ||||
| const uploadData: UploadData = reactive({ | ||||
|   type: 'image', | ||||
|   accountId: 1 | ||||
| }) | ||||
|  | @ -273,34 +287,28 @@ const uploadData = reactive({ | |||
| // ========== 草稿新建 or 修改 ========== | ||||
| const dialogNewsVisible = ref(false) | ||||
| const addMaterialLoading = ref(false) // 添加草稿的 loading 标识 | ||||
| const articlesAdd = ref([]) | ||||
| const articlesAdd = ref<any[]>([]) | ||||
| const isActiveAddNews = ref(0) | ||||
| const dialogImageVisible = ref(false) | ||||
| const operateMaterial = ref('add') | ||||
| const operateMaterial = ref<'add' | 'edit'>('add') | ||||
| const articlesMediaId = ref('') | ||||
| const hackResetEditor = ref(false) | ||||
| 
 | ||||
| /** 侦听公众号变化 **/ | ||||
| const accountChanged = (accountId) => { | ||||
|   setAccountId(accountId) | ||||
| const onAccountChanged = (id?: number) => { | ||||
|   setAccountId(id) | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
| // ======================== 列表查询 ======================== | ||||
| /** 设置账号编号 */ | ||||
| const setAccountId = (accountId) => { | ||||
|   queryParams.accountId = accountId | ||||
|   uploadData.accountId = accountId | ||||
| const setAccountId = (id?: number) => { | ||||
|   queryParams.accountId = id | ||||
|   uploadData.accountId = id | ||||
| } | ||||
| 
 | ||||
| /** 查询列表 */ | ||||
| const getList = async () => { | ||||
|   // 如果没有选中公众号账号,则进行提示。 | ||||
|   if (!queryParams.accountId) { | ||||
|     message.error('未选中公众号,无法查询草稿箱') | ||||
|     return false | ||||
|   } | ||||
| 
 | ||||
|   loading.value = true | ||||
|   try { | ||||
|     const drafts = await MpDraftApi.getDraftPage(queryParams) | ||||
|  | @ -329,7 +337,7 @@ const handleAdd = () => { | |||
| } | ||||
| 
 | ||||
| /** 更新按钮操作 */ | ||||
| const handleUpdate = (item) => { | ||||
| const handleUpdate = (item: any) => { | ||||
|   resetEditor() | ||||
|   reset() | ||||
|   articlesMediaId.value = item.mediaId | ||||
|  | @ -340,39 +348,30 @@ const handleUpdate = (item) => { | |||
| } | ||||
| 
 | ||||
| /** 提交按钮 */ | ||||
| const submitForm = () => { | ||||
|   // TODO @Dhb52: 参考别的模块写法,改成 await 方式 | ||||
| const submitForm = async () => { | ||||
|   addMaterialLoading.value = true | ||||
|   try { | ||||
|     if (operateMaterial.value === 'add') { | ||||
|     MpDraftApi.createDraft(queryParams.accountId, articlesAdd.value) | ||||
|       .then(() => { | ||||
|       await MpDraftApi.createDraft(queryParams.accountId, articlesAdd.value) | ||||
|       message.notifySuccess('新增成功') | ||||
|         dialogNewsVisible.value = false | ||||
|         getList() | ||||
|       }) | ||||
|       .finally(() => { | ||||
|         addMaterialLoading.value = false | ||||
|       }) | ||||
|     } else { | ||||
|     MpDraftApi.updateDraft(queryParams.accountId, articlesMediaId.value, articlesAdd.value) | ||||
|       .then(() => { | ||||
|       await MpDraftApi.updateDraft(queryParams.accountId, articlesMediaId.value, articlesAdd.value) | ||||
|       message.notifySuccess('更新成功') | ||||
|     } | ||||
|   } finally { | ||||
|     dialogNewsVisible.value = false | ||||
|         getList() | ||||
|       }) | ||||
|       .finally(() => { | ||||
|     addMaterialLoading.value = false | ||||
|       }) | ||||
|     await getList() | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 关闭弹窗 | ||||
| const dialogNewsClose = async (done) => { | ||||
| const dialogNewsClose = async (onDone: () => {}) => { | ||||
|   try { | ||||
|     await message.confirm('修改内容可能还未保存,确定关闭吗?') | ||||
|     reset() | ||||
|     resetEditor() | ||||
|     done() | ||||
|     onDone() | ||||
|   } catch {} | ||||
| } | ||||
| 
 | ||||
|  | @ -391,7 +390,7 @@ const resetEditor = () => { | |||
| } | ||||
| 
 | ||||
| // 将图文向下移动 | ||||
| const downNews = (index) => { | ||||
| const downNews = (index: number) => { | ||||
|   let temp = articlesAdd.value[index] | ||||
|   articlesAdd.value[index] = articlesAdd.value[index + 1] | ||||
|   articlesAdd.value[index + 1] = temp | ||||
|  | @ -407,13 +406,13 @@ const upNews = (index) => { | |||
| } | ||||
| 
 | ||||
| // 选中指定 index 的图文 | ||||
| const activeNews = (index) => { | ||||
| const activeNews = (index: number) => { | ||||
|   resetEditor() | ||||
|   isActiveAddNews.value = index | ||||
| } | ||||
| 
 | ||||
| // 删除指定 index 的图文 | ||||
| const minusNews = async (index) => { | ||||
| const minusNews = async (index: number) => { | ||||
|   try { | ||||
|     await message.confirm('确定删除该图文吗?') | ||||
|     articlesAdd.value.splice(index, 1) | ||||
|  | @ -446,20 +445,17 @@ const buildEmptyArticle = () => { | |||
| } | ||||
| 
 | ||||
| // ======================== 文件上传 ======================== | ||||
| const beforeThumbImageUpload = (file) => { | ||||
| const beforeThumbImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => { | ||||
|   addMaterialLoading.value = true | ||||
|   const isType = | ||||
|     file.type === 'image/jpeg' || | ||||
|     file.type === 'image/png' || | ||||
|     file.type === 'image/gif' || | ||||
|     file.type === 'image/bmp' || | ||||
|     file.type === 'image/jpg' | ||||
|   const isType = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'].includes( | ||||
|     rawFile.type | ||||
|   ) | ||||
|   if (!isType) { | ||||
|     message.error('上传图片格式不对!') | ||||
|     addMaterialLoading.value = false | ||||
|     return false | ||||
|   } | ||||
|   const isLt = file.size / 1024 / 1024 < 2 | ||||
|   const isLt = rawFile.size / 1024 / 1024 < 2 | ||||
|   if (!isLt) { | ||||
|     message.error('上传图片大小不能超过 2M!') | ||||
|     addMaterialLoading.value = false | ||||
|  | @ -469,7 +465,7 @@ const beforeThumbImageUpload = (file) => { | |||
|   return true | ||||
| } | ||||
| 
 | ||||
| const handleUploadSuccess = (response, file, fileList) => { | ||||
| const handleUploadSuccess: UploadProps['onSuccess'] = (response: any) => { | ||||
|   addMaterialLoading.value = false | ||||
|   if (response.code !== 0) { | ||||
|     message.error('上传出错:' + response.msg) | ||||
|  | @ -485,7 +481,7 @@ const handleUploadSuccess = (response, file, fileList) => { | |||
| } | ||||
| 
 | ||||
| // 选择 or 上传完素材,设置回草稿 | ||||
| const selectMaterial = (item) => { | ||||
| const selectMaterial = (item: any) => { | ||||
|   dialogImageVisible.value = false | ||||
|   articlesAdd.value[isActiveAddNews.value].thumbMediaId = item.mediaId | ||||
|   articlesAdd.value[isActiveAddNews.value].thumbUrl = item.url | ||||
|  | @ -494,14 +490,10 @@ const selectMaterial = (item) => { | |||
| // 打开素材选择 | ||||
| const openMaterial = () => { | ||||
|   dialogImageVisible.value = true | ||||
|   try { | ||||
|     materialSelectRef.value.queryParams.accountId = queryParams.accountId // 强制设置下 accountId,避免二次查询不对 | ||||
|     materialSelectRef.value.handleQuery() // 刷新列表,失败也无所谓 | ||||
|   } catch (e) {} | ||||
| } | ||||
| 
 | ||||
| // ======================== 草稿箱发布 ======================== | ||||
| const handlePublish = async (item) => { | ||||
| const handlePublish = async (item: any) => { | ||||
|   const accountId = queryParams.accountId | ||||
|   const mediaId = item.mediaId | ||||
|   const content = | ||||
|  | @ -510,19 +502,19 @@ const handlePublish = async (item) => { | |||
|     await message.confirm(content) | ||||
|     await MpFreePublishApi.submitFreePublish(accountId, mediaId) | ||||
|     message.notifySuccess('发布成功') | ||||
|     await getList() | ||||
|     getList() | ||||
|   } catch {} | ||||
| } | ||||
| 
 | ||||
| /** 删除按钮操作 */ | ||||
| const handleDelete = async (item) => { | ||||
| const handleDelete = async (item: any) => { | ||||
|   const accountId = queryParams.accountId | ||||
|   const mediaId = item.mediaId | ||||
|   try { | ||||
|     await message.confirm('此操作将永久删除该草稿, 是否继续?') | ||||
|     await MpDraftApi.deleteDraft(accountId, mediaId) | ||||
|     message.notifySuccess('删除成功') | ||||
|     await getList() | ||||
|     getList() | ||||
|   } catch {} | ||||
| } | ||||
| </script> | ||||
|  |  | |||
|  | @ -3,8 +3,17 @@ | |||
| 
 | ||||
|   <!-- 搜索工作栏 --> | ||||
|   <ContentWrap> | ||||
|     <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect  --> | ||||
|     <WxAccountSelect @change="(accountId) => accountChanged(accountId)" /> | ||||
|     <el-form | ||||
|       class="-mb-15px" | ||||
|       :model="queryParams" | ||||
|       ref="queryFormRef" | ||||
|       :inline="true" | ||||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item label="公众号" prop="accountId"> | ||||
|         <WxMpSelect @change="onAccountChanged" /> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 列表 --> | ||||
|  | @ -39,26 +48,32 @@ | |||
|   </ContentWrap> | ||||
| </template> | ||||
| 
 | ||||
| <script setup name="MpFreePublish"> | ||||
| <script lang="ts" setup name="MpFreePublish"> | ||||
| import * as FreePublishApi from '@/api/mp/freePublish' | ||||
| import WxNews from '@/views/mp/components/wx-news/main.vue' | ||||
| import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' | ||||
| import WxMpSelect from '@/views/mp/components/WxMpSelect.vue' | ||||
| 
 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| const { t } = useI18n() // 国际化 | ||||
| 
 | ||||
| const loading = ref(true) // 列表的加载中 | ||||
| const total = ref(0) // 列表的总页数 | ||||
| const list = ref([]) // 列表的数据 | ||||
| const queryParams = reactive({ | ||||
| const list = ref<any[]>([]) // 列表的数据 | ||||
| 
 | ||||
| interface QueryParams { | ||||
|   pageNo: number | ||||
|   pageSize: number | ||||
|   accountId?: number | ||||
| } | ||||
| const queryParams: QueryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   accountId: undefined | ||||
| }) | ||||
| 
 | ||||
| /** 侦听公众号变化 **/ | ||||
| const accountChanged = (accountId) => { | ||||
|   queryParams.accountId = accountId | ||||
| const onAccountChanged = (id: number | undefined) => { | ||||
|   queryParams.accountId = id | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
|  | @ -75,7 +90,7 @@ const getList = async () => { | |||
| } | ||||
| 
 | ||||
| /** 删除按钮操作 */ | ||||
| const handleDelete = async (item) => { | ||||
| const handleDelete = async (item: any) => { | ||||
|   try { | ||||
|     // 删除的二次确认 | ||||
|     await message.delConfirm('删除后用户将无法访问此页面,确定删除?') | ||||
|  |  | |||
|  | @ -4,13 +4,13 @@ | |||
|   <ContentWrap> | ||||
|     <el-form class="-mb-15px" :inline="true" label-width="68px"> | ||||
|       <el-form-item label="公众号" prop="accountId"> | ||||
|         <WxMpSelect @change="accountChange" /> | ||||
|         <WxMpSelect @change="onAccountChanged" /> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <ContentWrap> | ||||
|     <el-tabs v-model="type" @tab-change="handleTabChange"> | ||||
|     <el-tabs v-model="type" @tab-change="onTabChange"> | ||||
|       <!-- tab 1:图片  --> | ||||
|       <el-tab-pane name="image"> | ||||
|         <template #label> | ||||
|  | @ -93,7 +93,7 @@ | |||
|           <el-table-column label="文件名" align="center" prop="name" /> | ||||
|           <el-table-column label="语音" align="center"> | ||||
|             <template #default="scope"> | ||||
|               <WxVoicePlayer :url="scope.row.url" /> | ||||
|               <WxVoicePlayer v-if="scope.row.url" :url="scope.row.url" /> | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|           <el-table-column | ||||
|  | @ -188,10 +188,8 @@ | |||
|             </el-row> | ||||
|           </el-form> | ||||
|           <template #footer> | ||||
|             <!-- <span class="dialog-footer"> --> | ||||
|             <el-button @click="cancelVideo">取 消</el-button> | ||||
|             <el-button type="primary" @click="submitVideo">提 交</el-button> | ||||
|             <!-- </span> --> | ||||
|           </template> | ||||
|         </el-dialog> | ||||
| 
 | ||||
|  | @ -203,7 +201,7 @@ | |||
|           <el-table-column label="介绍" align="center" prop="introduction" /> | ||||
|           <el-table-column label="视频" align="center"> | ||||
|             <template #default="scope"> | ||||
|               <WxVideoPlayer :url="scope.row.url" /> | ||||
|               <WxVideoPlayer v-if="scope.row.url" :url="scope.row.url" /> | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|           <el-table-column | ||||
|  | @ -279,14 +277,11 @@ const uploadRules: FormRules = { | |||
| } | ||||
| 
 | ||||
| // 素材类型 | ||||
| type MatertialType = 'image' | 'voice' | 'video' | ||||
| const type = ref<MatertialType>('image') | ||||
| // 遮罩层 | ||||
| const loading = ref(false) | ||||
| // 总条数 | ||||
| const total = ref(0) | ||||
| // 数据列表 | ||||
| const list = ref([]) | ||||
| type MaterialType = 'image' | 'voice' | 'video' | ||||
| const type = ref<MaterialType>('image') | ||||
| const loading = ref(false) // 遮罩层 | ||||
| const list = ref<any[]>([]) // 总条数 | ||||
| const total = ref(0) // 数据列表 | ||||
| // 查询参数 | ||||
| interface QueryParams { | ||||
|   pageNo: number | ||||
|  | @ -304,7 +299,7 @@ const queryParams: QueryParams = reactive({ | |||
| const fileList = ref<UploadUserFile[]>([]) | ||||
| 
 | ||||
| interface UploadData { | ||||
|   type: MatertialType | ||||
|   type: MaterialType | ||||
|   title: string | ||||
|   introduction: string | ||||
| } | ||||
|  | @ -319,8 +314,8 @@ const dialogVideoVisible = ref(false) | |||
| const addMaterialLoading = ref(false) | ||||
| 
 | ||||
| /** 侦听公众号变化 **/ | ||||
| const accountChange = (accountId: number | undefined) => { | ||||
|   queryParams.accountId = accountId | ||||
| const onAccountChanged = (id?: number) => { | ||||
|   queryParams.accountId = id | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
|  | @ -346,9 +341,9 @@ const handleQuery = () => { | |||
|   getList() | ||||
| } | ||||
| 
 | ||||
| const handleTabChange = (tabName: TabPaneName) => { | ||||
| const onTabChange = (tabName: TabPaneName) => { | ||||
|   // 设置 type | ||||
|   uploadData.type = tabName as MatertialType | ||||
|   uploadData.type = tabName as MaterialType | ||||
| 
 | ||||
|   // 提前情况数据,避免tab切换后显示垃圾数据 | ||||
|   list.value = [] | ||||
|  | @ -359,54 +354,49 @@ const handleTabChange = (tabName: TabPaneName) => { | |||
| } | ||||
| 
 | ||||
| // ======================== 文件上传 ======================== | ||||
| const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => { | ||||
|   const isType = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'].includes( | ||||
|     rawFile.type | ||||
|   ) | ||||
|   if (!isType) { | ||||
|     message.error('上传图片格式不对!') | ||||
| const beforeUpload = (rawFile: UploadRawFile, category: 'image' | 'audio' | 'video'): boolean => { | ||||
|   let allowTypes: string[] = [] | ||||
|   let maxSizeMB = 0 | ||||
|   let name = '' | ||||
| 
 | ||||
|   switch (category) { | ||||
|     case 'image': | ||||
|       allowTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'] | ||||
|       maxSizeMB = 2 | ||||
|       name = '图片' | ||||
|       break | ||||
|     case 'audio': | ||||
|       allowTypes = ['audio/mp3', 'audio/mpeg', 'audio/wma', 'audio/wav', 'audio/amr'] | ||||
|       maxSizeMB = 2 | ||||
|       name = '图片' | ||||
|       break | ||||
|     case 'video': | ||||
|       allowTypes = ['video/mp4'] | ||||
|       maxSizeMB = 10 | ||||
|       name = '视频' | ||||
|   } | ||||
| 
 | ||||
|   if (!allowTypes.includes(rawFile.type)) { | ||||
|     message.error(`上传${name}格式不对!`) | ||||
|     return false | ||||
|   } | ||||
|   const isLt = rawFile.size / 1024 / 1024 < 2 | ||||
|   if (!isLt) { | ||||
|     message.error('上传图片大小不能超过 2M!') | ||||
|   // 校验大小 | ||||
|   if (rawFile.size / 1024 / 1024 > maxSizeMB) { | ||||
|     message.error(`上传${name}大小不能超过${maxSizeMB}M!`) | ||||
|     return false | ||||
|   } | ||||
|   loading.value = true | ||||
|   return true | ||||
| } | ||||
| 
 | ||||
| const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => { | ||||
|   const isType = ['audio/mp3', 'audio/wma', 'audio/wav', 'audio/amr'].includes(file.type) | ||||
|   const isLt = rawFile.size / 1024 / 1024 < 2 | ||||
|   if (!isType) { | ||||
|     message.error('上传语音格式不对!') | ||||
|     return false | ||||
|   } | ||||
|   if (!isLt) { | ||||
|     message.error('上传语音大小不能超过 2M!') | ||||
|     return false | ||||
|   } | ||||
|   loading.value = true | ||||
|   return true | ||||
| } | ||||
| const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => | ||||
|   beforeUpload(rawFile, 'image') | ||||
| 
 | ||||
| const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => { | ||||
|   const isType = rawFile.type === 'video/mp4' | ||||
|   if (!isType) { | ||||
|     message.error('上传视频格式不对!') | ||||
|     return false | ||||
|   } | ||||
| const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => | ||||
|   beforeUpload(rawFile, 'audio') | ||||
| 
 | ||||
|   const isLt = rawFile.size / 1024 / 1024 < 10 | ||||
|   if (!isLt) { | ||||
|     message.error('上传视频大小不能超过 10M!') | ||||
|     return false | ||||
|   } | ||||
| 
 | ||||
|   addMaterialLoading.value = true | ||||
|   return true | ||||
| } | ||||
| const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => | ||||
|   beforeUpload(rawFile, 'video') | ||||
| 
 | ||||
| const handleUploadSuccess: UploadProps['onSuccess'] = (response: any) => { | ||||
|   loading.value = false | ||||
|  | @ -441,6 +431,7 @@ const submitVideo = () => { | |||
|   }) | ||||
| } | ||||
| 
 | ||||
| // 弹出 video 新建的表单 | ||||
| const handleAddVideo = () => { | ||||
|   resetVideo() | ||||
|   dialogVideoVisible.value = true | ||||
|  |  | |||
|  | @ -2,8 +2,11 @@ | |||
|   <doc-alert title="公众号菜单" url="https://doc.iocoder.cn/mp/menu/" /> | ||||
|   <!-- 搜索工作栏 --> | ||||
|   <ContentWrap> | ||||
|     <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect  --> | ||||
|     <WxAccountSelect @change="accountChanged" /> | ||||
|     <el-form class="-mb-15px" ref="queryFormRef" :inline="true" label-width="68px"> | ||||
|       <el-form-item label="公众号" prop="accountId"> | ||||
|         <WxMpSelect @change="onAccountChanged" /> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 列表 --> | ||||
|  | @ -12,7 +15,7 @@ | |||
|       <!--左边配置菜单--> | ||||
|       <div class="left"> | ||||
|         <div class="weixin-hd"> | ||||
|           <div class="weixin-title">{{ name }}</div> | ||||
|           <div class="weixin-title">{{ accountName }}</div> | ||||
|         </div> | ||||
|         <div class="weixin-menu menu_main clearfix"> | ||||
|           <div class="menu_bottom" v-for="(item, i) of menuList" :key="i"> | ||||
|  | @ -68,7 +71,7 @@ | |||
|       <div v-if="showRightFlag" class="right"> | ||||
|         <div class="configure_page"> | ||||
|           <div class="delete_btn"> | ||||
|             <el-button size="small" type="danger" @click="handleDeleteMenu(tempObj)"> | ||||
|             <el-button size="small" type="danger" @click="handleDeleteMenu"> | ||||
|               删除当前菜单<Icon icon="ep:delete" /> | ||||
|             </el-button> | ||||
|           </div> | ||||
|  | @ -155,7 +158,7 @@ | |||
|                 <div v-else> | ||||
|                   <el-row justify="center"> | ||||
|                     <el-col :span="24" style="text-align: center"> | ||||
|                       <el-button type="success" @click="openMaterial"> | ||||
|                       <el-button type="success" @click="dialogNewsVisible = true"> | ||||
|                         素材库选择<Icon icon="ep:circle-check" /> | ||||
|                       </el-button> | ||||
|                     </el-col> | ||||
|  | @ -185,24 +188,26 @@ | |||
|     </div> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| <script setup name="MpMenu"> | ||||
| import { handleTree } from '@/utils/tree' | ||||
| <script lang="ts" setup name="MpMenu"> | ||||
| import WxReplySelect from '@/views/mp/components/wx-reply/main.vue' | ||||
| import WxNews from '@/views/mp/components/wx-news/main.vue' | ||||
| import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue' | ||||
| import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' | ||||
| import WxMpSelect from '@/views/mp/components/WxMpSelect.vue' | ||||
| import * as MpMenuApi from '@/api/mp/menu' | ||||
| import { handleTree } from '@/utils/tree' | ||||
| import menuOptions from './menuOptions' | ||||
| 
 | ||||
| const message = useMessage() // 消息 | ||||
| 
 | ||||
| // ======================== 列表查询 ======================== | ||||
| const loading = ref(true) // 遮罩层 | ||||
| const accountId = ref(undefined) // 公众号Id | ||||
| const menuList = ref({ children: [] }) | ||||
| const loading = ref(false) // 遮罩层 | ||||
| const accountId = ref<number | undefined>() | ||||
| const accountName = ref<string | undefined>('') | ||||
| const menuList = ref<any>({ children: [] }) | ||||
| 
 | ||||
| // ======================== 菜单操作 ======================== | ||||
| const isActive = ref(-1) // 一级菜单点中样式 | ||||
| const isSubMenuActive = ref(-1) // 一级菜单点中样式 | ||||
| const isSubMenuActive = ref<string | number>(-1) // 一级菜单点中样式 | ||||
| const isSubMenuFlag = ref(-1) // 二级菜单显示标志 | ||||
| 
 | ||||
| // ======================== 菜单编辑 ======================== | ||||
|  | @ -210,15 +215,16 @@ const showRightFlag = ref(false) // 右边配置显示默认详情还是配置 | |||
| const nameMaxLength = ref(0) // 菜单名称最大长度;1 级是 4 字符;2 级是 7 字符; | ||||
| const showConfigureContent = ref(true) // 是否展示配置内容;如果有子菜单,就不显示配置内容 | ||||
| const hackResetWxReplySelect = ref(false) // 重置 WxReplySelect 组件 | ||||
| const tempObj = ref({}) // 右边临时变量,作为中间值牵引关系 | ||||
| const tempObj = ref<any>({}) // 右边临时变量,作为中间值牵引关系 | ||||
| 
 | ||||
| // 一些临时值放在这里进行判断,如果放在 tempObj,由于引用关系,menu 也会多了多余的参数 | ||||
| const tempSelfObj = ref({}) | ||||
| const tempSelfObj = ref<any>({}) | ||||
| const dialogNewsVisible = ref(false) // 跳转图文时的素材选择弹窗 | ||||
| 
 | ||||
| /** 侦听公众号变化 **/ | ||||
| const accountChanged = (id) => { | ||||
| const onAccountChanged = (id?: number, name?: string) => { | ||||
|   accountId.value = id | ||||
|   accountName.value = name | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
|  | @ -241,10 +247,10 @@ const handleQuery = () => { | |||
| } | ||||
| 
 | ||||
| // 将后端返回的 menuList,转换成前端的 menuList | ||||
| const convertMenuList = (list) => { | ||||
| const convertMenuList = (list: any[]) => { | ||||
|   if (!list) return [] | ||||
| 
 | ||||
|   const menuList = [] | ||||
|   const result: any[] = [] | ||||
|   list.forEach((item) => { | ||||
|     const menu = { | ||||
|       ...item | ||||
|  | @ -271,9 +277,9 @@ const convertMenuList = (list) => { | |||
|         hqMusicUrl: item.replyHqMusicUrl | ||||
|       } | ||||
|     } | ||||
|     menuList.push(menu) | ||||
|     result.push(menu) | ||||
|   }) | ||||
|   return menuList | ||||
|   return result | ||||
| } | ||||
| 
 | ||||
| // 重置表单,清空表单数据 | ||||
|  | @ -286,7 +292,7 @@ const resetForm = () => { | |||
|   // 菜单编辑 | ||||
|   showRightFlag.value = false | ||||
|   nameMaxLength.value = 0 | ||||
|   showConfigureContent.value = 0 | ||||
|   showConfigureContent.value = false | ||||
|   hackResetWxReplySelect.value = false | ||||
|   tempObj.value = {} | ||||
|   tempSelfObj.value = {} | ||||
|  | @ -295,7 +301,7 @@ const resetForm = () => { | |||
| 
 | ||||
| // ======================== 菜单操作 ======================== | ||||
| // 一级菜单点击事件 | ||||
| const menuClick = (i, item) => { | ||||
| const menuClick = (i: number, item: any) => { | ||||
|   // 右侧的表单相关 | ||||
|   resetEditor() | ||||
|   showRightFlag.value = true // 右边菜单 | ||||
|  | @ -312,11 +318,10 @@ const menuClick = (i, item) => { | |||
| } | ||||
| 
 | ||||
| // 二级菜单点击事件 | ||||
| const subMenuClick = (subItem, index, k) => { | ||||
| const subMenuClick = (subItem: any, index: number, k: number) => { | ||||
|   // 右侧的表单相关 | ||||
|   resetEditor() | ||||
|   showRightFlag.value = true // 右边菜单 | ||||
|   console.log(subItem) | ||||
|   tempObj.value = subItem // 将点击的数据放到临时变量,对象有引用作用 | ||||
|   tempSelfObj.value.grand = '2' // 表示二级菜单 | ||||
|   tempSelfObj.value.index = index // 表示一级菜单索引 | ||||
|  | @ -331,7 +336,7 @@ const subMenuClick = (subItem, index, k) => { | |||
| 
 | ||||
| // 添加横向一级菜单 | ||||
| const addMenu = () => { | ||||
|   const menuKeyLength = menuList.value.length | ||||
|   const menuKeyLength: number = menuList.value.length | ||||
|   const addButton = { | ||||
|     name: '菜单名称', | ||||
|     children: [], | ||||
|  | @ -342,10 +347,10 @@ const addMenu = () => { | |||
|     } | ||||
|   } | ||||
|   menuList.value[menuKeyLength] = addButton | ||||
|   menuClick(menuKeyLength.value - 1, addButton) | ||||
|   menuClick(menuKeyLength - 1, addButton) | ||||
| } | ||||
| // 添加横向二级菜单;item 表示要操作的父菜单 | ||||
| const addSubMenu = (i, item) => { | ||||
| const addSubMenu = (i: number, item: any) => { | ||||
|   // 清空父菜单的属性,因为它只需要 name 属性即可 | ||||
|   if (!item.children || item.children.length <= 0) { | ||||
|     item.children = [] | ||||
|  | @ -361,8 +366,8 @@ const addSubMenu = (i, item) => { | |||
|     showConfigureContent.value = false | ||||
|   } | ||||
| 
 | ||||
|   let subMenuKeyLength = item.children.length // 获取二级菜单key长度 | ||||
|   let addButton = { | ||||
|   const subMenuKeyLength = item.children.length // 获取二级菜单key长度 | ||||
|   const addButton = { | ||||
|     name: '子菜单名称', | ||||
|     reply: { | ||||
|       // 用于存储回复内容 | ||||
|  | @ -399,7 +404,7 @@ const handleDeleteMenu = async () => { | |||
| // ======================== 菜单编辑 ======================== | ||||
| const handleSave = async () => { | ||||
|   try { | ||||
|     await message.confirm('确定要删除吗?') | ||||
|     await message.confirm('确定要保存吗?') | ||||
|     loading.value = true | ||||
|     await MpMenuApi.saveMenu(accountId.value, convertMenuFormList()) | ||||
|     getList() | ||||
|  | @ -413,7 +418,6 @@ const handleSave = async () => { | |||
| const resetEditor = () => { | ||||
|   hackResetWxReplySelect.value = false // 销毁组件 | ||||
|   nextTick(() => { | ||||
|     console.log('nextTick') | ||||
|     hackResetWxReplySelect.value = true // 重建组件 | ||||
|   }) | ||||
| } | ||||
|  | @ -432,9 +436,9 @@ const handleDelete = async () => { | |||
| 
 | ||||
| // 将前端的 menuList,转换成后端接收的 menuList | ||||
| const convertMenuFormList = () => { | ||||
|   const result = [] | ||||
|   const result: any[] = [] | ||||
|   menuList.value.forEach((item) => { | ||||
|     let menu = convertMenuForm(item) | ||||
|     const menu = convertMenuForm(item) | ||||
|     result.push(menu) | ||||
| 
 | ||||
|     // 处理子菜单 | ||||
|  | @ -450,7 +454,7 @@ const convertMenuFormList = () => { | |||
| } | ||||
| 
 | ||||
| // 将前端的 menu,转换成后端接收的 menu | ||||
| const convertMenuForm = (menu) => { | ||||
| const convertMenuForm = (menu: any) => { | ||||
|   let result = { | ||||
|     ...menu, | ||||
|     children: undefined, // 不处理子节点 | ||||
|  | @ -473,11 +477,7 @@ const convertMenuForm = (menu) => { | |||
| } | ||||
| 
 | ||||
| // ======================== 菜单编辑(素材选择) ======================== | ||||
| const openMaterial = () => { | ||||
|   dialogNewsVisible.value = true | ||||
| } | ||||
| 
 | ||||
| const selectMaterial = (item) => { | ||||
| const selectMaterial = (item: any) => { | ||||
|   const articleId = item.articleId | ||||
|   const articles = item.content.newsItem | ||||
|   // 提示,针对多图文 | ||||
|  |  | |||
|  | @ -9,14 +9,7 @@ | |||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item label="公众号" prop="accountId"> | ||||
|         <el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px"> | ||||
|           <el-option | ||||
|             v-for="item in accountList" | ||||
|             :key="item.id" | ||||
|             :label="item.name" | ||||
|             :value="item.id" | ||||
|           /> | ||||
|         </el-select> | ||||
|         <WxMpSelect @change="onAccountChanged" /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="消息类型" prop="type"> | ||||
|         <el-select v-model="queryParams.type" placeholder="请选择消息类型" class="!w-240px"> | ||||
|  | @ -84,70 +77,76 @@ | |||
|       <el-table-column label="内容" prop="content"> | ||||
|         <template #default="scope"> | ||||
|           <!-- 【事件】区域 --> | ||||
|           <div v-if="scope.row.type === 'event' && scope.row.event === 'subscribe'"> | ||||
|           <div v-if="scope.row.type === MsgType.Event && scope.row.event === 'subscribe'"> | ||||
|             <el-tag type="success">关注</el-tag> | ||||
|           </div> | ||||
|           <div v-else-if="scope.row.type === 'event' && scope.row.event === 'unsubscribe'"> | ||||
|           <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'unsubscribe'"> | ||||
|             <el-tag type="danger">取消关注</el-tag> | ||||
|           </div> | ||||
|           <div v-else-if="scope.row.type === 'event' && scope.row.event === 'CLICK'"> | ||||
|           <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'CLICK'"> | ||||
|             <el-tag>点击菜单</el-tag> | ||||
|             【{{ scope.row.eventKey }}】 | ||||
|           </div> | ||||
|           <div v-else-if="scope.row.type === 'event' && scope.row.event === 'VIEW'"> | ||||
|           <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'VIEW'"> | ||||
|             <el-tag>点击菜单链接</el-tag> | ||||
|             【{{ scope.row.eventKey }}】 | ||||
|           </div> | ||||
|           <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_waitmsg'"> | ||||
|           <div | ||||
|             v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'scancode_waitmsg'" | ||||
|           > | ||||
|             <el-tag>扫码结果</el-tag> | ||||
|             【{{ scope.row.eventKey }}】 | ||||
|           </div> | ||||
|           <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_push'"> | ||||
|           <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'scancode_push'"> | ||||
|             <el-tag>扫码结果</el-tag> | ||||
|             【{{ scope.row.eventKey }}】 | ||||
|           </div> | ||||
|           <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_sysphoto'"> | ||||
|           <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_sysphoto'"> | ||||
|             <el-tag>系统拍照发图</el-tag> | ||||
|           </div> | ||||
|           <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_photo_or_album'"> | ||||
|           <div | ||||
|             v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_photo_or_album'" | ||||
|           > | ||||
|             <el-tag>拍照或者相册</el-tag> | ||||
|           </div> | ||||
|           <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_weixin'"> | ||||
|           <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_weixin'"> | ||||
|             <el-tag>微信相册</el-tag> | ||||
|           </div> | ||||
|           <div v-else-if="scope.row.type === 'event' && scope.row.event === 'location_select'"> | ||||
|           <div | ||||
|             v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'location_select'" | ||||
|           > | ||||
|             <el-tag>选择地理位置</el-tag> | ||||
|           </div> | ||||
|           <div v-else-if="scope.row.type === 'event'"> | ||||
|           <div v-else-if="scope.row.type === MsgType.Event"> | ||||
|             <el-tag type="danger">未知事件类型</el-tag> | ||||
|           </div> | ||||
|           <!-- 【消息】区域 --> | ||||
|           <div v-else-if="scope.row.type === 'text'">{{ scope.row.content }}</div> | ||||
|           <div v-else-if="scope.row.type === 'voice'"> | ||||
|           <div v-else-if="scope.row.type === MsgType.Text">{{ scope.row.content }}</div> | ||||
|           <div v-else-if="scope.row.type === MsgType.Voice"> | ||||
|             <wx-voice-player :url="scope.row.mediaUrl" :content="scope.row.recognition" /> | ||||
|           </div> | ||||
|           <div v-else-if="scope.row.type === 'image'"> | ||||
|           <div v-else-if="scope.row.type === MsgType.Image"> | ||||
|             <a target="_blank" :href="scope.row.mediaUrl"> | ||||
|               <img :src="scope.row.mediaUrl" style="width: 100px" /> | ||||
|             </a> | ||||
|           </div> | ||||
|           <div v-else-if="scope.row.type === 'video' || scope.row.type === 'shortvideo'"> | ||||
|           <div v-else-if="scope.row.type === MsgType.Video || scope.row.type === 'shortvideo'"> | ||||
|             <wx-video-player :url="scope.row.mediaUrl" style="margin-top: 10px" /> | ||||
|           </div> | ||||
|           <div v-else-if="scope.row.type === 'link'"> | ||||
|           <div v-else-if="scope.row.type === MsgType.Link"> | ||||
|             <el-tag>链接</el-tag> | ||||
|             : | ||||
|             <a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a> | ||||
|           </div> | ||||
|           <div v-else-if="scope.row.type === 'location'"> | ||||
|             <wx-location | ||||
|           <div v-else-if="scope.row.type === MsgType.Location"> | ||||
|             <WxLocation | ||||
|               :label="scope.row.label" | ||||
|               :location-y="scope.row.locationY" | ||||
|               :location-x="scope.row.locationX" | ||||
|             /> | ||||
|           </div> | ||||
|           <div v-else-if="scope.row.type === 'music'"> | ||||
|             <wx-music | ||||
|           <div v-else-if="scope.row.type === MsgType.Music"> | ||||
|             <WxMusic | ||||
|               :title="scope.row.title" | ||||
|               :description="scope.row.description" | ||||
|               :thumb-media-url="scope.row.thumbMediaUrl" | ||||
|  | @ -155,8 +154,8 @@ | |||
|               :hq-music-url="scope.row.hqMusicUrl" | ||||
|             /> | ||||
|           </div> | ||||
|           <div v-else-if="scope.row.type === 'news'"> | ||||
|             <wx-news :articles="scope.row.articles" /> | ||||
|           <div v-else-if="scope.row.type === MsgType.News"> | ||||
|             <WxNews :articles="scope.row.articles" /> | ||||
|           </div> | ||||
|           <div v-else> | ||||
|             <el-tag type="danger">未知消息类型</el-tag> | ||||
|  | @ -177,7 +176,7 @@ | |||
|       </el-table-column> | ||||
|     </el-table> | ||||
|     <!-- 分页组件 --> | ||||
|     <pagination | ||||
|     <Pagination | ||||
|       v-show="total > 0" | ||||
|       :total="total" | ||||
|       v-model:page="queryParams.pageNo" | ||||
|  | @ -186,9 +185,14 @@ | |||
|     /> | ||||
| 
 | ||||
|     <!-- 发送消息的弹窗 --> | ||||
|     <el-dialog title="粉丝消息列表" v-model="open" @click="openDialog()" width="50%"> | ||||
|     <el-dialog | ||||
|       title="粉丝消息列表" | ||||
|       v-model="showMessageBox" | ||||
|       @click="showMessageBox = true" | ||||
|       width="50%" | ||||
|     > | ||||
|       <template #footer> | ||||
|         <wx-msg :user-id="userId" v-if="open" /> | ||||
|         <WxMsg :user-id="userId" v-if="showMessageBox" /> | ||||
|       </template> | ||||
|     </el-dialog> | ||||
|   </ContentWrap> | ||||
|  | @ -200,17 +204,27 @@ import WxMsg from '@/views/mp/components/wx-msg/main.vue' | |||
| import WxLocation from '@/views/mp/components/wx-location/main.vue' | ||||
| import WxMusic from '@/views/mp/components/wx-music/main.vue' | ||||
| import WxNews from '@/views/mp/components/wx-news/main.vue' | ||||
| import * as MpAccountApi from '@/api/mp/account' | ||||
| import WxMpSelect from '@/views/mp/components/WxMpSelect.vue' | ||||
| import * as MpMessageApi from '@/api/mp/message' | ||||
| 
 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import { MsgType } from '@/views/mp/components/wx-msg/types' | ||||
| import type { FormInstance } from 'element-plus' | ||||
| 
 | ||||
| const loading = ref(true) // 列表的加载中 | ||||
| const total = ref(0) // 列表的总页数 | ||||
| const list = ref([]) // 列表的数据 | ||||
| const queryParams = reactive({ | ||||
| const list = ref<any[]>([]) // 列表的数据 | ||||
| 
 | ||||
| // 搜索参数 | ||||
| interface QueryParams { | ||||
|   pageNo: number | ||||
|   pageSize: number | ||||
|   openid: string | null | ||||
|   accountId: number | null | ||||
|   type: MsgType | null | ||||
|   createTime: string[] | [] | ||||
| } | ||||
| const queryParams: QueryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   openid: null, | ||||
|  | @ -218,19 +232,18 @@ const queryParams = reactive({ | |||
|   type: null, | ||||
|   createTime: [] | ||||
| }) | ||||
| const queryFormRef = ref() // 搜索的表单 | ||||
| // TODO 芋艿:下面应该移除 | ||||
| const open = ref(false) // 是否显示弹出层 | ||||
| const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单 | ||||
| const showMessageBox = ref(false) // 是否显示弹出层 | ||||
| const userId = ref(0) // 操作的用户编号 | ||||
| const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表 | ||||
| 
 | ||||
| /** 侦听accountId */ | ||||
| const onAccountChanged = (id?: number) => { | ||||
|   queryParams.accountId = id as number | ||||
|   handleQuery() | ||||
| } | ||||
| 
 | ||||
| /** 查询列表 */ | ||||
| const getList = async () => { | ||||
|   // 如果没有选中公众号账号,则进行提示。 | ||||
|   if (!queryParams.accountId) { | ||||
|     await message.error('未选中公众号,无法查询消息') | ||||
|     return | ||||
|   } | ||||
|   try { | ||||
|     loading.value = true | ||||
|     const data = await MpMessageApi.getMessagePage(queryParams) | ||||
|  | @ -249,34 +262,15 @@ const handleQuery = () => { | |||
| 
 | ||||
| /** 重置按钮操作 */ | ||||
| const resetQuery = async () => { | ||||
|   queryFormRef.value.resetFields() | ||||
|   // 默认选中第一个 | ||||
|   if (accountList.value.length > 0) { | ||||
|     // @ts-ignore | ||||
|     queryParams.accountId = accountList.value[0].id | ||||
|   } | ||||
|   const accountId = queryParams.accountId | ||||
|   queryFormRef.value?.resetFields() | ||||
|   queryParams.accountId = accountId | ||||
|   handleQuery() | ||||
| } | ||||
| const handleSend = async (row) => { | ||||
| 
 | ||||
| /** 打开消息发送窗口 */ | ||||
| const handleSend = async (row: any) => { | ||||
|   userId.value = row.userId | ||||
|   open.value = true | ||||
|   showMessageBox.value = true | ||||
| } | ||||
| 
 | ||||
| const openDialog = () => { | ||||
|   open.value = true | ||||
| } | ||||
| // const closeDiaLog = () => { | ||||
| //   open.value = false | ||||
| // } | ||||
| 
 | ||||
| /** 初始化 **/ | ||||
| onMounted(async () => { | ||||
|   accountList.value = await MpAccountApi.getSimpleAccountList() | ||||
|   // 选中第一个 | ||||
|   if (accountList.value.length > 0) { | ||||
|     // @ts-ignore | ||||
|     queryParams.accountId = accountList.value[0].id | ||||
|   } | ||||
|   await getList() | ||||
| }) | ||||
| </script> | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ | |||
| </template> | ||||
| <script lang="ts" name="MpTagForm" setup> | ||||
| import * as MpTagApi from '@/api/mp/tag' | ||||
| import type { FormInstance, FormRules } from 'element-plus' | ||||
| 
 | ||||
| const { t } = useI18n() // 国际化 | ||||
| const message = useMessage() // 消息弹窗 | ||||
|  | @ -26,18 +27,22 @@ const message = useMessage() // 消息弹窗 | |||
| const dialogVisible = ref(false) // 弹窗的是否展示 | ||||
| const dialogTitle = ref('') // 弹窗的标题 | ||||
| const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 | ||||
| const formType = ref('') // 表单的类型:create - 新增;update - 修改 | ||||
| const formType = ref<'create' | 'update' | ''>('') // 表单的类型:create - 新增;update - 修改 | ||||
| const formData = ref({ | ||||
|   accountId: -1, | ||||
|   name: '' | ||||
| }) | ||||
| const formRules = reactive({ | ||||
| const formRules: FormRules = { | ||||
|   name: [{ required: true, message: '请输入标签名称', trigger: 'blur' }] | ||||
| }) | ||||
| const formRef = ref() // 表单 Ref | ||||
| } | ||||
| const formRef = ref<FormInstance | null>(null) // 表单 Ref | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
|   (e: 'success'): void | ||||
| }>() | ||||
| 
 | ||||
| /** 打开弹窗 */ | ||||
| const open = async (type: string, accountId: number, id?: number) => { | ||||
| const open = async (type: 'create' | 'update', accountId: number, id?: number) => { | ||||
|   dialogVisible.value = true | ||||
|   dialogTitle.value = t('action.' + type) | ||||
|   formType.value = type | ||||
|  | @ -56,11 +61,10 @@ const open = async (type: string, accountId: number, id?: number) => { | |||
| defineExpose({ open }) // 提供 open 方法,用于打开弹窗 | ||||
| 
 | ||||
| /** 提交表单 */ | ||||
| const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 | ||||
| const submitForm = async () => { | ||||
|   // 校验表单 | ||||
|   if (!formRef) return | ||||
|   const valid = await formRef.value.validate() | ||||
|   const valid = await formRef.value?.validate() | ||||
|   if (!valid) return | ||||
|   // 提交请求 | ||||
|   formLoading.value = true | ||||
|  |  | |||
|  | @ -3,17 +3,25 @@ | |||
| 
 | ||||
|   <!-- 搜索工作栏 --> | ||||
|   <ContentWrap> | ||||
|     <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect  --> | ||||
|     <WxAccountSelect @change="accountChanged"> | ||||
|       <template #actions> | ||||
|     <el-form | ||||
|       class="-mb-15px" | ||||
|       :model="queryParams" | ||||
|       ref="queryFormRef" | ||||
|       :inline="true" | ||||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item label="公众号" prop="accountId"> | ||||
|         <WxMpSelect @change="onAccountChanged" /> | ||||
|       </el-form-item> | ||||
|       <el-form-item> | ||||
|         <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['mp:tag:create']"> | ||||
|           <Icon icon="ep:plus" class="mr-5px" /> 新增 | ||||
|         </el-button> | ||||
|         <el-button type="success" plain @click="handleSync" v-hasPermi="['mp:tag:sync']"> | ||||
|           <Icon icon="ep:refresh" class="mr-5px" /> 同步 | ||||
|         </el-button> | ||||
|       </template> | ||||
|     </WxAccountSelect> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 列表 --> | ||||
|  | @ -64,25 +72,32 @@ | |||
| </template> | ||||
| <script setup lang="ts" name="MpTag"> | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' | ||||
| import * as MpTagApi from '@/api/mp/tag' | ||||
| import TagForm from './TagForm.vue' | ||||
| import WxMpSelect from '@/views/mp/components/WxMpSelect.vue' | ||||
| const message = useMessage() // 消息弹窗 | ||||
| const { t } = useI18n() // 国际化 | ||||
| 
 | ||||
| const loading = ref(true) // 列表的加载中 | ||||
| const total = ref(0) // 列表的总页数 | ||||
| const list = ref([]) // 列表的数据 | ||||
| const queryParams = reactive({ | ||||
| const list = ref<any>([]) // 列表的数据 | ||||
| 
 | ||||
| interface QueryParams { | ||||
|   pageNo: number | ||||
|   pageSize: number | ||||
|   accountId?: number | ||||
| } | ||||
| const queryParams: QueryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   accountId: undefined | ||||
| }) | ||||
| const formRef = ref<InstanceType<typeof TagForm> | null>(null) | ||||
| 
 | ||||
| /** 侦听公众号变化 **/ | ||||
| const accountChanged = (accountId) => { | ||||
| const onAccountChanged = (id?: number) => { | ||||
|   queryParams.pageNo = 1 | ||||
|   queryParams.accountId = accountId | ||||
|   queryParams.accountId = id | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
|  | @ -99,9 +114,8 @@ const getList = async () => { | |||
| } | ||||
| 
 | ||||
| /** 添加/修改操作 */ | ||||
| const formRef = ref() | ||||
| const openForm = (type: string, id?: number) => { | ||||
|   formRef.value.open(type, queryParams.accountId, id) | ||||
|   formRef.value?.open(type, queryParams.accountId as number, id) | ||||
| } | ||||
| 
 | ||||
| /** 删除按钮操作 */ | ||||
|  | @ -121,8 +135,7 @@ const handleDelete = async (id: number) => { | |||
| const handleSync = async () => { | ||||
|   try { | ||||
|     await message.confirm('是否确认同步标签?') | ||||
|     // @ts-ignore | ||||
|     await MpTagApi.syncTag(queryParams.accountId) | ||||
|     await MpTagApi.syncTag(queryParams.accountId as number) | ||||
|     message.success('同步标签成功') | ||||
|     await getList() | ||||
|   } catch {} | ||||
|  |  | |||
|  | @ -3,14 +3,42 @@ | |||
| 
 | ||||
|   <!-- 搜索工作栏 --> | ||||
|   <ContentWrap> | ||||
|     <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect  --> | ||||
|     <WxAccountSelect @change="(accountId) => accountChanged(accountId)"> | ||||
|       <template #actions> | ||||
|     <el-form | ||||
|       class="-mb-15px" | ||||
|       :model="queryParams" | ||||
|       ref="queryFormRef" | ||||
|       :inline="true" | ||||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item label="公众号" prop="accountId"> | ||||
|         <WxMpSelect @change="onAccountChanged" /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="用户标识" prop="openid"> | ||||
|         <el-input | ||||
|           v-model="queryParams.openid" | ||||
|           placeholder="请输入用户标识" | ||||
|           clearable | ||||
|           @keyup.enter="handleQuery" | ||||
|           class="!w-240px" | ||||
|         /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="昵称" prop="nickname"> | ||||
|         <el-input | ||||
|           v-model="queryParams.nickname" | ||||
|           placeholder="请输入昵称" | ||||
|           clearable | ||||
|           @keyup.enter="handleQuery" | ||||
|           class="!w-240px" | ||||
|         /> | ||||
|       </el-form-item> | ||||
|       <el-form-item> | ||||
|         <el-button @click="handleQuery"> <Icon icon="ep:search" />搜索 </el-button> | ||||
|         <el-button @click="resetQuery"> <Icon icon="ep:refresh" />重置 </el-button> | ||||
|         <el-button type="success" plain @click="handleSync" v-hasPermi="['mp:user:sync']"> | ||||
|           <Icon icon="ep:refresh" class="mr-5px" /> 同步 | ||||
|         </el-button> | ||||
|       </template> | ||||
|     </WxAccountSelect> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 列表 --> | ||||
|  | @ -66,35 +94,40 @@ | |||
|   <UserForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| <script lang="ts" setup name="MpUser"> | ||||
| import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import * as MpUserApi from '@/api/mp/user' | ||||
| import * as MpTagApi from '@/api/mp/tag' | ||||
| import WxMpSelect from '@/views/mp/components/WxMpSelect.vue' | ||||
| import type { FormInstance } from 'element-plus' | ||||
| import UserForm from './UserForm.vue' | ||||
| 
 | ||||
| const message = useMessage() // 消息 | ||||
| 
 | ||||
| const loading = ref(true) // 列表的加载中 | ||||
| const total = ref(0) // 列表的总页数 | ||||
| const list = ref([]) // 列表的数据 | ||||
| const queryParams = reactive({ | ||||
| const list = ref<any[]>([]) // 列表的数据 | ||||
| 
 | ||||
| interface QueryParams { | ||||
|   pageNo: number | ||||
|   pageSize: number | ||||
|   accountId?: number | ||||
|   openid: string | null | ||||
|   nickname: string | null | ||||
| } | ||||
| const queryParams: QueryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   accountId: null, | ||||
|   accountId: undefined, | ||||
|   openid: null, | ||||
|   nickname: null | ||||
| }) | ||||
| const tagList = ref([]) // 公众号标签列表 | ||||
| 
 | ||||
| /** 初始化 */ | ||||
| onMounted(async () => { | ||||
|   tagList.value = await MpTagApi.getSimpleTagList() | ||||
| }) | ||||
| const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单 | ||||
| const tagList = ref<any[]>([]) // 公众号标签列表 | ||||
| 
 | ||||
| /** 侦听公众号变化 **/ | ||||
| const accountChanged = (accountId) => { | ||||
| const onAccountChanged = (id?: number) => { | ||||
|   queryParams.pageNo = 1 | ||||
|   queryParams.accountId = accountId | ||||
|   queryParams.accountId = id | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
|  | @ -110,20 +143,38 @@ const getList = async () => { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 搜索按钮操作 */ | ||||
| const handleQuery = () => { | ||||
|   queryParams.pageNo = 1 | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
| /** 重置按钮操作 */ | ||||
| const resetQuery = () => { | ||||
|   const accountId = queryParams.accountId | ||||
|   queryFormRef.value?.resetFields() | ||||
|   queryParams.accountId = accountId | ||||
|   handleQuery() | ||||
| } | ||||
| 
 | ||||
| /** 添加/修改操作 */ | ||||
| const formRef = ref() | ||||
| const formRef = ref<InstanceType<typeof UserForm> | null>(null) | ||||
| const openForm = (id: number) => { | ||||
|   formRef.value.open(id) | ||||
|   formRef.value?.open(id) | ||||
| } | ||||
| 
 | ||||
| /** 同步标签 */ | ||||
| const handleSync = async () => { | ||||
|   const accountId = queryParams.accountId | ||||
|   try { | ||||
|     await message.confirm('是否确认同步粉丝?') | ||||
|     await MpUserApi.syncUser(accountId) | ||||
|     await MpUserApi.syncUser(queryParams.accountId) | ||||
|     message.success('开始从微信公众号同步粉丝信息,同步需要一段时间,建议稍后再查询') | ||||
|     await getList() | ||||
|   } catch {} | ||||
| } | ||||
| 
 | ||||
| /** 初始化 */ | ||||
| onMounted(async () => { | ||||
|   tagList.value = await MpTagApi.getSimpleTagList() | ||||
| }) | ||||
| </script> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 puhui999
						puhui999