!118 refactor: mp模块ts重构

Merge pull request !118 from dhb52/dev
This commit is contained in:
芋道源码 2023-04-13 13:14:22 +00:00 committed by Gitee
commit 3aefedff6a
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
17 changed files with 1208 additions and 858 deletions

View File

@ -62,6 +62,8 @@ declare global {
const useRouter: typeof import('vue-router')['useRouter'] const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots'] const useSlots: typeof import('vue')['useSlots']
const useTable: typeof import('@/hooks/web/useTable')['useTable'] 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 watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect'] const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect'] const watchPostEffect: typeof import('vue')['watchPostEffect']

View File

@ -3,13 +3,16 @@
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<ContentWrap> <ContentWrap>
<!-- TODO @芋艿调整成 el-form WxAccountSelect --> <el-form class="-mb-15px" :model="queryParams" :inline="true" label-width="68px">
<WxAccountSelect @change="accountChanged" /> <el-form-item label="公众号" prop="accountId">
<WxMpSelect @change="onAccountChanged" />
</el-form-item>
</el-form>
</ContentWrap> </ContentWrap>
<!-- tab 切换 --> <!-- tab 切换 -->
<ContentWrap> <ContentWrap>
<el-tabs v-model="type" @tab-change="handleTabChange"> <el-tabs v-model="msgType" @tab-change="handleTabChange">
<!-- 操作工具栏 --> <!-- 操作工具栏 -->
<el-row :gutter="10" class="mb8"> <el-row :gutter="10" class="mb8">
<el-col :span="1.5"> <el-col :span="1.5">
@ -18,26 +21,26 @@
plain plain
@click="handleAdd" @click="handleAdd"
v-hasPermi="['mp:auto-reply:create']" 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" />新增 <Icon icon="ep:plus" />新增
</el-button> </el-button>
</el-col> </el-col>
</el-row> </el-row>
<!-- tab --> <!-- tab -->
<el-tab-pane name="1"> <el-tab-pane :name="MsgType.Follow">
<template #label> <template #label>
<span><Icon icon="ep:star-off" /> 关注时回复</span> <span><Icon icon="ep:star" /> 关注时回复</span>
</template> </template>
</el-tab-pane> </el-tab-pane>
<el-tab-pane name="2"> <el-tab-pane :name="MsgType.Message">
<template #label> <template #label>
<span><Icon icon="ep:chat-line-round" /> 消息回复</span> <span><Icon icon="ep:chat-line-round" /> 消息回复</span>
</template> </template>
</el-tab-pane> </el-tab-pane>
<el-tab-pane name="3"> <el-tab-pane :name="MsgType.Keyword">
<template #label> <template #label>
<span><Icon icon="ep:news" /> 关键词回复</span> <span><Icon icon="fa:newspaper-o" /> 关键词回复</span>
</template> </template>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
@ -47,10 +50,20 @@
label="请求消息类型" label="请求消息类型"
align="center" align="center"
prop="requestMessageType" 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
<el-table-column label="匹配类型" align="center" prop="requestMatch" v-if="type === '3'"> 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"> <template #default="scope">
<dict-tag :type="DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH" :value="scope.row.requestMatch" /> <dict-tag :type="DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH" :value="scope.row.requestMatch" />
</template> </template>
@ -64,7 +77,7 @@
<template #default="scope"> <template #default="scope">
<div v-if="scope.row.responseMessageType === 'text'">{{ scope.row.responseContent }}</div> <div v-if="scope.row.responseMessageType === 'text'">{{ scope.row.responseContent }}</div>
<div v-else-if="scope.row.responseMessageType === 'voice'"> <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>
<div v-else-if="scope.row.responseMessageType === 'image'"> <div v-else-if="scope.row.responseMessageType === 'image'">
<a target="_blank" :href="scope.row.responseMediaUrl"> <a target="_blank" :href="scope.row.responseMediaUrl">
@ -77,7 +90,11 @@
scope.row.responseMessageType === 'shortvideo' 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>
<div v-else-if="scope.row.responseMessageType === 'news'"> <div v-else-if="scope.row.responseMessageType === 'news'">
<WxNews :articles="scope.row.responseArticles" /> <WxNews :articles="scope.row.responseArticles" />
@ -123,21 +140,21 @@
</el-table> </el-table>
<!-- 添加或修改自动回复的对话框 --> <!-- 添加或修改自动回复的对话框 -->
<el-dialog :title="title" v-model="open" width="800px" append-to-body> <el-dialog :title="title" v-model="showReplyFormDialog" width="800px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px"> <el-form ref="formRef" :model="replyForm" :rules="rules" label-width="80px">
<el-form-item label="消息类型" prop="requestMessageType" v-if="type === '2'"> <el-form-item label="消息类型" prop="requestMessageType" v-if="msgType === MsgType.Message">
<el-select v-model="form.requestMessageType" placeholder="请选择"> <el-select v-model="replyForm.requestMessageType" placeholder="请选择">
<template v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)" :key="dict.value"> <template v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)" :key="dict.value">
<el-option <el-option
v-if="requestMessageTypes.includes(dict.value)" v-if="RequestMessageTypes.includes(dict.value)"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
/> />
</template> </template>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="匹配类型" prop="requestMatch" v-if="type === '3'"> <el-form-item label="匹配类型" prop="requestMatch" v-if="msgType === MsgType.Keyword">
<el-select v-model="form.requestMatch" placeholder="请选择匹配类型" clearable> <el-select v-model="replyForm.requestMatch" placeholder="请选择匹配类型" clearable>
<el-option <el-option
v-for="dict in getDictOptions(DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH)" v-for="dict in getDictOptions(DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH)"
:key="dict.value" :key="dict.value"
@ -146,8 +163,8 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="关键词" prop="requestKeyword" v-if="type === '3'"> <el-form-item label="关键词" prop="requestKeyword" v-if="msgType === MsgType.Keyword">
<el-input v-model="form.requestKeyword" placeholder="请输入内容" clearable /> <el-input v-model="replyForm.requestKeyword" placeholder="请输入内容" clearable />
</el-form-item> </el-form-item>
<el-form-item label="回复消息"> <el-form-item label="回复消息">
<WxReplySelect :objData="objData" v-if="hackResetWxReplySelect" /> <WxReplySelect :objData="objData" v-if="hackResetWxReplySelect" />
@ -160,38 +177,47 @@
</el-dialog> </el-dialog>
</ContentWrap> </ContentWrap>
</template> </template>
<script setup name="MpAutoReply"> <script setup lang="ts" name="MpAutoReply">
import WxVideoPlayer from '@/views/mp/components/wx-video-play/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 WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
import WxMusic from '@/views/mp/components/wx-music/main.vue' import WxMusic from '@/views/mp/components/wx-music/main.vue'
import WxNews from '@/views/mp/components/wx-news/main.vue' import WxNews from '@/views/mp/components/wx-news/main.vue'
import WxReplySelect from '@/views/mp/components/wx-reply/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 * as MpAutoReplyApi from '@/api/mp/autoReply'
import { DICT_TYPE, getDictOptions } from '@/utils/dict' import { DICT_TYPE, getDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import { ContentWrap } from '@/components/ContentWrap' import { ContentWrap } from '@/components/ContentWrap'
import type { TabPaneName } from 'element-plus'
const message = useMessage() const message = useMessage()
// const queryFormRef = ref()
const formRef = ref() const formRef = ref()
// tab 123 // Follow: Message: Keyword:
const type = ref('3') // 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 loading = ref(true)
//
// const showSearch = ref(true)
// //
const total = ref(0) 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, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: undefined accountId: undefined
@ -200,12 +226,50 @@ const queryParams = reactive({
// //
const title = ref('') 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({ const objData = ref<ObjData>({
type: 'text' type: 'text',
accountId: undefined
}) })
// //
const rules = { const rules = {
@ -216,8 +280,8 @@ const rules = {
// WxReplySelect // WxReplySelect
const hackResetWxReplySelect = ref(false) const hackResetWxReplySelect = ref(false)
const accountChanged = (accountId) => { const onAccountChanged = (id?: number) => {
queryParams.accountId = accountId queryParams.accountId = id
getList() getList()
} }
@ -227,7 +291,7 @@ const getList = async () => {
try { try {
const data = await MpAutoReplyApi.getAutoReplyPage({ const data = await MpAutoReplyApi.getAutoReplyPage({
...queryParams, ...queryParams,
type: type.value type: msgType.value
}) })
list.value = data.list list.value = data.list
total.value = data.total total.value = data.total
@ -242,8 +306,8 @@ const handleQuery = () => {
getList() getList()
} }
const handleTabChange = (tabName) => { const handleTabChange = (tabName: TabPaneName) => {
type.value = tabName msgType.value = tabName as MsgType
handleQuery() handleQuery()
} }
@ -252,94 +316,87 @@ const handleAdd = () => {
reset() reset()
resetEditor() resetEditor()
// //
open.value = true
title.value = '新增自动回复'
objData.value = { objData.value = {
type: 'text', type: 'text',
accountId: queryParams.accountId accountId: queryParams.accountId
} }
title.value = '新增自动回复'
showReplyFormDialog.value = true
} }
/** 修改按钮操作 */ /** 修改按钮操作 */
const handleUpdate = (row) => { const handleUpdate = async (row: any) => {
reset() reset()
resetEditor() resetEditor()
console.log(row)
MpAutoReplyApi.getAutoReply(row.id).then((data) => { const data = await MpAutoReplyApi.getAutoReply(row.id)
// //
form.value = { ...data } replyForm.value = { ...data }
delete form.value['responseMessageType'] delete replyForm.value['responseMessageType']
delete form.value['responseContent'] delete replyForm.value['responseContent']
delete form.value['responseMediaId'] delete replyForm.value['responseMediaId']
delete form.value['responseMediaUrl'] delete replyForm.value['responseMediaUrl']
delete form.value['responseDescription'] delete replyForm.value['responseDescription']
delete form.value['responseArticles'] delete replyForm.value['responseArticles']
objData.value = { objData.value = {
type: data.responseMessageType, type: data.responseMessageType,
accountId: queryParams.accountId, accountId: queryParams.accountId,
content: data.responseContent, content: data.responseContent,
mediaId: data.responseMediaId, mediaId: data.responseMediaId,
url: data.responseMediaUrl, url: data.responseMediaUrl,
title: data.responseTitle, title: data.responseTitle,
description: data.responseDescription, description: data.responseDescription,
thumbMediaId: data.responseThumbMediaId, thumbMediaId: data.responseThumbMediaId,
thumbMediaUrl: data.responseThumbMediaUrl, thumbMediaUrl: data.responseThumbMediaUrl,
articles: data.responseArticles, articles: data.responseArticles,
musicUrl: data.responseMusicUrl, musicUrl: data.responseMusicUrl,
hqMusicUrl: data.responseHqMusicUrl hqMusicUrl: data.responseHqMusicUrl
} }
// //
open.value = true title.value = '修改自动回复'
title.value = '修改自动回复' showReplyFormDialog.value = true
})
} }
const handleSubmit = () => { const handleSubmit = async () => {
formRef.value?.validate((valid) => { const valid = await formRef.value?.validate()
if (!valid) { if (!valid) return
return
}
// //
const form = { ...form.value } const submitForm: any = { ...replyForm.value }
form.responseMessageType = objData.value.type submitForm.responseMessageType = objData.value.type
form.responseContent = objData.value.content submitForm.responseContent = objData.value.content
form.responseMediaId = objData.value.mediaId submitForm.responseMediaId = objData.value.mediaId
form.responseMediaUrl = objData.value.url submitForm.responseMediaUrl = objData.value.url
form.responseTitle = objData.value.title submitForm.responseTitle = objData.value.title
form.responseDescription = objData.value.description submitForm.responseDescription = objData.value.description
form.responseThumbMediaId = objData.value.thumbMediaId submitForm.responseThumbMediaId = objData.value.thumbMediaId
form.responseThumbMediaUrl = objData.value.thumbMediaUrl submitForm.responseThumbMediaUrl = objData.value.thumbMediaUrl
form.responseArticles = objData.value.articles submitForm.responseArticles = objData.value.articles
form.responseMusicUrl = objData.value.musicUrl submitForm.responseMusicUrl = objData.value.musicUrl
form.responseHqMusicUrl = objData.value.hqMusicUrl submitForm.responseHqMusicUrl = objData.value.hqMusicUrl
if (form.value.id !== undefined) { if (replyForm.value.id !== undefined) {
MpAutoReplyApi.updateAutoReply(form).then(() => { await MpAutoReplyApi.updateAutoReply(submitForm)
message.success('修改成功') message.success('修改成功')
open.value = false } else {
getList() await MpAutoReplyApi.createAutoReply(submitForm)
}) message.success('新增成功')
} else { }
MpAutoReplyApi.createAutoReply(form).then(() => {
message.success('新增成功') showReplyFormDialog.value = false
open.value = false getList()
getList()
})
}
})
} }
// //
const reset = () => { const reset = () => {
form.value = { replyForm.value = {
id: undefined, id: undefined,
accountId: queryParams.accountId, accountId: queryParams.accountId,
type: type.value, type: msgType.value,
requestKeyword: undefined, requestKeyword: undefined,
requestMatch: type.value === '3' ? 1 : undefined, requestMatch: msgType.value === MsgType.Keyword ? 1 : undefined,
requestMessageType: undefined requestMessageType: undefined
} }
formRef.value?.resetFields() formRef.value?.resetFields()
@ -347,7 +404,7 @@ const reset = () => {
// //
const cancel = () => { const cancel = () => {
open.value = false showReplyFormDialog.value = false
reset() reset()
} }

View File

@ -1,10 +1,5 @@
<template> <template>
<el-select <el-select v-model="account.id" placeholder="请选择公众号" class="!w-240px" @change="onChanged">
v-model="accountId"
placeholder="请选择公众号"
class="!w-240px"
@change="accountChanged"
>
<el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" /> <el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" />
</el-select> </el-select>
</template> </template>
@ -12,11 +7,14 @@
<script lang="ts" setup name="WxMpSelect"> <script lang="ts" setup name="WxMpSelect">
import * as MpAccountApi from '@/api/mp/account' 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 accountList: Ref<MpAccountApi.AccountVO[]> = ref([])
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'change', id: number | undefined): void (e: 'change', id?: number, name?: string): void
}>() }>()
onMounted(() => { onMounted(() => {
@ -27,12 +25,12 @@ const handleQuery = async () => {
accountList.value = await MpAccountApi.getSimpleAccountList() accountList.value = await MpAccountApi.getSimpleAccountList()
// //
if (accountList.value.length > 0) { if (accountList.value.length > 0) {
accountId.value = accountList.value[0].id account.id = accountList.value[0].id
emit('change', accountId.value) emit('change', account.id, account.name)
} }
} }
const accountChanged = () => { const onChanged = () => {
emit('change', accountId.value) emit('change', account.id, account.name)
} }
</script> </script>

View File

@ -1,11 +1,11 @@
<script setup> <script setup>
import { ref, reactive } from 'vue' 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 { getAccessToken } from '@/utils/auth'
import editorOptions from './quill-options' import { Editor } from '@/components/Editor'
const BASE_URL = import.meta.env.VITE_BASE_URL 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() const message = useMessage()
@ -30,21 +30,16 @@ const props = defineProps({
const emit = defineEmits(['input']) const emit = defineEmits(['input'])
const myQuillEditorRef = ref() const myQuillEditorRef = ref()
const content = ref(props.value.replace(/data-src/g, 'src')) const content = ref(props.value.replace(/data-src/g, 'src'))
const loading = ref(false) // loadingfalse, const loading = ref(false) // loadingfalse,
const actionUrl = ref(BASE_URL + '/admin-api/mp/material/upload-news-image') //
const headers = ref({ Authorization: 'Bearer ' + getAccessToken() }) //
const uploadData = reactive({ const uploadData = reactive({
type: 'image', // TODO thumb type: 'image', // TODO thumb
accountId: props.accountId accountId: props.accountId
}) })
const onEditorChange = () => { const onEditorChange = (text) => {
// //
emit('input', content.value) emit('input', text)
} }
// //
@ -98,104 +93,14 @@ const uploadError = () => {
:on-error="uploadError" :on-error="uploadError"
:before-upload="beforeUpload" :before-upload="beforeUpload"
/> />
<QuillEditor <Editor
class="editor" editor-id="wxEditor"
v-model="content"
ref="quillEditorRef" ref="quillEditorRef"
:options="editorOptions" :modelValue="content"
@change="onEditorChange($event)" @change="(editor) => onEditorChange(editor.getText())"
/> />
</div> </div>
</div> </div>
</template> </template>
<style> <style></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>

View File

@ -31,7 +31,6 @@
/> />
</el-row> </el-row>
<el-row> <el-row>
<el-icon><Location /></el-icon>
<Icon icon="ep:location" /> <Icon icon="ep:location" />
{{ label }} {{ label }}
</el-row> </el-row>
@ -39,6 +38,7 @@
</el-link> </el-link>
</div> </div>
</template> </template>
<script setup lang="ts" name="WxLocation"> <script setup lang="ts" name="WxLocation">
const props = defineProps({ const props = defineProps({
locationX: { locationX: {

View File

@ -39,79 +39,79 @@
:style="item.sendFrom === 2 ? 'background: #6BED72;' : ''" :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> <el-tag type="success">关注</el-tag>
</div> </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> <el-tag type="danger">取消关注</el-tag>
</div> </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> <el-tag>点击菜单</el-tag>
{{ item.eventKey }} {{ item.eventKey }}
</div> </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> <el-tag>点击菜单链接</el-tag>
{{ item.eventKey }} {{ item.eventKey }}
</div> </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> <el-tag>扫码结果</el-tag>
{{ item.eventKey }} {{ item.eventKey }}
</div> </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> <el-tag>扫码结果</el-tag>
{{ item.eventKey }} {{ item.eventKey }}
</div> </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> <el-tag>系统拍照发图</el-tag>
</div> </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> <el-tag>拍照或者相册</el-tag>
</div> </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> <el-tag>微信相册</el-tag>
</div> </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> <el-tag>选择地理位置</el-tag>
</div> </div>
<div v-else-if="item.type === 'event'"> <div v-else-if="item.type === MsgType.Event">
<el-tag type="danger">未知事件类型</el-tag> <el-tag type="danger">未知事件类型</el-tag>
</div> </div>
<!-- 消息区域 --> <!-- 消息区域 -->
<div v-else-if="item.type === 'text'">{{ item.content }}</div> <div v-else-if="item.type === MsgType.Text">{{ item.content }}</div>
<div v-else-if="item.type === 'voice'"> <div v-else-if="item.type === MsgType.Voice">
<wx-voice-player :url="item.mediaUrl" :content="item.recognition" /> <WxVoicePlayer :url="item.mediaUrl" :content="item.recognition" />
</div> </div>
<div v-else-if="item.type === 'image'"> <div v-else-if="item.type === MsgType.Image">
<a target="_blank" :href="item.mediaUrl"> <a target="_blank" :href="item.mediaUrl">
<img :src="item.mediaUrl" style="width: 100px" /> <img :src="item.mediaUrl" style="width: 100px" />
</a> </a>
</div> </div>
<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" style="text-align: center"
> >
<wx-video-player :url="item.mediaUrl" /> <WxVideoPlayer :url="item.mediaUrl" />
</div> </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"> <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> <div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div>
</el-link> </el-link>
<div class="avue-card__info" style="height: unset">{{ item.description }}</div> <div class="avue-card__info" style="height: unset">{{ item.description }}</div>
</div> </div>
<!-- TODO 芋艿待完善 --> <!-- TODO 芋艿待完善 -->
<div v-else-if="item.type === 'location'"> <div v-else-if="item.type === MsgType.Location">
<wx-location <WxLocation
:label="item.label" :label="item.label"
:location-y="item.locationY" :location-y="item.locationY"
:location-x="item.locationX" :location-x="item.locationX"
/> />
</div> </div>
<div v-else-if="item.type === 'news'" style="width: 300px"> <div v-else-if="item.type === MsgType.News" style="width: 300px">
<!-- TODO 芋艿待测试详情页也存在类似的情况 --> <!-- TODO 芋艿待测试详情页也存在类似的情况 -->
<wx-news :articles="item.articles" /> <WxNews :articles="item.articles" />
</div> </div>
<div v-else-if="item.type === 'music'"> <div v-else-if="item.type === MsgType.Music">
<wx-music <WxMusic
:title="item.title" :title="item.title"
:description="item.description" :description="item.description"
:thumb-media-url="item.thumbMediaUrl" :thumb-media-url="item.thumbMediaUrl"
@ -125,182 +125,185 @@
</div> </div>
</div> </div>
<div class="msg-send" v-loading="sendLoading"> <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> <el-button type="success" size="small" class="send-but" @click="sendMsg">发送(S)</el-button>
</div> </div>
</ContentWrap> </ContentWrap>
</template> </template>
<script lang="ts" name="WxMsg"> <script setup lang="ts" name="WxMsg">
import { getMessagePage, sendMessage } from '@/api/mp/message'
import WxReplySelect from '@/views/mp/components/wx-reply/main.vue' import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
import WxVideoPlayer from '@/views/mp/components/wx-video-play/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 WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
import WxNews from '@/views/mp/components/wx-news/main.vue' import WxNews from '@/views/mp/components/wx-news/main.vue'
import WxLocation from '@/views/mp/components/wx-location/main.vue' import WxLocation from '@/views/mp/components/wx-location/main.vue'
import WxMusic from '@/views/mp/components/wx-music/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 { getUser } from '@/api/mp/user'
import { defineComponent } from 'vue' import { formatDate } from '@/utils/formatTime'
const message = useMessage() //
import profile from '@/assets/imgs/profile.jpg' import profile from '@/assets/imgs/profile.jpg'
import wechat from '@/assets/imgs/wechat.png' import wechat from '@/assets/imgs/wechat.png'
import { formatDate } from '@/utils/formatTime' import { MsgType } from './types'
export default defineComponent({ const message = useMessage() //
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 props = defineProps({
const sendLoading = ref(false) // userId: {
const objData = reactive({ type: Number,
// required: true
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 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
/* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc */ /* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc */
@import './comment.scss'; @import './comment.scss';

View File

@ -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'
}

View File

@ -3,14 +3,22 @@
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<ContentWrap> <ContentWrap>
<!-- TODO @芋艿调整成 el-form WxAccountSelect --> <el-form
<WxAccountSelect @change="accountChanged"> class="-mb-15px"
<template #actions> :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']"> <el-button type="primary" plain @click="handleAdd" v-hasPermi="['mp:draft:create']">
<Icon icon="ep:plus" />新增 <Icon icon="ep:plus" />新增
</el-button> </el-button>
</template> </el-form-item>
</WxAccountSelect> </el-form>
</ContentWrap> </ContentWrap>
<!-- 列表 --> <!-- 列表 -->
@ -58,214 +66,221 @@
/> />
</ContentWrap> </ContentWrap>
<!-- TODO @Dhb52迁移成独立路由 -->
<div class="app-container"> <div class="app-container">
<!-- 添加或修改草稿对话框 --> <!-- 添加或修改草稿对话框 -->
<Teleport to="body"> <el-dialog
<el-dialog :title="operateMaterial === 'add' ? '新建图文' : '修改图文'"
:title="operateMaterial === 'add' ? '新建图文' : '修改图文'" width="80%"
width="80%" top="20px"
top="20px" v-model="dialogNewsVisible"
v-model="dialogNewsVisible" :before-close="dialogNewsClose"
:before-close="dialogNewsClose" :close-on-click-modal="false"
:close-on-click-modal="false" >
> <div class="left">
<div class="left"> <div class="select-item">
<div class="select-item"> <div v-for="(news, index) in articlesAdd" :key="news.id">
<div v-for="(news, index) in articlesAdd" :key="news.id"> <div
<div class="news-main father"
class="news-main father" v-if="index === 0"
v-if="index === 0" :class="{ activeAddNews: isActiveAddNews === index }"
:class="{ activeAddNews: isActiveAddNews === index }" @click="activeNews(index)"
@click="activeNews(index)" >
> <div class="news-content">
<div class="news-content"> <img class="material-img" v-if="news.thumbUrl" :src="news.thumbUrl" />
<img class="material-img" v-if="news.thumbUrl" :src="news.thumbUrl" /> <div class="news-content-title">{{ news.title }}</div>
<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>
</div> </div>
<div <div class="child" v-if="articlesAdd.length > 1">
class="news-main-item father" <el-button size="small" @click="downNews(index)"
v-if="index > 0" ><Icon icon="ep:sort-down" />下移</el-button
: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"
> >
<template #trigger> <el-button v-if="operateMaterial === 'add'" size="small" @click="minusNews(index)"
<el-button size="small" type="primary">本地上传</el-button> ><Icon icon="ep:delete" />删除
</template> </el-button>
<el-button </div>
size="small" </div>
type="primary" <div
@click="openMaterial" class="news-main-item father"
style="margin-left: 5px" v-if="index > 0"
>素材库选择</el-button :class="{ activeAddNews: isActiveAddNews === index }"
> @click="activeNews(index)"
<template #tip> >
<div class="el-upload__tip">支持 bmp/png/jpeg/jpg/gif 格式大小不超过 2M</div> <div class="news-content-item">
</template> <div class="news-content-item-title">{{ news.title }}</div>
</el-upload> <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>
<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> </div>
<el-input
:rows="8"
type="textarea"
v-model="articlesAdd[isActiveAddNews].digest"
placeholder="请输入摘要"
class="digest"
maxlength="120"
style="float: right"
/>
</div> </div>
<!--富文本编辑器组件--> <el-row justify="center" class="ope-row">
<el-row> <el-button
<WxEditor type="primary"
v-model="articlesAdd[isActiveAddNews].content" circle
:account-id="uploadData.accountId" @click="plusNews"
v-if="hackResetEditor" v-if="articlesAdd.length < 8 && operateMaterial === 'add'"
/> >
<Icon icon="ep:plus" />
</el-button>
</el-row> </el-row>
</div> </div>
<template #footer> </div>
<el-button @click="dialogNewsVisible = false"> </el-button> <div class="right" v-loading="addMaterialLoading" v-if="articlesAdd.length > 0">
<el-button type="primary" @click="submitForm"> </el-button> <br />
</template> <br />
</el-dialog> <br />
</Teleport> <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> </div>
</template> </template>
<script setup name="MpDraft">
<script setup lang="ts" name="MpDraft">
import WxEditor from '@/views/mp/components/wx-editor/WxEditor.vue' import WxEditor from '@/views/mp/components/wx-editor/WxEditor.vue'
import WxNews from '@/views/mp/components/wx-news/main.vue' import WxNews from '@/views/mp/components/wx-news/main.vue'
import WxMaterialSelect from '@/views/mp/components/wx-material-select/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 { getAccessToken } from '@/utils/auth'
import * as MpDraftApi from '@/api/mp/draft' import * as MpDraftApi from '@/api/mp/draft'
import * as MpFreePublishApi from '@/api/mp/freePublish' import * as MpFreePublishApi from '@/api/mp/freePublish'
import type { UploadFiles, UploadProps, UploadRawFile } from 'element-plus'
// API // API
// import drafts from './mock' // import drafts from './mock'
const message = useMessage() // const message = useMessage() //
const loading = ref(true) // const loading = ref(true) //
const list = ref<any[]>([]) //
const total = ref(0) // const total = ref(0) //
const list = ref([]) // interface QueryParams {
const queryParams = reactive({ pageNo: number
pageSize: number
accountId?: number
}
const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: undefined accountId: undefined
}) })
// ========== ========== // ========== ==========
const materialSelectRef = ref()
const BASE_URL = import.meta.env.VITE_BASE_URL const BASE_URL = import.meta.env.VITE_BASE_URL
const actionUrl = ref(BASE_URL + '/admin-api/mp/material/upload-permanent') // const uploadUrl = BASE_URL + '/admin-api/mp/material/upload-permanent' //
const headers = ref({ Authorization: 'Bearer ' + getAccessToken() }) // const headers = { Authorization: 'Bearer ' + getAccessToken() } //
const fileList = ref([])
const uploadData = reactive({ 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', type: 'image',
accountId: 1 accountId: 1
}) })
@ -273,34 +288,28 @@ const uploadData = reactive({
// ========== 稿 or ========== // ========== 稿 or ==========
const dialogNewsVisible = ref(false) const dialogNewsVisible = ref(false)
const addMaterialLoading = ref(false) // 稿 loading const addMaterialLoading = ref(false) // 稿 loading
const articlesAdd = ref([]) const articlesAdd = ref<any[]>([])
const isActiveAddNews = ref(0) const isActiveAddNews = ref(0)
const dialogImageVisible = ref(false) const dialogImageVisible = ref(false)
const operateMaterial = ref('add') const operateMaterial = ref<'add' | 'edit'>('add')
const articlesMediaId = ref('') const articlesMediaId = ref('')
const hackResetEditor = ref(false) const hackResetEditor = ref(false)
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const accountChanged = (accountId) => { const onAccountChanged = (id?: number) => {
setAccountId(accountId) setAccountId(id)
getList() getList()
} }
// ======================== ======================== // ======================== ========================
/** 设置账号编号 */ /** 设置账号编号 */
const setAccountId = (accountId) => { const setAccountId = (id?: number) => {
queryParams.accountId = accountId queryParams.accountId = id
uploadData.accountId = accountId uploadData.accountId = id
} }
/** 查询列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
//
if (!queryParams.accountId) {
message.error('未选中公众号,无法查询草稿箱')
return false
}
loading.value = true loading.value = true
try { try {
const drafts = await MpDraftApi.getDraftPage(queryParams) const drafts = await MpDraftApi.getDraftPage(queryParams)
@ -329,7 +338,7 @@ const handleAdd = () => {
} }
/** 更新按钮操作 */ /** 更新按钮操作 */
const handleUpdate = (item) => { const handleUpdate = (item: any) => {
resetEditor() resetEditor()
reset() reset()
articlesMediaId.value = item.mediaId articlesMediaId.value = item.mediaId
@ -340,39 +349,30 @@ const handleUpdate = (item) => {
} }
/** 提交按钮 */ /** 提交按钮 */
const submitForm = () => { const submitForm = async () => {
// TODO @Dhb52: await
addMaterialLoading.value = true addMaterialLoading.value = true
if (operateMaterial.value === 'add') { try {
MpDraftApi.createDraft(queryParams.accountId, articlesAdd.value) if (operateMaterial.value === 'add') {
.then(() => { await MpDraftApi.createDraft(queryParams.accountId, articlesAdd.value)
message.notifySuccess('新增成功') message.notifySuccess('新增成功')
dialogNewsVisible.value = false } else {
getList() await MpDraftApi.updateDraft(queryParams.accountId, articlesMediaId.value, articlesAdd.value)
}) message.notifySuccess('更新成功')
.finally(() => { }
addMaterialLoading.value = false } finally {
}) dialogNewsVisible.value = false
} else { addMaterialLoading.value = false
MpDraftApi.updateDraft(queryParams.accountId, articlesMediaId.value, articlesAdd.value) getList()
.then(() => {
message.notifySuccess('更新成功')
dialogNewsVisible.value = false
getList()
})
.finally(() => {
addMaterialLoading.value = false
})
} }
} }
// //
const dialogNewsClose = async (done) => { const dialogNewsClose = async (onDone: () => {}) => {
try { try {
await message.confirm('修改内容可能还未保存,确定关闭吗?') await message.confirm('修改内容可能还未保存,确定关闭吗?')
reset() reset()
resetEditor() resetEditor()
done() onDone()
} catch {} } catch {}
} }
@ -391,7 +391,7 @@ const resetEditor = () => {
} }
// //
const downNews = (index) => { const downNews = (index: number) => {
let temp = articlesAdd.value[index] let temp = articlesAdd.value[index]
articlesAdd.value[index] = articlesAdd.value[index + 1] articlesAdd.value[index] = articlesAdd.value[index + 1]
articlesAdd.value[index + 1] = temp articlesAdd.value[index + 1] = temp
@ -407,13 +407,13 @@ const upNews = (index) => {
} }
// index // index
const activeNews = (index) => { const activeNews = (index: number) => {
resetEditor() resetEditor()
isActiveAddNews.value = index isActiveAddNews.value = index
} }
// index // index
const minusNews = async (index) => { const minusNews = async (index: number) => {
try { try {
await message.confirm('确定删除该图文吗?') await message.confirm('确定删除该图文吗?')
articlesAdd.value.splice(index, 1) articlesAdd.value.splice(index, 1)
@ -446,20 +446,17 @@ const buildEmptyArticle = () => {
} }
// ======================== ======================== // ======================== ========================
const beforeThumbImageUpload = (file) => { const beforeThumbImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
addMaterialLoading.value = true addMaterialLoading.value = true
const isType = const isType = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'].includes(
file.type === 'image/jpeg' || rawFile.type
file.type === 'image/png' || )
file.type === 'image/gif' ||
file.type === 'image/bmp' ||
file.type === 'image/jpg'
if (!isType) { if (!isType) {
message.error('上传图片格式不对!') message.error('上传图片格式不对!')
addMaterialLoading.value = false addMaterialLoading.value = false
return false return false
} }
const isLt = file.size / 1024 / 1024 < 2 const isLt = rawFile.size / 1024 / 1024 < 2
if (!isLt) { if (!isLt) {
message.error('上传图片大小不能超过 2M!') message.error('上传图片大小不能超过 2M!')
addMaterialLoading.value = false addMaterialLoading.value = false
@ -469,7 +466,7 @@ const beforeThumbImageUpload = (file) => {
return true return true
} }
const handleUploadSuccess = (response, file, fileList) => { const handleUploadSuccess: UploadProps['onSuccess'] = (response: any) => {
addMaterialLoading.value = false addMaterialLoading.value = false
if (response.code !== 0) { if (response.code !== 0) {
message.error('上传出错:' + response.msg) message.error('上传出错:' + response.msg)
@ -485,7 +482,7 @@ const handleUploadSuccess = (response, file, fileList) => {
} }
// or 稿 // or 稿
const selectMaterial = (item) => { const selectMaterial = (item: any) => {
dialogImageVisible.value = false dialogImageVisible.value = false
articlesAdd.value[isActiveAddNews.value].thumbMediaId = item.mediaId articlesAdd.value[isActiveAddNews.value].thumbMediaId = item.mediaId
articlesAdd.value[isActiveAddNews.value].thumbUrl = item.url articlesAdd.value[isActiveAddNews.value].thumbUrl = item.url
@ -494,14 +491,10 @@ const selectMaterial = (item) => {
// //
const openMaterial = () => { const openMaterial = () => {
dialogImageVisible.value = true 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 accountId = queryParams.accountId
const mediaId = item.mediaId const mediaId = item.mediaId
const content = const content =
@ -510,19 +503,19 @@ const handlePublish = async (item) => {
await message.confirm(content) await message.confirm(content)
await MpFreePublishApi.submitFreePublish(accountId, mediaId) await MpFreePublishApi.submitFreePublish(accountId, mediaId)
message.notifySuccess('发布成功') message.notifySuccess('发布成功')
await getList() getList()
} catch {} } catch {}
} }
/** 删除按钮操作 */ /** 删除按钮操作 */
const handleDelete = async (item) => { const handleDelete = async (item: any) => {
const accountId = queryParams.accountId const accountId = queryParams.accountId
const mediaId = item.mediaId const mediaId = item.mediaId
try { try {
await message.confirm('此操作将永久删除该草稿, 是否继续?') await message.confirm('此操作将永久删除该草稿, 是否继续?')
await MpDraftApi.deleteDraft(accountId, mediaId) await MpDraftApi.deleteDraft(accountId, mediaId)
message.notifySuccess('删除成功') message.notifySuccess('删除成功')
await getList() getList()
} catch {} } catch {}
} }
</script> </script>

View File

@ -3,8 +3,17 @@
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<ContentWrap> <ContentWrap>
<!-- TODO @芋艿调整成 el-form WxAccountSelect --> <el-form
<WxAccountSelect @change="(accountId) => accountChanged(accountId)" /> 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> </ContentWrap>
<!-- 列表 --> <!-- 列表 -->
@ -39,26 +48,32 @@
</ContentWrap> </ContentWrap>
</template> </template>
<script setup name="MpFreePublish"> <script lang="ts" setup name="MpFreePublish">
import * as FreePublishApi from '@/api/mp/freePublish' import * as FreePublishApi from '@/api/mp/freePublish'
import WxNews from '@/views/mp/components/wx-news/main.vue' 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 message = useMessage() //
const { t } = useI18n() // const { t } = useI18n() //
const loading = ref(true) // const loading = ref(true) //
const total = ref(0) // 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, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: undefined accountId: undefined
}) })
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const accountChanged = (accountId) => { const onAccountChanged = (id: number | undefined) => {
queryParams.accountId = accountId queryParams.accountId = id
getList() getList()
} }
@ -75,7 +90,7 @@ const getList = async () => {
} }
/** 删除按钮操作 */ /** 删除按钮操作 */
const handleDelete = async (item) => { const handleDelete = async (item: any) => {
try { try {
// //
await message.delConfirm('删除后用户将无法访问此页面,确定删除?') await message.delConfirm('删除后用户将无法访问此页面,确定删除?')

View File

@ -4,13 +4,13 @@
<ContentWrap> <ContentWrap>
<el-form class="-mb-15px" :inline="true" label-width="68px"> <el-form class="-mb-15px" :inline="true" label-width="68px">
<el-form-item label="公众号" prop="accountId"> <el-form-item label="公众号" prop="accountId">
<WxMpSelect @change="accountChange" /> <WxMpSelect @change="onAccountChanged" />
</el-form-item> </el-form-item>
</el-form> </el-form>
</ContentWrap> </ContentWrap>
<ContentWrap> <ContentWrap>
<el-tabs v-model="type" @tab-change="handleTabChange"> <el-tabs v-model="type" @tab-change="onTabChange">
<!-- tab 1图片 --> <!-- tab 1图片 -->
<el-tab-pane name="image"> <el-tab-pane name="image">
<template #label> <template #label>
@ -93,7 +93,7 @@
<el-table-column label="文件名" align="center" prop="name" /> <el-table-column label="文件名" align="center" prop="name" />
<el-table-column label="语音" align="center"> <el-table-column label="语音" align="center">
<template #default="scope"> <template #default="scope">
<WxVoicePlayer :url="scope.row.url" /> <WxVoicePlayer v-if="scope.row.url" :url="scope.row.url" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
@ -188,10 +188,8 @@
</el-row> </el-row>
</el-form> </el-form>
<template #footer> <template #footer>
<!-- <span class="dialog-footer"> -->
<el-button @click="cancelVideo"> </el-button> <el-button @click="cancelVideo"> </el-button>
<el-button type="primary" @click="submitVideo"> </el-button> <el-button type="primary" @click="submitVideo"> </el-button>
<!-- </span> -->
</template> </template>
</el-dialog> </el-dialog>
@ -203,7 +201,7 @@
<el-table-column label="介绍" align="center" prop="introduction" /> <el-table-column label="介绍" align="center" prop="introduction" />
<el-table-column label="视频" align="center"> <el-table-column label="视频" align="center">
<template #default="scope"> <template #default="scope">
<WxVideoPlayer :url="scope.row.url" /> <WxVideoPlayer v-if="scope.row.url" :url="scope.row.url" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
@ -284,9 +282,9 @@ const type = ref<MatertialType>('image')
// //
const loading = ref(false) const loading = ref(false)
// //
const total = ref(0)
// //
const list = ref([]) const list = ref<any[]>([])
const total = ref(0)
// //
interface QueryParams { interface QueryParams {
pageNo: number pageNo: number
@ -319,8 +317,8 @@ const dialogVideoVisible = ref(false)
const addMaterialLoading = ref(false) const addMaterialLoading = ref(false)
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const accountChange = (accountId: number | undefined) => { const onAccountChanged = (id?: number) => {
queryParams.accountId = accountId queryParams.accountId = id
getList() getList()
} }
@ -346,7 +344,7 @@ const handleQuery = () => {
getList() getList()
} }
const handleTabChange = (tabName: TabPaneName) => { const onTabChange = (tabName: TabPaneName) => {
// type // type
uploadData.type = tabName as MatertialType uploadData.type = tabName as MatertialType
@ -359,54 +357,49 @@ const handleTabChange = (tabName: TabPaneName) => {
} }
// ======================== ======================== // ======================== ========================
const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => { const beforeUpload = (rawFile: UploadRawFile, category: 'image' | 'audio' | 'video'): boolean => {
const isType = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'].includes( let allowTypes: string[] = []
rawFile.type let maxSizeMB = 0
) let name = ''
if (!isType) {
message.error('上传图片格式不对!') 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 return false
} }
const isLt = rawFile.size / 1024 / 1024 < 2 //
if (!isLt) { if (rawFile.size / 1024 / 1024 > maxSizeMB) {
message.error('上传图片大小不能超过 2M!') message.error(`上传${name}大小不能超过${maxSizeMB}M!`)
return false return false
} }
loading.value = true loading.value = true
return true return true
} }
const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => { const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
const isType = ['audio/mp3', 'audio/wma', 'audio/wav', 'audio/amr'].includes(file.type) beforeUpload(rawFile, 'image')
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 beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => { const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
const isType = rawFile.type === 'video/mp4' beforeUpload(rawFile, 'audio')
if (!isType) {
message.error('上传视频格式不对!')
return false
}
const isLt = rawFile.size / 1024 / 1024 < 10 const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
if (!isLt) { beforeUpload(rawFile, 'video')
message.error('上传视频大小不能超过 10M!')
return false
}
addMaterialLoading.value = true
return true
}
const handleUploadSuccess: UploadProps['onSuccess'] = (response: any) => { const handleUploadSuccess: UploadProps['onSuccess'] = (response: any) => {
loading.value = false loading.value = false
@ -441,6 +434,7 @@ const submitVideo = () => {
}) })
} }
// video
const handleAddVideo = () => { const handleAddVideo = () => {
resetVideo() resetVideo()
dialogVideoVisible.value = true dialogVideoVisible.value = true

View File

@ -2,8 +2,11 @@
<doc-alert title="公众号菜单" url="https://doc.iocoder.cn/mp/menu/" /> <doc-alert title="公众号菜单" url="https://doc.iocoder.cn/mp/menu/" />
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<ContentWrap> <ContentWrap>
<!-- TODO @芋艿调整成 el-form WxAccountSelect --> <el-form class="-mb-15px" ref="queryFormRef" :inline="true" label-width="68px">
<WxAccountSelect @change="accountChanged" /> <el-form-item label="公众号" prop="accountId">
<WxMpSelect @change="onAccountChanged" />
</el-form-item>
</el-form>
</ContentWrap> </ContentWrap>
<!-- 列表 --> <!-- 列表 -->
@ -12,7 +15,7 @@
<!--左边配置菜单--> <!--左边配置菜单-->
<div class="left"> <div class="left">
<div class="weixin-hd"> <div class="weixin-hd">
<div class="weixin-title">{{ name }}</div> <div class="weixin-title">{{ accountName }}</div>
</div> </div>
<div class="weixin-menu menu_main clearfix"> <div class="weixin-menu menu_main clearfix">
<div class="menu_bottom" v-for="(item, i) of menuList" :key="i"> <div class="menu_bottom" v-for="(item, i) of menuList" :key="i">
@ -68,7 +71,7 @@
<div v-if="showRightFlag" class="right"> <div v-if="showRightFlag" class="right">
<div class="configure_page"> <div class="configure_page">
<div class="delete_btn"> <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" /> 删除当前菜单<Icon icon="ep:delete" />
</el-button> </el-button>
</div> </div>
@ -155,7 +158,7 @@
<div v-else> <div v-else>
<el-row justify="center"> <el-row justify="center">
<el-col :span="24" style="text-align: 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" /> 素材库选择<Icon icon="ep:circle-check" />
</el-button> </el-button>
</el-col> </el-col>
@ -185,24 +188,26 @@
</div> </div>
</ContentWrap> </ContentWrap>
</template> </template>
<script setup name="MpMenu"> <script lang="ts" setup name="MpMenu">
import { handleTree } from '@/utils/tree'
import WxReplySelect from '@/views/mp/components/wx-reply/main.vue' import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
import WxNews from '@/views/mp/components/wx-news/main.vue' import WxNews from '@/views/mp/components/wx-news/main.vue'
import WxMaterialSelect from '@/views/mp/components/wx-material-select/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 * as MpMenuApi from '@/api/mp/menu'
import { handleTree } from '@/utils/tree'
import menuOptions from './menuOptions' import menuOptions from './menuOptions'
const message = useMessage() // const message = useMessage() //
// ======================== ======================== // ======================== ========================
const loading = ref(true) // const loading = ref(false) //
const accountId = ref(undefined) // Id const accountId = ref<number | undefined>()
const menuList = ref({ children: [] }) const accountName = ref<string | undefined>('')
const menuList = ref<any>({ children: [] })
// ======================== ======================== // ======================== ========================
const isActive = ref(-1) // const isActive = ref(-1) //
const isSubMenuActive = ref(-1) // const isSubMenuActive = ref<string | number>(-1) //
const isSubMenuFlag = ref(-1) // const isSubMenuFlag = ref(-1) //
// ======================== ======================== // ======================== ========================
@ -210,15 +215,16 @@ const showRightFlag = ref(false) // 右边配置显示默认详情还是配置
const nameMaxLength = ref(0) // 1 4 2 7 const nameMaxLength = ref(0) // 1 4 2 7
const showConfigureContent = ref(true) // const showConfigureContent = ref(true) //
const hackResetWxReplySelect = ref(false) // WxReplySelect const hackResetWxReplySelect = ref(false) // WxReplySelect
const tempObj = ref({}) // const tempObj = ref<any>({}) //
// tempObjmenu // tempObjmenu
const tempSelfObj = ref({}) const tempSelfObj = ref<any>({})
const dialogNewsVisible = ref(false) // const dialogNewsVisible = ref(false) //
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const accountChanged = (id) => { const onAccountChanged = (id?: number, name?: string) => {
accountId.value = id accountId.value = id
accountName.value = name
getList() getList()
} }
@ -241,10 +247,10 @@ const handleQuery = () => {
} }
// menuList menuList // menuList menuList
const convertMenuList = (list) => { const convertMenuList = (list: any[]) => {
if (!list) return [] if (!list) return []
const menuList = [] const result: any[] = []
list.forEach((item) => { list.forEach((item) => {
const menu = { const menu = {
...item ...item
@ -271,9 +277,9 @@ const convertMenuList = (list) => {
hqMusicUrl: item.replyHqMusicUrl hqMusicUrl: item.replyHqMusicUrl
} }
} }
menuList.push(menu) result.push(menu)
}) })
return menuList return result
} }
// //
@ -286,7 +292,7 @@ const resetForm = () => {
// //
showRightFlag.value = false showRightFlag.value = false
nameMaxLength.value = 0 nameMaxLength.value = 0
showConfigureContent.value = 0 showConfigureContent.value = false
hackResetWxReplySelect.value = false hackResetWxReplySelect.value = false
tempObj.value = {} tempObj.value = {}
tempSelfObj.value = {} tempSelfObj.value = {}
@ -295,7 +301,7 @@ const resetForm = () => {
// ======================== ======================== // ======================== ========================
// //
const menuClick = (i, item) => { const menuClick = (i: number, item: any) => {
// //
resetEditor() resetEditor()
showRightFlag.value = true // 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() resetEditor()
showRightFlag.value = true // showRightFlag.value = true //
console.log(subItem)
tempObj.value = subItem // tempObj.value = subItem //
tempSelfObj.value.grand = '2' // tempSelfObj.value.grand = '2' //
tempSelfObj.value.index = index // tempSelfObj.value.index = index //
@ -331,7 +336,7 @@ const subMenuClick = (subItem, index, k) => {
// //
const addMenu = () => { const addMenu = () => {
const menuKeyLength = menuList.value.length const menuKeyLength: number = menuList.value.length
const addButton = { const addButton = {
name: '菜单名称', name: '菜单名称',
children: [], children: [],
@ -342,10 +347,10 @@ const addMenu = () => {
} }
} }
menuList.value[menuKeyLength] = addButton menuList.value[menuKeyLength] = addButton
menuClick(menuKeyLength.value - 1, addButton) menuClick(menuKeyLength - 1, addButton)
} }
// item // item
const addSubMenu = (i, item) => { const addSubMenu = (i: number, item: any) => {
// name // name
if (!item.children || item.children.length <= 0) { if (!item.children || item.children.length <= 0) {
item.children = [] item.children = []
@ -361,8 +366,8 @@ const addSubMenu = (i, item) => {
showConfigureContent.value = false showConfigureContent.value = false
} }
let subMenuKeyLength = item.children.length // key const subMenuKeyLength = item.children.length // key
let addButton = { const addButton = {
name: '子菜单名称', name: '子菜单名称',
reply: { reply: {
// //
@ -399,7 +404,7 @@ const handleDeleteMenu = async () => {
// ======================== ======================== // ======================== ========================
const handleSave = async () => { const handleSave = async () => {
try { try {
await message.confirm('确定要删除吗?') await message.confirm('确定要保存吗?')
loading.value = true loading.value = true
await MpMenuApi.saveMenu(accountId.value, convertMenuFormList()) await MpMenuApi.saveMenu(accountId.value, convertMenuFormList())
getList() getList()
@ -413,7 +418,6 @@ const handleSave = async () => {
const resetEditor = () => { const resetEditor = () => {
hackResetWxReplySelect.value = false // hackResetWxReplySelect.value = false //
nextTick(() => { nextTick(() => {
console.log('nextTick')
hackResetWxReplySelect.value = true // hackResetWxReplySelect.value = true //
}) })
} }
@ -432,9 +436,9 @@ const handleDelete = async () => {
// menuList menuList // menuList menuList
const convertMenuFormList = () => { const convertMenuFormList = () => {
const result = [] const result: any[] = []
menuList.value.forEach((item) => { menuList.value.forEach((item) => {
let menu = convertMenuForm(item) const menu = convertMenuForm(item)
result.push(menu) result.push(menu)
// //
@ -450,7 +454,7 @@ const convertMenuFormList = () => {
} }
// menu menu // menu menu
const convertMenuForm = (menu) => { const convertMenuForm = (menu: any) => {
let result = { let result = {
...menu, ...menu,
children: undefined, // children: undefined, //
@ -473,11 +477,7 @@ const convertMenuForm = (menu) => {
} }
// ======================== ======================== // ======================== ========================
const openMaterial = () => { const selectMaterial = (item: any) => {
dialogNewsVisible.value = true
}
const selectMaterial = (item) => {
const articleId = item.articleId const articleId = item.articleId
const articles = item.content.newsItem const articles = item.content.newsItem
// //

View File

@ -9,14 +9,7 @@
label-width="68px" label-width="68px"
> >
<el-form-item label="公众号" prop="accountId"> <el-form-item label="公众号" prop="accountId">
<el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px"> <WxMpSelect @change="onAccountChanged" />
<el-option
v-for="item in accountList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="消息类型" prop="type"> <el-form-item label="消息类型" prop="type">
<el-select v-model="queryParams.type" placeholder="请选择消息类型" class="!w-240px"> <el-select v-model="queryParams.type" placeholder="请选择消息类型" class="!w-240px">
@ -84,70 +77,76 @@
<el-table-column label="内容" prop="content"> <el-table-column label="内容" prop="content">
<template #default="scope"> <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> <el-tag type="success">关注</el-tag>
</div> </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> <el-tag type="danger">取消关注</el-tag>
</div> </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> <el-tag>点击菜单</el-tag>
{{ scope.row.eventKey }} {{ scope.row.eventKey }}
</div> </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> <el-tag>点击菜单链接</el-tag>
{{ scope.row.eventKey }} {{ scope.row.eventKey }}
</div> </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> <el-tag>扫码结果</el-tag>
{{ scope.row.eventKey }} {{ scope.row.eventKey }}
</div> </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> <el-tag>扫码结果</el-tag>
{{ scope.row.eventKey }} {{ scope.row.eventKey }}
</div> </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> <el-tag>系统拍照发图</el-tag>
</div> </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> <el-tag>拍照或者相册</el-tag>
</div> </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> <el-tag>微信相册</el-tag>
</div> </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> <el-tag>选择地理位置</el-tag>
</div> </div>
<div v-else-if="scope.row.type === 'event'"> <div v-else-if="scope.row.type === MsgType.Event">
<el-tag type="danger">未知事件类型</el-tag> <el-tag type="danger">未知事件类型</el-tag>
</div> </div>
<!-- 消息区域 --> <!-- 消息区域 -->
<div v-else-if="scope.row.type === 'text'">{{ scope.row.content }}</div> <div v-else-if="scope.row.type === MsgType.Text">{{ scope.row.content }}</div>
<div v-else-if="scope.row.type === 'voice'"> <div v-else-if="scope.row.type === MsgType.Voice">
<wx-voice-player :url="scope.row.mediaUrl" :content="scope.row.recognition" /> <wx-voice-player :url="scope.row.mediaUrl" :content="scope.row.recognition" />
</div> </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"> <a target="_blank" :href="scope.row.mediaUrl">
<img :src="scope.row.mediaUrl" style="width: 100px" /> <img :src="scope.row.mediaUrl" style="width: 100px" />
</a> </a>
</div> </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" /> <wx-video-player :url="scope.row.mediaUrl" style="margin-top: 10px" />
</div> </div>
<div v-else-if="scope.row.type === 'link'"> <div v-else-if="scope.row.type === MsgType.Link">
<el-tag>链接</el-tag> <el-tag>链接</el-tag>
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a> <a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
</div> </div>
<div v-else-if="scope.row.type === 'location'"> <div v-else-if="scope.row.type === MsgType.Location">
<wx-location <WxLocation
:label="scope.row.label" :label="scope.row.label"
:location-y="scope.row.locationY" :location-y="scope.row.locationY"
:location-x="scope.row.locationX" :location-x="scope.row.locationX"
/> />
</div> </div>
<div v-else-if="scope.row.type === 'music'"> <div v-else-if="scope.row.type === MsgType.Music">
<wx-music <WxMusic
:title="scope.row.title" :title="scope.row.title"
:description="scope.row.description" :description="scope.row.description"
:thumb-media-url="scope.row.thumbMediaUrl" :thumb-media-url="scope.row.thumbMediaUrl"
@ -155,8 +154,8 @@
:hq-music-url="scope.row.hqMusicUrl" :hq-music-url="scope.row.hqMusicUrl"
/> />
</div> </div>
<div v-else-if="scope.row.type === 'news'"> <div v-else-if="scope.row.type === MsgType.News">
<wx-news :articles="scope.row.articles" /> <WxNews :articles="scope.row.articles" />
</div> </div>
<div v-else> <div v-else>
<el-tag type="danger">未知消息类型</el-tag> <el-tag type="danger">未知消息类型</el-tag>
@ -177,7 +176,7 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 分页组件 --> <!-- 分页组件 -->
<pagination <Pagination
v-show="total > 0" v-show="total > 0"
:total="total" :total="total"
v-model:page="queryParams.pageNo" 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> <template #footer>
<wx-msg :user-id="userId" v-if="open" /> <WxMsg :user-id="userId" v-if="showMessageBox" />
</template> </template>
</el-dialog> </el-dialog>
</ContentWrap> </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 WxLocation from '@/views/mp/components/wx-location/main.vue'
import WxMusic from '@/views/mp/components/wx-music/main.vue' import WxMusic from '@/views/mp/components/wx-music/main.vue'
import WxNews from '@/views/mp/components/wx-news/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' import * as MpMessageApi from '@/api/mp/message'
const message = useMessage() //
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime' 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 loading = ref(true) //
const total = ref(0) // const total = ref(0) //
const list = ref([]) // const list = ref<any[]>([]) //
const queryParams = reactive({
//
interface QueryParams {
pageNo: number
pageSize: number
openid: string | null
accountId: number | null
type: MsgType | null
createTime: string[] | []
}
const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
openid: null, openid: null,
@ -218,19 +232,18 @@ const queryParams = reactive({
type: null, type: null,
createTime: [] createTime: []
}) })
const queryFormRef = ref() // const queryFormRef = ref<FormInstance | null>(null) //
// TODO const showMessageBox = ref(false) //
const open = ref(false) //
const userId = ref(0) // const userId = ref(0) //
const accountList = ref<MpAccountApi.AccountVO[]>([]) //
/** 侦听accountId */
const onAccountChanged = (id?: number) => {
queryParams.accountId = id as number
handleQuery()
}
/** 查询列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
//
if (!queryParams.accountId) {
await message.error('未选中公众号,无法查询消息')
return
}
try { try {
loading.value = true loading.value = true
const data = await MpMessageApi.getMessagePage(queryParams) const data = await MpMessageApi.getMessagePage(queryParams)
@ -249,34 +262,15 @@ const handleQuery = () => {
/** 重置按钮操作 */ /** 重置按钮操作 */
const resetQuery = async () => { const resetQuery = async () => {
queryFormRef.value.resetFields() const accountId = queryParams.accountId
// queryFormRef.value?.resetFields()
if (accountList.value.length > 0) { queryParams.accountId = accountId
// @ts-ignore
queryParams.accountId = accountList.value[0].id
}
handleQuery() handleQuery()
} }
const handleSend = async (row) => {
/** 打开消息发送窗口 */
const handleSend = async (row: any) => {
userId.value = row.userId 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> </script>

View File

@ -19,24 +19,30 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import * as MpTagApi from '@/api/mp/tag' import * as MpTagApi from '@/api/mp/tag'
import type { FormInstance, FormRules } from 'element-plus'
const { t } = useI18n() // const { t } = useI18n() //
const message = useMessage() // const message = useMessage() //
const dialogVisible = ref(false) // const dialogVisible = ref(false) //
const dialogTitle = ref('') // const dialogTitle = ref('') //
const formLoading = ref(false) // 12 const formLoading = ref(false) // 12
const formType = ref('') // create - update - const formType = ref<'create' | 'update' | ''>('') // create - update -
const formData = ref({ const formData = ref({
accountId: -1, accountId: -1,
name: '' name: ''
}) })
const formRules = reactive({ const formRules: FormRules = {
name: [{ required: true, message: '请输入标签名称', trigger: 'blur' }] 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 dialogVisible.value = true
dialogTitle.value = t('action.' + type) dialogTitle.value = t('action.' + type)
formType.value = type formType.value = type
@ -55,11 +61,10 @@ const open = async (type: string, accountId: number, id?: number) => {
defineExpose({ open }) // open defineExpose({ open }) // open
/** 提交表单 */ /** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => { const submitForm = async () => {
// //
if (!formRef) return if (!formRef) return
const valid = await formRef.value.validate() const valid = await formRef.value?.validate()
if (!valid) return if (!valid) return
// //
formLoading.value = true formLoading.value = true

View File

@ -3,17 +3,25 @@
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<ContentWrap> <ContentWrap>
<!-- TODO @芋艿调整成 el-form WxAccountSelect --> <el-form
<WxAccountSelect @change="accountChanged"> class="-mb-15px"
<template #actions> :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']"> <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['mp:tag:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增 <Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button> </el-button>
<el-button type="success" plain @click="handleSync" v-hasPermi="['mp:tag:sync']"> <el-button type="success" plain @click="handleSync" v-hasPermi="['mp:tag:sync']">
<Icon icon="ep:refresh" class="mr-5px" /> 同步 <Icon icon="ep:refresh" class="mr-5px" /> 同步
</el-button> </el-button>
</template> </el-form-item>
</WxAccountSelect> </el-form>
</ContentWrap> </ContentWrap>
<!-- 列表 --> <!-- 列表 -->
@ -63,26 +71,35 @@
<TagForm ref="formRef" @success="getList" /> <TagForm ref="formRef" @success="getList" />
</template> </template>
<script setup lang="ts" name="MpTag"> <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 * as MpTagApi from '@/api/mp/tag'
import TagForm from './TagForm.vue' import TagForm from './TagForm.vue'
import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
import { dateFormatter } from '@/utils/formatTime'
const message = useMessage() // const message = useMessage() //
const { t } = useI18n() // const { t } = useI18n() //
const loading = ref(true) // const loading = ref(true) //
const total = ref(0) // 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, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: undefined accountId: undefined
}) })
const formRef = ref<InstanceType<typeof TagForm> | null>(null)
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const accountChanged = (accountId) => { const onAccountChanged = (id?: number) => {
queryParams.pageNo = 1 queryParams.pageNo = 1
queryParams.accountId = accountId queryParams.accountId = id
getList() getList()
} }
@ -99,9 +116,8 @@ const getList = async () => {
} }
/** 添加/修改操作 */ /** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => { 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 () => { const handleSync = async () => {
try { try {
await message.confirm('是否确认同步标签?') await message.confirm('是否确认同步标签?')
// @ts-ignore await MpTagApi.syncTag(queryParams.accountId as number)
await MpTagApi.syncTag(queryParams.accountId)
message.success('同步标签成功') message.success('同步标签成功')
await getList() await getList()
} catch {} } catch {}

View File

@ -3,14 +3,42 @@
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<ContentWrap> <ContentWrap>
<!-- TODO @芋艿调整成 el-form WxAccountSelect --> <el-form
<WxAccountSelect @change="(accountId) => accountChanged(accountId)"> class="-mb-15px"
<template #actions> :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']"> <el-button type="success" plain @click="handleSync" v-hasPermi="['mp:user:sync']">
<Icon icon="ep:refresh" class="mr-5px" /> 同步 <Icon icon="ep:refresh" class="mr-5px" /> 同步
</el-button> </el-button>
</template> </el-form-item>
</WxAccountSelect> </el-form>
</ContentWrap> </ContentWrap>
<!-- 列表 --> <!-- 列表 -->
@ -66,25 +94,37 @@
<UserForm ref="formRef" @success="getList" /> <UserForm ref="formRef" @success="getList" />
</template> </template>
<script lang="ts" setup name="MpUser"> <script lang="ts" setup name="MpUser">
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import * as MpUserApi from '@/api/mp/user' import * as MpUserApi from '@/api/mp/user'
import * as MpTagApi from '@/api/mp/tag' import * as MpTagApi from '@/api/mp/tag'
import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
import type { FormInstance } from 'element-plus'
import UserForm from './UserForm.vue' import UserForm from './UserForm.vue'
const message = useMessage() // const message = useMessage() //
const loading = ref(true) // const loading = ref(true) //
const total = ref(0) // const total = ref(0) //
const list = ref([]) // const list = ref<any[]>([]) //
const queryParams = reactive({
interface QueryParams {
pageNo: number
pageSize: number
accountId?: number
openid: string | null
nickname: string | null
}
const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: null, accountId: undefined,
openid: null, openid: null,
nickname: 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 () => { onMounted(async () => {
@ -92,9 +132,9 @@ onMounted(async () => {
}) })
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const accountChanged = (accountId) => { const onAccountChanged = (id?: number) => {
queryParams.pageNo = 1 queryParams.pageNo = 1
queryParams.accountId = accountId queryParams.accountId = id
getList() 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) => { const openForm = (id: number) => {
formRef.value.open(id) formRef.value?.open(id)
} }
/** 同步标签 */ /** 同步标签 */
const handleSync = async () => { const handleSync = async () => {
const accountId = queryParams.accountId
try { try {
await message.confirm('是否确认同步粉丝?') await message.confirm('是否确认同步粉丝?')
await MpUserApi.syncUser(accountId) await MpUserApi.syncUser(queryParams.accountId)
message.success('开始从微信公众号同步粉丝信息,同步需要一段时间,建议稍后再查询') message.success('开始从微信公众号同步粉丝信息,同步需要一段时间,建议稍后再查询')
await getList() getList()
} catch {} } catch {}
} }
</script> </script>

View File

@ -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) // 12
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>

View File

@ -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) // 12
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>