From 1134921a0ab6364dec2366999f54d67d729cfc04 Mon Sep 17 00:00:00 2001 From: wuxiran <wuxiran@outlook.com> Date: Sun, 26 Mar 2023 04:25:34 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E5=BE=AE=E4=BF=A1=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E6=9B=B4=E6=96=B0vue3=EF=BC=8C=E9=83=A8=E5=88=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=8F=AF=E8=83=BD=E8=BF=98=E6=9C=89=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/dict.ts | 23 +- src/utils/formatTime.ts | 57 +- src/views/mp/components/img.png | Bin 0 -> 15404 bytes src/views/mp/components/wx-location/main.vue | 72 ++ src/views/mp/components/wx-msg/card.scss | 101 +++ src/views/mp/components/wx-msg/comment.scss | 88 +++ src/views/mp/components/wx-msg/main.vue | 338 ++++++++++ src/views/mp/components/wx-music/main.vue | 60 ++ src/views/mp/components/wx-news/main.vue | 107 +++ src/views/mp/components/wx-reply/main.vue | 634 ++++++++++++++++++ .../mp/components/wx-video-play/main.vue | 117 ++++ .../mp/components/wx-voice-play/main.vue | 100 +++ src/views/mp/freePublish/index.vue | 394 ++++++++++- src/views/mp/message/index.vue | 261 ++++++- 14 files changed, 2348 insertions(+), 4 deletions(-) create mode 100644 src/views/mp/components/img.png create mode 100644 src/views/mp/components/wx-location/main.vue create mode 100644 src/views/mp/components/wx-msg/card.scss create mode 100644 src/views/mp/components/wx-msg/comment.scss create mode 100644 src/views/mp/components/wx-msg/main.vue create mode 100644 src/views/mp/components/wx-music/main.vue create mode 100644 src/views/mp/components/wx-news/main.vue create mode 100644 src/views/mp/components/wx-reply/main.vue create mode 100644 src/views/mp/components/wx-video-play/main.vue create mode 100644 src/views/mp/components/wx-voice-play/main.vue diff --git a/src/utils/dict.ts b/src/utils/dict.ts index 15e57ff2..05c70dad 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -70,6 +70,23 @@ export const getDictObj = (dictType: string, value: any) => { }) } +/** + * 获得字典数据的文本展示 + * + * @param dictType 字典类型 + * @param value 字典数据的值 + */ +export const getDictLabel = (dictType: string, value: any) => { + const dictOptions: DictDataType[] = getDictOptions(dictType) + const dictLabel = ref('') + dictOptions.forEach((dict: DictDataType) => { + if (dict.value === value) { + dictLabel.value = dict.label + } + }) + return dictLabel.value +} + export enum DICT_TYPE { USER_TYPE = 'user_type', COMMON_STATUS = 'common_status', @@ -123,5 +140,9 @@ export enum DICT_TYPE { PAY_ORDER_STATUS = 'pay_order_status', // 商户支付订单状态 PAY_ORDER_REFUND_STATUS = 'pay_order_refund_status', // 商户支付订单退款状态 PAY_REFUND_ORDER_STATUS = 'pay_refund_order_status', // 退款订单状态 - PAY_REFUND_ORDER_TYPE = 'pay_refund_order_type' // 退款订单类别 + PAY_REFUND_ORDER_TYPE = 'pay_refund_order_type', // 退款订单类别 + + // ========== MP 模块 ========== + MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型 + MP_MESSAGE_TYPE = 'mp_message_type' // 消息类型 } diff --git a/src/utils/formatTime.ts b/src/utils/formatTime.ts index 2582beee..ec7f3744 100644 --- a/src/utils/formatTime.ts +++ b/src/utils/formatTime.ts @@ -11,10 +11,65 @@ import dayjs from 'dayjs' * @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ" * @returns 返回拼接后的时间字符串 */ -export function formatDate(date: Date, format: string): string { +export function formatDate(date: Date, format?: string): string { + // 日期不存在,则返回空 + if (!date) { + return '' + } + // 日期存在,则进行格式化 + if (format === undefined) { + format = 'YYYY-MM-DD HH:mm:ss' + } return dayjs(date).format(format) } +// TODO 芋艿:稍后去掉 +// 日期格式化 +export function parseTime(time: any, pattern?: string) { + if (arguments.length === 0 || !time) { + return null + } + const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}' + let date + if (typeof time === 'object') { + date = time + } else { + if (typeof time === 'string' && /^[0-9]+$/.test(time)) { + time = parseInt(time) + } else if (typeof time === 'string') { + time = time + .replace(new RegExp(/-/gm), '/') + .replace('T', ' ') + .replace(new RegExp(/\.\d{3}/gm), '') + } + if (typeof time === 'number' && time.toString().length === 10) { + time = time * 1000 + } + date = new Date(time) + } + const formatObj = { + y: date.getFullYear(), + m: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + i: date.getMinutes(), + s: date.getSeconds(), + a: date.getDay() + } + const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => { + let value = formatObj[key] + // Note: getDay() returns 0 on Sunday + if (key === 'a') { + return ['日', '一', '二', '三', '四', '五', '六'][value] + } + if (result.length > 0 && value < 10) { + value = '0' + value + } + return value || 0 + }) + return time_str +} + /** * 获取当前日期是第几周 * @param dateTime 当前传入的日期值 diff --git a/src/views/mp/components/img.png b/src/views/mp/components/img.png new file mode 100644 index 0000000000000000000000000000000000000000..c25a6e762f3a84c5a7f05c6383de843937725fcf GIT binary patch literal 15404 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 D*J2vT literal 0 HcmV?d00001 diff --git a/src/views/mp/components/wx-location/main.vue b/src/views/mp/components/wx-location/main.vue new file mode 100644 index 00000000..c0d67e29 --- /dev/null +++ b/src/views/mp/components/wx-location/main.vue @@ -0,0 +1,72 @@ +<!-- + 【微信消息 - 定位】 +--> +<template> + <div> + <el-link + type="primary" + target="_blank" + :href=" + 'https://map.qq.com/?type=marker&isopeninfowin=1&markertype=1&pointx=' + + locationY + + '&pointy=' + + locationX + + '&name=' + + label + + '&ref=yudao' + " + > + <el-col> + <el-row> + <img + :src=" + 'https://apis.map.qq.com/ws/staticmap/v2/?zoom=10&markers=color:blue|label:A|' + + locationX + + ',' + + locationY + + '&key=' + + qqMapKey + + '&size=250*180' + " + /> + </el-row> + <el-row> + <el-icon><Location /></el-icon>{{ label }} + </el-row> + </el-col> + </el-link> + </div> +</template> + +<script setup lang="ts" name="WxLocation"> +import { Location } from '@element-plus/icons-vue' + +const props = defineProps({ + locationX: { + required: true, + type: Number + }, + locationY: { + required: true, + type: Number + }, + label: { + // 地名 + required: true, + type: String + }, + qqMapKey: { + // QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc + required: false, + type: String, + default: 'TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E' // 需要自定义 + } +}) + +defineExpose({ + locationX: props.locationX, + locationY: props.locationY, + label: props.label, + qqMapKey: props.qqMapKey +}) +</script> diff --git a/src/views/mp/components/wx-msg/card.scss b/src/views/mp/components/wx-msg/card.scss new file mode 100644 index 00000000..67ac9219 --- /dev/null +++ b/src/views/mp/components/wx-msg/card.scss @@ -0,0 +1,101 @@ +.avue-card{ + &__item{ + margin-bottom: 16px; + border: 1px solid #e8e8e8; + background-color: #fff; + box-sizing: border-box; + color: rgba(0,0,0,.65); + font-size: 14px; + font-variant: tabular-nums; + line-height: 1.5; + list-style: none; + font-feature-settings: "tnum"; + cursor: pointer; + height:200px; + &:hover{ + border-color: rgba(0,0,0,.09); + box-shadow: 0 2px 8px rgba(0,0,0,.09); + } + &--add{ + border:1px dashed #000; + width: 100%; + color: rgba(0,0,0,.45); + background-color: #fff; + border-color: #d9d9d9; + border-radius: 2px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + i{ + margin-right: 10px; + } + &:hover{ + color: #40a9ff; + background-color: #fff; + border-color: #40a9ff; + } + } + } + &__body{ + display: flex; + padding: 24px; + } + &__detail{ + flex:1 + } + &__avatar{ + width: 48px; + height: 48px; + border-radius: 48px; + overflow: hidden; + margin-right: 12px; + img{ + width: 100%; + height: 100%; + } + } + &__title{ + color: rgba(0,0,0,.85); + margin-bottom: 12px; + font-size: 16px; + &:hover{ + color:#1890ff; + } + } + &__info{ + color: rgba(0,0,0,.45); + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; + height: 64px; + } + &__menu{ + display: flex; + justify-content:space-around; + height: 50px; + background: #f7f9fa; + color: rgba(0,0,0,.45); + text-align: center; + line-height: 50px; + &:hover{ + color:#1890ff; + } + } +} + +/** joolun 额外加的 */ +.avue-comment__main { + flex: unset!important; + border-radius: 5px!important; + margin: 0 8px!important; +} +.avue-comment__header { + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} +.avue-comment__body { + border-bottom-right-radius: 5px; + border-bottom-left-radius: 5px; +} diff --git a/src/views/mp/components/wx-msg/comment.scss b/src/views/mp/components/wx-msg/comment.scss new file mode 100644 index 00000000..3f1341b2 --- /dev/null +++ b/src/views/mp/components/wx-msg/comment.scss @@ -0,0 +1,88 @@ +/* 来自 https://github.com/nmxiaowei/avue/blob/master/styles/src/element-ui/comment.scss */ +.avue-comment{ + margin-bottom: 30px; + display: flex; + align-items: flex-start; + &--reverse{ + flex-direction:row-reverse; + .avue-comment__main{ + &:before,&:after{ + left: auto; + right: -8px; + border-width: 8px 0 8px 8px; + } + &:before{ + border-left-color: #dedede; + } + &:after{ + border-left-color: #f8f8f8; + margin-right: 1px; + margin-left: auto; + } + } + } + &__avatar{ + width: 48px; + height: 48px; + border-radius: 50%; + border: 1px solid transparent; + box-sizing: border-box; + vertical-align: middle; + } + &__header{ + padding: 5px 15px; + background: #f8f8f8; + border-bottom: 1px solid #eee; + display: flex; + align-items: center; + justify-content: space-between; + } + &__author{ + font-weight: 700; + font-size: 14px; + color: #999; + } + &__main{ + flex:1; + margin: 0 20px; + position: relative; + border: 1px solid #dedede; + border-radius: 2px; + &:before,&:after{ + position: absolute; + top: 10px; + left: -8px; + right: 100%; + width: 0; + height: 0; + display: block; + content: " "; + border-color: transparent; + border-style: solid solid outset; + border-width: 8px 8px 8px 0; + pointer-events: none; + } + &:before { + border-right-color: #dedede; + z-index: 1; + } + &:after{ + border-right-color: #f8f8f8; + margin-left: 1px; + z-index: 2; + } + } + &__body{ + padding: 15px; + overflow: hidden; + background: #fff; + font-family: Segoe UI,Lucida Grande,Helvetica,Arial,Microsoft YaHei,FreeSans,Arimo,Droid Sans,wenquanyi micro hei,Hiragino Sans GB,Hiragino Sans GB W3,FontAwesome,sans-serif;color: #333; + font-size: 14px; + } + blockquote{ + margin:0; + font-family: Georgia,Times New Roman,Times,Kai,Kaiti SC,KaiTi,BiauKai,FontAwesome,serif; + padding: 1px 0 1px 15px; + border-left: 4px solid #ddd; + } +} diff --git a/src/views/mp/components/wx-msg/main.vue b/src/views/mp/components/wx-msg/main.vue new file mode 100644 index 00000000..b514a73e --- /dev/null +++ b/src/views/mp/components/wx-msg/main.vue @@ -0,0 +1,338 @@ +<!-- + - Copyright (C) 2018-2019 + - All rights reserved, Designed By www.joolun.com + 芋道源码: + ① 移除暂时用不到的 websocket + ② 代码优化,补充注释,提升阅读性 +--> +<template> + <div class="msg-main"> + <div class="msg-div" :id="'msg-div' + nowStr"> + <!-- 加载更多 --> + <div v-loading="loading"></div> + <div v-if="!loading"> + <div class="el-table__empty-block" v-if="loadMore" @click="loadingMore" + ><span class="el-table__empty-text">点击加载更多</span></div + > + <div class="el-table__empty-block" v-if="!loadMore" + ><span class="el-table__empty-text">没有更多了</span></div + > + </div> + <!-- 消息列表 --> + <div class="execution" v-for="item in list" :key="item.id"> + <div class="avue-comment" :class="item.sendFrom === 2 ? 'avue-comment--reverse' : ''"> + <div class="avatar-div"> + <img + :src="item.sendFrom === 1 ? user.avatar : mp.avatar" + class="avue-comment__avatar" + /> + <div class="avue-comment__author">{{ + item.sendFrom === 1 ? user.nickname : mp.nickname + }}</div> + </div> + <div class="avue-comment__main"> + <div class="avue-comment__header"> + <div class="avue-comment__create_time">{{ parseTime(item.createTime) }}</div> + </div> + <div + class="avue-comment__body" + :style="item.sendFrom === 2 ? 'background: #6BED72;' : ''" + > + <!-- 【事件】区域 --> + <div v-if="item.type === 'event' && item.event === 'subscribe'"> + <el-tag type="success" size="mini">关注</el-tag> + </div> + <div v-else-if="item.type === 'event' && item.event === 'unsubscribe'"> + <el-tag type="danger" size="mini">取消关注</el-tag> + </div> + <div v-else-if="item.type === 'event' && item.event === 'CLICK'"> + <el-tag size="mini">点击菜单</el-tag>【{{ item.eventKey }}】 + </div> + <div v-else-if="item.type === 'event' && item.event === 'VIEW'"> + <el-tag size="mini">点击菜单链接</el-tag>【{{ item.eventKey }}】 + </div> + <div v-else-if="item.type === 'event' && item.event === 'scancode_waitmsg'"> + <el-tag size="mini">扫码结果</el-tag>【{{ item.eventKey }}】 + </div> + <div v-else-if="item.type === 'event' && item.event === 'scancode_push'"> + <el-tag size="mini">扫码结果</el-tag>【{{ item.eventKey }}】 + </div> + <div v-else-if="item.type === 'event' && item.event === 'pic_sysphoto'"> + <el-tag size="mini">系统拍照发图</el-tag> + </div> + <div v-else-if="item.type === 'event' && item.event === 'pic_photo_or_album'"> + <el-tag size="mini">拍照或者相册</el-tag> + </div> + <div v-else-if="item.type === 'event' && item.event === 'pic_weixin'"> + <el-tag size="mini">微信相册</el-tag> + </div> + <div v-else-if="item.type === 'event' && item.event === 'location_select'"> + <el-tag size="mini">选择地理位置</el-tag> + </div> + <div v-else-if="item.type === 'event'"> + <el-tag type="danger" size="mini">未知事件类型</el-tag> + </div> + <!-- 【消息】区域 --> + <div v-else-if="item.type === 'text'">{{ item.content }}</div> + <div v-else-if="item.type === 'voice'"> + <wx-voice-player :url="item.mediaUrl" :content="item.recognition" /> + </div> + <div v-else-if="item.type === 'image'"> + <a target="_blank" :href="item.mediaUrl"> + <img :src="item.mediaUrl" style="width: 100px" /> + </a> + </div> + <div + v-else-if="item.type === 'video' || item.type === 'shortvideo'" + style="text-align: center" + > + <wx-video-player :url="item.mediaUrl" /> + </div> + <div v-else-if="item.type === 'link'" class="avue-card__detail"> + <el-link type="success" :underline="false" target="_blank" :href="item.url"> + <div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div> + </el-link> + <div class="avue-card__info" style="height: unset">{{ item.description }}</div> + </div> + <!-- TODO 芋艿:待完善 --> + <div v-else-if="item.type === 'location'"> + <wx-location + :label="item.label" + :location-y="item.locationY" + :location-x="item.locationX" + /> + </div> + <div v-else-if="item.type === 'news'" style="width: 300px"> + <!-- TODO 芋艿:待测试;详情页也存在类似的情况 --> + <wx-news :articles="item.articles" /> + </div> + <div v-else-if="item.type === 'music'"> + <wx-music + :title="item.title" + :description="item.description" + :thumb-media-url="item.thumbMediaUrl" + :music-url="item.musicUrl" + :hq-music-url="item.hqMusicUrl" + /> + </div> + </div> + </div> + </div> + </div> + </div> + <div class="msg-send" v-loading="sendLoading"> + <wx-reply-select ref="replySelect" :objData="objData" /> + <el-button type="success" size="small" class="send-but" @click="sendMsg">发送(S)</el-button> + </div> + </div> +</template> + +<script> +import { getMessagePage, sendMessage } from '@/api/mp/message' +import WxReplySelect from '@/views/mp/components/wx-reply/main.vue' +import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue' +import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue' +import WxNews from '@/views/mp/components/wx-news/main.vue' +import WxLocation from '@/views/mp/components/wx-location/main.vue' +import WxMusic from '@/views/mp/components/wx-music/main.vue' +import { getUser } from '@/api/mp/mpuser' + +export default { + name: 'WxMsg', + components: { + WxReplySelect, + WxVideoPlayer, + WxVoicePlayer, + WxNews, + WxLocation, + WxMusic + }, + props: { + userId: { + type: Number, + required: true + } + }, + data() { + return { + nowStr: new Date().getTime(), // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处 + loading: false, // 消息列表是否正在加载中 + loadMore: true, // 是否可以加载更多 + list: [], // 消息列表 + queryParams: { + pageNo: 1, // 当前页数 + pageSize: 14, // 每页显示多少条 + accountId: undefined + }, + user: { + // 由于微信不再提供昵称,直接使用“用户”展示 + nickname: '用户', + avatar: require('@/assets/images/profile.jpg'), + accountId: 0 // 公众号账号编号 + }, + mp: { + nickname: '公众号', + avatar: require('@/assets/images/wechat.png') + }, + + // ========= 消息发送 ========= + sendLoading: false, // 发送消息是否加载中 + objData: { + // 微信发送消息 + type: 'text' + } + } + }, + created() { + // 获得用户信息 + getUser(this.userId).then((response) => { + this.user.nickname = + response.data.nickname && response.data.nickname.length > 0 + ? response.data.nickname + : this.user.nickname + this.user.avatar = + response.data.avatar && this.user.avatar.length > 0 + ? response.data.avatar + : this.user.avatar + this.user.accountId = response.data.accountId + // 设置公众号账号编号 + this.queryParams.accountId = response.data.accountId + this.objData.accountId = response.data.accountId + + // 加载消息 + console.log(this.queryParams) + this.refreshChange() + }) + }, + methods: { + sendMsg() { + if (!this.objData) { + return + } + // 公众号限制:客服消息,公众号只允许发送一条 + if (this.objData.type === 'news' && this.objData.articles.length > 1) { + this.objData.articles = [this.objData.articles[0]] + this.$message({ + showClose: true, + message: '图文消息条数限制在 1 条以内,已默认发送第一条', + type: 'success' + }) + } + + // 执行发送 + this.sendLoading = true + sendMessage( + Object.assign( + { + userId: this.userId + }, + { + ...this.objData + } + ) + ) + .then((response) => { + this.sendLoading = false + // 添加到消息列表,并滚动 + this.list = [...this.list, ...[response.data]] + this.scrollToBottom() + // 重置 objData 状态 + this.$refs['replySelect'].deleteObj() // 重置,避免 tab 的数据未清理 + }) + .catch(() => { + this.sendLoading = false + }) + }, + loadingMore() { + this.queryParams.pageNo++ + this.getPage(this.queryParams) + }, + getPage(page, params) { + this.loading = true + getMessagePage( + Object.assign( + { + pageNo: page.pageNo, + pageSize: page.pageSize, + userId: this.userId, + accountId: page.accountId + }, + params + ) + ).then((response) => { + // 计算当前的滚动高度 + const msgDiv = document.getElementById('msg-div' + this.nowStr) + let scrollHeight = 0 + if (msgDiv) { + scrollHeight = msgDiv.scrollHeight + } + + // 处理数据 + const data = response.data.list.reverse() + this.list = [...data, ...this.list] + this.loading = false + if (data.length < this.queryParams.pageSize || data.length === 0) { + this.loadMore = false + } + this.queryParams.pageNo = page.pageNo + this.queryParams.pageSize = page.pageSize + + // 滚动到原来的位置 + if (this.queryParams.pageNo === 1) { + // 定位到消息底部 + this.scrollToBottom() + } else if (data.length !== 0) { + // 定位滚动条 + this.$nextTick(() => { + if (scrollHeight !== 0) { + msgDiv.scrollTop = + document.getElementById('msg-div' + this.nowStr).scrollHeight - scrollHeight - 100 + } + }) + } + }) + }, + /** + * 刷新回调 + */ + refreshChange() { + this.getPage(this.queryParams) + }, + /** 定位到消息底部 */ + scrollToBottom: function () { + this.$nextTick(() => { + let div = document.getElementById('msg-div' + this.nowStr) + div.scrollTop = div.scrollHeight + }) + } + } +} +</script> +<style lang="scss" scoped> +/* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc */ +@import './comment.scss'; +@import './card.scss'; + +.msg-main { + margin-top: -30px; + padding: 10px; +} +.msg-div { + height: 50vh; + overflow: auto; + background-color: #eaeaea; + margin-left: 10px; + margin-right: 10px; +} +.msg-send { + padding: 10px; +} +.avatar-div { + text-align: center; + width: 80px; +} +.send-but { + float: right; + margin-top: 8px !important; +} +</style> diff --git a/src/views/mp/components/wx-music/main.vue b/src/views/mp/components/wx-music/main.vue new file mode 100644 index 00000000..52555f15 --- /dev/null +++ b/src/views/mp/components/wx-music/main.vue @@ -0,0 +1,60 @@ +<!-- + 【微信消息 - 音乐】 +--> +<template> + <div> + <el-link + type="success" + :underline="false" + target="_blank" + :href="hqMusicUrl ? hqMusicUrl : musicUrl" + > + <div + class="avue-card__body" + style="padding: 10px; background-color: #fff; border-radius: 5px" + > + <div class="avue-card__avatar"> + <img :src="thumbMediaUrl" alt="" /> + </div> + <div class="avue-card__detail"> + <div class="avue-card__title" style="margin-bottom: unset">{{ title }}</div> + <div class="avue-card__info" style="height: unset">{{ description }}</div> + </div> + </div> + </el-link> + </div> +</template> + +<script setup lang="ts" name="WxMusic"> +const props = defineProps({ + title: { + required: false, + type: String + }, + description: { + required: false, + type: String + }, + musicUrl: { + required: false, + type: String + }, + hqMusicUrl: { + required: false, + type: String + }, + thumbMediaUrl: { + required: true, + type: String + } +}) + +defineExpose({ + musicUrl: props.musicUrl +}) +</script> + +<style lang="scss" scoped> +/* 因为 joolun 实现依赖 avue 组件,该页面使用了 card.scc */ +@import '../wx-msg/card.scss'; +</style> diff --git a/src/views/mp/components/wx-news/main.vue b/src/views/mp/components/wx-news/main.vue new file mode 100644 index 00000000..d08e2813 --- /dev/null +++ b/src/views/mp/components/wx-news/main.vue @@ -0,0 +1,107 @@ +<!-- + - Copyright (C) 2018-2019 + - All rights reserved, Designed By www.joolun.com + 【微信消息 - 图文】 + 芋道源码: + ① 代码优化,补充注释,提升阅读性 +--> +<template> + <div class="news-home"> + <div v-for="(article, index) in articles" :key="index" class="news-div"> + <!-- 头条 --> + <a target="_blank" :href="article.url" v-if="index === 0"> + <div class="news-main"> + <div class="news-content"> + <el-image + class="material-img" + style="width: 100%; height: 120px" + :src="article.picUrl" + /> + <div class="news-content-title"> + <span>{{ article.title }}</span> + </div> + </div> + </div> + </a> + <!-- 二条/三条等等 --> + <a target="_blank" :href="article.url" v-else> + <div class="news-main-item"> + <div class="news-content-item"> + <div class="news-content-item-title">{{ article.title }}</div> + <div class="news-content-item-img"> + <img class="material-img" :src="article.picUrl" height="100%" /> + </div> + </div> + </div> + </a> + </div> + </div> +</template> + +<script setup lang="ts"> +const props = defineProps({ + articles: { + type: Array, + default: () => null + } +}) + +defineExpose({ + articles: props.articles +}) +</script> + +<style lang="scss" scoped> +.news-home { + background-color: #ffffff; + width: 100%; + margin: auto; +} +.news-main { + width: 100%; + margin: auto; +} +.news-content { + background-color: #acadae; + width: 100%; + position: relative; +} +.news-content-title { + display: inline-block; + font-size: 12px; + color: #ffffff; + position: absolute; + left: 0; + bottom: 0; + background-color: black; + width: 98%; + padding: 1%; + opacity: 0.65; + white-space: normal; + box-sizing: unset !important; +} +.news-main-item { + background-color: #ffffff; + padding: 5px 0; + border-top: 1px solid #eaeaea; +} +.news-content-item { + position: relative; +} +.news-content-item-title { + display: inline-block; + font-size: 10px; + width: 70%; + margin-left: 1%; + white-space: normal; +} +.news-content-item-img { + display: inline-block; + width: 25%; + background-color: #acadae; + margin-right: 1%; +} +.material-img { + width: 100%; +} +</style> diff --git a/src/views/mp/components/wx-reply/main.vue b/src/views/mp/components/wx-reply/main.vue new file mode 100644 index 00000000..57a3cd84 --- /dev/null +++ b/src/views/mp/components/wx-reply/main.vue @@ -0,0 +1,634 @@ +<!--<!–--> +<!-- - Copyright (C) 2018-2019--> +<!-- - All rights reserved, Designed By www.joolun.com--> +<!-- 芋道源码:--> +<!-- ① 移除多余的 rep 为前缀的变量,让 message 消息更简单--> +<!-- ② 代码优化,补充注释,提升阅读性--> +<!-- ③ 优化消息的临时缓存策略,发送消息时,只清理被发送消息的 tab,不会强制切回到 text 输入--> +<!-- ④ 支持发送【视频】消息时,支持新建视频--> +<!--–>--> +<!--<template>--> +<!-- <el-tabs type="border-card" v-model="objData.type" @tab-click="handleClick">--> +<!-- <!– 类型 1:文本 –>--> +<!-- <el-tab-pane name="text">--> +<!-- <span slot="label"><i class="el-icon-document"></i> 文本</span>--> +<!-- <el-input--> +<!-- type="textarea"--> +<!-- :rows="5"--> +<!-- placeholder="请输入内容"--> +<!-- v-model="objData.content"--> +<!-- @input="inputContent"--> +<!-- />--> +<!-- </el-tab-pane>--> +<!-- <!– 类型 2:图片 –>--> +<!-- <el-tab-pane name="image">--> +<!-- <span slot="label"><i class="el-icon-picture"></i> 图片</span>--> +<!-- <el-row>--> +<!-- <!– 情况一:已经选择好素材、或者上传好图片 –>--> +<!-- <div class="select-item" v-if="objData.url">--> +<!-- <img class="material-img" :src="objData.url" />--> +<!-- <p class="item-name" v-if="objData.name">{{ objData.name }}</p>--> +<!-- <el-row class="ope-row">--> +<!-- <el-button type="danger" icon="el-icon-delete" circle @click="deleteObj" />--> +<!-- </el-row>--> +<!-- </div>--> +<!-- <!– 情况二:未做完上述操作 –>--> +<!-- <div v-else>--> +<!-- <el-row style="text-align: center">--> +<!-- <!– 选择素材 –>--> +<!-- <el-col :span="12" class="col-select">--> +<!-- <el-button type="success" @click="openMaterial">--> +<!-- 素材库选择<i class="el-icon-circle-check el-icon--right"></i>--> +<!-- </el-button>--> +<!-- <el-dialog--> +<!-- title="选择图片"--> +<!-- v-model:visible="dialogImageVisible"--> +<!-- width="90%"--> +<!-- append-to-body--> +<!-- >--> +<!-- <wx-material-select :obj-data="objData" @selectMaterial="selectMaterial" />--> +<!-- </el-dialog>--> +<!-- </el-col>--> +<!-- <!– 文件上传 –>--> +<!-- <el-col :span="12" class="col-add">--> +<!-- <el-upload--> +<!-- :action="actionUrl"--> +<!-- :headers="headers"--> +<!-- multiple--> +<!-- :limit="1"--> +<!-- :file-list="fileList"--> +<!-- :data="uploadData"--> +<!-- :before-upload="beforeImageUpload"--> +<!-- :on-success="handleUploadSuccess"--> +<!-- >--> +<!-- <el-button type="primary">上传图片</el-button>--> +<!-- <div slot="tip" class="el-upload__tip"--> +<!-- >支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div--> +<!-- >--> +<!-- </el-upload>--> +<!-- </el-col>--> +<!-- </el-row>--> +<!-- </div>--> +<!-- </el-row>--> +<!-- </el-tab-pane>--> +<!-- <!– 类型 3:语音 –>--> +<!-- <el-tab-pane name="voice">--> +<!-- <span slot="label"><i class="el-icon-phone"></i> 语音</span>--> +<!-- <el-row>--> +<!-- <div class="select-item2" v-if="objData.url">--> +<!-- <p class="item-name">{{ objData.name }}</p>--> +<!-- <div class="item-infos">--> +<!-- <wx-voice-player :url="objData.url" />--> +<!-- </div>--> +<!-- <el-row class="ope-row">--> +<!-- <el-button type="danger" icon="el-icon-delete" circle @click="deleteObj" />--> +<!-- </el-row>--> +<!-- </div>--> +<!-- <div v-else>--> +<!-- <el-row style="text-align: center">--> +<!-- <!– 选择素材 –>--> +<!-- <el-col :span="12" class="col-select">--> +<!-- <el-button type="success" @click="openMaterial">--> +<!-- 素材库选择<i class="el-icon-circle-check el-icon--right"></i>--> +<!-- </el-button>--> +<!-- <el-dialog--> +<!-- title="选择语音"--> +<!-- v-model:visible="dialogVoiceVisible"--> +<!-- width="90%"--> +<!-- append-to-body--> +<!-- >--> +<!-- <WxMaterialSelect :objData="objData" @selectMaterial="selectMaterial" />--> +<!-- </el-dialog>--> +<!-- </el-col>--> +<!-- <!– 文件上传 –>--> +<!-- <el-col :span="12" class="col-add">--> +<!-- <el-upload--> +<!-- :action="actionUrl"--> +<!-- :headers="headers"--> +<!-- multiple--> +<!-- :limit="1"--> +<!-- :file-list="fileList"--> +<!-- :data="uploadData"--> +<!-- :before-upload="beforeVoiceUpload"--> +<!-- :on-success="handleUploadSuccess"--> +<!-- >--> +<!-- <el-button type="primary">点击上传</el-button>--> +<!-- <div slot="tip" class="el-upload__tip"--> +<!-- >格式支持 mp3/wma/wav/amr,文件大小不超过 2M,播放长度不超过 60s</div--> +<!-- >--> +<!-- </el-upload>--> +<!-- </el-col>--> +<!-- </el-row>--> +<!-- </div>--> +<!-- </el-row>--> +<!-- </el-tab-pane>--> +<!-- <!– 类型 4:视频 –>--> +<!-- <el-tab-pane name="video">--> +<!-- <span slot="label"><i class="el-icon-share"></i> 视频</span>--> +<!-- <el-row>--> +<!-- <el-input v-model="objData.title" placeholder="请输入标题" @input="inputContent" />--> +<!-- <div style="margin: 20px 0"></div>--> +<!-- <el-input v-model="objData.description" placeholder="请输入描述" @input="inputContent" />--> +<!-- <div style="margin: 20px 0"></div>--> +<!-- <div style="text-align: center">--> +<!-- <wx-video-player v-if="objData.url" :url="objData.url" />--> +<!-- </div>--> +<!-- <div style="margin: 20px 0"></div>--> +<!-- <el-row style="text-align: center">--> +<!-- <!– 选择素材 –>--> +<!-- <el-col :span="12">--> +<!-- <el-button type="success" @click="openMaterial">--> +<!-- 素材库选择<i class="el-icon-circle-check el-icon--right"></i>--> +<!-- </el-button>--> +<!-- <el-dialog--> +<!-- title="选择视频"--> +<!-- v-model:visible="dialogVideoVisible"--> +<!-- width="90%"--> +<!-- append-to-body--> +<!-- >--> +<!-- <wx-material-select :objData="objData" @selectMaterial="selectMaterial" />--> +<!-- </el-dialog>--> +<!-- </el-col>--> +<!-- <!– 文件上传 –>--> +<!-- <el-col :span="12">--> +<!-- <el-upload--> +<!-- :action="actionUrl"--> +<!-- :headers="headers"--> +<!-- multiple--> +<!-- :limit="1"--> +<!-- :file-list="fileList"--> +<!-- :data="uploadData"--> +<!-- :before-upload="beforeVideoUpload"--> +<!-- :on-success="handleUploadSuccess"--> +<!-- >--> +<!-- <el-button type="primary"--> +<!-- >新建视频<i class="el-icon-upload el-icon--right"></i--> +<!-- ></el-button>--> +<!-- </el-upload>--> +<!-- </el-col>--> +<!-- </el-row>--> +<!-- </el-row>--> +<!-- </el-tab-pane>--> +<!-- <!– 类型 5:图文 –>--> +<!-- <el-tab-pane name="news">--> +<!-- <span slot="label"><i class="el-icon-news"></i> 图文</span>--> +<!-- <el-row>--> +<!-- <div class="select-item" v-if="objData.articles">--> +<!-- <wx-news :articles="objData.articles" />--> +<!-- <el-row class="ope-row">--> +<!-- <el-button type="danger" icon="el-icon-delete" circle @click="deleteObj" />--> +<!-- </el-row>--> +<!-- </div>--> +<!-- <!– 选择素材 –>--> +<!-- <div v-if="!objData.content">--> +<!-- <el-row style="text-align: center">--> +<!-- <el-col :span="24">--> +<!-- <el-button type="success" @click="openMaterial"--> +<!-- >{{ newsType === '1' ? '选择已发布图文' : '选择草稿箱图文'--> +<!-- }}<i class="el-icon-circle-check el-icon--right"></i--> +<!-- ></el-button>--> +<!-- </el-col>--> +<!-- </el-row>--> +<!-- </div>--> +<!-- <el-dialog title="选择图文" v-model:visible="dialogNewsVisible" width="90%" append-to-body>--> +<!-- <wx-material-select--> +<!-- :objData="objData"--> +<!-- @selectMaterial="selectMaterial"--> +<!-- :newsType="newsType"--> +<!-- />--> +<!-- </el-dialog>--> +<!-- </el-row>--> +<!-- </el-tab-pane>--> +<!-- <!– 类型 6:音乐 –>--> +<!-- <el-tab-pane name="music">--> +<!-- <span slot="label"><i class="el-icon-service"></i> 音乐</span>--> +<!-- <el-row>--> +<!-- <el-col :span="6">--> +<!-- <div class="thumb-div">--> +<!-- <img style="width: 100px" v-if="objData.thumbMediaUrl" :src="objData.thumbMediaUrl" />--> +<!-- <i v-else class="el-icon-plus avatar-uploader-icon"></i>--> +<!-- <div class="thumb-but">--> +<!-- <el-upload--> +<!-- :action="actionUrl"--> +<!-- :headers="headers"--> +<!-- multiple--> +<!-- :limit="1"--> +<!-- :file-list="fileList"--> +<!-- :data="uploadData"--> +<!-- :before-upload="beforeThumbImageUpload"--> +<!-- :on-success="handleUploadSuccess"--> +<!-- >--> +<!-- <el-button slot="trigger" size="mini" type="text">本地上传</el-button>--> +<!-- <el-button size="mini" type="text" @click="openMaterial" style="margin-left: 5px"--> +<!-- >素材库选择</el-button--> +<!-- >--> +<!-- </el-upload>--> +<!-- </div>--> +<!-- </div>--> +<!-- <el-dialog--> +<!-- title="选择图片"--> +<!-- v-model:visible="dialogThumbVisible"--> +<!-- width="80%"--> +<!-- append-to-body--> +<!-- >--> +<!-- <wx-material-select--> +<!-- :objData="{ type: 'image', accountId: objData.accountId }"--> +<!-- @selectMaterial="selectMaterial"--> +<!-- />--> +<!-- </el-dialog>--> +<!-- </el-col>--> +<!-- <el-col :span="18">--> +<!-- <el-input v-model="objData.title" placeholder="请输入标题" @input="inputContent" />--> +<!-- <div style="margin: 20px 0"></div>--> +<!-- <el-input v-model="objData.description" placeholder="请输入描述" @input="inputContent" />--> +<!-- </el-col>--> +<!-- </el-row>--> +<!-- <div style="margin: 20px 0"></div>--> +<!-- <el-input v-model="objData.musicUrl" placeholder="请输入音乐链接" @input="inputContent" />--> +<!-- <div style="margin: 20px 0"></div>--> +<!-- <el-input--> +<!-- v-model="objData.hqMusicUrl"--> +<!-- placeholder="请输入高质量音乐链接"--> +<!-- @input="inputContent"--> +<!-- />--> +<!-- </el-tab-pane>--> +<!-- </el-tabs>--> +<!--</template>--> + +<!--<script>--> +<!--import WxNews from '@/views/mp/components/wx-news/main.vue'--> +<!--import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'--> +<!--import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'--> +<!--import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'--> + +<!--import { getAccessToken } from '@/utils/auth'--> + +<!--export default {--> +<!-- name: 'WxReplySelect',--> +<!-- components: {--> +<!-- WxNews,--> +<!-- WxMaterialSelect,--> +<!-- WxVoicePlayer,--> +<!-- WxVideoPlayer--> +<!-- },--> +<!-- props: {--> +<!-- objData: {--> +<!-- // 消息对象。--> +<!-- type: Object, // 设置为 Object 的原因,方便属性的传递--> +<!-- required: true--> +<!-- },--> +<!-- newsType: {--> +<!-- // 图文类型:1、已发布图文;2、草稿箱图文--> +<!-- type: String,--> +<!-- default: '1'--> +<!-- }--> +<!-- },--> +<!-- data() {--> +<!-- return {--> +<!-- tempPlayerObj: {--> +<!-- type: '2'--> +<!-- },--> + +<!-- tempObj: new Map().set(--> +<!-- // 临时缓存,切换消息类型的 tab 的时候,可以保存对应的数据;--> +<!-- this.objData.type, // 消息类型--> +<!-- Object.assign({}, this.objData)--> +<!-- ), // 消息内容--> + +<!-- // ========== 素材选择的弹窗,是否可见 ==========--> +<!-- dialogNewsVisible: false, // 图文--> +<!-- dialogImageVisible: false, // 图片--> +<!-- dialogVoiceVisible: false, // 语音--> +<!-- dialogVideoVisible: false, // 视频--> +<!-- dialogThumbVisible: false, // 缩略图--> + +<!-- // ========== 文件上传(图片、语音、视频) ==========--> +<!-- fileList: [], // 文件列表--> +<!-- uploadData: {--> +<!-- accountId: undefined,--> +<!-- type: this.objData.type,--> +<!-- title: '',--> +<!-- introduction: ''--> +<!-- },--> +<!-- actionUrl: process.env.VUE_APP_BASE_API + '/admin-api/mp/material/upload-temporary',--> +<!-- headers: { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部--> +<!-- }--> +<!-- },--> +<!-- methods: {--> +<!-- beforeThumbImageUpload(file) {--> +<!-- const isType =--> +<!-- file.type === 'image/jpeg' ||--> +<!-- file.type === 'image/png' ||--> +<!-- file.type === 'image/gif' ||--> +<!-- file.type === 'image/bmp' ||--> +<!-- file.type === 'image/jpg'--> +<!-- if (!isType) {--> +<!-- this.$message.error('上传图片格式不对!')--> +<!-- return false--> +<!-- }--> +<!-- const isLt = file.size / 1024 / 1024 < 2--> +<!-- if (!isLt) {--> +<!-- this.$message.error('上传图片大小不能超过 2M!')--> +<!-- return false--> +<!-- }--> +<!-- this.uploadData.accountId = this.objData.accountId--> +<!-- return true--> +<!-- },--> +<!-- beforeVoiceUpload(file) {--> +<!-- // 校验格式--> +<!-- const isType =--> +<!-- file.type === 'audio/mp3' ||--> +<!-- file.type === 'audio/mpeg' ||--> +<!-- file.type === 'audio/wma' ||--> +<!-- file.type === 'audio/wav' ||--> +<!-- file.type === 'audio/amr'--> +<!-- if (!isType) {--> +<!-- this.$message.error('上传语音格式不对!' + file.type)--> +<!-- return false--> +<!-- }--> +<!-- // 校验大小--> +<!-- const isLt = file.size / 1024 / 1024 < 2--> +<!-- if (!isLt) {--> +<!-- this.$message.error('上传语音大小不能超过 2M!')--> +<!-- return false--> +<!-- }--> +<!-- this.uploadData.accountId = this.objData.accountId--> +<!-- return true--> +<!-- },--> +<!-- beforeImageUpload(file) {--> +<!-- // 校验格式--> +<!-- const isType =--> +<!-- file.type === 'image/jpeg' ||--> +<!-- file.type === 'image/png' ||--> +<!-- file.type === 'image/gif' ||--> +<!-- file.type === 'image/bmp' ||--> +<!-- file.type === 'image/jpg'--> +<!-- if (!isType) {--> +<!-- this.$message.error('上传图片格式不对!')--> +<!-- return false--> +<!-- }--> +<!-- // 校验大小--> +<!-- const isLt = file.size / 1024 / 1024 < 2--> +<!-- if (!isLt) {--> +<!-- this.$message.error('上传图片大小不能超过 2M!')--> +<!-- return false--> +<!-- }--> +<!-- this.uploadData.accountId = this.objData.accountId--> +<!-- return true--> +<!-- },--> +<!-- beforeVideoUpload(file) {--> +<!-- // 校验格式--> +<!-- const isType = file.type === 'video/mp4'--> +<!-- if (!isType) {--> +<!-- this.$message.error('上传视频格式不对!')--> +<!-- return false--> +<!-- }--> +<!-- // 校验大小--> +<!-- const isLt = file.size / 1024 / 1024 < 10--> +<!-- if (!isLt) {--> +<!-- this.$message.error('上传视频大小不能超过 10M!')--> +<!-- return false--> +<!-- }--> +<!-- this.uploadData.accountId = this.objData.accountId--> +<!-- return true--> +<!-- },--> +<!-- handleUploadSuccess(response, file, fileList) {--> +<!-- if (response.code !== 0) {--> +<!-- this.$message.error('上传出错:' + response.msg)--> +<!-- return false--> +<!-- }--> + +<!-- // 清空上传时的各种数据--> +<!-- this.fileList = []--> +<!-- this.uploadData.title = ''--> +<!-- this.uploadData.introduction = ''--> + +<!-- // 上传好的文件,本质是个素材,所以可以进行选中--> +<!-- let item = response.data--> +<!-- this.selectMaterial(item)--> +<!-- },--> +<!-- /**--> +<!-- * 切换消息类型的 tab--> +<!-- *--> +<!-- * @param tab tab--> +<!-- */--> +<!-- handleClick(tab) {--> +<!-- // 设置后续文件上传的文件类型--> +<!-- this.uploadData.type = this.objData.type--> +<!-- if (this.uploadData.type === 'music') {--> +<!-- // 【音乐】上传的是缩略图--> +<!-- this.uploadData.type = 'thumb'--> +<!-- }--> + +<!-- // 从 tempObj 临时缓存中,获取对应的数据,并设置回 objData--> +<!-- let tempObjItem = this.tempObj.get(this.objData.type)--> +<!-- if (tempObjItem) {--> +<!-- this.objData.content = tempObjItem.content ? tempObjItem.content : null--> +<!-- this.objData.mediaId = tempObjItem.mediaId ? tempObjItem.mediaId : null--> +<!-- this.objData.url = tempObjItem.url ? tempObjItem.url : null--> +<!-- this.objData.name = tempObjItem.url ? tempObjItem.name : null--> +<!-- this.objData.title = tempObjItem.title ? tempObjItem.title : null--> +<!-- this.objData.description = tempObjItem.description ? tempObjItem.description : null--> +<!-- return--> +<!-- }--> +<!-- // 如果获取不到,需要把 objData 复原--> +<!-- // 必须使用 $set 赋值,不然 input 无法输入内容--> +<!-- this.$set(this.objData, 'content', '')--> +<!-- this.$delete(this.objData, 'mediaId')--> +<!-- this.$delete(this.objData, 'url')--> +<!-- this.$set(this.objData, 'title', '')--> +<!-- this.$set(this.objData, 'description', '')--> +<!-- },--> +<!-- /**--> +<!-- * 选择素材,将设置设置到 objData 变量--> +<!-- *--> +<!-- * @param item 素材--> +<!-- */--> +<!-- selectMaterial(item) {--> +<!-- // 选择好素材,所以隐藏弹窗--> +<!-- this.closeMaterial()--> + +<!-- // 创建 tempObjItem 对象,并设置对应的值--> +<!-- let tempObjItem = {}--> +<!-- tempObjItem.type = this.objData.type--> +<!-- if (this.objData.type === 'news') {--> +<!-- tempObjItem.articles = item.content.newsItem--> +<!-- this.objData.articles = item.content.newsItem--> +<!-- } else if (this.objData.type === 'music') {--> +<!-- // 音乐需要特殊处理,因为选择的是图片的缩略图--> +<!-- tempObjItem.thumbMediaId = item.mediaId--> +<!-- this.objData.thumbMediaId = item.mediaId--> +<!-- tempObjItem.thumbMediaUrl = item.url--> +<!-- this.objData.thumbMediaUrl = item.url--> +<!-- // title、introduction、musicUrl、hqMusicUrl:从 objData 到 tempObjItem,避免上传素材后,被覆盖掉--> +<!-- tempObjItem.title = this.objData.title || ''--> +<!-- tempObjItem.introduction = this.objData.introduction || ''--> +<!-- tempObjItem.musicUrl = this.objData.musicUrl || ''--> +<!-- tempObjItem.hqMusicUrl = this.objData.hqMusicUrl || ''--> +<!-- } else if (this.objData.type === 'image' || this.objData.type === 'voice') {--> +<!-- tempObjItem.mediaId = item.mediaId--> +<!-- this.objData.mediaId = item.mediaId--> +<!-- tempObjItem.url = item.url--> +<!-- this.objData.url = item.url--> +<!-- tempObjItem.name = item.name--> +<!-- this.objData.name = item.name--> +<!-- } else if (this.objData.type === 'video') {--> +<!-- tempObjItem.mediaId = item.mediaId--> +<!-- this.objData.mediaId = item.mediaId--> +<!-- tempObjItem.url = item.url--> +<!-- this.objData.url = item.url--> +<!-- tempObjItem.name = item.name--> +<!-- this.objData.name = item.name--> +<!-- // title、introduction:从 item 到 tempObjItem,因为素材里有 title、introduction--> +<!-- if (item.title) {--> +<!-- this.objData.title = item.title || ''--> +<!-- tempObjItem.title = item.title || ''--> +<!-- }--> +<!-- if (item.introduction) {--> +<!-- this.objData.description = item.introduction || '' // 消息使用的是 description,素材使用的是 introduction,所以转换下--> +<!-- tempObjItem.description = item.introduction || ''--> +<!-- }--> +<!-- } else if (this.objData.type === 'text') {--> +<!-- this.objData.content = item.content || ''--> +<!-- }--> +<!-- // 最终设置到临时缓存--> +<!-- this.tempObj.set(this.objData.type, tempObjItem)--> +<!-- },--> +<!-- openMaterial() {--> +<!-- if (this.objData.type === 'news') {--> +<!-- this.dialogNewsVisible = true--> +<!-- } else if (this.objData.type === 'image') {--> +<!-- this.dialogImageVisible = true--> +<!-- } else if (this.objData.type === 'voice') {--> +<!-- this.dialogVoiceVisible = true--> +<!-- } else if (this.objData.type === 'video') {--> +<!-- this.dialogVideoVisible = true--> +<!-- } else if (this.objData.type === 'music') {--> +<!-- this.dialogThumbVisible = true--> +<!-- }--> +<!-- },--> +<!-- closeMaterial() {--> +<!-- this.dialogNewsVisible = false--> +<!-- this.dialogImageVisible = false--> +<!-- this.dialogVoiceVisible = false--> +<!-- this.dialogVideoVisible = false--> +<!-- this.dialogThumbVisible = false--> +<!-- },--> +<!-- deleteObj() {--> +<!-- if (this.objData.type === 'news') {--> +<!-- this.$delete(this.objData, 'articles')--> +<!-- } else if (this.objData.type === 'image') {--> +<!-- this.objData.mediaId = null--> +<!-- this.$delete(this.objData, 'url')--> +<!-- this.objData.name = null--> +<!-- } else if (this.objData.type === 'voice') {--> +<!-- this.objData.mediaId = null--> +<!-- this.$delete(this.objData, 'url')--> +<!-- this.objData.name = null--> +<!-- } else if (this.objData.type === 'video') {--> +<!-- this.objData.mediaId = null--> +<!-- this.$delete(this.objData, 'url')--> +<!-- this.objData.name = null--> +<!-- this.objData.title = null--> +<!-- this.objData.description = null--> +<!-- } else if (this.objData.type === 'music') {--> +<!-- this.objData.thumbMediaId = null--> +<!-- this.objData.thumbMediaUrl = null--> +<!-- this.objData.title = null--> +<!-- this.objData.description = null--> +<!-- this.objData.musicUrl = null--> +<!-- this.objData.hqMusicUrl = null--> +<!-- } else if (this.objData.type === 'text') {--> +<!-- this.objData.content = null--> +<!-- }--> +<!-- // 覆盖缓存--> +<!-- this.tempObj.set(this.objData.type, Object.assign({}, this.objData))--> +<!-- },--> +<!-- /**--> +<!-- * 输入时,缓存每次 objData 到 tempObj 中--> +<!-- *--> +<!-- * why?不确定为什么 v-model="objData.content" 不能自动缓存,所以通过这样的方式--> +<!-- */--> +<!-- inputContent(str) {--> +<!-- // 覆盖缓存--> +<!-- this.tempObj.set(this.objData.type, Object.assign({}, this.objData))--> +<!-- }--> +<!-- }--> +<!--}--> +<!--</script>--> + +<!--<style lang="scss" scoped>--> +<!--.public-account-management {--> +<!-- .el-input {--> +<!-- width: 70%;--> +<!-- margin-right: 2%;--> +<!-- }--> +<!--}--> +<!--.pagination {--> +<!-- text-align: right;--> +<!-- margin-right: 25px;--> +<!--}--> +<!--.select-item {--> +<!-- width: 280px;--> +<!-- padding: 10px;--> +<!-- margin: 0 auto 10px auto;--> +<!-- border: 1px solid #eaeaea;--> +<!--}--> +<!--.select-item2 {--> +<!-- padding: 10px;--> +<!-- margin: 0 auto 10px auto;--> +<!-- border: 1px solid #eaeaea;--> +<!--}--> +<!--.ope-row {--> +<!-- padding-top: 10px;--> +<!-- text-align: center;--> +<!--}--> +<!--.item-name {--> +<!-- font-size: 12px;--> +<!-- overflow: hidden;--> +<!-- text-overflow: ellipsis;--> +<!-- white-space: nowrap;--> +<!-- text-align: center;--> +<!--}--> +<!--.el-form-item__content {--> +<!-- line-height: unset !important;--> +<!--}--> +<!--.col-select {--> +<!-- border: 1px solid rgb(234, 234, 234);--> +<!-- padding: 50px 0px;--> +<!-- height: 160px;--> +<!-- width: 49.5%;--> +<!--}--> +<!--.col-select2 {--> +<!-- border: 1px solid rgb(234, 234, 234);--> +<!-- padding: 50px 0px;--> +<!-- height: 160px;--> +<!--}--> +<!--.col-add {--> +<!-- border: 1px solid rgb(234, 234, 234);--> +<!-- padding: 50px 0px;--> +<!-- height: 160px;--> +<!-- width: 49.5%;--> +<!-- float: right;--> +<!--}--> +<!--.avatar-uploader-icon {--> +<!-- border: 1px solid #d9d9d9;--> +<!-- font-size: 28px;--> +<!-- color: #8c939d;--> +<!-- width: 100px !important;--> +<!-- height: 100px !important;--> +<!-- line-height: 100px !important;--> +<!-- text-align: center;--> +<!--}--> +<!--.material-img {--> +<!-- width: 100%;--> +<!--}--> +<!--.thumb-div {--> +<!-- display: inline-block;--> +<!-- text-align: center;--> +<!--}--> +<!--.item-infos {--> +<!-- width: 30%;--> +<!-- margin: auto;--> +<!--}--> +<!--</style>--> diff --git a/src/views/mp/components/wx-video-play/main.vue b/src/views/mp/components/wx-video-play/main.vue new file mode 100644 index 00000000..880d10f8 --- /dev/null +++ b/src/views/mp/components/wx-video-play/main.vue @@ -0,0 +1,117 @@ +<!-- + - Copyright (C) 2018-2019 + - All rights reserved, Designed By www.joolun.com + 【微信消息 - 视频】 + 芋道源码: + ① bug 修复: + 1)joolun 的做法:使用 mediaId 从微信公众号,下载对应的 mp4 素材,从而播放内容; + 存在的问题:mediaId 有效期是 3 天,超过时间后无法播放 + 2)重构后的做法:后端接收到微信公众号的视频消息后,将视频消息的 media_id 的文件内容保存到文件服务器中,这样前端可以直接使用 URL 播放。 + ② 体验优化:弹窗关闭后,自动暂停视频的播放 +--> +<template> + <div> + <!-- 提示 --> + <div @click="playVideo()"> + <el-icon> + <VideoPlay /> + </el-icon> + <p>点击播放视频</p> + </div> + + <!-- 弹窗播放 --> + <el-dialog + title="视频播放" + v-model:visible="dialogVideo" + width="40%" + append-to-body + @close="closeDialog" + > + <video-player + v-if="playerOptions.sources[0].src" + class="video-player vjs-custom-skin" + ref="videoPlayerRef" + :playsinline="true" + :options="playerOptions" + @play="onPlayerPlay($event)" + @pause="onPlayerPause($event)" + /> + </el-dialog> + </div> +</template> + +<script setup lang="ts" name="WxVideoPlayer"> +// 引入 videoPlayer 相关组件。教程:https://juejin.cn/post/6923056942281654285 +import { videoPlayer } from 'vue-video-player' +import 'video.js/dist/video-js.css' +import 'vue-video-player/src/custom-theme.css' +import { VideoPlay } from '@element-plus/icons-vue' + +const props = defineProps({ + url: { + // 视频地址,例如说:https://www.iocoder.cn/xxx.mp4 + type: String, + required: true + } +}) +const videoPlayerRef = ref() +const dialogVideo = ref(false) +const playerOptions = reactive({ + playbackRates: [0.5, 1.0, 1.5, 2.0], // 播放速度 + autoplay: false, // 如果 true,浏览器准备好时开始回放。 + muted: false, // 默认情况下将会消除任何音频。 + loop: false, // 导致视频一结束就重新开始。 + preload: 'auto', // 建议浏览器在 <video> 加载元素后是否应该开始下载视频数据。auto 浏览器选择最佳行为,立即开始加载视频(如果浏览器支持) + language: 'zh-CN', + aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3") + fluid: true, // 当true时,Video.js player 将拥有流体大小。换句话说,它将按比例缩放以适应其容器。 + sources: [ + { + type: 'video/mp4', + src: '' // 你的视频地址(必填)【重要】 + } + ], + poster: '', // 你的封面地址 + width: document.documentElement.clientWidth, + notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖 Video.js 无法播放媒体源时显示的默认信息。 + controlBar: { + timeDivider: true, + durationDisplay: true, + remainingTimeDisplay: false, + fullscreenToggle: true //全屏按钮 + } +}) + +const playVideo = () => { + dialogVideo.value = true + playerOptions.sources[0].src = props.url +} +const closeDialog = () => { + // 暂停播放 + // videoPlayerRef.player.pause() +} +// onPlayerPlay(player) {}, +// // // eslint-disable-next-line @typescript-eslint/no-unused-vars +// // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars +// onPlayerPause(player) {} + +// methods: { +// playVideo() { +// this.dialogVideo = true +// // 设置地址 +// this.playerOptions.sources[0]['src'] = this.url +// }, +// closeDialog() { +// // 暂停播放 +// this.$refs.videoPlayer.player.pause() +// }, +// +// //todo player组件引入可能有问题 +// +// // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars +// onPlayerPlay(player) {}, +// // // eslint-disable-next-line @typescript-eslint/no-unused-vars +// // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars +// onPlayerPause(player) {} +// } +</script> diff --git a/src/views/mp/components/wx-voice-play/main.vue b/src/views/mp/components/wx-voice-play/main.vue new file mode 100644 index 00000000..f98ac681 --- /dev/null +++ b/src/views/mp/components/wx-voice-play/main.vue @@ -0,0 +1,100 @@ +<!-- + - Copyright (C) 2018-2019 + - All rights reserved, Designed By www.joolun.com + 【微信消息 - 语音】 + 芋道源码: + ① bug 修复: + 1)joolun 的做法:使用 mediaId 从微信公众号,下载对应的 mp4 素材,从而播放内容; + 存在的问题:mediaId 有效期是 3 天,超过时间后无法播放 + 2)重构后的做法:后端接收到微信公众号的视频消息后,将视频消息的 media_id 的文件内容保存到文件服务器中,这样前端可以直接使用 URL 播放。 + ② 代码优化:将 props 中的 objData 调成为 data 中对应的属性,并补充相关注释 +--> +<template> + <div class="wx-voice-div" @click="playVoice"> + <el-icon + ><VideoPlay v-if="playing !== true" /> + <VideoPause v-if="playing === true" /> + <span class="amr-duration" v-if="duration">{{ duration }} 秒</span> + </el-icon> + <div v-if="content"> + <el-tag type="success" size="mini">语音识别</el-tag> + {{ content }} + </div> + </div> +</template> + +<script setup lang="ts" name="WxVoicePlayer"> +// 因为微信语音是 amr 格式,所以需要用到 amr 解码器:https://www.npmjs.com/package/benz-amr-recorder + +import BenzAMRRecorder from 'benz-amr-recorder' +import { VideoPause, VideoPlay } from '@element-plus/icons-vue' + +const props = defineProps({ + url: { + // 语音地址,例如说:https://www.iocoder.cn/xxx.amr + type: String, + required: true + }, + content: { + // 语音文本 + type: String, + required: false + } +}) + +const amr = ref() +const playing = ref(false) +const duration = ref() + +const playVoice = () => { + // 情况一:未初始化,则创建 BenzAMRRecorder + debugger + console.log('进入' + amr.value) + if (amr.value === undefined) { + console.log('开始初始化') + amrInit() + return + } + + if (amr.value.isPlaying()) { + amrStop() + } else { + amrPlay() + } +} + +const amrInit = () => { + amr.value = new BenzAMRRecorder() + console.log(amr.value) + console.log(props.url) + // 设置播放 + amr.value.initWithUrl(props.url).then(function () { + amrPlay() + duration.value = amr.value.getDuration() + }) + // 监听暂停 + amr.value.onEnded(function () { + playing.value = false + }) +} +const amrPlay = () => { + playing.value = true + amr.value.play() +} +const amrStop = () => { + playing.value = false + amr.value.stop() +} +</script> + +<style lang="scss" scoped> +.wx-voice-div { + padding: 5px; + background-color: #eaeaea; + border-radius: 10px; +} +.amr-duration { + font-size: 11px; + margin-left: 5px; +} +</style> diff --git a/src/views/mp/freePublish/index.vue b/src/views/mp/freePublish/index.vue index 497f72ec..1d9b331e 100644 --- a/src/views/mp/freePublish/index.vue +++ b/src/views/mp/freePublish/index.vue @@ -1,3 +1,395 @@ <template> - <span>开发中</span> + <content-wrap> + <doc-alert title="公众号图文" url="https://doc.iocoder.cn/mp/article/" /> + + <!-- 搜索工作栏 --> + <el-form + :model="queryParams" + ref="queryFormRef" + size="small" + :inline="true" + v-show="showSearch" + label-width="68px" + > + <el-form-item label="公众号" prop="accountId"> + <el-select v-model="queryParams.accountId" placeholder="请选择公众号"> + <el-option + v-for="item in accounts" + :key="parseInt(item.id)" + :label="item.name" + :value="parseInt(item.id)" + /> + </el-select> + </el-form-item> + <el-form-item> + <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button> + <el-button :icon="Refresh" @click="resetQuery">重置</el-button> + </el-form-item> + </el-form> + + <!-- 列表 --> + <div class="waterfall" v-loading="loading"> + <div + v-show="item.content && item.content.newsItem" + class="waterfall-item" + v-for="item in list" + :key="item.articleId" + > + <wx-news :articles="item.content.newsItem" /> + <!-- 操作 --> + <el-row justify="center" class="ope-row"> + <el-button + type="danger" + :icon="Delete" + circle + @click="handleDelete(item)" + v-hasPermi="['mp:free-publish:delete']" + /> + </el-row> + </div> + </div> + <!-- 分页组件 --> + <pagination + v-show="total > 0" + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </content-wrap> </template> + +<script setup lang="ts" name="freePublish"> +import { getFreePublishPage, deleteFreePublish } from '@/api/mp/freePublish' +import { getSimpleAccounts } from '@/api/mp/account' +import WxNews from '@/views/mp/components/wx-news/main.vue' +import { Delete, Search, Refresh } from '@element-plus/icons-vue' + +const message = useMessage() // 消息弹窗 + +const queryParams = reactive({ + total: 0, // 总页数 + currentPage: 1, // 当前页数 + pageNo: 1, // 当前页数 + accountId: undefined, // 当前页数 + queryParamsSize: 10 // 每页显示多少条 +}) +const loading = ref(false) // 列表的加载中 +const showSearch = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const accounts = ref([]) // 列表的数据 +const queryFormRef = ref() // 搜索的表单 +/** 查询列表 */ +const getList = async () => { + // 如果没有选中公众号账号,则进行提示。 + if (!queryParams.accountId) { + message.error('未选中公众号,无法查询已发表图文') + return false + } + loading.value = true + getFreePublishPage(queryParams) + .then((data) => { + console.log(data) + // 将 thumbUrl 转成 picUrl,保证 wx-news 组件可以预览封面 + data.list.forEach((item) => { + console.log(item) + const newsItem = item.content.newsItem + newsItem.forEach((article) => { + article.picUrl = article.thumbUrl + }) + }) + list.value = data.list + total.value = data.total + }) + .finally(() => { + loading.value = false + }) +} +/** 搜索按钮操作 */ +const handleQuery = async () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = async () => { + queryFormRef.value.resetFields() + // 默认选中第一个 + if (accounts.value.length > 0) { + queryParams.accountId = accounts[0].id + } + handleQuery() +} + +/** 删除按钮操作 */ +const handleDelete = async (item) => { + { + const articleId = item.articleId + const accountId = queryParams.accountId + message + .confirm('删除后用户将无法访问此页面,确定删除?') + .then(function () { + return deleteFreePublish(accountId, articleId) + }) + .then(() => { + getList() + message.success('删除成功') + }) + .catch(() => {}) + } +} + +onMounted(() => { + getSimpleAccounts().then((response) => { + accounts.value = response + // 默认选中第一个 + if (accounts.value.length > 0) { + queryParams.accountId = accounts.value[0]['id'] + } + // 加载数据 + getList() + }) +}) +</script> + +<style lang="scss" scoped> +.pagination { + float: right; + margin-right: 25px; +} + +.add_but { + padding: 10px; +} + +.ope-row { + margin-top: 5px; + text-align: center; + border-top: 1px solid #eaeaea; + padding-top: 5px; +} + +.item-name { + font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: center; +} + +.el-upload__tip { + margin-left: 5px; +} + +/*新增图文*/ +.left { + display: inline-block; + width: 35%; + vertical-align: top; + margin-top: 200px; +} + +.right { + display: inline-block; + width: 60%; + margin-top: -40px; +} + +.avatar-uploader { + width: 20%; + display: inline-block; +} + +.avatar-uploader .el-upload { + border-radius: 6px; + cursor: pointer; + position: relative; + overflow: hidden; + text-align: unset !important; +} + +.avatar-uploader .el-upload:hover { + border-color: #165dff; +} + +.avatar-uploader-icon { + border: 1px solid #d9d9d9; + font-size: 28px; + color: #8c939d; + width: 120px; + height: 120px; + line-height: 120px; + text-align: center; +} + +.avatar { + width: 230px; + height: 120px; +} + +.avatar1 { + width: 120px; + height: 120px; +} + +.digest { + width: 60%; + display: inline-block; + vertical-align: top; +} + +/*新增图文*/ +/*瀑布流样式*/ +.waterfall { + width: 100%; + column-gap: 10px; + column-count: 5; + margin: 0 auto; +} + +.waterfall-item { + padding: 10px; + margin-bottom: 10px; + break-inside: avoid; + border: 1px solid #eaeaea; +} + +p { + line-height: 30px; +} + +@media (min-width: 992px) and (max-width: 1300px) { + .waterfall { + column-count: 3; + } + p { + color: red; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .waterfall { + column-count: 2; + } + p { + color: orange; + } +} + +@media (max-width: 767px) { + .waterfall { + column-count: 1; + } +} + +/*瀑布流样式*/ +.news-main { + background-color: #ffffff; + width: 100%; + margin: auto; + height: 120px; +} + +.news-content { + background-color: #acadae; + width: 100%; + height: 120px; + position: relative; +} + +.news-content-title { + display: inline-block; + font-size: 15px; + color: #ffffff; + position: absolute; + left: 0px; + bottom: 0px; + background-color: black; + width: 98%; + padding: 1%; + opacity: 0.65; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + height: 25px; +} + +.news-main-item { + background-color: #ffffff; + padding: 5px 0px; + border-top: 1px solid #eaeaea; + width: 100%; + margin: auto; +} + +.news-content-item { + position: relative; + margin-left: -3px; +} + +.news-content-item-title { + display: inline-block; + font-size: 12px; + width: 70%; +} + +.news-content-item-img { + display: inline-block; + width: 25%; + background-color: #acadae; +} + +.input-tt { + padding: 5px; +} + +.activeAddNews { + border: 5px solid #2bb673; +} + +.news-main-plus { + width: 280px; + text-align: center; + margin: auto; + height: 50px; +} + +.icon-plus { + margin: 10px; + font-size: 25px; +} + +.select-item { + width: 60%; + padding: 10px; + margin: 0 auto 10px auto; + border: 1px solid #eaeaea; +} + +.father .child { + display: none; + text-align: center; + position: relative; + bottom: 25px; +} + +.father:hover .child { + display: block; +} + +.thumb-div { + display: inline-block; + width: 30%; + text-align: center; +} + +.thumb-but { + margin: 5px; +} + +.material-img { + width: 100%; + height: 100%; +} +</style> diff --git a/src/views/mp/message/index.vue b/src/views/mp/message/index.vue index 497f72ec..34e64ebf 100644 --- a/src/views/mp/message/index.vue +++ b/src/views/mp/message/index.vue @@ -1,3 +1,262 @@ <template> - <span>开发中</span> + <ContentWrap> + <doc-alert title="公众号消息" url="https://doc.iocoder.cn/mp/message/" /> + + <!-- 搜索工作栏 --> + <el-form + :model="queryParams" + ref="queryFormRef" + size="small" + :inline="true" + v-show="showSearch" + label-width="68px" + > + <el-form-item label="公众号" prop="accountId"> + <el-select v-model="queryParams.accountId" placeholder="请选择公众号"> + <el-option + v-for="item in accounts" + :key="parseInt(item.id)" + :label="item.name" + :value="parseInt(item.id)" + /> + </el-select> + </el-form-item> + <el-form-item label="消息类型" prop="type"> + <el-select v-model="queryParams.type" placeholder="请选择消息类型" clearable size="small"> + <el-option + v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="用户标识" prop="openid"> + <el-input + v-model="queryParams.openid" + placeholder="请输入用户标识" + clearable + :v-on="handleQuery" + /> + </el-form-item> + <el-form-item label="创建时间" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + style="width: 240px" + value-format="yyyy-MM-dd HH:mm:ss" + type="daterange" + range-separator="-" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="['00:00:00', '23:59:59']" + /> + </el-form-item> + <el-form-item> + <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button> + <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button> + </el-form-item> + </el-form> + + <!--todo 操作工具栏 --> + <!-- <el-row :gutter="10" class="mb8">--> + <!-- <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />--> + <!-- </el-row>--> + + <!-- 列表 --> + <el-table v-loading="loading" :data="list"> + <el-table-column label="发送时间" align="center" prop="createTime" width="180"> + <template #default="scope"> + <span>{{ parseTime(scope.row.createTime) }}</span> + </template> + </el-table-column> + <el-table-column label="消息类型" align="center" prop="type" width="80" /> + <el-table-column label="发送方" align="center" prop="sendFrom" width="80"> + <template #default="scope"> + <el-tag v-if="scope.row.sendFrom === 1" type="success">粉丝</el-tag> + <el-tag v-else type="info">公众号</el-tag> + </template> + </el-table-column> + <el-table-column label="用户标识" align="center" prop="openid" width="300" /> + <el-table-column label="内容" prop="content"> + <template #default="scope"> + <!-- 【事件】区域 --> + <div v-if="scope.row.type === 'event' && scope.row.event === 'subscribe'"> + <el-tag type="success" size="mini">关注</el-tag> + </div> + <div v-else-if="scope.row.type === 'event' && scope.row.event === 'unsubscribe'"> + <el-tag type="danger" size="mini">取消关注</el-tag> + </div> + <div v-else-if="scope.row.type === 'event' && scope.row.event === 'CLICK'"> + <el-tag size="mini">点击菜单</el-tag>【{{ scope.row.eventKey }}】 + </div> + <div v-else-if="scope.row.type === 'event' && scope.row.event === 'VIEW'"> + <el-tag size="mini">点击菜单链接</el-tag>【{{ scope.row.eventKey }}】 + </div> + <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_waitmsg'"> + <el-tag size="mini">扫码结果</el-tag>【{{ scope.row.eventKey }}】 + </div> + <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_push'"> + <el-tag size="mini">扫码结果</el-tag>【{{ scope.row.eventKey }}】 + </div> + <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_sysphoto'"> + <el-tag size="mini">系统拍照发图</el-tag> + </div> + <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_photo_or_album'"> + <el-tag size="mini">拍照或者相册</el-tag> + </div> + <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_weixin'"> + <el-tag size="mini">微信相册</el-tag> + </div> + <div v-else-if="scope.row.type === 'event' && scope.row.event === 'location_select'"> + <el-tag size="mini">选择地理位置</el-tag> + </div> + <div v-else-if="scope.row.type === 'event'"> + <el-tag type="danger" size="mini">未知事件类型</el-tag> + </div> + <!-- 【消息】区域 --> + <div v-else-if="scope.row.type === 'text'">{{ scope.row.content }}</div> + <div v-else-if="scope.row.type === 'voice'"> + <wx-voice-player :url="scope.row.mediaUrl" :content="scope.row.recognition" /> + </div> + <div v-else-if="scope.row.type === 'image'"> + <a target="_blank" :href="scope.row.mediaUrl"> + <img :src="scope.row.mediaUrl" style="width: 100px" /> + </a> + </div> + <div v-else-if="scope.row.type === 'video' || scope.row.type === 'shortvideo'"> + <wx-video-player :url="scope.row.mediaUrl" style="margin-top: 10px" /> + </div> + <div v-else-if="scope.row.type === 'link'"> + <el-tag size="mini">链接</el-tag>: + <a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a> + </div> + <div v-else-if="scope.row.type === 'location'"> + <wx-location + :label="scope.row.label" + :location-y="scope.row.locationY" + :location-x="scope.row.locationX" + /> + </div> + <div v-else-if="scope.row.type === 'music'"> + <wx-music + :title="scope.row.title" + :description="scope.row.description" + :thumb-media-url="scope.row.thumbMediaUrl" + :music-url="scope.row.musicUrl" + :hq-music-url="scope.row.hqMusicUrl" + /> + </div> + <div v-else-if="scope.row.type === 'news'"> + <wx-news :articles="scope.row.articles" /> + </div> + <div v-else> + <el-tag type="danger" size="mini">未知消息类型</el-tag> + </div> + </template> + </el-table-column> + <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> + <template #default="scope"> + <el-button + size="mini" + type="text" + icon="el-icon-edit" + @click="handleSend(scope.row)" + v-hasPermi="['mp:message:send']" + >消息 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页组件 --> + <pagination + v-show="total > 0" + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + + <!-- 发送消息的弹窗 --> + <el-dialog title="粉丝消息列表" v-model:visible="open" width="50%"> + <wx-msg :user-id="userId" v-if="open" /> + </el-dialog> + </ContentWrap> </template> + +<script setup lang="ts" name="MpMessage"> +import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue' +import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue' +// import WxMsg from '@/views/mp/components/wx-msg/main.vue' +import WxLocation from '@/views/mp/components/wx-location/main.vue' +import WxMusic from '@/views/mp/components/wx-music/main.vue' +import WxNews from '@/views/mp/components/wx-news/main.vue' +import { getMessagePage } from '@/api/mp/message' +import { getSimpleAccounts } from '@/api/mp/account' +import { DICT_TYPE, getDictOptions } from '@/utils/dict' +import { parseTime } from '@/utils/formatTime' + +// ========== CRUD 相关 ========== +const loading = ref(false) // 遮罩层 +const showSearch = ref(true) // 显示搜索条件 +const total = ref(0) // 总条数 +const list = ref([]) // 粉丝消息列表 +const accounts = ref([]) // 公众号账号列表 +const open = ref(false) // 是否显示弹出层 +const userId = ref(0) // 操作的用户编号 +const message = useMessage() // 消息弹窗 +const queryFormRef = ref() // 搜索的表单 + +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + openid: null, + accountId: null, + type: null, + createTime: [] +}) // 是否显示弹出层 + +const getList = async () => { + // 如果没有选中公众号账号,则进行提示。 + if (!queryParams.accountId) { + message.error('未选中公众号,无法查询消息') + return false + } + + loading.value = true + // 执行查询 + getMessagePage(queryParams).then((data) => { + console.log(data) + list.value = data.list + total.value = data.total + loading.value = false + }) +} + +const handleQuery = async () => { + queryParams.pageNo = 1 + getList() +} +const resetQuery = async () => { + queryFormRef.value.resetFields() + // 默认选中第一个 + if (accounts.value.length > 0) { + queryParams.accountId = accounts[0].id + } + handleQuery() +} +const handleSend = async (row) => { + userId.value = row.userId + open.value = true +} +onMounted(() => { + getSimpleAccounts().then((response) => { + accounts.value = response + // 默认选中第一个 + if (accounts.value.length > 0) { + queryParams.accountId = accounts.value[0]['id'] + } + // 加载数据 + getList() + }) +}) +</script>