index.vue 13 KB

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