index.vue 15 KB

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