From df3b381d6fc340489b825906f1f39fe0e0687442 Mon Sep 17 00:00:00 2001 From: dhb52 <dhb52@126.com> Date: Wed, 12 Apr 2023 13:29:24 +0800 Subject: [PATCH 1/6] =?UTF-8?q?refactor:=20mp=E6=A8=A1=E5=9D=97ts=E9=87=8D?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/mp/autoReply/index.vue | 277 ++++++---- src/views/mp/components/WxMpSelect.vue | 22 +- .../mp/components/wx-editor/WxEditor.vue | 115 +---- src/views/mp/draft/index.vue | 475 +++++++++--------- src/views/mp/freePublish/index.vue | 33 +- src/views/mp/material/index.vue | 92 ++-- src/views/mp/menu/index.vue | 76 +-- src/views/mp/tag/TagForm.vue | 19 +- src/views/mp/tag/index.vue | 45 +- src/views/mp/user/index.vue | 86 +++- src/views/pay/order/orderForm.vue | 152 ++++++ src/views/pay/refund/refundForm.vue | 154 ++++++ 12 files changed, 943 insertions(+), 603 deletions(-) create mode 100644 src/views/pay/order/orderForm.vue create mode 100644 src/views/pay/refund/refundForm.vue diff --git a/src/views/mp/autoReply/index.vue b/src/views/mp/autoReply/index.vue index 9208d1e7..94fe34b3 100644 --- a/src/views/mp/autoReply/index.vue +++ b/src/views/mp/autoReply/index.vue @@ -3,13 +3,16 @@ <!-- 搜索工作栏 --> <ContentWrap> - <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect --> - <WxAccountSelect @change="accountChanged" /> + <el-form class="-mb-15px" :model="queryParams" :inline="true" label-width="68px"> + <el-form-item label="公众号" prop="accountId"> + <WxMpSelect @change="onAccountChanged" /> + </el-form-item> + </el-form> </ContentWrap> <!-- tab 切换 --> <ContentWrap> - <el-tabs v-model="type" @tab-change="handleTabChange"> + <el-tabs v-model="msgType" @tab-change="handleTabChange"> <!-- 操作工具栏 --> <el-row :gutter="10" class="mb8"> <el-col :span="1.5"> @@ -18,26 +21,26 @@ plain @click="handleAdd" v-hasPermi="['mp:auto-reply:create']" - v-if="type !== '1' || list.length <= 0" + v-if="msgType !== MsgType.Follow || list.length <= 0" > <Icon icon="ep:plus" />新增 </el-button> </el-col> </el-row> <!-- tab 项 --> - <el-tab-pane name="1"> + <el-tab-pane :name="MsgType.Follow"> <template #label> - <span><Icon icon="ep:star-off" /> 关注时回复</span> + <span><Icon icon="ep:star" /> 关注时回复</span> </template> </el-tab-pane> - <el-tab-pane name="2"> + <el-tab-pane :name="MsgType.Message"> <template #label> <span><Icon icon="ep:chat-line-round" /> 消息回复</span> </template> </el-tab-pane> - <el-tab-pane name="3"> + <el-tab-pane :name="MsgType.Keyword"> <template #label> - <span><Icon icon="ep:news" /> 关键词回复</span> + <span><Icon icon="fa:newspaper-o" /> 关键词回复</span> </template> </el-tab-pane> </el-tabs> @@ -47,10 +50,20 @@ label="请求消息类型" align="center" prop="requestMessageType" - v-if="type === '2'" + v-if="msgType === MsgType.Message" /> - <el-table-column label="关键词" align="center" prop="requestKeyword" v-if="type === '3'" /> - <el-table-column label="匹配类型" align="center" prop="requestMatch" v-if="type === '3'"> + <el-table-column + label="关键词" + align="center" + prop="requestKeyword" + v-if="msgType === MsgType.Keyword" + /> + <el-table-column + label="匹配类型" + align="center" + prop="requestMatch" + v-if="msgType === MsgType.Keyword" + > <template #default="scope"> <dict-tag :type="DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH" :value="scope.row.requestMatch" /> </template> @@ -64,7 +77,7 @@ <template #default="scope"> <div v-if="scope.row.responseMessageType === 'text'">{{ scope.row.responseContent }}</div> <div v-else-if="scope.row.responseMessageType === 'voice'"> - <WxVoicePlayer :url="scope.row.responseMediaUrl" /> + <WxVoicePlayer v-if="scope.row.responseMediaUrl" :url="scope.row.responseMediaUrl" /> </div> <div v-else-if="scope.row.responseMessageType === 'image'"> <a target="_blank" :href="scope.row.responseMediaUrl"> @@ -77,7 +90,11 @@ scope.row.responseMessageType === 'shortvideo' " > - <WxVideoPlayer :url="scope.row.responseMediaUrl" style="margin-top: 10px" /> + <WxVideoPlayer + v-if="scope.row.responseMediaUrl" + :url="scope.row.responseMediaUrl" + style="margin-top: 10px" + /> </div> <div v-else-if="scope.row.responseMessageType === 'news'"> <WxNews :articles="scope.row.responseArticles" /> @@ -123,21 +140,21 @@ </el-table> <!-- 添加或修改自动回复的对话框 --> - <el-dialog :title="title" v-model="open" width="800px" append-to-body> - <el-form ref="formRef" :model="form" :rules="rules" label-width="80px"> - <el-form-item label="消息类型" prop="requestMessageType" v-if="type === '2'"> - <el-select v-model="form.requestMessageType" placeholder="请选择"> + <el-dialog :title="title" v-model="showReplyFormDialog" width="800px" append-to-body> + <el-form ref="formRef" :model="replyForm" :rules="rules" label-width="80px"> + <el-form-item label="消息类型" prop="requestMessageType" v-if="msgType === MsgType.Message"> + <el-select v-model="replyForm.requestMessageType" placeholder="请选择"> <template v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)" :key="dict.value"> <el-option - v-if="requestMessageTypes.includes(dict.value)" + v-if="RequestMessageTypes.includes(dict.value)" :label="dict.label" :value="dict.value" /> </template> </el-select> </el-form-item> - <el-form-item label="匹配类型" prop="requestMatch" v-if="type === '3'"> - <el-select v-model="form.requestMatch" placeholder="请选择匹配类型" clearable> + <el-form-item label="匹配类型" prop="requestMatch" v-if="msgType === MsgType.Keyword"> + <el-select v-model="replyForm.requestMatch" placeholder="请选择匹配类型" clearable> <el-option v-for="dict in getDictOptions(DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH)" :key="dict.value" @@ -146,8 +163,8 @@ /> </el-select> </el-form-item> - <el-form-item label="关键词" prop="requestKeyword" v-if="type === '3'"> - <el-input v-model="form.requestKeyword" placeholder="请输入内容" clearable /> + <el-form-item label="关键词" prop="requestKeyword" v-if="msgType === MsgType.Keyword"> + <el-input v-model="replyForm.requestKeyword" placeholder="请输入内容" clearable /> </el-form-item> <el-form-item label="回复消息"> <WxReplySelect :objData="objData" v-if="hackResetWxReplySelect" /> @@ -160,38 +177,47 @@ </el-dialog> </ContentWrap> </template> -<script setup name="MpAutoReply"> +<script setup lang="ts" name="MpAutoReply"> import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue' import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue' import WxMusic from '@/views/mp/components/wx-music/main.vue' import WxNews from '@/views/mp/components/wx-news/main.vue' import WxReplySelect from '@/views/mp/components/wx-reply/main.vue' -import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' +import WxMpSelect from '@/views/mp/components/WxMpSelect.vue' import * as MpAutoReplyApi from '@/api/mp/autoReply' - import { DICT_TYPE, getDictOptions } from '@/utils/dict' import { dateFormatter } from '@/utils/formatTime' import { ContentWrap } from '@/components/ContentWrap' +import { TabPaneName } from 'element-plus' const message = useMessage() -// const queryFormRef = ref() const formRef = ref() -// tab 类型(1、关注时回复;2、消息回复;3、关键词回复) -const type = ref('3') +// 消息类型(Follow: 关注时回复;Message: 消息回复;Keyword: 关键词回复) +// 作为tab.name +enum MsgType { + Follow = 1, + Message = 2, + Keyword = 3 +} +const msgType = ref<MsgType>(MsgType.Keyword) // 允许选择的请求消息类型 -const requestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link'] +const RequestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link'] // 遮罩层 const loading = ref(true) -// 显示搜索条件 -// const showSearch = ref(true) // 总条数 const total = ref(0) // 自动回复列表 -const list = ref([]) +const list = ref<any[]>([]) + // 查询参数 -const queryParams = reactive({ +interface QueryParams { + pageNo: number + pageSize: number + accountId?: number +} +const queryParams: QueryParams = reactive({ pageNo: 1, pageSize: 10, accountId: undefined @@ -200,12 +226,50 @@ const queryParams = reactive({ // 弹出层标题 const title = ref('') // 是否显示弹出层 -const open = ref(false) +const showReplyFormDialog = ref(false) // 表单参数 -const form = ref({}) +type ReplyType = 'text' | 'image' | 'voice' | 'video' | 'shortvideo' | 'location' | 'link' +interface ReplyForm { + // relation: + id?: number + accountId?: number + type?: MsgType + // request: + requestMessageType?: ReplyType + requestMatch?: number + requestKeyword?: string + // response: + responseMessageType?: ReplyType + responseContent?: string + responseMediaId?: number + responseMediaUrl?: string + responseTitle?: string + responseDescription?: number + responseThumbMediaId?: string + responseThumbMediaUrl?: string + responseArticles?: any[] + responseMusicUrl?: string + responseHqMusicUrl?: string +} +interface ObjData { + type: ReplyType + accountId?: number + content?: string + mediaId?: number + url?: string + title?: string + description?: string + thumbMediaId?: number + thumbMediaUrl?: string + articles?: any[] + musicUrl?: string + hqMusicUrl?: string +} +const replyForm = ref<ReplyForm>({}) // 回复消息 -const objData = ref({ - type: 'text' +const objData = ref<ObjData>({ + type: 'text', + accountId: undefined }) // 表单校验 const rules = { @@ -216,8 +280,8 @@ const rules = { // 重置 WxReplySelect 组件,解决无法清除的问题 const hackResetWxReplySelect = ref(false) -const accountChanged = (accountId) => { - queryParams.accountId = accountId +const onAccountChanged = (id?: number) => { + queryParams.accountId = id getList() } @@ -227,7 +291,7 @@ const getList = async () => { try { const data = await MpAutoReplyApi.getAutoReplyPage({ ...queryParams, - type: type.value + type: msgType.value }) list.value = data.list total.value = data.total @@ -242,8 +306,8 @@ const handleQuery = () => { getList() } -const handleTabChange = (tabName) => { - type.value = tabName +const handleTabChange = (tabName: TabPaneName) => { + msgType.value = tabName as MsgType handleQuery() } @@ -252,94 +316,87 @@ const handleAdd = () => { reset() resetEditor() // 打开表单,并设置初始化 - open.value = true - title.value = '新增自动回复' objData.value = { type: 'text', accountId: queryParams.accountId } + + title.value = '新增自动回复' + showReplyFormDialog.value = true } /** 修改按钮操作 */ -const handleUpdate = (row) => { +const handleUpdate = async (row: any) => { reset() resetEditor() - console.log(row) - MpAutoReplyApi.getAutoReply(row.id).then((data) => { - // 设置属性 - form.value = { ...data } - delete form.value['responseMessageType'] - delete form.value['responseContent'] - delete form.value['responseMediaId'] - delete form.value['responseMediaUrl'] - delete form.value['responseDescription'] - delete form.value['responseArticles'] - objData.value = { - type: data.responseMessageType, - accountId: queryParams.accountId, - content: data.responseContent, - mediaId: data.responseMediaId, - url: data.responseMediaUrl, - title: data.responseTitle, - description: data.responseDescription, - thumbMediaId: data.responseThumbMediaId, - thumbMediaUrl: data.responseThumbMediaUrl, - articles: data.responseArticles, - musicUrl: data.responseMusicUrl, - hqMusicUrl: data.responseHqMusicUrl - } + const data = await MpAutoReplyApi.getAutoReply(row.id) + // 设置属性 + replyForm.value = { ...data } + delete replyForm.value['responseMessageType'] + delete replyForm.value['responseContent'] + delete replyForm.value['responseMediaId'] + delete replyForm.value['responseMediaUrl'] + delete replyForm.value['responseDescription'] + delete replyForm.value['responseArticles'] + objData.value = { + type: data.responseMessageType, + accountId: queryParams.accountId, + content: data.responseContent, + mediaId: data.responseMediaId, + url: data.responseMediaUrl, + title: data.responseTitle, + description: data.responseDescription, + thumbMediaId: data.responseThumbMediaId, + thumbMediaUrl: data.responseThumbMediaUrl, + articles: data.responseArticles, + musicUrl: data.responseMusicUrl, + hqMusicUrl: data.responseHqMusicUrl + } - // 打开表单 - open.value = true - title.value = '修改自动回复' - }) + // 打开表单 + title.value = '修改自动回复' + showReplyFormDialog.value = true } -const handleSubmit = () => { - formRef.value?.validate((valid) => { - if (!valid) { - return - } +const handleSubmit = async () => { + const valid = await formRef.value?.validate() + if (!valid) return - // 处理回复消息 - const form = { ...form.value } - form.responseMessageType = objData.value.type - form.responseContent = objData.value.content - form.responseMediaId = objData.value.mediaId - form.responseMediaUrl = objData.value.url - form.responseTitle = objData.value.title - form.responseDescription = objData.value.description - form.responseThumbMediaId = objData.value.thumbMediaId - form.responseThumbMediaUrl = objData.value.thumbMediaUrl - form.responseArticles = objData.value.articles - form.responseMusicUrl = objData.value.musicUrl - form.responseHqMusicUrl = objData.value.hqMusicUrl + // 处理回复消息 + const submitForm: any = { ...replyForm.value } + submitForm.responseMessageType = objData.value.type + submitForm.responseContent = objData.value.content + submitForm.responseMediaId = objData.value.mediaId + submitForm.responseMediaUrl = objData.value.url + submitForm.responseTitle = objData.value.title + submitForm.responseDescription = objData.value.description + submitForm.responseThumbMediaId = objData.value.thumbMediaId + submitForm.responseThumbMediaUrl = objData.value.thumbMediaUrl + submitForm.responseArticles = objData.value.articles + submitForm.responseMusicUrl = objData.value.musicUrl + submitForm.responseHqMusicUrl = objData.value.hqMusicUrl - if (form.value.id !== undefined) { - MpAutoReplyApi.updateAutoReply(form).then(() => { - message.success('修改成功') - open.value = false - getList() - }) - } else { - MpAutoReplyApi.createAutoReply(form).then(() => { - message.success('新增成功') - open.value = false - getList() - }) - } - }) + if (replyForm.value.id !== undefined) { + await MpAutoReplyApi.updateAutoReply(submitForm) + message.success('修改成功') + } else { + await MpAutoReplyApi.createAutoReply(submitForm) + message.success('新增成功') + } + + showReplyFormDialog.value = false + getList() } // 表单重置 const reset = () => { - form.value = { + replyForm.value = { id: undefined, accountId: queryParams.accountId, - type: type.value, + type: msgType.value, requestKeyword: undefined, - requestMatch: type.value === '3' ? 1 : undefined, + requestMatch: msgType.value === MsgType.Keyword ? 1 : undefined, requestMessageType: undefined } formRef.value?.resetFields() @@ -347,7 +404,7 @@ const reset = () => { // 取消按钮 const cancel = () => { - open.value = false + showReplyFormDialog.value = false reset() } diff --git a/src/views/mp/components/WxMpSelect.vue b/src/views/mp/components/WxMpSelect.vue index e39bbf96..14f347d6 100644 --- a/src/views/mp/components/WxMpSelect.vue +++ b/src/views/mp/components/WxMpSelect.vue @@ -1,10 +1,5 @@ <template> - <el-select - v-model="accountId" - placeholder="请选择公众号" - class="!w-240px" - @change="accountChanged" - > + <el-select v-model="account.id" placeholder="请选择公众号" class="!w-240px" @change="onChanged"> <el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" /> </el-select> </template> @@ -12,11 +7,14 @@ <script lang="ts" setup name="WxMpSelect"> import * as MpAccountApi from '@/api/mp/account' -const accountId: Ref<number | undefined> = ref() +const account: MpAccountApi.AccountVO = reactive({ + id: undefined, + name: '' +}) const accountList: Ref<MpAccountApi.AccountVO[]> = ref([]) const emit = defineEmits<{ - (e: 'change', id: number | undefined): void + (e: 'change', id?: number, name?: string): void }>() onMounted(() => { @@ -27,12 +25,12 @@ const handleQuery = async () => { accountList.value = await MpAccountApi.getSimpleAccountList() // 默认选中第一个 if (accountList.value.length > 0) { - accountId.value = accountList.value[0].id - emit('change', accountId.value) + account.id = accountList.value[0].id + emit('change', account.id, account.name) } } -const accountChanged = () => { - emit('change', accountId.value) +const onChanged = () => { + emit('change', account.id, account.name) } </script> diff --git a/src/views/mp/components/wx-editor/WxEditor.vue b/src/views/mp/components/wx-editor/WxEditor.vue index 50c65c6e..cf384113 100644 --- a/src/views/mp/components/wx-editor/WxEditor.vue +++ b/src/views/mp/components/wx-editor/WxEditor.vue @@ -1,11 +1,11 @@ <script setup> import { ref, reactive } from 'vue' -import { QuillEditor } from '@vueup/vue-quill' -import '@vueup/vue-quill/dist/vue-quill.snow.css' import { getAccessToken } from '@/utils/auth' -import editorOptions from './quill-options' +import { Editor } from '@/components/Editor' const BASE_URL = import.meta.env.VITE_BASE_URL +const actionUrl = BASE_URL + '/admin-api/mp/material/upload-news-image' // 这里写你要上传的图片服务器地址 +const headers = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部 const message = useMessage() @@ -30,21 +30,16 @@ const props = defineProps({ const emit = defineEmits(['input']) const myQuillEditorRef = ref() - const content = ref(props.value.replace(/data-src/g, 'src')) - const loading = ref(false) // 根据图片上传状态来确定是否显示loading动画,刚开始是false,不显示 - -const actionUrl = ref(BASE_URL + '/admin-api/mp/material/upload-news-image') // 这里写你要上传的图片服务器地址 -const headers = ref({ Authorization: 'Bearer ' + getAccessToken() }) // 设置上传的请求头部 const uploadData = reactive({ type: 'image', // TODO 芋艿:试试要不要换成 thumb accountId: props.accountId }) -const onEditorChange = () => { +const onEditorChange = (text) => { //内容改变事件 - emit('input', content.value) + emit('input', text) } // 富文本图片上传前 @@ -98,104 +93,14 @@ const uploadError = () => { :on-error="uploadError" :before-upload="beforeUpload" /> - <QuillEditor - class="editor" - v-model="content" + <Editor + editor-id="wxEditor" ref="quillEditorRef" - :options="editorOptions" - @change="onEditorChange($event)" + :modelValue="content" + @change="(editor) => onEditorChange(editor.getText())" /> </div> </div> </template> -<style> -.editor { - line-height: normal !important; - height: 500px; -} - -.ql-snow .ql-tooltip[data-mode='link']::before { - content: '请输入链接地址:'; -} - -.ql-snow .ql-tooltip.ql-editing a.ql-action::after { - border-right: 0; - content: '保存'; - padding-right: 0; -} - -.ql-snow .ql-tooltip[data-mode='video']::before { - content: '请输入视频地址:'; -} - -.ql-snow .ql-picker.ql-size .ql-picker-label::before, -.ql-snow .ql-picker.ql-size .ql-picker-item::before { - content: '14px'; -} - -.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before { - content: '10px'; -} - -.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before { - content: '18px'; -} - -.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before { - content: '32px'; -} - -.ql-snow .ql-picker.ql-header .ql-picker-label::before, -.ql-snow .ql-picker.ql-header .ql-picker-item::before { - content: '文本'; -} - -.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before, -.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before { - content: '标题1'; -} - -.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before, -.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before { - content: '标题2'; -} - -.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before, -.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before { - content: '标题3'; -} - -.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before, -.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before { - content: '标题4'; -} - -.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before, -.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before { - content: '标题5'; -} - -.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before, -.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before { - content: '标题6'; -} - -.ql-snow .ql-picker.ql-font .ql-picker-label::before, -.ql-snow .ql-picker.ql-font .ql-picker-item::before { - content: '标准字体'; -} - -.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before, -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before { - content: '衬线字体'; -} - -.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before, -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before { - content: '等宽字体'; -} -</style> +<style></style> diff --git a/src/views/mp/draft/index.vue b/src/views/mp/draft/index.vue index 299ec68a..5df7687a 100644 --- a/src/views/mp/draft/index.vue +++ b/src/views/mp/draft/index.vue @@ -3,14 +3,22 @@ <!-- 搜索工作栏 --> <ContentWrap> - <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect --> - <WxAccountSelect @change="accountChanged"> - <template #actions> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="公众号" prop="accountId"> + <WxMpSelect @change="onAccountChanged" /> + </el-form-item> + <el-form-item> <el-button type="primary" plain @click="handleAdd" v-hasPermi="['mp:draft:create']"> <Icon icon="ep:plus" />新增 </el-button> - </template> - </WxAccountSelect> + </el-form-item> + </el-form> </ContentWrap> <!-- 列表 --> @@ -58,214 +66,221 @@ /> </ContentWrap> - <!-- TODO @Dhb52:迁移成独立路由 --> <div class="app-container"> <!-- 添加或修改草稿对话框 --> - <Teleport to="body"> - <el-dialog - :title="operateMaterial === 'add' ? '新建图文' : '修改图文'" - width="80%" - top="20px" - v-model="dialogNewsVisible" - :before-close="dialogNewsClose" - :close-on-click-modal="false" - > - <div class="left"> - <div class="select-item"> - <div v-for="(news, index) in articlesAdd" :key="news.id"> - <div - class="news-main father" - v-if="index === 0" - :class="{ activeAddNews: isActiveAddNews === index }" - @click="activeNews(index)" - > - <div class="news-content"> - <img class="material-img" v-if="news.thumbUrl" :src="news.thumbUrl" /> - <div class="news-content-title">{{ news.title }}</div> - </div> - <div class="child" v-if="articlesAdd.length > 1"> - <el-button size="small" @click="downNews(index)" - ><Icon icon="ep:sort-down" />下移</el-button - > - <el-button v-if="operateMaterial === 'add'" size="small" @click="minusNews(index)" - ><Icon icon="ep:delete" />删除 - </el-button> - </div> + <el-dialog + :title="operateMaterial === 'add' ? '新建图文' : '修改图文'" + width="80%" + top="20px" + v-model="dialogNewsVisible" + :before-close="dialogNewsClose" + :close-on-click-modal="false" + > + <div class="left"> + <div class="select-item"> + <div v-for="(news, index) in articlesAdd" :key="news.id"> + <div + class="news-main father" + v-if="index === 0" + :class="{ activeAddNews: isActiveAddNews === index }" + @click="activeNews(index)" + > + <div class="news-content"> + <img class="material-img" v-if="news.thumbUrl" :src="news.thumbUrl" /> + <div class="news-content-title">{{ news.title }}</div> </div> - <div - class="news-main-item father" - v-if="index > 0" - :class="{ activeAddNews: isActiveAddNews === index }" - @click="activeNews(index)" - > - <div class="news-content-item"> - <div class="news-content-item-title">{{ news.title }}</div> - <div class="news-content-item-img"> - <img - class="material-img" - v-if="news.thumbUrl" - :src="news.thumbUrl" - height="100%" - /> - </div> - </div> - <div class="child"> - <el-button - v-if="articlesAdd.length > index + 1" - size="small" - @click="downNews(index)" - ><Icon icon="ep:sort-down" />下移 - </el-button> - <el-button size="small" @click="upNews(index)" - ><Icon icon="ep:sort-up" />上移</el-button - > - <el-button - v-if="operateMaterial === 'add'" - type="danger" - size="small" - @click="minusNews(index)" - ><Icon icon="ep:delete" />删除 - </el-button> - </div> - </div> - </div> - <el-row justify="center" class="ope-row"> - <el-button - type="primary" - circle - @click="plusNews(item)" - v-if="articlesAdd.length < 8 && operateMaterial === 'add'" - > - <Icon icon="ep:plus" /> - </el-button> - </el-row> - </div> - </div> - <div class="right" v-loading="addMaterialLoading" v-if="articlesAdd.length > 0"> - <br /> - <br /> - <br /> - <br /> - <!-- 标题、作者、原文地址 --> - <el-input v-model="articlesAdd[isActiveAddNews].title" placeholder="请输入标题(必填)" /> - <el-input - v-model="articlesAdd[isActiveAddNews].author" - placeholder="请输入作者" - style="margin-top: 5px" - /> - <el-input - v-model="articlesAdd[isActiveAddNews].contentSourceUrl" - placeholder="请输入原文地址" - style="margin-top: 5px" - /> - <!-- 封面和摘要 --> - <div class="input-tt">封面和摘要:</div> - <div> - <div class="thumb-div"> - <img - class="material-img" - v-if="articlesAdd[isActiveAddNews].thumbUrl" - :src="articlesAdd[isActiveAddNews].thumbUrl" - :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'" - /> - <Icon - v-else - icon="ep:plus" - class="avatar-uploader-icon" - :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'" - /> - <div class="thumb-but"> - <el-upload - :action="actionUrl" - :headers="headers" - multiple - :limit="1" - :file-list="fileList" - :data="uploadData" - :before-upload="beforeThumbImageUpload" - :on-success="handleUploadSuccess" + <div class="child" v-if="articlesAdd.length > 1"> + <el-button size="small" @click="downNews(index)" + ><Icon icon="ep:sort-down" />下移</el-button > - <template #trigger> - <el-button size="small" type="primary">本地上传</el-button> - </template> - <el-button - size="small" - type="primary" - @click="openMaterial" - style="margin-left: 5px" - >素材库选择</el-button - > - <template #tip> - <div class="el-upload__tip">支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div> - </template> - </el-upload> + <el-button v-if="operateMaterial === 'add'" size="small" @click="minusNews(index)" + ><Icon icon="ep:delete" />删除 + </el-button> + </div> + </div> + <div + class="news-main-item father" + v-if="index > 0" + :class="{ activeAddNews: isActiveAddNews === index }" + @click="activeNews(index)" + > + <div class="news-content-item"> + <div class="news-content-item-title">{{ news.title }}</div> + <div class="news-content-item-img"> + <img + class="material-img" + v-if="news.thumbUrl" + :src="news.thumbUrl" + height="100%" + /> + </div> + </div> + <div class="child"> + <el-button + v-if="articlesAdd.length > index + 1" + size="small" + @click="downNews(index)" + ><Icon icon="ep:sort-down" />下移 + </el-button> + <el-button size="small" @click="upNews(index)" + ><Icon icon="ep:sort-up" />上移</el-button + > + <el-button + v-if="operateMaterial === 'add'" + type="danger" + size="small" + @click="minusNews(index)" + ><Icon icon="ep:delete" />删除 + </el-button> </div> - <Teleport to="body"> - <el-dialog title="选择图片" v-model="dialogImageVisible" width="80%"> - <WxMaterialSelect - ref="materialSelectRef" - :objData="{ type: 'image', accountId: queryParams.accountId }" - @select-material="selectMaterial" - /> - </el-dialog> - </Teleport> </div> - <el-input - :rows="8" - type="textarea" - v-model="articlesAdd[isActiveAddNews].digest" - placeholder="请输入摘要" - class="digest" - maxlength="120" - style="float: right" - /> </div> - <!--富文本编辑器组件--> - <el-row> - <WxEditor - v-model="articlesAdd[isActiveAddNews].content" - :account-id="uploadData.accountId" - v-if="hackResetEditor" - /> + <el-row justify="center" class="ope-row"> + <el-button + type="primary" + circle + @click="plusNews" + v-if="articlesAdd.length < 8 && operateMaterial === 'add'" + > + <Icon icon="ep:plus" /> + </el-button> </el-row> </div> - <template #footer> - <el-button @click="dialogNewsVisible = false">取 消</el-button> - <el-button type="primary" @click="submitForm">提 交</el-button> - </template> - </el-dialog> - </Teleport> + </div> + <div class="right" v-loading="addMaterialLoading" v-if="articlesAdd.length > 0"> + <br /> + <br /> + <br /> + <br /> + <!-- 标题、作者、原文地址 --> + <el-input v-model="articlesAdd[isActiveAddNews].title" placeholder="请输入标题(必填)" /> + <el-input + v-model="articlesAdd[isActiveAddNews].author" + placeholder="请输入作者" + style="margin-top: 5px" + /> + <el-input + v-model="articlesAdd[isActiveAddNews].contentSourceUrl" + placeholder="请输入原文地址" + style="margin-top: 5px" + /> + <!-- 封面和摘要 --> + <div class="input-tt">封面和摘要:</div> + <div> + <div class="thumb-div"> + <img + class="material-img" + v-if="articlesAdd[isActiveAddNews].thumbUrl" + :src="articlesAdd[isActiveAddNews].thumbUrl" + :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'" + /> + <Icon + v-else + icon="ep:plus" + class="avatar-uploader-icon" + :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'" + /> + <div class="thumb-but"> + <el-upload + :action="uploadUrl" + :headers="headers" + multiple + :limit="1" + :file-list="fileList" + :data="uploadData" + :before-upload="beforeThumbImageUpload" + :on-success="handleUploadSuccess" + > + <template #trigger> + <el-button size="small" type="primary">本地上传</el-button> + </template> + <el-button + size="small" + type="primary" + @click="openMaterial" + style="margin-left: 5px" + >素材库选择</el-button + > + <template #tip> + <div class="el-upload__tip">支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div> + </template> + </el-upload> + </div> + <el-dialog title="选择图片" v-model="dialogImageVisible" width="80%" append-to-body> + <WxMaterialSelect + ref="materialSelectRef" + :objData="{ type: 'image', accountId: queryParams.accountId }" + @select-material="selectMaterial" + /> + </el-dialog> + </div> + <el-input + :rows="8" + type="textarea" + v-model="articlesAdd[isActiveAddNews].digest" + placeholder="请输入摘要" + class="digest" + maxlength="120" + style="float: right" + /> + </div> + <!--富文本编辑器组件--> + <el-row> + <WxEditor + v-model="articlesAdd[isActiveAddNews].content" + :account-id="uploadData.accountId" + v-if="hackResetEditor" + /> + </el-row> + </div> + <template #footer> + <el-button @click="dialogNewsVisible = false">取 消</el-button> + <el-button type="primary" @click="submitForm">提 交</el-button> + </template> + </el-dialog> </div> </template> -<script setup name="MpDraft"> + +<script setup lang="ts" name="MpDraft"> import WxEditor from '@/views/mp/components/wx-editor/WxEditor.vue' import WxNews from '@/views/mp/components/wx-news/main.vue' import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue' -import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' +import WxMpSelect from '@/views/mp/components/WxMpSelect.vue' import { getAccessToken } from '@/utils/auth' import * as MpDraftApi from '@/api/mp/draft' import * as MpFreePublishApi from '@/api/mp/freePublish' +import { UploadFiles, UploadProps, UploadRawFile } from 'element-plus' // 可以用改本地数据模拟,避免API调用超限 // import drafts from './mock' const message = useMessage() // 消息 const loading = ref(true) // 列表的加载中 +const list = ref<any[]>([]) // 列表的数据 const total = ref(0) // 列表的总页数 -const list = ref([]) // 列表的数据 -const queryParams = reactive({ +interface QueryParams { + pageNo: number + pageSize: number + accountId?: number +} +const queryParams: QueryParams = reactive({ pageNo: 1, pageSize: 10, accountId: undefined }) // ========== 文件上传 ========== -const materialSelectRef = ref() const BASE_URL = import.meta.env.VITE_BASE_URL -const actionUrl = ref(BASE_URL + '/admin-api/mp/material/upload-permanent') // 上传永久素材的地址 -const headers = ref({ Authorization: 'Bearer ' + getAccessToken() }) // 设置上传的请求头部 -const fileList = ref([]) -const uploadData = reactive({ +const uploadUrl = BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传永久素材的地址 +const headers = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部 + +const materialSelectRef = ref<InstanceType<typeof WxMaterialSelect> | null>(null) +const fileList = ref<UploadFiles>([]) +interface UploadData { + type: 'image' | 'video' | 'audio' + accountId?: number +} +const uploadData: UploadData = reactive({ type: 'image', accountId: 1 }) @@ -273,34 +288,28 @@ const uploadData = reactive({ // ========== 草稿新建 or 修改 ========== const dialogNewsVisible = ref(false) const addMaterialLoading = ref(false) // 添加草稿的 loading 标识 -const articlesAdd = ref([]) +const articlesAdd = ref<any[]>([]) const isActiveAddNews = ref(0) const dialogImageVisible = ref(false) -const operateMaterial = ref('add') +const operateMaterial = ref<'add' | 'edit'>('add') const articlesMediaId = ref('') const hackResetEditor = ref(false) /** 侦听公众号变化 **/ -const accountChanged = (accountId) => { - setAccountId(accountId) +const onAccountChanged = (id?: number) => { + setAccountId(id) getList() } // ======================== 列表查询 ======================== /** 设置账号编号 */ -const setAccountId = (accountId) => { - queryParams.accountId = accountId - uploadData.accountId = accountId +const setAccountId = (id?: number) => { + queryParams.accountId = id + uploadData.accountId = id } /** 查询列表 */ const getList = async () => { - // 如果没有选中公众号账号,则进行提示。 - if (!queryParams.accountId) { - message.error('未选中公众号,无法查询草稿箱') - return false - } - loading.value = true try { const drafts = await MpDraftApi.getDraftPage(queryParams) @@ -329,7 +338,7 @@ const handleAdd = () => { } /** 更新按钮操作 */ -const handleUpdate = (item) => { +const handleUpdate = (item: any) => { resetEditor() reset() articlesMediaId.value = item.mediaId @@ -340,39 +349,30 @@ const handleUpdate = (item) => { } /** 提交按钮 */ -const submitForm = () => { - // TODO @Dhb52: 参考别的模块写法,改成 await 方式 +const submitForm = async () => { addMaterialLoading.value = true - if (operateMaterial.value === 'add') { - MpDraftApi.createDraft(queryParams.accountId, articlesAdd.value) - .then(() => { - message.notifySuccess('新增成功') - dialogNewsVisible.value = false - getList() - }) - .finally(() => { - addMaterialLoading.value = false - }) - } else { - MpDraftApi.updateDraft(queryParams.accountId, articlesMediaId.value, articlesAdd.value) - .then(() => { - message.notifySuccess('更新成功') - dialogNewsVisible.value = false - getList() - }) - .finally(() => { - addMaterialLoading.value = false - }) + try { + if (operateMaterial.value === 'add') { + await MpDraftApi.createDraft(queryParams.accountId, articlesAdd.value) + message.notifySuccess('新增成功') + } else { + await MpDraftApi.updateDraft(queryParams.accountId, articlesMediaId.value, articlesAdd.value) + message.notifySuccess('更新成功') + } + } finally { + dialogNewsVisible.value = false + addMaterialLoading.value = false + getList() } } // 关闭弹窗 -const dialogNewsClose = async (done) => { +const dialogNewsClose = async (onDone: () => {}) => { try { await message.confirm('修改内容可能还未保存,确定关闭吗?') reset() resetEditor() - done() + onDone() } catch {} } @@ -391,7 +391,7 @@ const resetEditor = () => { } // 将图文向下移动 -const downNews = (index) => { +const downNews = (index: number) => { let temp = articlesAdd.value[index] articlesAdd.value[index] = articlesAdd.value[index + 1] articlesAdd.value[index + 1] = temp @@ -407,13 +407,13 @@ const upNews = (index) => { } // 选中指定 index 的图文 -const activeNews = (index) => { +const activeNews = (index: number) => { resetEditor() isActiveAddNews.value = index } // 删除指定 index 的图文 -const minusNews = async (index) => { +const minusNews = async (index: number) => { try { await message.confirm('确定删除该图文吗?') articlesAdd.value.splice(index, 1) @@ -446,20 +446,17 @@ const buildEmptyArticle = () => { } // ======================== 文件上传 ======================== -const beforeThumbImageUpload = (file) => { +const beforeThumbImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => { addMaterialLoading.value = true - const isType = - file.type === 'image/jpeg' || - file.type === 'image/png' || - file.type === 'image/gif' || - file.type === 'image/bmp' || - file.type === 'image/jpg' + const isType = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'].includes( + rawFile.type + ) if (!isType) { message.error('上传图片格式不对!') addMaterialLoading.value = false return false } - const isLt = file.size / 1024 / 1024 < 2 + const isLt = rawFile.size / 1024 / 1024 < 2 if (!isLt) { message.error('上传图片大小不能超过 2M!') addMaterialLoading.value = false @@ -469,7 +466,7 @@ const beforeThumbImageUpload = (file) => { return true } -const handleUploadSuccess = (response, file, fileList) => { +const handleUploadSuccess: UploadProps['onSuccess'] = (response: any) => { addMaterialLoading.value = false if (response.code !== 0) { message.error('上传出错:' + response.msg) @@ -485,7 +482,7 @@ const handleUploadSuccess = (response, file, fileList) => { } // 选择 or 上传完素材,设置回草稿 -const selectMaterial = (item) => { +const selectMaterial = (item: any) => { dialogImageVisible.value = false articlesAdd.value[isActiveAddNews.value].thumbMediaId = item.mediaId articlesAdd.value[isActiveAddNews.value].thumbUrl = item.url @@ -494,14 +491,10 @@ const selectMaterial = (item) => { // 打开素材选择 const openMaterial = () => { dialogImageVisible.value = true - try { - materialSelectRef.value.queryParams.accountId = queryParams.accountId // 强制设置下 accountId,避免二次查询不对 - materialSelectRef.value.handleQuery() // 刷新列表,失败也无所谓 - } catch (e) {} } // ======================== 草稿箱发布 ======================== -const handlePublish = async (item) => { +const handlePublish = async (item: any) => { const accountId = queryParams.accountId const mediaId = item.mediaId const content = @@ -510,19 +503,19 @@ const handlePublish = async (item) => { await message.confirm(content) await MpFreePublishApi.submitFreePublish(accountId, mediaId) message.notifySuccess('发布成功') - await getList() + getList() } catch {} } /** 删除按钮操作 */ -const handleDelete = async (item) => { +const handleDelete = async (item: any) => { const accountId = queryParams.accountId const mediaId = item.mediaId try { await message.confirm('此操作将永久删除该草稿, 是否继续?') await MpDraftApi.deleteDraft(accountId, mediaId) message.notifySuccess('删除成功') - await getList() + getList() } catch {} } </script> diff --git a/src/views/mp/freePublish/index.vue b/src/views/mp/freePublish/index.vue index 0d370f4a..7a15b31f 100644 --- a/src/views/mp/freePublish/index.vue +++ b/src/views/mp/freePublish/index.vue @@ -3,8 +3,17 @@ <!-- 搜索工作栏 --> <ContentWrap> - <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect --> - <WxAccountSelect @change="(accountId) => accountChanged(accountId)" /> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="公众号" prop="accountId"> + <WxMpSelect @change="onAccountChanged" /> + </el-form-item> + </el-form> </ContentWrap> <!-- 列表 --> @@ -39,26 +48,32 @@ </ContentWrap> </template> -<script setup name="MpFreePublish"> +<script lang="ts" setup name="MpFreePublish"> import * as FreePublishApi from '@/api/mp/freePublish' import WxNews from '@/views/mp/components/wx-news/main.vue' -import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' +import WxMpSelect from '@/views/mp/components/WxMpSelect.vue' const message = useMessage() // 消息弹窗 const { t } = useI18n() // 国际化 const loading = ref(true) // 列表的加载中 const total = ref(0) // 列表的总页数 -const list = ref([]) // 列表的数据 -const queryParams = reactive({ +const list = ref<any[]>([]) // 列表的数据 + +interface QueryParams { + pageNo: number + pageSize: number + accountId?: number +} +const queryParams: QueryParams = reactive({ pageNo: 1, pageSize: 10, accountId: undefined }) /** 侦听公众号变化 **/ -const accountChanged = (accountId) => { - queryParams.accountId = accountId +const onAccountChanged = (id: number | undefined) => { + queryParams.accountId = id getList() } @@ -75,7 +90,7 @@ const getList = async () => { } /** 删除按钮操作 */ -const handleDelete = async (item) => { +const handleDelete = async (item: any) => { try { // 删除的二次确认 await message.delConfirm('删除后用户将无法访问此页面,确定删除?') diff --git a/src/views/mp/material/index.vue b/src/views/mp/material/index.vue index e4382b69..4d4017d4 100644 --- a/src/views/mp/material/index.vue +++ b/src/views/mp/material/index.vue @@ -4,13 +4,13 @@ <ContentWrap> <el-form class="-mb-15px" :inline="true" label-width="68px"> <el-form-item label="公众号" prop="accountId"> - <WxMpSelect @change="accountChange" /> + <WxMpSelect @change="onAccountChanged" /> </el-form-item> </el-form> </ContentWrap> <ContentWrap> - <el-tabs v-model="type" @tab-change="handleTabChange"> + <el-tabs v-model="type" @tab-change="onTabChange"> <!-- tab 1:图片 --> <el-tab-pane name="image"> <template #label> @@ -93,7 +93,7 @@ <el-table-column label="文件名" align="center" prop="name" /> <el-table-column label="语音" align="center"> <template #default="scope"> - <WxVoicePlayer :url="scope.row.url" /> + <WxVoicePlayer v-if="scope.row.url" :url="scope.row.url" /> </template> </el-table-column> <el-table-column @@ -188,10 +188,8 @@ </el-row> </el-form> <template #footer> - <!-- <span class="dialog-footer"> --> <el-button @click="cancelVideo">取 消</el-button> <el-button type="primary" @click="submitVideo">提 交</el-button> - <!-- </span> --> </template> </el-dialog> @@ -203,7 +201,7 @@ <el-table-column label="介绍" align="center" prop="introduction" /> <el-table-column label="视频" align="center"> <template #default="scope"> - <WxVideoPlayer :url="scope.row.url" /> + <WxVideoPlayer v-if="scope.row.url" :url="scope.row.url" /> </template> </el-table-column> <el-table-column @@ -284,9 +282,9 @@ const type = ref<MatertialType>('image') // 遮罩层 const loading = ref(false) // 总条数 -const total = ref(0) // 数据列表 -const list = ref([]) +const list = ref<any[]>([]) +const total = ref(0) // 查询参数 interface QueryParams { pageNo: number @@ -319,8 +317,8 @@ const dialogVideoVisible = ref(false) const addMaterialLoading = ref(false) /** 侦听公众号变化 **/ -const accountChange = (accountId: number | undefined) => { - queryParams.accountId = accountId +const onAccountChanged = (id?: number) => { + queryParams.accountId = id getList() } @@ -346,7 +344,7 @@ const handleQuery = () => { getList() } -const handleTabChange = (tabName: TabPaneName) => { +const onTabChange = (tabName: TabPaneName) => { // 设置 type uploadData.type = tabName as MatertialType @@ -359,54 +357,49 @@ const handleTabChange = (tabName: TabPaneName) => { } // ======================== 文件上传 ======================== -const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => { - const isType = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'].includes( - rawFile.type - ) - if (!isType) { - message.error('上传图片格式不对!') +const beforeUpload = (rawFile: UploadRawFile, category: 'image' | 'audio' | 'video'): boolean => { + let allowTypes: string[] = [] + let maxSizeMB = 0 + let name = '' + + switch (category) { + case 'image': + allowTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'] + maxSizeMB = 2 + name = '图片' + break + case 'audio': + allowTypes = ['audio/mp3', 'audio/mpeg', 'audio/wma', 'audio/wav', 'audio/amr'] + maxSizeMB = 2 + name = '图片' + break + case 'video': + allowTypes = ['video/mp4'] + maxSizeMB = 10 + name = '视频' + } + + if (!allowTypes.includes(rawFile.type)) { + message.error(`上传${name}格式不对!`) return false } - const isLt = rawFile.size / 1024 / 1024 < 2 - if (!isLt) { - message.error('上传图片大小不能超过 2M!') + // 校验大小 + if (rawFile.size / 1024 / 1024 > maxSizeMB) { + message.error(`上传${name}大小不能超过${maxSizeMB}M!`) return false } loading.value = true return true } -const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => { - const isType = ['audio/mp3', 'audio/wma', 'audio/wav', 'audio/amr'].includes(file.type) - const isLt = rawFile.size / 1024 / 1024 < 2 - if (!isType) { - message.error('上传语音格式不对!') - return false - } - if (!isLt) { - message.error('上传语音大小不能超过 2M!') - return false - } - loading.value = true - return true -} +const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => + beforeUpload(rawFile, 'image') -const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => { - const isType = rawFile.type === 'video/mp4' - if (!isType) { - message.error('上传视频格式不对!') - return false - } +const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => + beforeUpload(rawFile, 'audio') - const isLt = rawFile.size / 1024 / 1024 < 10 - if (!isLt) { - message.error('上传视频大小不能超过 10M!') - return false - } - - addMaterialLoading.value = true - return true -} +const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => + beforeUpload(rawFile, 'video') const handleUploadSuccess: UploadProps['onSuccess'] = (response: any) => { loading.value = false @@ -441,6 +434,7 @@ const submitVideo = () => { }) } +// 弹出 video 新建的表单 const handleAddVideo = () => { resetVideo() dialogVideoVisible.value = true diff --git a/src/views/mp/menu/index.vue b/src/views/mp/menu/index.vue index a6a08c97..fea08614 100644 --- a/src/views/mp/menu/index.vue +++ b/src/views/mp/menu/index.vue @@ -2,8 +2,11 @@ <doc-alert title="公众号菜单" url="https://doc.iocoder.cn/mp/menu/" /> <!-- 搜索工作栏 --> <ContentWrap> - <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect --> - <WxAccountSelect @change="accountChanged" /> + <el-form class="-mb-15px" ref="queryFormRef" :inline="true" label-width="68px"> + <el-form-item label="公众号" prop="accountId"> + <WxMpSelect @change="onAccountChanged" /> + </el-form-item> + </el-form> </ContentWrap> <!-- 列表 --> @@ -12,7 +15,7 @@ <!--左边配置菜单--> <div class="left"> <div class="weixin-hd"> - <div class="weixin-title">{{ name }}</div> + <div class="weixin-title">{{ accountName }}</div> </div> <div class="weixin-menu menu_main clearfix"> <div class="menu_bottom" v-for="(item, i) of menuList" :key="i"> @@ -68,7 +71,7 @@ <div v-if="showRightFlag" class="right"> <div class="configure_page"> <div class="delete_btn"> - <el-button size="small" type="danger" @click="handleDeleteMenu(tempObj)"> + <el-button size="small" type="danger" @click="handleDeleteMenu"> 删除当前菜单<Icon icon="ep:delete" /> </el-button> </div> @@ -155,7 +158,7 @@ <div v-else> <el-row justify="center"> <el-col :span="24" style="text-align: center"> - <el-button type="success" @click="openMaterial"> + <el-button type="success" @click="dialogNewsVisible = true"> 素材库选择<Icon icon="ep:circle-check" /> </el-button> </el-col> @@ -185,24 +188,26 @@ </div> </ContentWrap> </template> -<script setup name="MpMenu"> -import { handleTree } from '@/utils/tree' +<script lang="ts" setup name="MpMenu"> import WxReplySelect from '@/views/mp/components/wx-reply/main.vue' import WxNews from '@/views/mp/components/wx-news/main.vue' import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue' -import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' +import WxMpSelect from '@/views/mp/components/WxMpSelect.vue' import * as MpMenuApi from '@/api/mp/menu' +import { handleTree } from '@/utils/tree' import menuOptions from './menuOptions' + const message = useMessage() // 消息 // ======================== 列表查询 ======================== -const loading = ref(true) // 遮罩层 -const accountId = ref(undefined) // 公众号Id -const menuList = ref({ children: [] }) +const loading = ref(false) // 遮罩层 +const accountId = ref<number | undefined>() +const accountName = ref<string | undefined>('') +const menuList = ref<any>({ children: [] }) // ======================== 菜单操作 ======================== const isActive = ref(-1) // 一级菜单点中样式 -const isSubMenuActive = ref(-1) // 一级菜单点中样式 +const isSubMenuActive = ref<string | number>(-1) // 一级菜单点中样式 const isSubMenuFlag = ref(-1) // 二级菜单显示标志 // ======================== 菜单编辑 ======================== @@ -210,15 +215,16 @@ const showRightFlag = ref(false) // 右边配置显示默认详情还是配置 const nameMaxLength = ref(0) // 菜单名称最大长度;1 级是 4 字符;2 级是 7 字符; const showConfigureContent = ref(true) // 是否展示配置内容;如果有子菜单,就不显示配置内容 const hackResetWxReplySelect = ref(false) // 重置 WxReplySelect 组件 -const tempObj = ref({}) // 右边临时变量,作为中间值牵引关系 +const tempObj = ref<any>({}) // 右边临时变量,作为中间值牵引关系 // 一些临时值放在这里进行判断,如果放在 tempObj,由于引用关系,menu 也会多了多余的参数 -const tempSelfObj = ref({}) +const tempSelfObj = ref<any>({}) const dialogNewsVisible = ref(false) // 跳转图文时的素材选择弹窗 /** 侦听公众号变化 **/ -const accountChanged = (id) => { +const onAccountChanged = (id?: number, name?: string) => { accountId.value = id + accountName.value = name getList() } @@ -241,10 +247,10 @@ const handleQuery = () => { } // 将后端返回的 menuList,转换成前端的 menuList -const convertMenuList = (list) => { +const convertMenuList = (list: any[]) => { if (!list) return [] - const menuList = [] + const result: any[] = [] list.forEach((item) => { const menu = { ...item @@ -271,9 +277,9 @@ const convertMenuList = (list) => { hqMusicUrl: item.replyHqMusicUrl } } - menuList.push(menu) + result.push(menu) }) - return menuList + return result } // 重置表单,清空表单数据 @@ -286,7 +292,7 @@ const resetForm = () => { // 菜单编辑 showRightFlag.value = false nameMaxLength.value = 0 - showConfigureContent.value = 0 + showConfigureContent.value = false hackResetWxReplySelect.value = false tempObj.value = {} tempSelfObj.value = {} @@ -295,7 +301,7 @@ const resetForm = () => { // ======================== 菜单操作 ======================== // 一级菜单点击事件 -const menuClick = (i, item) => { +const menuClick = (i: number, item: any) => { // 右侧的表单相关 resetEditor() showRightFlag.value = true // 右边菜单 @@ -312,11 +318,10 @@ const menuClick = (i, item) => { } // 二级菜单点击事件 -const subMenuClick = (subItem, index, k) => { +const subMenuClick = (subItem: any, index: number, k: number) => { // 右侧的表单相关 resetEditor() showRightFlag.value = true // 右边菜单 - console.log(subItem) tempObj.value = subItem // 将点击的数据放到临时变量,对象有引用作用 tempSelfObj.value.grand = '2' // 表示二级菜单 tempSelfObj.value.index = index // 表示一级菜单索引 @@ -331,7 +336,7 @@ const subMenuClick = (subItem, index, k) => { // 添加横向一级菜单 const addMenu = () => { - const menuKeyLength = menuList.value.length + const menuKeyLength: number = menuList.value.length const addButton = { name: '菜单名称', children: [], @@ -342,10 +347,10 @@ const addMenu = () => { } } menuList.value[menuKeyLength] = addButton - menuClick(menuKeyLength.value - 1, addButton) + menuClick(menuKeyLength - 1, addButton) } // 添加横向二级菜单;item 表示要操作的父菜单 -const addSubMenu = (i, item) => { +const addSubMenu = (i: number, item: any) => { // 清空父菜单的属性,因为它只需要 name 属性即可 if (!item.children || item.children.length <= 0) { item.children = [] @@ -361,8 +366,8 @@ const addSubMenu = (i, item) => { showConfigureContent.value = false } - let subMenuKeyLength = item.children.length // 获取二级菜单key长度 - let addButton = { + const subMenuKeyLength = item.children.length // 获取二级菜单key长度 + const addButton = { name: '子菜单名称', reply: { // 用于存储回复内容 @@ -399,7 +404,7 @@ const handleDeleteMenu = async () => { // ======================== 菜单编辑 ======================== const handleSave = async () => { try { - await message.confirm('确定要删除吗?') + await message.confirm('确定要保存吗?') loading.value = true await MpMenuApi.saveMenu(accountId.value, convertMenuFormList()) getList() @@ -413,7 +418,6 @@ const handleSave = async () => { const resetEditor = () => { hackResetWxReplySelect.value = false // 销毁组件 nextTick(() => { - console.log('nextTick') hackResetWxReplySelect.value = true // 重建组件 }) } @@ -432,9 +436,9 @@ const handleDelete = async () => { // 将前端的 menuList,转换成后端接收的 menuList const convertMenuFormList = () => { - const result = [] + const result: any[] = [] menuList.value.forEach((item) => { - let menu = convertMenuForm(item) + const menu = convertMenuForm(item) result.push(menu) // 处理子菜单 @@ -450,7 +454,7 @@ const convertMenuFormList = () => { } // 将前端的 menu,转换成后端接收的 menu -const convertMenuForm = (menu) => { +const convertMenuForm = (menu: any) => { let result = { ...menu, children: undefined, // 不处理子节点 @@ -473,11 +477,7 @@ const convertMenuForm = (menu) => { } // ======================== 菜单编辑(素材选择) ======================== -const openMaterial = () => { - dialogNewsVisible.value = true -} - -const selectMaterial = (item) => { +const selectMaterial = (item: any) => { const articleId = item.articleId const articles = item.content.newsItem // 提示,针对多图文 diff --git a/src/views/mp/tag/TagForm.vue b/src/views/mp/tag/TagForm.vue index e190af89..81f19988 100644 --- a/src/views/mp/tag/TagForm.vue +++ b/src/views/mp/tag/TagForm.vue @@ -19,24 +19,30 @@ </template> <script setup lang="ts"> import * as MpTagApi from '@/api/mp/tag' +import { FormInstance, FormRules } from 'element-plus' + const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 const dialogVisible = ref(false) // 弹窗的是否展示 const dialogTitle = ref('') // 弹窗的标题 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 -const formType = ref('') // 表单的类型:create - 新增;update - 修改 +const formType = ref<'create' | 'update' | ''>('') // 表单的类型:create - 新增;update - 修改 const formData = ref({ accountId: -1, name: '' }) -const formRules = reactive({ +const formRules: FormRules = { name: [{ required: true, message: '请输入标签名称', trigger: 'blur' }] -}) -const formRef = ref() // 表单 Ref +} +const formRef = ref<FormInstance | null>(null) // 表单 Ref + +const emit = defineEmits<{ + (e: 'success'): void +}>() /** 打开弹窗 */ -const open = async (type: string, accountId: number, id?: number) => { +const open = async (type: 'create' | 'update', accountId: number, id?: number) => { dialogVisible.value = true dialogTitle.value = t('action.' + type) formType.value = type @@ -55,11 +61,10 @@ const open = async (type: string, accountId: number, id?: number) => { defineExpose({ open }) // 提供 open 方法,用于打开弹窗 /** 提交表单 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 const submitForm = async () => { // 校验表单 if (!formRef) return - const valid = await formRef.value.validate() + const valid = await formRef.value?.validate() if (!valid) return // 提交请求 formLoading.value = true diff --git a/src/views/mp/tag/index.vue b/src/views/mp/tag/index.vue index 44578519..6dc0d11a 100644 --- a/src/views/mp/tag/index.vue +++ b/src/views/mp/tag/index.vue @@ -3,17 +3,25 @@ <!-- 搜索工作栏 --> <ContentWrap> - <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect --> - <WxAccountSelect @change="accountChanged"> - <template #actions> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="公众号" prop="accountId"> + <WxMpSelect @change="onAccountChanged" /> + </el-form-item> + <el-form-item> <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['mp:tag:create']"> <Icon icon="ep:plus" class="mr-5px" /> 新增 </el-button> <el-button type="success" plain @click="handleSync" v-hasPermi="['mp:tag:sync']"> <Icon icon="ep:refresh" class="mr-5px" /> 同步 </el-button> - </template> - </WxAccountSelect> + </el-form-item> + </el-form> </ContentWrap> <!-- 列表 --> @@ -63,26 +71,35 @@ <TagForm ref="formRef" @success="getList" /> </template> <script setup lang="ts" name="MpTag"> -import { dateFormatter } from '@/utils/formatTime' -import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' import * as MpTagApi from '@/api/mp/tag' import TagForm from './TagForm.vue' +import WxMpSelect from '@/views/mp/components/WxMpSelect.vue' +import { dateFormatter } from '@/utils/formatTime' + const message = useMessage() // 消息弹窗 const { t } = useI18n() // 国际化 const loading = ref(true) // 列表的加载中 const total = ref(0) // 列表的总页数 -const list = ref([]) // 列表的数据 -const queryParams = reactive({ +const list = ref<any>([]) // 列表的数据 + +interface QueryParams { + pageNo: number + pageSize: number + accountId?: number +} +const queryParams: QueryParams = reactive({ pageNo: 1, pageSize: 10, accountId: undefined }) +const formRef = ref<InstanceType<typeof TagForm> | null>(null) + /** 侦听公众号变化 **/ -const accountChanged = (accountId) => { +const onAccountChanged = (id?: number) => { queryParams.pageNo = 1 - queryParams.accountId = accountId + queryParams.accountId = id getList() } @@ -99,9 +116,8 @@ const getList = async () => { } /** 添加/修改操作 */ -const formRef = ref() const openForm = (type: string, id?: number) => { - formRef.value.open(type, queryParams.accountId, id) + formRef.value?.open(type, queryParams.accountId as number, id) } /** 删除按钮操作 */ @@ -121,8 +137,7 @@ const handleDelete = async (id: number) => { const handleSync = async () => { try { await message.confirm('是否确认同步标签?') - // @ts-ignore - await MpTagApi.syncTag(queryParams.accountId) + await MpTagApi.syncTag(queryParams.accountId as number) message.success('同步标签成功') await getList() } catch {} diff --git a/src/views/mp/user/index.vue b/src/views/mp/user/index.vue index 91fd7dff..e8e23aaa 100644 --- a/src/views/mp/user/index.vue +++ b/src/views/mp/user/index.vue @@ -3,14 +3,42 @@ <!-- 搜索工作栏 --> <ContentWrap> - <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect --> - <WxAccountSelect @change="(accountId) => accountChanged(accountId)"> - <template #actions> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="公众号" prop="accountId"> + <WxMpSelect @change="onAccountChanged" /> + </el-form-item> + <el-form-item label="用户标识" prop="openid"> + <el-input + v-model="queryParams.openid" + placeholder="请输入用户标识" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="昵称" prop="nickname"> + <el-input + v-model="queryParams.nickname" + placeholder="请输入昵称" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"> <Icon icon="ep:search" />搜索 </el-button> + <el-button @click="resetQuery"> <Icon icon="ep:refresh" />重置 </el-button> <el-button type="success" plain @click="handleSync" v-hasPermi="['mp:user:sync']"> <Icon icon="ep:refresh" class="mr-5px" /> 同步 </el-button> - </template> - </WxAccountSelect> + </el-form-item> + </el-form> </ContentWrap> <!-- 列表 --> @@ -66,25 +94,37 @@ <UserForm ref="formRef" @success="getList" /> </template> <script lang="ts" setup name="MpUser"> -import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' import { dateFormatter } from '@/utils/formatTime' import * as MpUserApi from '@/api/mp/user' import * as MpTagApi from '@/api/mp/tag' +import WxMpSelect from '@/views/mp/components/WxMpSelect.vue' +import { FormInstance } from 'element-plus' import UserForm from './UserForm.vue' const message = useMessage() // 消息 const loading = ref(true) // 列表的加载中 const total = ref(0) // 列表的总页数 -const list = ref([]) // 列表的数据 -const queryParams = reactive({ +const list = ref<any[]>([]) // 列表的数据 + +interface QueryParams { + pageNo: number + pageSize: number + accountId?: number + openid: string | null + nickname: string | null +} +const queryParams: QueryParams = reactive({ pageNo: 1, pageSize: 10, - accountId: null, + accountId: undefined, openid: null, nickname: null }) -const tagList = ref([]) // 公众号标签列表 + +const tagList = ref<any[]>([]) // 公众号标签列表 +const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单 +const formRef = ref<InstanceType<typeof UserForm> | null>(null) /** 初始化 */ onMounted(async () => { @@ -92,9 +132,9 @@ onMounted(async () => { }) /** 侦听公众号变化 **/ -const accountChanged = (accountId) => { +const onAccountChanged = (id?: number) => { queryParams.pageNo = 1 - queryParams.accountId = accountId + queryParams.accountId = id getList() } @@ -110,20 +150,32 @@ const getList = async () => { } } +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + const accountId = queryParams.accountId + queryFormRef.value?.resetFields() + queryParams.accountId = accountId + handleQuery() +} + /** 添加/修改操作 */ -const formRef = ref() const openForm = (id: number) => { - formRef.value.open(id) + formRef.value?.open(id) } /** 同步标签 */ const handleSync = async () => { - const accountId = queryParams.accountId try { await message.confirm('是否确认同步粉丝?') - await MpUserApi.syncUser(accountId) + await MpUserApi.syncUser(queryParams.accountId) message.success('开始从微信公众号同步粉丝信息,同步需要一段时间,建议稍后再查询') - await getList() + getList() } catch {} } </script> diff --git a/src/views/pay/order/orderForm.vue b/src/views/pay/order/orderForm.vue new file mode 100644 index 00000000..f9625085 --- /dev/null +++ b/src/views/pay/order/orderForm.vue @@ -0,0 +1,152 @@ +<template> + <Dialog :title="dialogTitle" v-model="dialogVisible" width="50%"> + <el-descriptions :column="2" label-class-name="desc-label"> + <el-descriptions-item label="商户名称">{{ orderDetail.merchantName }}</el-descriptions-item> + <el-descriptions-item label="应用名称">{{ orderDetail.appName }}</el-descriptions-item> + <el-descriptions-item label="商品名称">{{ orderDetail.subject }}</el-descriptions-item> + </el-descriptions> + <el-divider /> + <el-descriptions :column="2" label-class-name="desc-label"> + <el-descriptions-item label="商户订单号"> + <el-tag size="small">{{ orderDetail.merchantOrderId }}</el-tag> + </el-descriptions-item> + <el-descriptions-item label="渠道订单号"> + <el-tag class="tag-purple" size="small">{{ orderDetail.channelOrderNo }}</el-tag> + </el-descriptions-item> + <el-descriptions-item label="支付订单号"> + <el-tag v-if="orderDetail.payOrderExtension.no !== ''" class="tag-pink" size="small"> + {{ orderDetail.payOrderExtension.no }} + </el-tag> + </el-descriptions-item> + <el-descriptions-item label="金额"> + <el-tag type="success" size="small">{{ parseFloat(orderDetail.amount / 100, 2) }}</el-tag> + </el-descriptions-item> + <el-descriptions-item label="手续费"> + <el-tag type="warning" size="small" + >{{ parseFloat(orderDetail.channelFeeAmount / 100, 2) }} + </el-tag> + </el-descriptions-item> + <el-descriptions-item label="手续费比例"> + {{ parseFloat(orderDetail.channelFeeRate / 100, 2) }}% + </el-descriptions-item> + <el-descriptions-item label="支付状态"> + <dict-tag :type="DICT_TYPE.PAY_ORDER_STATUS" :value="orderDetail.status" /> + </el-descriptions-item> + <el-descriptions-item label="回调状态"> + <dict-tag :type="DICT_TYPE.PAY_ORDER_NOTIFY_STATUS" :value="orderDetail.notifyStatus" /> + </el-descriptions-item> + <el-descriptions-item label="回调地址">{{ orderDetail.notifyUrl }}</el-descriptions-item> + <el-descriptions-item label="创建时间" :formatter="dateFormatter"> + {{ formatDate(orderDetail.createTime) }} + </el-descriptions-item> + <el-descriptions-item label="支付时间" :formatter="dateFormatter"> + {{ formatDate(orderDetail.successTime) }} + </el-descriptions-item> + <el-descriptions-item label="失效时间" :formatter="dateFormatter"> + {{ formatDate(orderDetail.expireTime) }} + </el-descriptions-item> + <el-descriptions-item label="通知时间" :formatter="dateFormatter"> + {{ formatDate(orderDetail.notifyTime) }} + </el-descriptions-item> + </el-descriptions> + <el-divider /> + <el-descriptions :column="2" label-class-name="desc-label"> + <el-descriptions-item label="支付渠道" + >{{ orderDetail.channelCodeName }} + </el-descriptions-item> + <el-descriptions-item label="支付IP">{{ orderDetail.userIp }}</el-descriptions-item> + <el-descriptions-item label="退款状态"> + <dict-tag :type="DICT_TYPE.PAY_ORDER_REFUND_STATUS" :value="orderDetail.refundStatus" /> + </el-descriptions-item> + <el-descriptions-item label="退款次数">{{ orderDetail.refundTimes }}</el-descriptions-item> + <el-descriptions-item label="退款金额"> + <el-tag type="warning"> + {{ parseFloat(orderDetail.refundAmount / 100, 2) }} + </el-tag> + </el-descriptions-item> + </el-descriptions> + <el-divider /> + <el-descriptions :column="1" label-class-name="desc-label" direction="vertical" border> + <el-descriptions-item label="商品描述"> + {{ orderDetail.body }} + </el-descriptions-item> + <el-descriptions-item label="支付通道异步回调内容"> + {{ orderDetail.payOrderExtension.channelNotifyData }} + </el-descriptions-item> + </el-descriptions> + </Dialog> +</template> +<script setup lang="ts" name="orderForm"> +import { DICT_TYPE } from '@/utils/dict' +import * as OrderApi from '@/api/pay/order' +import { dateFormatter, formatDate } from '@/utils/formatTime' + +const { t } = useI18n() // 国际化 +// const message = useMessage() // 消息弹窗 +const dialogVisible = ref(false) // 弹窗的是否展示 +const dialogTitle = ref('订单详情') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const defaultOrderDetail = { + merchantName: '', + appName: '', + channelCodeName: '', + subject: '', + merchantOrderId: null, + channelOrderNo: '', + body: '', + amount: null, + channelFeeRate: null, + channelFeeAmount: null, + userIp: '', + status: null, + notifyUrl: '', + notifyStatus: null, + refundStatus: null, + refundTimes: '', + refundAmount: null, + createTime: '', + successTime: '', + notifyTime: '', + expireTime: '', + payOrderExtension: { + channelNotifyData: '', + no: '' + } +} +const orderDetail = ref(JSON.parse(JSON.stringify(defaultOrderDetail))) + +/** 打开弹窗 */ +const open = async (id?: number) => { + dialogVisible.value = true + dialogTitle.value = t('action.preview') + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + orderDetail.value = await OrderApi.getOrderDetail(id) + if (orderDetail.value.payOrderExtension === null) { + orderDetail.value.payOrderExtension = Object.assign( + defaultOrderDetail.payOrderExtension, + {} + ) + } + } finally { + formLoading.value = false + } + } +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 +</script> +<style> +.tag-purple { + color: #722ed1; + background: #f9f0ff; + border-color: #d3adf7; +} + +.tag-pink { + color: #eb2f96; + background: #fff0f6; + border-color: #ffadd2; +} +</style> diff --git a/src/views/pay/refund/refundForm.vue b/src/views/pay/refund/refundForm.vue new file mode 100644 index 00000000..cc9d8726 --- /dev/null +++ b/src/views/pay/refund/refundForm.vue @@ -0,0 +1,154 @@ +<template> + <Dialog :title="dialogTitle" v-model="dialogVisible" width="50%"> + <el-descriptions :column="2" label-class-name="desc-label"> + <el-descriptions-item label="商户名称">{{ refundDetail.merchantName }}</el-descriptions-item> + <el-descriptions-item label="应用名称">{{ refundDetail.appName }}</el-descriptions-item> + <el-descriptions-item label="商品名称">{{ refundDetail.subject }}</el-descriptions-item> + </el-descriptions> + <el-divider /> + <el-descriptions :column="2" label-class-name="desc-label"> + <el-descriptions-item label="商户退款单号"> + <el-tag size="small">{{ refundDetail.merchantRefundNo }}</el-tag> + </el-descriptions-item> + <el-descriptions-item label="商户订单号" + >{{ refundDetail.merchantOrderId }} + </el-descriptions-item> + <el-descriptions-item label="交易订单号">{{ refundDetail.tradeNo }}</el-descriptions-item> + </el-descriptions> + <el-divider /> + <el-descriptions :column="2" label-class-name="desc-label"> + <el-descriptions-item label="支付金额"> + {{ parseFloat(refundDetail.payAmount / 100).toFixed(2) }} + </el-descriptions-item> + <el-descriptions-item label="退款金额" size="small"> + <el-tag class="tag-purple" size="small"> + {{ parseFloat(refundDetail.refundAmount / 100).toFixed(2) }} + </el-tag> + </el-descriptions-item> + <el-descriptions-item label="退款类型"> + <dict-tag :type="DICT_TYPE.PAY_REFUND_ORDER_TYPE" :value="refundDetail.type" /> + </el-descriptions-item> + <el-descriptions-item label="退款状态"> + <dict-tag :type="DICT_TYPE.PAY_REFUND_ORDER_STATUS" :value="refundDetail.status" /> + </el-descriptions-item> + <el-descriptions-item label="创建时间" + >{{ formatDate(refundDetail.createTime) }} + </el-descriptions-item> + <el-descriptions-item label="退款成功时间" + >{{ formatDate(refundDetail.successTime) }} + </el-descriptions-item> + <el-descriptions-item label="退款失效时间" + >{{ formatDate(refundDetail.expireTime) }} + </el-descriptions-item> + <el-descriptions-item label="更新时间" + >{{ formatDate(refundDetail.updateTime) }} + </el-descriptions-item> + </el-descriptions> + <el-divider /> + <el-descriptions :column="2" label-class-name="desc-label"> + <el-descriptions-item label="支付渠道"> + {{ refundDetail.channelCodeName }} + </el-descriptions-item> + <el-descriptions-item label="支付IP" size="small"> + {{ refundDetail.userIp }} + </el-descriptions-item> + <el-descriptions-item label="回调地址">{{ refundDetail.notifyUrl }}</el-descriptions-item> + <el-descriptions-item label="回调状态"> + <dict-tag :type="DICT_TYPE.PAY_ORDER_NOTIFY_STATUS" :value="refundDetail.notifyStatus" /> + </el-descriptions-item> + <el-descriptions-item label="回调时间" + >{{ formatDate(refundDetail.notifyTime) }} + </el-descriptions-item> + </el-descriptions> + <el-divider /> + <el-descriptions :column="2" label-class-name="desc-label"> + <el-descriptions-item label="渠道订单号" + >{{ refundDetail.channelOrderNo }} + </el-descriptions-item> + <el-descriptions-item label="渠道退款单号" + >{{ refundDetail.channelRefundNo }} + </el-descriptions-item> + <el-descriptions-item label="渠道错误码" + >{{ refundDetail.channelErrorCode }} + </el-descriptions-item> + <el-descriptions-item label="渠道错误码描述" + >{{ refundDetail.channelErrorMsg }} + </el-descriptions-item> + </el-descriptions> + <br /> + <el-descriptions :column="1" label-class-name="desc-label" direction="vertical" border> + <el-descriptions-item label="渠道额外参数" + >{{ refundDetail.channelExtras }} + </el-descriptions-item> + <el-descriptions-item label="退款原因">{{ refundDetail.reason }}</el-descriptions-item> + </el-descriptions> + </Dialog> +</template> +<script setup lang="ts" name="refundForm"> +import { DICT_TYPE } from '@/utils/dict' +import * as RefundApi from '@/api/pay/refund' +import { formatDate } from '@/utils/formatTime' + +const { t } = useI18n() // 国际化 +// const message = useMessage() // 消息弹窗 +const dialogVisible = ref(false) // 弹窗的是否展示 +const dialogTitle = ref('退款订单详情') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const defaultrefundDetail = { + id: null, + appId: null, + appName: '', + channelCode: '', + channelCodeName: '', + channelErrorCode: '', + channelErrorMsg: '', + channelExtras: '', + channelId: null, + channelOrderNo: '', + channelRefundNo: '', + createTime: null, + expireTime: null, + merchantId: null, + merchantName: '', + merchantOrderId: '', + merchantRefundNo: '', + notifyStatus: null, + notifyTime: null, + notifyUrl: '', + orderId: null, + payAmount: null, + reason: '', + refundAmount: null, + status: null, + subject: '', + successTime: null, + tradeNo: '', + type: null, + userIp: '' +} +const refundDetail = ref(JSON.parse(JSON.stringify(defaultrefundDetail))) + +/** 打开弹窗 */ +const open = async (id?: number) => { + dialogVisible.value = true + dialogTitle.value = t('action.preview') + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + refundDetail.value = await RefundApi.getRefundApi(id) + } finally { + formLoading.value = false + } + } +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 +</script> + +<style> +.tag-purple { + color: #722ed1; + background: #f9f0ff; + border-color: #d3adf7; +} +</style> From fbbf4131f67706d77042744813a865b0c16e29e7 Mon Sep 17 00:00:00 2001 From: dhb52 <dhb52@126.com> Date: Wed, 12 Apr 2023 14:05:21 +0800 Subject: [PATCH 2/6] =?UTF-8?q?fix:=20=E5=8E=BB=E9=99=A4WxLocation?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=A4=9A=E4=BD=99Location=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/mp/components/wx-location/main.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/mp/components/wx-location/main.vue b/src/views/mp/components/wx-location/main.vue index 47eab571..17598136 100644 --- a/src/views/mp/components/wx-location/main.vue +++ b/src/views/mp/components/wx-location/main.vue @@ -31,7 +31,6 @@ /> </el-row> <el-row> - <el-icon><Location /></el-icon> <Icon icon="ep:location" /> {{ label }} </el-row> @@ -39,6 +38,7 @@ </el-link> </div> </template> + <script setup lang="ts" name="WxLocation"> const props = defineProps({ locationX: { From 357a4789f4bf737a175a9b4657c7890c7775bf32 Mon Sep 17 00:00:00 2001 From: dhb52 <dhb52@126.com> Date: Wed, 12 Apr 2023 14:19:31 +0800 Subject: [PATCH 3/6] fix: import type { FormInstance ...} from 'element-plus' --- src/types/auto-imports.d.ts | 2 ++ src/views/mp/autoReply/index.vue | 2 +- src/views/mp/draft/index.vue | 2 +- src/views/mp/tag/TagForm.vue | 2 +- src/views/mp/user/index.vue | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/types/auto-imports.d.ts b/src/types/auto-imports.d.ts index cfda3e66..75cf16d9 100644 --- a/src/types/auto-imports.d.ts +++ b/src/types/auto-imports.d.ts @@ -62,6 +62,8 @@ declare global { const useRouter: typeof import('vue-router')['useRouter'] const useSlots: typeof import('vue')['useSlots'] const useTable: typeof import('@/hooks/web/useTable')['useTable'] + const useVxeCrudSchemas: typeof import('@/hooks/web/useVxeCrudSchemas')['useVxeCrudSchemas'] + const useXTable: typeof import('@/hooks/web/useXTable')['useXTable'] const watch: typeof import('vue')['watch'] const watchEffect: typeof import('vue')['watchEffect'] const watchPostEffect: typeof import('vue')['watchPostEffect'] diff --git a/src/views/mp/autoReply/index.vue b/src/views/mp/autoReply/index.vue index 94fe34b3..4c8eb388 100644 --- a/src/views/mp/autoReply/index.vue +++ b/src/views/mp/autoReply/index.vue @@ -188,7 +188,7 @@ import * as MpAutoReplyApi from '@/api/mp/autoReply' import { DICT_TYPE, getDictOptions } from '@/utils/dict' import { dateFormatter } from '@/utils/formatTime' import { ContentWrap } from '@/components/ContentWrap' -import { TabPaneName } from 'element-plus' +import type { TabPaneName } from 'element-plus' const message = useMessage() diff --git a/src/views/mp/draft/index.vue b/src/views/mp/draft/index.vue index 5df7687a..844d01b2 100644 --- a/src/views/mp/draft/index.vue +++ b/src/views/mp/draft/index.vue @@ -249,7 +249,7 @@ import WxMpSelect from '@/views/mp/components/WxMpSelect.vue' import { getAccessToken } from '@/utils/auth' import * as MpDraftApi from '@/api/mp/draft' import * as MpFreePublishApi from '@/api/mp/freePublish' -import { UploadFiles, UploadProps, UploadRawFile } from 'element-plus' +import type { UploadFiles, UploadProps, UploadRawFile } from 'element-plus' // 可以用改本地数据模拟,避免API调用超限 // import drafts from './mock' diff --git a/src/views/mp/tag/TagForm.vue b/src/views/mp/tag/TagForm.vue index 81f19988..73b29baa 100644 --- a/src/views/mp/tag/TagForm.vue +++ b/src/views/mp/tag/TagForm.vue @@ -19,7 +19,7 @@ </template> <script setup lang="ts"> import * as MpTagApi from '@/api/mp/tag' -import { FormInstance, FormRules } from 'element-plus' +import type { FormInstance, FormRules } from 'element-plus' const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 diff --git a/src/views/mp/user/index.vue b/src/views/mp/user/index.vue index e8e23aaa..6fc355e0 100644 --- a/src/views/mp/user/index.vue +++ b/src/views/mp/user/index.vue @@ -98,7 +98,7 @@ import { dateFormatter } from '@/utils/formatTime' import * as MpUserApi from '@/api/mp/user' import * as MpTagApi from '@/api/mp/tag' import WxMpSelect from '@/views/mp/components/WxMpSelect.vue' -import { FormInstance } from 'element-plus' +import type { FormInstance } from 'element-plus' import UserForm from './UserForm.vue' const message = useMessage() // 消息 From 3536077a34f12d5ddb61927c07005a39a5f67d12 Mon Sep 17 00:00:00 2001 From: dhb52 <dhb52@126.com> Date: Wed, 12 Apr 2023 14:37:59 +0800 Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20MP=E6=B6=88=E6=81=AF=E7=AE=A1?= =?UTF-8?q?=E7=90=86=20ts=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/mp/components/wx-msg/main.vue | 365 ++++++++++++------------ src/views/mp/components/wx-msg/types.ts | 11 + src/views/mp/message/index.vue | 140 +++++---- 3 files changed, 262 insertions(+), 254 deletions(-) create mode 100644 src/views/mp/components/wx-msg/types.ts diff --git a/src/views/mp/components/wx-msg/main.vue b/src/views/mp/components/wx-msg/main.vue index 60c0a5a2..ad37c1f1 100644 --- a/src/views/mp/components/wx-msg/main.vue +++ b/src/views/mp/components/wx-msg/main.vue @@ -39,79 +39,79 @@ :style="item.sendFrom === 2 ? 'background: #6BED72;' : ''" > <!-- 【事件】区域 --> - <div v-if="item.type === 'event' && item.event === 'subscribe'"> + <div v-if="item.type === MsgType.Event && item.event === 'subscribe'"> <el-tag type="success">关注</el-tag> </div> - <div v-else-if="item.type === 'event' && item.event === 'unsubscribe'"> + <div v-else-if="item.type === MsgType.Event && item.event === 'unsubscribe'"> <el-tag type="danger">取消关注</el-tag> </div> - <div v-else-if="item.type === 'event' && item.event === 'CLICK'"> + <div v-else-if="item.type === MsgType.Event && item.event === 'CLICK'"> <el-tag>点击菜单</el-tag> 【{{ item.eventKey }}】 </div> - <div v-else-if="item.type === 'event' && item.event === 'VIEW'"> + <div v-else-if="item.type === MsgType.Event && item.event === 'VIEW'"> <el-tag>点击菜单链接</el-tag> 【{{ item.eventKey }}】 </div> - <div v-else-if="item.type === 'event' && item.event === 'scancode_waitmsg'"> + <div v-else-if="item.type === MsgType.Event && item.event === 'scancode_waitmsg'"> <el-tag>扫码结果</el-tag> 【{{ item.eventKey }}】 </div> - <div v-else-if="item.type === 'event' && item.event === 'scancode_push'"> + <div v-else-if="item.type === MsgType.Event && item.event === 'scancode_push'"> <el-tag>扫码结果</el-tag> 【{{ item.eventKey }}】 </div> - <div v-else-if="item.type === 'event' && item.event === 'pic_sysphoto'"> + <div v-else-if="item.type === MsgType.Event && item.event === 'pic_sysphoto'"> <el-tag>系统拍照发图</el-tag> </div> - <div v-else-if="item.type === 'event' && item.event === 'pic_photo_or_album'"> + <div v-else-if="item.type === MsgType.Event && item.event === 'pic_photo_or_album'"> <el-tag>拍照或者相册</el-tag> </div> - <div v-else-if="item.type === 'event' && item.event === 'pic_weixin'"> + <div v-else-if="item.type === MsgType.Event && item.event === 'pic_weixin'"> <el-tag>微信相册</el-tag> </div> - <div v-else-if="item.type === 'event' && item.event === 'location_select'"> + <div v-else-if="item.type === MsgType.Event && item.event === 'location_select'"> <el-tag>选择地理位置</el-tag> </div> - <div v-else-if="item.type === 'event'"> + <div v-else-if="item.type === MsgType.Event"> <el-tag type="danger">未知事件类型</el-tag> </div> <!-- 【消息】区域 --> - <div v-else-if="item.type === 'text'">{{ item.content }}</div> - <div v-else-if="item.type === 'voice'"> - <wx-voice-player :url="item.mediaUrl" :content="item.recognition" /> + <div v-else-if="item.type === MsgType.Text">{{ item.content }}</div> + <div v-else-if="item.type === MsgType.Voice"> + <WxVoicePlayer :url="item.mediaUrl" :content="item.recognition" /> </div> - <div v-else-if="item.type === 'image'"> + <div v-else-if="item.type === MsgType.Image"> <a target="_blank" :href="item.mediaUrl"> <img :src="item.mediaUrl" style="width: 100px" /> </a> </div> <div - v-else-if="item.type === 'video' || item.type === 'shortvideo'" + v-else-if="item.type === MsgType.Video || item.type === 'shortvideo'" style="text-align: center" > - <wx-video-player :url="item.mediaUrl" /> + <WxVideoPlayer :url="item.mediaUrl" /> </div> - <div v-else-if="item.type === 'link'" class="avue-card__detail"> + <div v-else-if="item.type === MsgType.Link" class="avue-card__detail"> <el-link type="success" :underline="false" target="_blank" :href="item.url"> <div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div> </el-link> <div class="avue-card__info" style="height: unset">{{ item.description }}</div> </div> <!-- TODO 芋艿:待完善 --> - <div v-else-if="item.type === 'location'"> - <wx-location + <div v-else-if="item.type === MsgType.Location"> + <WxLocation :label="item.label" :location-y="item.locationY" :location-x="item.locationX" /> </div> - <div v-else-if="item.type === 'news'" style="width: 300px"> + <div v-else-if="item.type === MsgType.News" style="width: 300px"> <!-- TODO 芋艿:待测试;详情页也存在类似的情况 --> - <wx-news :articles="item.articles" /> + <WxNews :articles="item.articles" /> </div> - <div v-else-if="item.type === 'music'"> - <wx-music + <div v-else-if="item.type === MsgType.Music"> + <WxMusic :title="item.title" :description="item.description" :thumb-media-url="item.thumbMediaUrl" @@ -125,182 +125,185 @@ </div> </div> <div class="msg-send" v-loading="sendLoading"> - <wx-reply-select ref="replySelect" :objData="objData" /> + <WxReplySelect ref="replySelectRef" :objData="objData" /> <el-button type="success" size="small" class="send-but" @click="sendMsg">发送(S)</el-button> </div> </ContentWrap> </template> -<script lang="ts" name="WxMsg"> -import { getMessagePage, sendMessage } from '@/api/mp/message' +<script setup lang="ts" name="WxMsg"> import WxReplySelect from '@/views/mp/components/wx-reply/main.vue' import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue' import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue' import WxNews from '@/views/mp/components/wx-news/main.vue' import WxLocation from '@/views/mp/components/wx-location/main.vue' import WxMusic from '@/views/mp/components/wx-music/main.vue' +import { getMessagePage, sendMessage } from '@/api/mp/message' import { getUser } from '@/api/mp/user' -import { defineComponent } from 'vue' - -const message = useMessage() // 消息弹窗 +import { formatDate } from '@/utils/formatTime' import profile from '@/assets/imgs/profile.jpg' import wechat from '@/assets/imgs/wechat.png' -import { formatDate } from '@/utils/formatTime' +import { MsgType } from './types' -export default defineComponent({ - components: { - WxReplySelect, - WxVideoPlayer, - WxVoicePlayer, - WxNews, - WxLocation, - WxMusic - }, - props: { - userId: { - type: Number, - required: true - } - }, - setup(props) { - const nowStr = ref(new Date().getTime()) // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处 - const loading = ref(false) // 消息列表是否正在加载中 - const loadMore = ref(true) // 是否可以加载更多 - const list = ref<any[]>([]) // 消息列表 - const queryParams = reactive({ - pageNo: 1, // 当前页数 - pageSize: 14, // 每页显示多少条 - accountId: undefined - }) - const user = reactive({ - // 由于微信不再提供昵称,直接使用“用户”展示 - nickname: '用户', - avatar: profile, - accountId: 0 // 公众号账号编号 - }) - const mp = reactive({ - nickname: '公众号', - avatar: wechat - }) +const message = useMessage() // 消息弹窗 - // ========= 消息发送 ========= - const sendLoading = ref(false) // 发送消息是否加载中 - const objData = reactive({ - // 微信发送消息 - type: 'text', - accountId: null, - articles: [] - }) - - const replySelect = ref(null) - // 执行发送 - const sendMsg = async () => { - if (!objData) { - return - } - // // 公众号限制:客服消息,公众号只允许发送一条 - if (objData.type === 'news' && objData.articles.length > 1) { - objData.articles = [objData.articles[0]] - message.success('图文消息条数限制在 1 条以内,已默认发送第一条') - } - let data = await sendMessage(Object.assign({ userId: props.userId }, { ...objData })) - sendLoading.value = false - list.value = [...list.value, ...[data]] - scrollToBottom() - //ts檢查的時候會判斷這個組件可能是空的,所以需要進行斷言。 - //避免 tab 的数据未清理 - const deleteObj = (replySelect.value as any).deleteObj - if (deleteObj) { - deleteObj() - } - } - const loadingMore = () => { - queryParams.pageNo++ - getPage(queryParams, null) - } - const getPage = async (page, params) => { - loading.value = true - let dataTemp = await getMessagePage( - Object.assign( - { - pageNo: page.pageNo, - pageSize: page.pageSize, - userId: props.userId, - accountId: page.accountId - }, - params - ) - ) - const msgDiv = document.getElementById('msg-div' + nowStr.value) - let scrollHeight = 0 - if (msgDiv) { - scrollHeight = msgDiv.scrollHeight - } - // 处理数据 - let data = dataTemp.list.reverse() - list.value = [...data, ...list.value] - loading.value = false - if (data.length < queryParams.pageSize || data.length === 0) { - loadMore.value = false - } - queryParams.pageNo = page.pageNo - queryParams.pageSize = page.pageSize - // 滚动到原来的位置 - if (queryParams.pageNo === 1) { - // 定位到消息底部 - scrollToBottom() - } else if (data.length !== 0) { - // 定位滚动条 - await nextTick(() => { - if (scrollHeight !== 0) { - let div = document.getElementById('msg-div' + nowStr.value) - if (div && msgDiv) { - msgDiv.scrollTop = div.scrollHeight - scrollHeight - 100 - } - } - }) - } - } - const refreshChange = () => { - getPage(queryParams, null) - } - /** 定位到消息底部 */ - const scrollToBottom = () => { - nextTick(() => { - let div = document.getElementById('msg-div' + nowStr.value) - if (div) { - div.scrollTop = div.scrollHeight - } - }) - } - - onMounted(async () => { - let data = await getUser(props.userId) - user.nickname = data.nickname && data.nickname.length > 0 ? data.nickname : user.nickname - user.avatar = data.avatar && user.avatar.length > 0 ? data.avatar : user.avatar - user.accountId = data.accountId - queryParams.accountId = data.accountId - objData.accountId = data.accountId - refreshChange() - }) - return { - sendMsg, - loadingMore, - formatDate, - scrollToBottom, - objData, - mp, - user, - queryParams, - list, - loadMore, - loading, - nowStr, - sendLoading - } +const props = defineProps({ + userId: { + type: Number, + required: true } }) + +const nowStr = ref(new Date().getTime()) // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处 +const loading = ref(false) // 消息列表是否正在加载中 +const loadMore = ref(true) // 是否可以加载更多 +const list = ref<any[]>([]) // 消息列表 +const queryParams = reactive({ + pageNo: 1, // 当前页数 + pageSize: 14, // 每页显示多少条 + accountId: undefined +}) + +interface User { + nickname: string + avatar: string + accountId: number +} +// 由于微信不再提供昵称,直接使用“用户”展示 +const user: User = reactive({ + nickname: '用户', + avatar: profile, + accountId: 0 // 公众号账号编号 +}) + +interface Mp { + nickname: string + avatar: string +} +const mp: Mp = reactive({ + nickname: '公众号', + avatar: wechat +}) + +// ========= 消息发送 ========= +const sendLoading = ref(false) // 发送消息是否加载中 +interface ObjData { + type: MsgType + accountId: number | null + articles: any[] +} + +// 微信发送消息 +const objData: ObjData = reactive({ + type: MsgType.Text, + accountId: null, + articles: [] +}) + +const replySelectRef = ref<InstanceType<typeof WxReplySelect> | null>(null) + +/** 完成加载 */ +onMounted(async () => { + const data = await getUser(props.userId) + user.nickname = data.nickname?.length > 0 ? data.nickname : user.nickname + user.avatar = user.avatar?.length > 0 ? data.avatar : user.avatar + user.accountId = data.accountId + queryParams.accountId = data.accountId + objData.accountId = data.accountId + + refreshChange() +}) + +// 执行发送 +const sendMsg = async () => { + if (!objData) { + return + } + // 公众号限制:客服消息,公众号只允许发送一条 + if (objData.type === MsgType.News && objData.articles.length > 1) { + objData.articles = [objData.articles[0]] + message.success('图文消息条数限制在 1 条以内,已默认发送第一条') + } + + const data = await sendMessage(Object.assign({ userId: props.userId }, { ...objData })) + sendLoading.value = false + + list.value = [...list.value, ...[data]] + scrollToBottom() + + //ts检查的時候会判断这个组件可能是空的,所以需要进行断言。 + //避免 tab 的数据未清理 + const deleteObj = replySelectRef.value?.deleteObj + if (deleteObj) { + deleteObj() + } +} + +const loadingMore = () => { + queryParams.pageNo++ + getPage(queryParams, null) +} + +const getPage = async (page, params) => { + loading.value = true + let dataTemp = await getMessagePage( + Object.assign( + { + pageNo: page.pageNo, + pageSize: page.pageSize, + userId: props.userId, + accountId: page.accountId + }, + params + ) + ) + + const msgDiv = document.getElementById('msg-div' + nowStr.value) + let scrollHeight = 0 + if (msgDiv) { + scrollHeight = msgDiv.scrollHeight + } + // 处理数据 + const data = dataTemp.list.reverse() + list.value = [...data, ...list.value] + loading.value = false + if (data.length < queryParams.pageSize || data.length === 0) { + loadMore.value = false + } + queryParams.pageNo = page.pageNo + queryParams.pageSize = page.pageSize + // 滚动到原来的位置 + if (queryParams.pageNo === 1) { + // 定位到消息底部 + scrollToBottom() + } else if (data.length !== 0) { + // 定位滚动条 + await nextTick(() => { + if (scrollHeight !== 0) { + let div = document.getElementById('msg-div' + nowStr.value) + if (div && msgDiv) { + msgDiv.scrollTop = div.scrollHeight - scrollHeight - 100 + } + } + }) + } +} + +const refreshChange = () => { + getPage(queryParams, null) +} + +/** 定位到消息底部 */ +const scrollToBottom = () => { + nextTick(() => { + let div = document.getElementById('msg-div' + nowStr.value) + if (div) { + div.scrollTop = div.scrollHeight + } + }) +} </script> + <style lang="scss" scoped> /* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc */ @import './comment.scss'; diff --git a/src/views/mp/components/wx-msg/types.ts b/src/views/mp/components/wx-msg/types.ts new file mode 100644 index 00000000..b1989ea7 --- /dev/null +++ b/src/views/mp/components/wx-msg/types.ts @@ -0,0 +1,11 @@ +export enum MsgType { + Event = 'event', + Text = 'text', + Voice = 'voice', + Image = 'image', + Video = 'video', + Link = 'link', + Location = 'location', + Music = 'music', + News = 'news' +} diff --git a/src/views/mp/message/index.vue b/src/views/mp/message/index.vue index 90db931a..d13acf81 100644 --- a/src/views/mp/message/index.vue +++ b/src/views/mp/message/index.vue @@ -9,14 +9,7 @@ label-width="68px" > <el-form-item label="公众号" prop="accountId"> - <el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px"> - <el-option - v-for="item in accountList" - :key="item.id" - :label="item.name" - :value="item.id" - /> - </el-select> + <WxMpSelect @change="onAccountChanged" /> </el-form-item> <el-form-item label="消息类型" prop="type"> <el-select v-model="queryParams.type" placeholder="请选择消息类型" class="!w-240px"> @@ -84,70 +77,76 @@ <el-table-column label="内容" prop="content"> <template #default="scope"> <!-- 【事件】区域 --> - <div v-if="scope.row.type === 'event' && scope.row.event === 'subscribe'"> + <div v-if="scope.row.type === MsgType.Event && scope.row.event === 'subscribe'"> <el-tag type="success">关注</el-tag> </div> - <div v-else-if="scope.row.type === 'event' && scope.row.event === 'unsubscribe'"> + <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'unsubscribe'"> <el-tag type="danger">取消关注</el-tag> </div> - <div v-else-if="scope.row.type === 'event' && scope.row.event === 'CLICK'"> + <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'CLICK'"> <el-tag>点击菜单</el-tag> 【{{ scope.row.eventKey }}】 </div> - <div v-else-if="scope.row.type === 'event' && scope.row.event === 'VIEW'"> + <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'VIEW'"> <el-tag>点击菜单链接</el-tag> 【{{ scope.row.eventKey }}】 </div> - <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_waitmsg'"> + <div + v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'scancode_waitmsg'" + > <el-tag>扫码结果</el-tag> 【{{ scope.row.eventKey }}】 </div> - <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_push'"> + <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'scancode_push'"> <el-tag>扫码结果</el-tag> 【{{ scope.row.eventKey }}】 </div> - <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_sysphoto'"> + <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_sysphoto'"> <el-tag>系统拍照发图</el-tag> </div> - <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_photo_or_album'"> + <div + v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_photo_or_album'" + > <el-tag>拍照或者相册</el-tag> </div> - <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_weixin'"> + <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_weixin'"> <el-tag>微信相册</el-tag> </div> - <div v-else-if="scope.row.type === 'event' && scope.row.event === 'location_select'"> + <div + v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'location_select'" + > <el-tag>选择地理位置</el-tag> </div> - <div v-else-if="scope.row.type === 'event'"> + <div v-else-if="scope.row.type === MsgType.Event"> <el-tag type="danger">未知事件类型</el-tag> </div> <!-- 【消息】区域 --> - <div v-else-if="scope.row.type === 'text'">{{ scope.row.content }}</div> - <div v-else-if="scope.row.type === 'voice'"> + <div v-else-if="scope.row.type === MsgType.Text">{{ scope.row.content }}</div> + <div v-else-if="scope.row.type === MsgType.Voice"> <wx-voice-player :url="scope.row.mediaUrl" :content="scope.row.recognition" /> </div> - <div v-else-if="scope.row.type === 'image'"> + <div v-else-if="scope.row.type === MsgType.Image"> <a target="_blank" :href="scope.row.mediaUrl"> <img :src="scope.row.mediaUrl" style="width: 100px" /> </a> </div> - <div v-else-if="scope.row.type === 'video' || scope.row.type === 'shortvideo'"> + <div v-else-if="scope.row.type === MsgType.Video || scope.row.type === 'shortvideo'"> <wx-video-player :url="scope.row.mediaUrl" style="margin-top: 10px" /> </div> - <div v-else-if="scope.row.type === 'link'"> + <div v-else-if="scope.row.type === MsgType.Link"> <el-tag>链接</el-tag> : <a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a> </div> - <div v-else-if="scope.row.type === 'location'"> - <wx-location + <div v-else-if="scope.row.type === MsgType.Location"> + <WxLocation :label="scope.row.label" :location-y="scope.row.locationY" :location-x="scope.row.locationX" /> </div> - <div v-else-if="scope.row.type === 'music'"> - <wx-music + <div v-else-if="scope.row.type === MsgType.Music"> + <WxMusic :title="scope.row.title" :description="scope.row.description" :thumb-media-url="scope.row.thumbMediaUrl" @@ -155,8 +154,8 @@ :hq-music-url="scope.row.hqMusicUrl" /> </div> - <div v-else-if="scope.row.type === 'news'"> - <wx-news :articles="scope.row.articles" /> + <div v-else-if="scope.row.type === MsgType.News"> + <WxNews :articles="scope.row.articles" /> </div> <div v-else> <el-tag type="danger">未知消息类型</el-tag> @@ -177,7 +176,7 @@ </el-table-column> </el-table> <!-- 分页组件 --> - <pagination + <Pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNo" @@ -186,9 +185,14 @@ /> <!-- 发送消息的弹窗 --> - <el-dialog title="粉丝消息列表" v-model="open" @click="openDialog()" width="50%"> + <el-dialog + title="粉丝消息列表" + v-model="showMessageBox" + @click="showMessageBox = true" + width="50%" + > <template #footer> - <wx-msg :user-id="userId" v-if="open" /> + <WxMsg :user-id="userId" v-if="showMessageBox" /> </template> </el-dialog> </ContentWrap> @@ -200,17 +204,27 @@ import WxMsg from '@/views/mp/components/wx-msg/main.vue' import WxLocation from '@/views/mp/components/wx-location/main.vue' import WxMusic from '@/views/mp/components/wx-music/main.vue' import WxNews from '@/views/mp/components/wx-news/main.vue' -import * as MpAccountApi from '@/api/mp/account' +import WxMpSelect from '@/views/mp/components/WxMpSelect.vue' import * as MpMessageApi from '@/api/mp/message' - -const message = useMessage() // 消息弹窗 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' import { dateFormatter } from '@/utils/formatTime' +import { MsgType } from '@/views/mp/components/wx-msg/types' +import type { FormInstance } from 'element-plus' const loading = ref(true) // 列表的加载中 const total = ref(0) // 列表的总页数 -const list = ref([]) // 列表的数据 -const queryParams = reactive({ +const list = ref<any[]>([]) // 列表的数据 + +// 搜索参数 +interface QueryParams { + pageNo: number + pageSize: number + openid: string | null + accountId: number | null + type: MsgType | null + createTime: string[] | [] +} +const queryParams: QueryParams = reactive({ pageNo: 1, pageSize: 10, openid: null, @@ -218,19 +232,18 @@ const queryParams = reactive({ type: null, createTime: [] }) -const queryFormRef = ref() // 搜索的表单 -// TODO 芋艿:下面应该移除 -const open = ref(false) // 是否显示弹出层 +const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单 +const showMessageBox = ref(false) // 是否显示弹出层 const userId = ref(0) // 操作的用户编号 -const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表 + +/** 侦听accountId */ +const onAccountChanged = (id?: number) => { + queryParams.accountId = id as number + handleQuery() +} /** 查询列表 */ const getList = async () => { - // 如果没有选中公众号账号,则进行提示。 - if (!queryParams.accountId) { - await message.error('未选中公众号,无法查询消息') - return - } try { loading.value = true const data = await MpMessageApi.getMessagePage(queryParams) @@ -249,34 +262,15 @@ const handleQuery = () => { /** 重置按钮操作 */ const resetQuery = async () => { - queryFormRef.value.resetFields() - // 默认选中第一个 - if (accountList.value.length > 0) { - // @ts-ignore - queryParams.accountId = accountList.value[0].id - } + const accountId = queryParams.accountId + queryFormRef.value?.resetFields() + queryParams.accountId = accountId handleQuery() } -const handleSend = async (row) => { + +/** 打开消息发送窗口 */ +const handleSend = async (row: any) => { userId.value = row.userId - open.value = true + showMessageBox.value = true } - -const openDialog = () => { - open.value = true -} -// const closeDiaLog = () => { -// open.value = false -// } - -/** 初始化 **/ -onMounted(async () => { - accountList.value = await MpAccountApi.getSimpleAccountList() - // 选中第一个 - if (accountList.value.length > 0) { - // @ts-ignore - queryParams.accountId = accountList.value[0].id - } - await getList() -}) </script> From f436490165b7adacf96fc4a793498ac4d334f77c Mon Sep 17 00:00:00 2001 From: YunaiV <zhijiantianya@gmail.com> Date: Thu, 13 Apr 2023 21:11:55 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20IconSelect=20=E6=8A=A5?= =?UTF-8?q?=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Icon/src/IconSelect.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Icon/src/IconSelect.vue b/src/components/Icon/src/IconSelect.vue index 1a124ca6..9b77fb2b 100644 --- a/src/components/Icon/src/IconSelect.vue +++ b/src/components/Icon/src/IconSelect.vue @@ -95,7 +95,7 @@ watch( return props.modelValue }, () => { - if (props.modelValue && props.modelValue.contains(':')) { + if (props.modelValue && props.modelValue.indexOf(':') >= 0) { currentActiveType.value = props.modelValue.substring(0, props.modelValue.indexOf(':') + 1) icon.value = props.modelValue.substring(props.modelValue.indexOf(':') + 1) } From 1a1bfe0ebbaeae9fab99929d3d8bf9fd7a2d3cb1 Mon Sep 17 00:00:00 2001 From: YunaiV <zhijiantianya@gmail.com> Date: Thu, 13 Apr 2023 22:25:35 +0800 Subject: [PATCH 6/6] =?UTF-8?q?REVIEW=20=E5=85=AC=E4=BC=97=E5=8F=B7?= =?UTF-8?q?=E7=9A=84=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build/vite/optimize.ts | 3 +- src/views/mp/components/WxMpSelect.vue | 2 +- .../mp/components/wx-editor/WxEditor.vue | 2 - src/views/mp/draft/index.vue | 3 +- src/views/mp/material/index.vue | 17 +- src/views/mp/tag/index.vue | 4 +- src/views/mp/user/index.vue | 17 +- src/views/pay/order/orderForm.vue | 152 ----------------- src/views/pay/refund/refundForm.vue | 154 ------------------ 9 files changed, 20 insertions(+), 334 deletions(-) delete mode 100644 src/views/pay/order/orderForm.vue delete mode 100644 src/views/pay/refund/refundForm.vue diff --git a/build/vite/optimize.ts b/build/vite/optimize.ts index f7a10eb9..416ea02e 100644 --- a/build/vite/optimize.ts +++ b/build/vite/optimize.ts @@ -75,7 +75,8 @@ const include = [ 'element-plus/es/components/dropdown-item/style/css', 'element-plus/es/components/badge/style/css', 'element-plus/es/components/breadcrumb/style/css', - 'element-plus/es/components/breadcrumb-item/style/css' + 'element-plus/es/components/breadcrumb-item/style/css', + 'element-plus/es/components/image/style/css' ] const exclude = ['@iconify/json'] diff --git a/src/views/mp/components/WxMpSelect.vue b/src/views/mp/components/WxMpSelect.vue index 14f347d6..615745de 100644 --- a/src/views/mp/components/WxMpSelect.vue +++ b/src/views/mp/components/WxMpSelect.vue @@ -3,7 +3,7 @@ <el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" /> </el-select> </template> - +<!-- TODO @芋艿:WxMpSelect 改成 WxAccountSelect,然后挪到现有的 wx-account-select 包下 --> <script lang="ts" setup name="WxMpSelect"> import * as MpAccountApi from '@/api/mp/account' diff --git a/src/views/mp/components/wx-editor/WxEditor.vue b/src/views/mp/components/wx-editor/WxEditor.vue index cf384113..d2220e10 100644 --- a/src/views/mp/components/wx-editor/WxEditor.vue +++ b/src/views/mp/components/wx-editor/WxEditor.vue @@ -102,5 +102,3 @@ const uploadError = () => { </div> </div> </template> - -<style></style> diff --git a/src/views/mp/draft/index.vue b/src/views/mp/draft/index.vue index 844d01b2..d9a44076 100644 --- a/src/views/mp/draft/index.vue +++ b/src/views/mp/draft/index.vue @@ -252,7 +252,6 @@ import * as MpFreePublishApi from '@/api/mp/freePublish' import type { UploadFiles, UploadProps, UploadRawFile } from 'element-plus' // 可以用改本地数据模拟,避免API调用超限 // import drafts from './mock' - const message = useMessage() // 消息 const loading = ref(true) // 列表的加载中 @@ -362,7 +361,7 @@ const submitForm = async () => { } finally { dialogNewsVisible.value = false addMaterialLoading.value = false - getList() + await getList() } } diff --git a/src/views/mp/material/index.vue b/src/views/mp/material/index.vue index 4d4017d4..81d8fb08 100644 --- a/src/views/mp/material/index.vue +++ b/src/views/mp/material/index.vue @@ -277,14 +277,11 @@ const uploadRules: FormRules = { } // 素材类型 -type MatertialType = 'image' | 'voice' | 'video' -const type = ref<MatertialType>('image') -// 遮罩层 -const loading = ref(false) -// 总条数 -// 数据列表 -const list = ref<any[]>([]) -const total = ref(0) +type MaterialType = 'image' | 'voice' | 'video' +const type = ref<MaterialType>('image') +const loading = ref(false) // 遮罩层 +const list = ref<any[]>([]) // 总条数 +const total = ref(0) // 数据列表 // 查询参数 interface QueryParams { pageNo: number @@ -302,7 +299,7 @@ const queryParams: QueryParams = reactive({ const fileList = ref<UploadUserFile[]>([]) interface UploadData { - type: MatertialType + type: MaterialType title: string introduction: string } @@ -346,7 +343,7 @@ const handleQuery = () => { const onTabChange = (tabName: TabPaneName) => { // 设置 type - uploadData.type = tabName as MatertialType + uploadData.type = tabName as MaterialType // 提前情况数据,避免tab切换后显示垃圾数据 list.value = [] diff --git a/src/views/mp/tag/index.vue b/src/views/mp/tag/index.vue index 6dc0d11a..125c8702 100644 --- a/src/views/mp/tag/index.vue +++ b/src/views/mp/tag/index.vue @@ -71,11 +71,10 @@ <TagForm ref="formRef" @success="getList" /> </template> <script setup lang="ts" name="MpTag"> +import { dateFormatter } from '@/utils/formatTime' import * as MpTagApi from '@/api/mp/tag' import TagForm from './TagForm.vue' import WxMpSelect from '@/views/mp/components/WxMpSelect.vue' -import { dateFormatter } from '@/utils/formatTime' - const message = useMessage() // 消息弹窗 const { t } = useI18n() // 国际化 @@ -93,7 +92,6 @@ const queryParams: QueryParams = reactive({ pageSize: 10, accountId: undefined }) - const formRef = ref<InstanceType<typeof TagForm> | null>(null) /** 侦听公众号变化 **/ diff --git a/src/views/mp/user/index.vue b/src/views/mp/user/index.vue index 6fc355e0..5b2ec3bd 100644 --- a/src/views/mp/user/index.vue +++ b/src/views/mp/user/index.vue @@ -121,15 +121,8 @@ const queryParams: QueryParams = reactive({ openid: null, nickname: null }) - -const tagList = ref<any[]>([]) // 公众号标签列表 const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单 -const formRef = ref<InstanceType<typeof UserForm> | null>(null) - -/** 初始化 */ -onMounted(async () => { - tagList.value = await MpTagApi.getSimpleTagList() -}) +const tagList = ref<any[]>([]) // 公众号标签列表 /** 侦听公众号变化 **/ const onAccountChanged = (id?: number) => { @@ -165,6 +158,7 @@ const resetQuery = () => { } /** 添加/修改操作 */ +const formRef = ref<InstanceType<typeof UserForm> | null>(null) const openForm = (id: number) => { formRef.value?.open(id) } @@ -175,7 +169,12 @@ const handleSync = async () => { await message.confirm('是否确认同步粉丝?') await MpUserApi.syncUser(queryParams.accountId) message.success('开始从微信公众号同步粉丝信息,同步需要一段时间,建议稍后再查询') - getList() + await getList() } catch {} } + +/** 初始化 */ +onMounted(async () => { + tagList.value = await MpTagApi.getSimpleTagList() +}) </script> diff --git a/src/views/pay/order/orderForm.vue b/src/views/pay/order/orderForm.vue deleted file mode 100644 index f9625085..00000000 --- a/src/views/pay/order/orderForm.vue +++ /dev/null @@ -1,152 +0,0 @@ -<template> - <Dialog :title="dialogTitle" v-model="dialogVisible" width="50%"> - <el-descriptions :column="2" label-class-name="desc-label"> - <el-descriptions-item label="商户名称">{{ orderDetail.merchantName }}</el-descriptions-item> - <el-descriptions-item label="应用名称">{{ orderDetail.appName }}</el-descriptions-item> - <el-descriptions-item label="商品名称">{{ orderDetail.subject }}</el-descriptions-item> - </el-descriptions> - <el-divider /> - <el-descriptions :column="2" label-class-name="desc-label"> - <el-descriptions-item label="商户订单号"> - <el-tag size="small">{{ orderDetail.merchantOrderId }}</el-tag> - </el-descriptions-item> - <el-descriptions-item label="渠道订单号"> - <el-tag class="tag-purple" size="small">{{ orderDetail.channelOrderNo }}</el-tag> - </el-descriptions-item> - <el-descriptions-item label="支付订单号"> - <el-tag v-if="orderDetail.payOrderExtension.no !== ''" class="tag-pink" size="small"> - {{ orderDetail.payOrderExtension.no }} - </el-tag> - </el-descriptions-item> - <el-descriptions-item label="金额"> - <el-tag type="success" size="small">{{ parseFloat(orderDetail.amount / 100, 2) }}</el-tag> - </el-descriptions-item> - <el-descriptions-item label="手续费"> - <el-tag type="warning" size="small" - >{{ parseFloat(orderDetail.channelFeeAmount / 100, 2) }} - </el-tag> - </el-descriptions-item> - <el-descriptions-item label="手续费比例"> - {{ parseFloat(orderDetail.channelFeeRate / 100, 2) }}% - </el-descriptions-item> - <el-descriptions-item label="支付状态"> - <dict-tag :type="DICT_TYPE.PAY_ORDER_STATUS" :value="orderDetail.status" /> - </el-descriptions-item> - <el-descriptions-item label="回调状态"> - <dict-tag :type="DICT_TYPE.PAY_ORDER_NOTIFY_STATUS" :value="orderDetail.notifyStatus" /> - </el-descriptions-item> - <el-descriptions-item label="回调地址">{{ orderDetail.notifyUrl }}</el-descriptions-item> - <el-descriptions-item label="创建时间" :formatter="dateFormatter"> - {{ formatDate(orderDetail.createTime) }} - </el-descriptions-item> - <el-descriptions-item label="支付时间" :formatter="dateFormatter"> - {{ formatDate(orderDetail.successTime) }} - </el-descriptions-item> - <el-descriptions-item label="失效时间" :formatter="dateFormatter"> - {{ formatDate(orderDetail.expireTime) }} - </el-descriptions-item> - <el-descriptions-item label="通知时间" :formatter="dateFormatter"> - {{ formatDate(orderDetail.notifyTime) }} - </el-descriptions-item> - </el-descriptions> - <el-divider /> - <el-descriptions :column="2" label-class-name="desc-label"> - <el-descriptions-item label="支付渠道" - >{{ orderDetail.channelCodeName }} - </el-descriptions-item> - <el-descriptions-item label="支付IP">{{ orderDetail.userIp }}</el-descriptions-item> - <el-descriptions-item label="退款状态"> - <dict-tag :type="DICT_TYPE.PAY_ORDER_REFUND_STATUS" :value="orderDetail.refundStatus" /> - </el-descriptions-item> - <el-descriptions-item label="退款次数">{{ orderDetail.refundTimes }}</el-descriptions-item> - <el-descriptions-item label="退款金额"> - <el-tag type="warning"> - {{ parseFloat(orderDetail.refundAmount / 100, 2) }} - </el-tag> - </el-descriptions-item> - </el-descriptions> - <el-divider /> - <el-descriptions :column="1" label-class-name="desc-label" direction="vertical" border> - <el-descriptions-item label="商品描述"> - {{ orderDetail.body }} - </el-descriptions-item> - <el-descriptions-item label="支付通道异步回调内容"> - {{ orderDetail.payOrderExtension.channelNotifyData }} - </el-descriptions-item> - </el-descriptions> - </Dialog> -</template> -<script setup lang="ts" name="orderForm"> -import { DICT_TYPE } from '@/utils/dict' -import * as OrderApi from '@/api/pay/order' -import { dateFormatter, formatDate } from '@/utils/formatTime' - -const { t } = useI18n() // 国际化 -// const message = useMessage() // 消息弹窗 -const dialogVisible = ref(false) // 弹窗的是否展示 -const dialogTitle = ref('订单详情') // 弹窗的标题 -const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 -const defaultOrderDetail = { - merchantName: '', - appName: '', - channelCodeName: '', - subject: '', - merchantOrderId: null, - channelOrderNo: '', - body: '', - amount: null, - channelFeeRate: null, - channelFeeAmount: null, - userIp: '', - status: null, - notifyUrl: '', - notifyStatus: null, - refundStatus: null, - refundTimes: '', - refundAmount: null, - createTime: '', - successTime: '', - notifyTime: '', - expireTime: '', - payOrderExtension: { - channelNotifyData: '', - no: '' - } -} -const orderDetail = ref(JSON.parse(JSON.stringify(defaultOrderDetail))) - -/** 打开弹窗 */ -const open = async (id?: number) => { - dialogVisible.value = true - dialogTitle.value = t('action.preview') - // 修改时,设置数据 - if (id) { - formLoading.value = true - try { - orderDetail.value = await OrderApi.getOrderDetail(id) - if (orderDetail.value.payOrderExtension === null) { - orderDetail.value.payOrderExtension = Object.assign( - defaultOrderDetail.payOrderExtension, - {} - ) - } - } finally { - formLoading.value = false - } - } -} -defineExpose({ open }) // 提供 open 方法,用于打开弹窗 -</script> -<style> -.tag-purple { - color: #722ed1; - background: #f9f0ff; - border-color: #d3adf7; -} - -.tag-pink { - color: #eb2f96; - background: #fff0f6; - border-color: #ffadd2; -} -</style> diff --git a/src/views/pay/refund/refundForm.vue b/src/views/pay/refund/refundForm.vue deleted file mode 100644 index cc9d8726..00000000 --- a/src/views/pay/refund/refundForm.vue +++ /dev/null @@ -1,154 +0,0 @@ -<template> - <Dialog :title="dialogTitle" v-model="dialogVisible" width="50%"> - <el-descriptions :column="2" label-class-name="desc-label"> - <el-descriptions-item label="商户名称">{{ refundDetail.merchantName }}</el-descriptions-item> - <el-descriptions-item label="应用名称">{{ refundDetail.appName }}</el-descriptions-item> - <el-descriptions-item label="商品名称">{{ refundDetail.subject }}</el-descriptions-item> - </el-descriptions> - <el-divider /> - <el-descriptions :column="2" label-class-name="desc-label"> - <el-descriptions-item label="商户退款单号"> - <el-tag size="small">{{ refundDetail.merchantRefundNo }}</el-tag> - </el-descriptions-item> - <el-descriptions-item label="商户订单号" - >{{ refundDetail.merchantOrderId }} - </el-descriptions-item> - <el-descriptions-item label="交易订单号">{{ refundDetail.tradeNo }}</el-descriptions-item> - </el-descriptions> - <el-divider /> - <el-descriptions :column="2" label-class-name="desc-label"> - <el-descriptions-item label="支付金额"> - {{ parseFloat(refundDetail.payAmount / 100).toFixed(2) }} - </el-descriptions-item> - <el-descriptions-item label="退款金额" size="small"> - <el-tag class="tag-purple" size="small"> - {{ parseFloat(refundDetail.refundAmount / 100).toFixed(2) }} - </el-tag> - </el-descriptions-item> - <el-descriptions-item label="退款类型"> - <dict-tag :type="DICT_TYPE.PAY_REFUND_ORDER_TYPE" :value="refundDetail.type" /> - </el-descriptions-item> - <el-descriptions-item label="退款状态"> - <dict-tag :type="DICT_TYPE.PAY_REFUND_ORDER_STATUS" :value="refundDetail.status" /> - </el-descriptions-item> - <el-descriptions-item label="创建时间" - >{{ formatDate(refundDetail.createTime) }} - </el-descriptions-item> - <el-descriptions-item label="退款成功时间" - >{{ formatDate(refundDetail.successTime) }} - </el-descriptions-item> - <el-descriptions-item label="退款失效时间" - >{{ formatDate(refundDetail.expireTime) }} - </el-descriptions-item> - <el-descriptions-item label="更新时间" - >{{ formatDate(refundDetail.updateTime) }} - </el-descriptions-item> - </el-descriptions> - <el-divider /> - <el-descriptions :column="2" label-class-name="desc-label"> - <el-descriptions-item label="支付渠道"> - {{ refundDetail.channelCodeName }} - </el-descriptions-item> - <el-descriptions-item label="支付IP" size="small"> - {{ refundDetail.userIp }} - </el-descriptions-item> - <el-descriptions-item label="回调地址">{{ refundDetail.notifyUrl }}</el-descriptions-item> - <el-descriptions-item label="回调状态"> - <dict-tag :type="DICT_TYPE.PAY_ORDER_NOTIFY_STATUS" :value="refundDetail.notifyStatus" /> - </el-descriptions-item> - <el-descriptions-item label="回调时间" - >{{ formatDate(refundDetail.notifyTime) }} - </el-descriptions-item> - </el-descriptions> - <el-divider /> - <el-descriptions :column="2" label-class-name="desc-label"> - <el-descriptions-item label="渠道订单号" - >{{ refundDetail.channelOrderNo }} - </el-descriptions-item> - <el-descriptions-item label="渠道退款单号" - >{{ refundDetail.channelRefundNo }} - </el-descriptions-item> - <el-descriptions-item label="渠道错误码" - >{{ refundDetail.channelErrorCode }} - </el-descriptions-item> - <el-descriptions-item label="渠道错误码描述" - >{{ refundDetail.channelErrorMsg }} - </el-descriptions-item> - </el-descriptions> - <br /> - <el-descriptions :column="1" label-class-name="desc-label" direction="vertical" border> - <el-descriptions-item label="渠道额外参数" - >{{ refundDetail.channelExtras }} - </el-descriptions-item> - <el-descriptions-item label="退款原因">{{ refundDetail.reason }}</el-descriptions-item> - </el-descriptions> - </Dialog> -</template> -<script setup lang="ts" name="refundForm"> -import { DICT_TYPE } from '@/utils/dict' -import * as RefundApi from '@/api/pay/refund' -import { formatDate } from '@/utils/formatTime' - -const { t } = useI18n() // 国际化 -// const message = useMessage() // 消息弹窗 -const dialogVisible = ref(false) // 弹窗的是否展示 -const dialogTitle = ref('退款订单详情') // 弹窗的标题 -const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 -const defaultrefundDetail = { - id: null, - appId: null, - appName: '', - channelCode: '', - channelCodeName: '', - channelErrorCode: '', - channelErrorMsg: '', - channelExtras: '', - channelId: null, - channelOrderNo: '', - channelRefundNo: '', - createTime: null, - expireTime: null, - merchantId: null, - merchantName: '', - merchantOrderId: '', - merchantRefundNo: '', - notifyStatus: null, - notifyTime: null, - notifyUrl: '', - orderId: null, - payAmount: null, - reason: '', - refundAmount: null, - status: null, - subject: '', - successTime: null, - tradeNo: '', - type: null, - userIp: '' -} -const refundDetail = ref(JSON.parse(JSON.stringify(defaultrefundDetail))) - -/** 打开弹窗 */ -const open = async (id?: number) => { - dialogVisible.value = true - dialogTitle.value = t('action.preview') - // 修改时,设置数据 - if (id) { - formLoading.value = true - try { - refundDetail.value = await RefundApi.getRefundApi(id) - } finally { - formLoading.value = false - } - } -} -defineExpose({ open }) // 提供 open 方法,用于打开弹窗 -</script> - -<style> -.tag-purple { - color: #722ed1; - background: #f9f0ff; - border-color: #d3adf7; -} -</style>