mergerMessage-item.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. <template>
  2. <div class="message-wrapper col-2">
  3. <div class="content-wrapper">
  4. <!--文本消息-->
  5. <div class="message-container" v-if="message.contentType === 101">
  6. <div class="text-message" v-for="(item, index) in contentList" :key="index">
  7. <span :key="index" v-if="item.name === 'text'">{{ item.text }}</span>
  8. <img v-else-if="item.name === 'img'" :src="item.src" width="20px" height="20px" :key="index"/>
  9. </div>
  10. </div>
  11. <!--图片消息-->
  12. <div class="message-container" v-else-if="message.contentType === 102">
  13. <img class="image-element" :src="message.pictureElem.sourcePicture.url" @load="onImageLoaded" @click="handlePreview()" />
  14. </div>
  15. <!--文件消息-->
  16. <div class="message-container" v-else-if="message.type === 105">
  17. <div class="file-element-wrapper" title="单击下载" @click="downloadFile">
  18. <div class="file-box">
  19. <i class="el-icon-document file-icon"></i>
  20. <div class="file-element">
  21. <span class="file-name">{{ message.fileElem.fileName }}</span>
  22. <span class="file-size">{{ message.fileElem.fileSize }}</span>
  23. </div>
  24. </div>
  25. </div>
  26. </div>
  27. <!--表情消息-->
  28. <div class="message-container" v-else-if="message.type === 115">
  29. <img :src="faceUrl"/>
  30. </div>
  31. <!--视频消息-->
  32. <div class="message-container" v-else-if="message.contentType === 104">
  33. <video
  34. :src="message.videoElem.videoUrl"
  35. controls
  36. class="merger-video"
  37. @error="videoError"
  38. ></video>
  39. </div>
  40. <!--音频消息-->
  41. <div class="sound-element-wrapper" v-else-if="message.contentType === 103" :title="playStatus === 'playing' ? '单击暂停' : '单击播放'" @click="handleClick">
  42. <i class="iconfont icon-voice"></i>
  43. {{ message.soundElem.duration}}
  44. </div>
  45. <!--自定义消息-->
  46. <div class="message-container" v-else-if="message.contentType === 110">
  47. <div class="custom-element-wrapper">
  48. <div class="survey" v-if="this.payload.data === 'survey'">
  49. <div class="title">对IM DEMO的评分和建议</div>
  50. <el-rate
  51. v-model="rate"
  52. disabled
  53. show-score
  54. text-color="#ff9900"
  55. score-template="{value}">
  56. </el-rate>
  57. <div class="suggestion">{{this.payload.extension}}</div>
  58. </div>
  59. <span class="text" title="您可以自行解析自定义消息" v-else>
  60. <template >{{translateCustomMessage(this.payload)}}</template>
  61. </span>
  62. </div>
  63. </div>
  64. <!--合并的消息-->
  65. <div class="message-container" @click="mergerHandler(message)" v-else-if="message.contentType === 107">
  66. <div class="merger-item">
  67. <p class="merger-title">{{message.mergeElem.title}}</p>
  68. <p class="merger-text" v-for="(item, index) in message.mergeElem.abstractList" :key="index">
  69. {{item}}
  70. </p>
  71. </div>
  72. </div>
  73. </div>
  74. </div>
  75. </template>
  76. <script>
  77. import { mapState } from 'vuex'
  78. import { decodeText } from '../../../utils/decodeText'
  79. import { getFullDate } from '../../../utils/date'
  80. import { Rate } from 'element-ui'
  81. export default {
  82. name: 'MessageItem',
  83. props: {
  84. message: {
  85. type: Object,
  86. required: true
  87. },
  88. payload: {
  89. type: Object,
  90. default: () => ({})
  91. }
  92. },
  93. components: {
  94. ElRate: Rate,
  95. },
  96. data() {
  97. return {
  98. renderDom: [],
  99. showConversationList: false,
  100. relayMessage: {},
  101. selectedConversation: [],
  102. messageSelected:[],
  103. amr: null,
  104. audio: null,
  105. isAMR: false,
  106. playStatus: 'stopped' // 'playing' | 'paused' | 'stopped'
  107. }
  108. },
  109. computed: {
  110. url() {
  111. return this.message.soundElem.sourceUrl
  112. },
  113. second() {
  114. return this.message.soundElem.duration
  115. },
  116. ...mapState({
  117. currentConversation: state => state.conversation.currentConversation,
  118. currentUserProfile: state => state.imuser.currentUserProfile,
  119. isShowConversationList: state => state.conversation.isShowConversationList,
  120. }),
  121. // 自定义消息
  122. rate() {
  123. return parseInt(this.payload.description)
  124. },
  125. // 图片消息
  126. imageUrl() {
  127. const url = this.message.pictureElem.sourcePicture.url
  128. if (typeof url !== 'string') {
  129. return ''
  130. }
  131. return url.slice(0, 2) === '//' ? `https:${url}` : url
  132. },
  133. // showProgressBar() {
  134. // return this.$parent.message.status === 'unSend'
  135. // },
  136. percentage() {
  137. return Math.floor((this.$parent.message.progress || 0) * 100)
  138. },
  139. // 表情消息
  140. faceUrl() {
  141. let name = ''
  142. if (this.payload.data.indexOf('@2x') > 0) {
  143. name = this.payload.data
  144. } else {
  145. name = this.payload.data + '@2x'
  146. }
  147. return `https://web.sdk.qcloud.com/im/assets/face-elem/${name}.png`
  148. },
  149. // 时间换算
  150. date() {
  151. return getFullDate(new Date(this.message.time * 1000))
  152. },
  153. // 文件消息大小
  154. fileSize() {
  155. const size = this.message.fileElem.fileSize
  156. if (size > 1024) {
  157. if (size / 1024 > 1024) {
  158. return `${this.toFixed(size / 1024 / 1024)} Mb`
  159. }
  160. return `${this.toFixed(size / 1024)} Kb`
  161. }
  162. return `${this.toFixed(size)}B`
  163. },
  164. // 消息昵称
  165. from() {
  166. const isC2C = this.currentConversation.type === 1
  167. // 自己发送的用昵称渲染
  168. if (this.isMine) {
  169. return this.currentUserProfile.nick || this.currentUserProfile.userID
  170. }
  171. // 1. C2C 的消息体中还无 nick / avatar 字段,需从 conversation.userProfile 中获取
  172. if (isC2C) {
  173. return (
  174. this.currentConversation.userProfile.nick ||
  175. this.currentConversation.userProfile.userID
  176. )
  177. }
  178. // 2. 群组消息,用消息体中的 nick 渲染。nameCard暂时支持不完善
  179. return this.message.nameCard || this.message.nick || this.message.from
  180. },
  181. avatar() {
  182. if (this.currentConversation.type === 'C2C') {
  183. return this.isMine
  184. ? this.currentUserProfile.avatar
  185. : this.currentConversation.userProfile.avatar
  186. } else if (this.currentConversation.type === 'GROUP') {
  187. return this.isMine
  188. ? this.currentUserProfile.avatar
  189. : this.message.avatar
  190. } else {
  191. return ''
  192. }
  193. },
  194. currentConversationType() {
  195. return this.currentConversation.type
  196. },
  197. isMine() {
  198. return this.message.flow === 'out'
  199. },
  200. contentList() {
  201. console.log("this.payload",this.payload)
  202. return decodeText(this.payload)
  203. },
  204. },
  205. methods: {
  206. // 自定义消息解析
  207. translateCustomMessage(payload) {
  208. let videoPayload = {}
  209. try{
  210. videoPayload = JSON.parse(payload.data)
  211. } catch(e) {
  212. videoPayload = {}
  213. }
  214. if (payload.data === 'group_create') {
  215. return `${payload.extension}`
  216. }
  217. if (videoPayload.roomId) {
  218. videoPayload.roomId = videoPayload.roomId.toString()
  219. videoPayload.isFromGroupLive = 1
  220. return videoPayload
  221. }
  222. if(payload.text) {
  223. return payload.text
  224. }else{
  225. return '[自定义消息]'
  226. }
  227. },
  228. // 图片消息
  229. onImageLoaded(event) {
  230. this.$bus.$emit('image-loaded', event)
  231. },
  232. handlePreview() {
  233. this.$bus.$emit('image-preview', {
  234. url: this.message.pictureElem.sourcePicture.url,
  235. flag: true
  236. })
  237. },
  238. toFixed(number, precision = 2) {
  239. return number.toFixed(precision)
  240. },
  241. showGroupMemberProfile(event) {
  242. this.tim
  243. .getGroupMemberProfile({
  244. groupID: this.message.to,
  245. userIDList: [this.message.from]
  246. })
  247. .then(({ data: { memberList } }) => {
  248. if (memberList[0]) {
  249. this.$bus.$emit('showMemberProfile', { event, member: memberList[0] })
  250. }
  251. })
  252. },
  253. messageClick(message) {
  254. this.$store.commit('showConversationList', false)
  255. this.showConversationList = true
  256. this.relayMessage = message // 需要深拷贝吗?
  257. },
  258. showMergerMessage() {
  259. this.$bus.$emit('mergerMessage', true)
  260. },
  261. cancel() {
  262. this.showConversationList = false
  263. },
  264. getList(value) {
  265. this.selectedConversation = value
  266. },
  267. messageRelay() {
  268. let type = ''
  269. let toUserId = ''
  270. this.selectedConversation.forEach((item) => {
  271. if(item.indexOf(this.OpenIM.TYPES.CONV_C2C) !== -1) {
  272. type = 1
  273. toUserId = item.substring(3,item.length)
  274. }
  275. if(item.indexOf(3) !== -1) {
  276. type = 3
  277. toUserId = item.substring(5,item.length)
  278. }
  279. const message = this.tim.createForwardMessage({
  280. to: toUserId,
  281. conversationType: type,
  282. payload: this.relayMessage,
  283. priority: this.OpenIM.TYPES.MSG_PRIORITY_NORMAL
  284. })
  285. this.tim.sendMessage(message).catch(imError => {
  286. this.$store.commit('showMessage', {
  287. message: imError.message,
  288. type: 'error'
  289. })
  290. })
  291. this.showConversationList = false
  292. })
  293. },
  294. // 合并的消息
  295. mergerHandler(message) {
  296. console.log("setMergerMessage",message)
  297. this.$store.commit('setMergerMessage', message)
  298. // this.$bus.$emit('mergerMessage', message)
  299. },
  300. // 视频消息
  301. videoError(e) {
  302. this.$store.commit('showMessage', { type: 'error', message: '视频出错,错误原因:' + e.target.error.message })
  303. },
  304. // 音频消息
  305. play() {
  306. this.cleanup()
  307. // 默认使用 HTML5 audio 播放
  308. this.audio = new Audio(this.url)
  309. console.log(this.audio)
  310. this.audio.addEventListener('error', this.tryPlayAMR)
  311. this.audio.addEventListener('ended', this.cleanup)
  312. this.audio.play()
  313. .then(() => {
  314. this.playStatus = 'playing'
  315. })
  316. .catch(() => {
  317. // 播放失败 fallback 到 tryPlayAMR
  318. })
  319. },
  320. handleClick() {
  321. console.log("this.message.soundElem.sourceUrl",this.message.soundElem.sourceUrl)
  322. if (this.playStatus === 'playing') {
  323. this.pause()
  324. } else if (this.playStatus === 'paused') {
  325. this.resume()
  326. } else {
  327. this.play()
  328. }
  329. },
  330. pause() {
  331. if (this.isAMR && this.amr) {
  332. this.amr.pause()
  333. } else if (this.audio) {
  334. this.audio.pause()
  335. }
  336. this.playStatus = 'paused'
  337. },
  338. resume() {
  339. if (this.isAMR && this.amr) {
  340. this.amr.play()
  341. } else if (this.audio) {
  342. this.audio.play()
  343. }
  344. this.playStatus = 'playing'
  345. },
  346. tryPlayAMR() {
  347. this.isAMR = true
  348. const isIE = /MSIE|Trident|Edge/.test(window.navigator.userAgent)
  349. if (isIE) {
  350. this.$store.commit('showMessage', {
  351. message: '您的浏览器不支持该格式的语音消息播放,请尝试更换浏览器,建议使用:谷歌浏览器',
  352. type: 'warning'
  353. })
  354. return
  355. }
  356. if (!window.BenzAMRRecorder) {
  357. const script = document.createElement('script')
  358. script.addEventListener('load', this.playAMR)
  359. script.src = '/BenzAMRRecorder.js'
  360. document.head.appendChild(script)
  361. return
  362. }
  363. this.playAMR()
  364. },
  365. playAMR() {
  366. if (!this.amr && window.BenzAMRRecorder) {
  367. this.amr = new window.BenzAMRRecorder()
  368. this.amr.onEnded(() => {
  369. this.cleanup()
  370. })
  371. }
  372. if (this.amr.isInit()) {
  373. this.amr.play()
  374. this.playStatus = 'playing'
  375. } else {
  376. this.amr.initWithUrl(this.url).then(() => {
  377. this.amr.play()
  378. this.playStatus = 'playing'
  379. })
  380. }
  381. },
  382. cleanup() {
  383. if (this.audio) {
  384. this.audio.pause()
  385. this.audio = null
  386. }
  387. if (this.amr) {
  388. this.amr.stop()
  389. }
  390. this.playStatus = 'stopped'
  391. },
  392. // 文件消息
  393. downloadFile() {
  394. const fileUrl = this.message.fileElem.sourceUrl;
  395. const fileName = this.message.fileElem.fileName;
  396. console.log(fileUrl)
  397. fetch(fileUrl)
  398. .then(response => {
  399. if (!response.ok) throw new Error(`下载失败,状态码:${response.status}`);
  400. return response.blob();
  401. })
  402. .then(blob => {
  403. /*const mime = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
  404. const docBlob = new Blob([blob], { type: mime });*/ // 💡 明确指定 MIME 类型
  405. const blobUrl = window.URL.createObjectURL(blob);
  406. const a = document.createElement('a');
  407. a.href = blobUrl;
  408. a.download = fileName;
  409. document.body.appendChild(a);
  410. a.click();
  411. document.body.removeChild(a);
  412. window.URL.revokeObjectURL(blobUrl); // 释放资源
  413. })
  414. .catch(err => {
  415. console.error('下载失败:', err);
  416. });
  417. }
  418. }
  419. }
  420. </script>
  421. <style lang="stylus" scoped>
  422. .conversation-container {
  423. position absolute
  424. top 0
  425. left 0px
  426. width 100%
  427. background-color #fff
  428. z-index 999
  429. }
  430. .conversation-list-btn {
  431. width 140px
  432. display flex
  433. float right
  434. margin 10px 0
  435. .conversation-btn {
  436. cursor pointer
  437. padding 6px 12px
  438. background #00A4FF
  439. color #ffffff
  440. font-size 14px
  441. border-radius 20px
  442. margin-left 13px
  443. }
  444. }
  445. .message-wrapper {
  446. margin: 5px 5px 10px 5px;
  447. .content-wrapper {
  448. display: flex
  449. align-items: center
  450. .message-container {
  451. width 100%
  452. .text-message {
  453. padding 3px 10px
  454. }
  455. .image-element {
  456. max-height 300px
  457. }
  458. .merger-item {
  459. border 1px solid #DEDEDE
  460. background-color #ffffff
  461. padding 0 10px
  462. border-radius 6px
  463. .merger-title {
  464. font-size 15px
  465. max-width 180px
  466. overflow hidden;
  467. text-overflow ellipsis;
  468. white-space nowrap;
  469. }
  470. .merger-text {
  471. color #B3B3B3
  472. margin 10px 0
  473. font-size 13px
  474. max-width 280px
  475. overflow hidden;
  476. text-overflow ellipsis;
  477. white-space nowrap;
  478. }
  479. }
  480. }
  481. }
  482. }
  483. .group-layout, .c2c-layout, .system-layout {
  484. display: flex;
  485. .col-1 {
  486. .avatar {
  487. width: 56px;
  488. height: 56px;
  489. border-radius: 50%;
  490. box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.1);
  491. }
  492. }
  493. .group-member-avatar {
  494. cursor: pointer;
  495. }
  496. .col-2 {
  497. display: flex;
  498. flex-direction: column;
  499. // max-width 50% // 此设置可以自适应宽度,目前由bubble限制
  500. }
  501. .col-3 {
  502. width: 30px;
  503. }
  504. &.position-left {
  505. .col-2 {
  506. align-items: flex-start;
  507. }
  508. }
  509. &.position-right {
  510. flex-direction: row-reverse;
  511. .col-2 {
  512. align-items: flex-end;
  513. }
  514. }
  515. &.position-center {
  516. justify-content: center;
  517. }
  518. }
  519. .c2c-layout {
  520. .col-2 {
  521. .base {
  522. margin-top: 3px;
  523. }
  524. }
  525. }
  526. .group-layout {
  527. .col-2 {
  528. .chat-bubble {
  529. margin-top: 5px;
  530. outline none
  531. }
  532. }
  533. }
  534. .right {
  535. display: flex;
  536. flex-direction: row-reverse;
  537. }
  538. .left {
  539. display: flex;
  540. flex-direction: row;
  541. }
  542. .base {
  543. color: $secondary;
  544. font-size: 12px;
  545. }
  546. .name {
  547. padding: 0 4px;
  548. max-width: 100px;
  549. overflow: hidden;
  550. text-overflow: ellipsis;
  551. white-space: nowrap;
  552. }
  553. .merger-video {
  554. width 100%
  555. max-height 300px
  556. }
  557. .file-box {
  558. display: flex;
  559. }
  560. .file-icon {
  561. font-size: 40px !important;
  562. }
  563. .file-element {
  564. display: flex;
  565. flex-direction: column;
  566. margin-left: 12px;
  567. }
  568. .file-size {
  569. font-size: 12px;
  570. padding-top 5px
  571. }
  572. .text
  573. font-weight bold
  574. .title
  575. font-size 16px
  576. font-weight 600
  577. padding-bottom 10px
  578. .survey
  579. background-color white
  580. color black
  581. padding 20px
  582. display flex
  583. flex-direction column
  584. .suggestion
  585. padding-top 10px
  586. font-size 14px
  587. .sound-element-wrapper {
  588. background-color #fff
  589. padding 2px 13px
  590. cursor pointer
  591. border-radius 3px
  592. }
  593. </style>