index.vue 12 KB

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