index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. <template>
  2. <div v-if="repealMsg" class="msg-item-common msg-repeal-item">
  3. {{repealStr}}
  4. </div>
  5. <div v-else-if="msg_type == 5 && joinMsg" class="msg-item-common msg-join-item">
  6. <span>{{joinMsg}}</span>
  7. </div>
  8. <redPack-tip
  9. v-else-if="msg_type == 5 && redpackGrapInfo"
  10. :info="redpackGrapInfo"
  11. :class="[{'show-full':isShowFullInfo || (timeMsg && timestamp)}]">
  12. </redPack-tip>
  13. <div class="msg-item-common msg-item clearfix" :class="[{'show-full':isShowFullInfo || (timeMsg && timestamp)},type]" v-else>
  14. <msg-time :timestamp="timestamp" v-if="timeMsg && timestamp"></msg-time>
  15. <img v-if="avatarUrl" class="user-avatar avatar" src="../../assets/loading.gif" :originurl="avatarUrl" @click="clickInfo" alt>
  16. <div v-else
  17. class="avatar"
  18. :class="'avatar_bg' + userId % 9"
  19. :data-name="showName.slice(0,2).toUpperCase()"
  20. @click="clickInfo"
  21. ></div>
  22. <div class="content">
  23. <div class="metabar">
  24. <span class="name" @contextmenu.prevent="onToolBtn($event,'username')">{{msgItem.ext_info && msgItem.ext_info.event_type}} {{showName}}</span>
  25. <span class="admin" v-if="creator == userId">
  26. <i class="icon-creator" v-if="type === 'me'"></i>
  27. {{$t('public.owner')}}
  28. <i class="icon-creator" v-if="type === 'you'"></i>
  29. </span>
  30. <span class="admin" v-else-if="adminList.includes(Number(userId))">
  31. <i class="el-icon-star-on" v-if="type === 'me'"></i>
  32. {{$t('public.admin')}}
  33. <i class="el-icon-star-on" v-if="type === 'you'"></i>
  34. </span>
  35. <i class="icon-tele" v-if="isFromTg"></i>
  36. </div>
  37. <red-packet
  38. v-if="msg_type == 4 && msgItem"
  39. @click.native="$packetGet(msgItem)"
  40. :info="msgItem">
  41. <span class="time">{{timestamp|formatTimestamp}}</span>
  42. </red-packet>
  43. <template v-else>
  44. <bubble-wrap
  45. :isMobile="isMobile"
  46. :showToolbar="showToolbar"
  47. @onTouchStartToolBtn="onTouchStartToolBtn"
  48. @onTouchEndToolBtn="onTouchEndToolBtn"
  49. @onToolBtn="onToolBtn"
  50. class="bubble-wrap"
  51. >
  52. <span class="time">{{timestamp|formatTimestamp}}</span>
  53. <p v-if="isFromTg && msgItem.ext_info.tg_nick" class="tg-name">{{msgItem.ext_info.tg_nick}}:</p>
  54. <i class="loading-icon" v-if="loading"></i>
  55. <i class="error-icon" v-if="fail" @click="reSend"></i>
  56. <a :href="content" target="_blank" v-if="msg_type == 1 && meechatType=='mini' && !isMobile" class="img-msg-wrap">
  57. <img class="img-msg"
  58. :style="{width:width,height:height}"
  59. src="" :originurl="formatUploadImg(content)"
  60. >
  61. <i class="pic-loading2" :style="{background:contentBg}"></i>
  62. </a>
  63. <div v-else-if="msg_type == 1" class="img-msg-wrap">
  64. <img
  65. @click="$showImgPreview(content)"
  66. class="img-msg isLoading"
  67. :style="{width:width,height:height}"
  68. src="" :originurl="formatUploadImg(content)"
  69. >
  70. <i class="pic-loading2" :style="{background:contentBg}"></i>
  71. </div>
  72. <video
  73. class="video-msg"
  74. :class="{'limit-height': msg_type == 3}"
  75. controls="controls"
  76. preload="meta"
  77. :poster="msgItem.ext_info && msgItem.ext_info.cover_url"
  78. v-else-if="msg_type == 2 || msg_type == 3"
  79. :src="content"
  80. ></video>
  81. <pre v-else class="text" v-html="content"></pre>
  82. <template v-if="toolBtnType=='username'">
  83. <ul @touchstart.stop class="pub-pop-toolbar ext-username" v-show="showToolbar">
  84. <li @click.prevent="handleCopy">{{$t('chat.copy')}}</li>
  85. </ul>
  86. </template>
  87. <template v-else>
  88. <ul @touchstart.stop class="pub-pop-toolbar username" v-show="showToolbar">
  89. <li @click.prevent="handleQuote" v-if="msg_type == 0 || msg_type == 4">{{$t('chat.quote')}}</li>
  90. <li @click.prevent="handleCopy">{{$t('chat.copy')}}</li>
  91. <!-- <li @click.prevent="handleDel">删除</li> -->
  92. <li class="split-line" v-if="(isAdmin && type === 'you') || (isAdmin || revoke)"></li>
  93. <li @click.prevent="handlePingMsg" v-if="isAdmin">{{$t('chat.sticky')}}</li>
  94. <li @click.prevent="handleBlock" v-if="isAdmin && type === 'you'">{{block?$t('chat.liftaBan'):$t('public.ban')}}</li>
  95. <li @click.prevent="handleRevoke" v-if="isAdmin || revoke">{{$t('chat.revoke')}}</li>
  96. </ul>
  97. </template>
  98. </bubble-wrap>
  99. </template>
  100. </div>
  101. </div>
  102. </template>
  103. <script>
  104. import dayjs from 'dayjs'
  105. import msgTime from '@/components/msgItem/time'
  106. import redPacket from '@/components/msgItem/redPacket'
  107. import redPackTip from '@/components/msgItem/redPackTip'
  108. import bubbleWrap from '@/components/msgItem/bubbleWrap'
  109. import { mapMutations, mapActions, mapState } from 'vuex'
  110. import { getMeechatType, setUserOpt } from '@/util/util'
  111. export default {
  112. name: 'msgItem',
  113. components: {
  114. msgTime,
  115. redPacket,
  116. redPackTip,
  117. bubbleWrap
  118. },
  119. props: {
  120. msgItem: Object,
  121. isPrivate: Boolean,
  122. repealMsg: Boolean,
  123. from: [String, Number],
  124. timeMsg: Boolean,
  125. avatar: {
  126. type: String
  127. },
  128. name: {
  129. type: String
  130. },
  131. timestamp: [String, Number],
  132. hash: String,
  133. content: {
  134. type: [String, Number, Object]
  135. },
  136. userId: [String, Number],
  137. /**
  138. * 消息来源 {me: 我发的, you: 其他人发的}
  139. */
  140. type: {
  141. type: String
  142. },
  143. /**
  144. * 消息种类 (1 => 图片, 2 => 视频, 3 => 音频, 4 => 链接, 5 => 红包)
  145. */
  146. msg_type: {
  147. type: [Number, String]
  148. },
  149. createTime: [Number],
  150. loading: [Boolean],
  151. fail: [Boolean],
  152. res: [File, Blob],
  153. isMobile: Boolean,
  154. isAdmin: Boolean
  155. },
  156. data () {
  157. return {
  158. showToolbar: false,
  159. revoke: false,
  160. block: false,
  161. revokeTimeAllow: false,
  162. width: 'auto',
  163. height: 'auto',
  164. longTapTimer: null,
  165. meechatType: getMeechatType(), // meechat版本
  166. toolBtnType: '',
  167. contentBg: ''// 图片背景色
  168. }
  169. },
  170. watch: {
  171. content (val, oldVal) {
  172. if (this.msg_type == 1 && val != oldVal) this.countPicSize()
  173. }
  174. },
  175. computed: {
  176. ...mapState({
  177. curSession: state => state.curSession,
  178. myId: state => state.userId,
  179. userInfo: state => state.group.userInfo,
  180. blockList: state => state.group.blockList,
  181. adminList: state => state.group.adminList,
  182. members: state => state.group.members,
  183. creator: state => state.group.creator,
  184. lastMsgUid: state => state.group.lastMsgUid
  185. }),
  186. isLogin () {
  187. return !!this.myId
  188. },
  189. isShowFullInfo () {
  190. return this.msgItem.isShowFullInfo
  191. },
  192. repealStr () {
  193. if (this.repealMsg) {
  194. if (!this.from || this.from == this.userId) {
  195. return `${this.type == 'me' ? this.$t('public.you') : this.name}${this.$t('chat.revokeMsg')}`
  196. } else if (this.from != this.userId) {
  197. let admin = this.members[this.from]
  198. let adminName = admin ? admin.nick_name : this.$t('public.admin')
  199. return `${adminName}${this.$t('chat.revoked')}${this.name}${this.$t('chat.aMsg')}`
  200. } else {
  201. return `${this.name}${this.$t('chat.revokeMsg')}`
  202. }
  203. } else {
  204. return ''
  205. }
  206. },
  207. avatarUrl () {
  208. let membersCover = this.members[this.userId] && this.members[this.userId].cover_photo
  209. return membersCover || this.avatar || ''
  210. },
  211. isFromTg () {
  212. return this.msgItem.ext_info && this.msgItem.ext_info.is_tg
  213. },
  214. joinMsg () {
  215. if (this.msg_type != 5) return
  216. let extInfo = this.msgItem.ext_info
  217. let username = (this.members[this.userId] && this.members[this.userId].nick_name) || this.name || ''
  218. let name = this.userId == this.myId ? this.$t('public.you') : username
  219. if (extInfo && extInfo.event_type == 'leave_group') {
  220. return `${name} ${this.$t('group.quitGroup')}`
  221. } else if (extInfo && extInfo.event_type == 'join') {
  222. return `${name} ${this.$t('chat.joinGroup')}`
  223. } else return ``
  224. },
  225. redpackGrapInfo () {
  226. if (this.msg_type != 5) return
  227. if (this.msgItem.ext_info && this.msgItem.ext_info.event_type != 'grab_redpack') return
  228. return this.msgItem.ext_info
  229. },
  230. showName () {
  231. return (this.members[this.userId] && this.members[this.userId].nick_name) || this.name || ''
  232. }
  233. },
  234. beforeMount () {
  235. },
  236. created () {
  237. if (this.msg_type == 1) this.countPicSize()
  238. if (this.msg_type == 5) setUserOpt('lastShowMsgUid', 0)
  239. },
  240. methods: {
  241. ...mapMutations([
  242. 'setCopyText',
  243. 'updateChatInputFocus',
  244. 'reSendChatItem',
  245. 'setSessionRepeal',
  246. 'setLastMsgUid'
  247. ]),
  248. ...mapActions([
  249. 'doRepealPersonMsg',
  250. 'doRepealGroupMsg',
  251. 'doBlockUser',
  252. 'doUnBlockUser',
  253. 'doPinMsg',
  254. 'doSendMsg',
  255. 'doSendFile'
  256. ]),
  257. clickInfo () {
  258. if (!this.isLogin) return
  259. if (this.meechatType == 'h5') {
  260. let infoUrl = this.type === 'me' ? '/me' : `/other/${this.userId}`
  261. this.$router.push(infoUrl)
  262. } else {
  263. this.type === 'me' ? this.$showUserInfo() : this.$showOtherInfo(this.userId)
  264. }
  265. },
  266. // 计算图片尺寸,背景色
  267. countPicSize () {
  268. let rect = /_size([0-9]+)x([0-9]+)/.exec(this.content)
  269. if (rect) {
  270. let originalWidth = parseInt(rect[1])
  271. let originalHeight = parseInt(rect[2])
  272. // let holderWidth = (document.body.offsetWidth - 35) * 0.84
  273. let scaleX = originalWidth > 400 ? 400 / originalWidth : 1
  274. let scaleY = originalHeight > 250 ? 250 / originalHeight : 1
  275. let scale = Math.min(scaleX, scaleY)
  276. this.width = scale * originalWidth + 'px'
  277. this.height = scale * originalHeight + 'px'
  278. }
  279. let bg = this.content.match(/_[^(size|len)].+?.(jpg|png)/gi)
  280. this.contentBg = ['#', bg && bg[0].replace(/_|.(jpg|png)/g, '')].join('')
  281. },
  282. formatUploadImg (val) {
  283. if (/^data:image/.test(val)) return val
  284. else return `${val}?imageview/0/w/400`
  285. },
  286. hideToolbar (event) {
  287. if (this.showToolbar !== false) {
  288. this.showToolbar = false
  289. document.body.removeEventListener('touchstart', this.hideToolbar, false)
  290. document.body.removeEventListener('click', this.hideToolbar, false)
  291. document.body.removeEventListener('contextmenu', this.hideToolbar, false)
  292. }
  293. },
  294. /**
  295. * @des 触发自定义右键菜单
  296. * @param {string} type [{'username':仅复制}]
  297. */
  298. onToolBtn (event, type) {
  299. this.toolBtnType = type
  300. if (this.showToolbar) {
  301. this.hideToolbar(event)
  302. return
  303. }
  304. if (!this.isMobile) {
  305. setTimeout(() => {
  306. document.body.addEventListener('click', this.hideToolbar, false)
  307. document.body.addEventListener('contextmenu', this.hideToolbar, false)
  308. }, 0)
  309. }
  310. this.showToolbar = true
  311. this.block = this.blockList.some(id => id == this.userId)
  312. this.revokeTimeAllow =
  313. Date.now() - parseInt(this.timestamp) < 1e3 * 60 * 3
  314. this.revoke = this.type === 'me' && this.revokeTimeAllow
  315. },
  316. onTouchStartToolBtn (event) {
  317. clearTimeout(this.longTapTimer)
  318. this.longTapTimer = setTimeout(() => {
  319. this.onToolBtn(event)
  320. }, 800)
  321. },
  322. onTouchEndToolBtn (event) {
  323. clearTimeout(this.longTapTimer)
  324. setTimeout(() => {
  325. document.body.addEventListener('touchstart', this.hideToolbar, false)
  326. document.body.addEventListener('click', this.hideToolbar, false)
  327. }, 0)
  328. },
  329. // 引用时转换表情标签
  330. replaceEmoji (content) {
  331. let emojiReg = /<img class="emoji" .+?\/>/gi
  332. return content.replace(emojiReg, function (match) {
  333. let emoji = match.match(/alt=.+?&*"/g)
  334. let emojiCont = emoji && emoji[0].replace(/"|alt=|/g, '')
  335. return emojiCont
  336. })
  337. },
  338. // 引用时转换链接标签
  339. replaceLink (content) {
  340. let linkReg = /<a href=".+?" class="link text" target="_blank">.+?<\/a>/gi
  341. return content.replace(linkReg, function (match) {
  342. let link = match.match(/>.+?&*<\/a>/g)
  343. let linkCont = link && link[0].replace(/>|<\/a>|/g, '')
  344. return linkCont
  345. })
  346. },
  347. handleQuote () {
  348. let { name, content } = this
  349. let newCont = this.replaceLink(this.replaceEmoji(content))
  350. let quoteStr = `「${name}:${newCont}」\n- - - - - - - - - - - - - - -\n`
  351. this.$emit('quoteMsg', quoteStr)
  352. this.$nextTick(() => {
  353. this.updateChatInputFocus(true)
  354. })
  355. },
  356. handleCopy () {
  357. let userSelection
  358. let selectedText = ''
  359. if (window.getSelection) { // 现代浏览器
  360. userSelection = window.getSelection()
  361. selectedText = userSelection.toString()
  362. } else if (document.selection) { // IE浏览器 考虑到Opera,应该放在后面
  363. userSelection = document.selection.createRange()
  364. selectedText = userSelection.text
  365. }
  366. let copyTxt = this.replaceLink(this.replaceEmoji(selectedText || this.content))
  367. this.$copyText(copyTxt).then(
  368. e => {
  369. this.updateChatInputFocus(true)
  370. },
  371. e => {
  372. console.log('Can not copy')
  373. }
  374. )
  375. this.setCopyText(copyTxt)
  376. },
  377. handleShare () {
  378. this.$showInvite(this.content)
  379. },
  380. handleDel () {
  381. this.$emit('deleteMsg', this.hash)
  382. },
  383. handlePingMsg () {
  384. this.doPinMsg({ hash: this.hash })
  385. },
  386. async handleRevoke () {
  387. if (this.isPrivate) {
  388. await this.doRepealPersonMsg({ hash: this.hash }).then((data) => {
  389. this.$store.commit('setSessionRepeal', {
  390. me: true,
  391. sessionId: this.curSession
  392. })
  393. this.$store.commit('repealChatItem', {
  394. hash: this.hash,
  395. from: this.from
  396. })
  397. })
  398. } else {
  399. await this.doRepealGroupMsg({ hash: this.hash })
  400. }
  401. },
  402. handleBlock () {
  403. if (this.block) {
  404. this.doUnBlockUser({ id: this.userId })
  405. } else {
  406. this.doBlockUser({ id: this.userId })
  407. }
  408. },
  409. reSend () {
  410. if (this.msg_type == 0 || this.msg_type == 4) {
  411. let opt = {
  412. type: 0,
  413. msg: this.content,
  414. createTime: this.createTime
  415. }
  416. this.reSendChatItem({ createTime: this.createTime })
  417. if (this.isPrivate) {
  418. this.doSendPrivateMsg(opt)
  419. } else {
  420. this.doSendMsg(opt)
  421. }
  422. } else {
  423. let opt = {
  424. res: this.res,
  425. createTime: this.createTime
  426. }
  427. this.reSendChatItem({ createTime: this.createTime })
  428. this.doSendFile(opt)
  429. }
  430. }
  431. },
  432. filters: {
  433. formatTimestamp (val) {
  434. if (!val) return ''
  435. return dayjs(val * 1).format('HH:mm')
  436. }
  437. }
  438. }
  439. </script>
  440. <style lang="scss">
  441. @import "./style.scss"
  442. </style>