main.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. <!--
  2. - Copyright (C) 2018-2019
  3. - All rights reserved, Designed By www.joolun.com
  4. 芋道源码:
  5. ① 移除暂时用不到的 websocket
  6. ② 代码优化,补充注释,提升阅读性
  7. -->
  8. <template>
  9. <ContentWrap>
  10. <div class="msg-div" :id="'msg-div' + nowStr">
  11. <!-- 加载更多 -->
  12. <div v-loading="loading"></div>
  13. <div v-if="!loading">
  14. <div class="el-table__empty-block" v-if="loadMore" @click="loadingMore"
  15. ><span class="el-table__empty-text">点击加载更多</span></div
  16. >
  17. <div class="el-table__empty-block" v-if="!loadMore"
  18. ><span class="el-table__empty-text">没有更多了</span></div
  19. >
  20. </div>
  21. <!-- 消息列表 -->
  22. <div class="execution" v-for="item in list" :key="item.id">
  23. <div class="avue-comment" :class="item.sendFrom === 2 ? 'avue-comment--reverse' : ''">
  24. <div class="avatar-div">
  25. <img
  26. :src="item.sendFrom === 1 ? user.avatar : mp.avatar"
  27. class="avue-comment__avatar"
  28. />
  29. <div class="avue-comment__author"
  30. >{{ item.sendFrom === 1 ? user.nickname : mp.nickname }}
  31. </div>
  32. </div>
  33. <div class="avue-comment__main">
  34. <div class="avue-comment__header">
  35. <div class="avue-comment__create_time">{{ parseTime(item.createTime) }}</div>
  36. </div>
  37. <div
  38. class="avue-comment__body"
  39. :style="item.sendFrom === 2 ? 'background: #6BED72;' : ''"
  40. >
  41. <!-- 【事件】区域 -->
  42. <div v-if="item.type === 'event' && item.event === 'subscribe'">
  43. <el-tag type="success">关注</el-tag>
  44. </div>
  45. <div v-else-if="item.type === 'event' && item.event === 'unsubscribe'">
  46. <el-tag type="danger">取消关注</el-tag>
  47. </div>
  48. <div v-else-if="item.type === 'event' && item.event === 'CLICK'">
  49. <el-tag>点击菜单</el-tag>
  50. 【{{ item.eventKey }}】
  51. </div>
  52. <div v-else-if="item.type === 'event' && item.event === 'VIEW'">
  53. <el-tag>点击菜单链接</el-tag>
  54. 【{{ item.eventKey }}】
  55. </div>
  56. <div v-else-if="item.type === 'event' && item.event === 'scancode_waitmsg'">
  57. <el-tag>扫码结果</el-tag>
  58. 【{{ item.eventKey }}】
  59. </div>
  60. <div v-else-if="item.type === 'event' && item.event === 'scancode_push'">
  61. <el-tag>扫码结果</el-tag>
  62. 【{{ item.eventKey }}】
  63. </div>
  64. <div v-else-if="item.type === 'event' && item.event === 'pic_sysphoto'">
  65. <el-tag>系统拍照发图</el-tag>
  66. </div>
  67. <div v-else-if="item.type === 'event' && item.event === 'pic_photo_or_album'">
  68. <el-tag>拍照或者相册</el-tag>
  69. </div>
  70. <div v-else-if="item.type === 'event' && item.event === 'pic_weixin'">
  71. <el-tag>微信相册</el-tag>
  72. </div>
  73. <div v-else-if="item.type === 'event' && item.event === 'location_select'">
  74. <el-tag>选择地理位置</el-tag>
  75. </div>
  76. <div v-else-if="item.type === 'event'">
  77. <el-tag type="danger">未知事件类型</el-tag>
  78. </div>
  79. <!-- 【消息】区域 -->
  80. <div v-else-if="item.type === 'text'">{{ item.content }}</div>
  81. <div v-else-if="item.type === 'voice'">
  82. <wx-voice-player :url="item.mediaUrl" :content="item.recognition" />
  83. </div>
  84. <div v-else-if="item.type === 'image'">
  85. <a target="_blank" :href="item.mediaUrl">
  86. <img :src="item.mediaUrl" style="width: 100px" />
  87. </a>
  88. </div>
  89. <div
  90. v-else-if="item.type === 'video' || item.type === 'shortvideo'"
  91. style="text-align: center"
  92. >
  93. <wx-video-player :url="item.mediaUrl" />
  94. </div>
  95. <div v-else-if="item.type === 'link'" class="avue-card__detail">
  96. <el-link type="success" :underline="false" target="_blank" :href="item.url">
  97. <div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div>
  98. </el-link>
  99. <div class="avue-card__info" style="height: unset">{{ item.description }}</div>
  100. </div>
  101. <!-- TODO 芋艿:待完善 -->
  102. <div v-else-if="item.type === 'location'">
  103. <wx-location
  104. :label="item.label"
  105. :location-y="item.locationY"
  106. :location-x="item.locationX"
  107. />
  108. </div>
  109. <div v-else-if="item.type === 'news'" style="width: 300px">
  110. <!-- TODO 芋艿:待测试;详情页也存在类似的情况 -->
  111. <wx-news :articles="item.articles" />
  112. </div>
  113. <div v-else-if="item.type === 'music'">
  114. <wx-music
  115. :title="item.title"
  116. :description="item.description"
  117. :thumb-media-url="item.thumbMediaUrl"
  118. :music-url="item.musicUrl"
  119. :hq-music-url="item.hqMusicUrl"
  120. />
  121. </div>
  122. </div>
  123. </div>
  124. </div>
  125. </div>
  126. </div>
  127. <div class="msg-send" v-loading="sendLoading">
  128. <wx-reply-select ref="replySelect" :objData="objData" />
  129. <el-button type="success" size="small" class="send-but" @click="sendMsg">发送(S)</el-button>
  130. </div>
  131. </ContentWrap>
  132. </template>
  133. <script lang="ts" name="WxMsg">
  134. import { getMessagePage, sendMessage } from '@/api/mp/message'
  135. import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
  136. import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
  137. import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
  138. import WxNews from '@/views/mp/components/wx-news/main.vue'
  139. import WxLocation from '@/views/mp/components/wx-location/main.vue'
  140. import WxMusic from '@/views/mp/components/wx-music/main.vue'
  141. import { getUser } from '@/api/mp/mpuser'
  142. import { defineComponent } from 'vue'
  143. const message = useMessage() // 消息弹窗
  144. import profile from '@/assets/imgs/profile.jpg'
  145. import wechat from '@/assets/imgs/wechat.png'
  146. import { parseTime } from '@/utils/formatTime'
  147. export default defineComponent({
  148. components: {
  149. WxReplySelect,
  150. WxVideoPlayer,
  151. WxVoicePlayer,
  152. WxNews,
  153. WxLocation,
  154. WxMusic
  155. },
  156. props: {
  157. userId: {
  158. type: Number,
  159. required: true
  160. }
  161. },
  162. setup(props) {
  163. const nowStr = ref(new Date().getTime()) // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处
  164. const loading = ref(false) // 消息列表是否正在加载中
  165. const loadMore = ref(true) // 是否可以加载更多
  166. const list = ref<any[]>([]) // 消息列表
  167. const queryParams = reactive({
  168. pageNo: 1, // 当前页数
  169. pageSize: 14, // 每页显示多少条
  170. accountId: undefined
  171. })
  172. const user = reactive({
  173. // 由于微信不再提供昵称,直接使用“用户”展示
  174. nickname: '用户',
  175. avatar: profile,
  176. accountId: 0 // 公众号账号编号
  177. })
  178. const mp = reactive({
  179. nickname: '公众号',
  180. avatar: wechat
  181. })
  182. // ========= 消息发送 =========
  183. const sendLoading = ref(false) // 发送消息是否加载中
  184. const objData = reactive({
  185. // 微信发送消息
  186. type: 'text',
  187. accountId: null,
  188. articles: []
  189. })
  190. const replySelect = ref(null)
  191. // 执行发送
  192. const sendMsg = async () => {
  193. if (!objData) {
  194. return
  195. }
  196. // // 公众号限制:客服消息,公众号只允许发送一条
  197. if (objData.type === 'news' && objData.articles.length > 1) {
  198. objData.articles = [objData.articles[0]]
  199. message.success('图文消息条数限制在 1 条以内,已默认发送第一条')
  200. }
  201. let data = await sendMessage(Object.assign({ userId: props.userId }, { ...objData }))
  202. sendLoading.value = false
  203. list.value = [...list.value, ...[data]]
  204. scrollToBottom()
  205. //ts檢查的時候會判斷這個組件可能是空的,所以需要進行斷言。
  206. //避免 tab 的数据未清理
  207. const deleteObj = (replySelect.value as any).deleteObj
  208. if (deleteObj) {
  209. deleteObj()
  210. }
  211. }
  212. const loadingMore = () => {
  213. queryParams.pageNo++
  214. getPage(queryParams, null)
  215. }
  216. const getPage = async (page, params) => {
  217. loading.value = true
  218. let dataTemp = await getMessagePage(
  219. Object.assign(
  220. {
  221. pageNo: page.pageNo,
  222. pageSize: page.pageSize,
  223. userId: props.userId,
  224. accountId: page.accountId
  225. },
  226. params
  227. )
  228. )
  229. const msgDiv = document.getElementById('msg-div' + nowStr.value)
  230. let scrollHeight = 0
  231. if (msgDiv) {
  232. scrollHeight = msgDiv.scrollHeight
  233. }
  234. // 处理数据
  235. let data = dataTemp.list.reverse()
  236. list.value = [...data, ...list.value]
  237. loading.value = false
  238. if (data.length < queryParams.pageSize || data.length === 0) {
  239. loadMore.value = false
  240. }
  241. queryParams.pageNo = page.pageNo
  242. queryParams.pageSize = page.pageSize
  243. // 滚动到原来的位置
  244. if (queryParams.pageNo === 1) {
  245. // 定位到消息底部
  246. scrollToBottom()
  247. } else if (data.length !== 0) {
  248. // 定位滚动条
  249. await nextTick(() => {
  250. if (scrollHeight !== 0) {
  251. let div = document.getElementById('msg-div' + nowStr.value)
  252. if (div && msgDiv) {
  253. msgDiv.scrollTop = div.scrollHeight - scrollHeight - 100
  254. }
  255. }
  256. })
  257. }
  258. }
  259. const refreshChange = () => {
  260. getPage(queryParams, null)
  261. }
  262. /** 定位到消息底部 */
  263. const scrollToBottom = () => {
  264. nextTick(() => {
  265. let div = document.getElementById('msg-div' + nowStr.value)
  266. if (div) {
  267. div.scrollTop = div.scrollHeight
  268. }
  269. })
  270. }
  271. onMounted(async () => {
  272. let data = await getUser(props.userId)
  273. user.nickname = data.nickname && data.nickname.length > 0 ? data.nickname : user.nickname
  274. user.avatar = data.avatar && user.avatar.length > 0 ? data.avatar : user.avatar
  275. user.accountId = data.accountId
  276. queryParams.accountId = data.accountId
  277. objData.accountId = data.accountId
  278. refreshChange()
  279. })
  280. return {
  281. sendMsg,
  282. loadingMore,
  283. parseTime,
  284. scrollToBottom,
  285. objData,
  286. mp,
  287. user,
  288. queryParams,
  289. list,
  290. loadMore,
  291. loading,
  292. nowStr,
  293. sendLoading
  294. }
  295. }
  296. })
  297. </script>
  298. <style lang="scss" scoped>
  299. /* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc */
  300. @import './comment.scss';
  301. @import './card.scss';
  302. .msg-main {
  303. margin-top: -30px;
  304. padding: 10px;
  305. }
  306. .msg-div {
  307. height: 50vh;
  308. overflow: auto;
  309. background-color: #eaeaea;
  310. margin-left: 10px;
  311. margin-right: 10px;
  312. }
  313. .msg-send {
  314. padding: 10px;
  315. }
  316. .avatar-div {
  317. text-align: center;
  318. width: 80px;
  319. }
  320. .send-but {
  321. float: right;
  322. margin-top: 8px !important;
  323. }
  324. </style>