index.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. <template>
  2. <div v-if="repealMsg" class="msg-repeal-item">
  3. {{repealStr}}
  4. </div>
  5. <redPack-tip
  6. v-else-if="msgItem && msgItem.redPackTip"
  7. :info="msgItem">
  8. </redPack-tip>
  9. <div class="msg-item clearfix" :class="type" v-else>
  10. <msg-time :timestamp="timestamp" v-if="timeMsg"></msg-time>
  11. <img
  12. v-if="avatar"
  13. class="user-avatar avatar"
  14. :src="avatar"
  15. @click="$showOtherInfo(userId)"
  16. alt
  17. >
  18. <div
  19. v-else
  20. class="avatar"
  21. :class="'avatar_bg' + userId % 9"
  22. :data-name="name.slice(0,2).toUpperCase()"
  23. @click="$showOtherInfo(userId)"
  24. ></div>
  25. <div class="content">
  26. <div class="metabar">
  27. <span class="name">{{name}}</span>
  28. <span class="time">{{timestamp|formatTimestamp}}</span>
  29. </div>
  30. <red-packet
  31. v-if="msg_type == 4 && msgItem"
  32. @click.native="$packetGet(msgItem)"
  33. :info="msgItem">
  34. </red-packet>
  35. <div v-else class="bubble" :class="{focus:showToolbar}" @contextmenu.prevent.stop="onToolBtn">
  36. <i class="loading-icon" v-if="loading"></i>
  37. <i class="error-icon" v-if="fail" @click="reSend"></i>
  38. <img @click="$showImgPreview(content)" class="img-msg" v-if="msg_type == 1" :style="{width:width,height:height}" :src="content" @load="imgLoad">
  39. <video class="video-msg"
  40. :class="{'limit-height': msg_type == 3}"
  41. controls="controls"
  42. preload="meta"
  43. v-else-if="msg_type == 2 || msg_type == 3"
  44. :src="content">
  45. </video>
  46. <pre class="text" v-else-if="msg_type == 10" v-html="content"></pre>
  47. <pre class="text" v-else>{{content}}</pre>
  48. <ul class="toolbar" v-show="showToolbar" :style="{left:toolBarX,top:toolBarY,bottom:toolBarBottom}">
  49. <li @click="handleQuote" v-if="msg_type == 0 || msg_type == 4">引用</li>
  50. <li @click="handleCopy">复制</li>
  51. <!-- <li @click="handleDel">删除</li> -->
  52. <li class="split-line" v-if="(isAdmin && type === 'you') || (isAdmin || revoke)"></li>
  53. <li @click="handlePingMsg" v-if="isAdmin">置顶</li>
  54. <li @click="handleBlock" v-if="isAdmin && type === 'you'">{{block?'解禁':'禁言'}}</li>
  55. <li @click="handleRevoke" v-if="isAdmin || revoke">撤回</li>
  56. </ul>
  57. </div>
  58. </div>
  59. </div>
  60. </template>
  61. <script>
  62. import dayjs from 'dayjs'
  63. import msgTime from '@/components/msgItem/time'
  64. import redPacket from '@/components/msgItem/redPacket'
  65. import redPackTip from '@/components/msgItem/redPackTip'
  66. import { mapMutations, mapActions, mapState } from 'vuex'
  67. export default {
  68. name: 'msgItem',
  69. components: {
  70. msgTime,
  71. redPacket,
  72. redPackTip
  73. },
  74. props: {
  75. msgItem: Object,
  76. isPrivate: Boolean,
  77. repealMsg: Boolean,
  78. from: String,
  79. timeMsg: Boolean,
  80. avatar: {
  81. type: String
  82. },
  83. name: {
  84. type: String
  85. },
  86. timestamp: [String, Number],
  87. hash: String,
  88. content: {
  89. type: [String, Number, Object]
  90. },
  91. userId: [String, Number],
  92. /**
  93. * 消息来源 {me: 我发的, you: 其他人发的}
  94. */
  95. type: {
  96. type: String
  97. },
  98. /**
  99. * 消息种类 (1 => 图片, 2 => 视频, 3 => 音频, 4 => 链接, 5 => 红包)
  100. */
  101. msg_type: {
  102. type: [Number, String]
  103. },
  104. createTime: [Number],
  105. loading: [Boolean],
  106. fail: [Boolean],
  107. res: [File]
  108. },
  109. data () {
  110. return {
  111. interactive: 0, // 1 鼠标 2 touch
  112. toolBarX: 0,
  113. toolBarY: 0,
  114. toolBarBottom: 0,
  115. showToolbar: false,
  116. revoke: false,
  117. block: false,
  118. revokeTimeAllow: false,
  119. width: 'auto',
  120. height: 'auto',
  121. longtapTimer: null
  122. }
  123. },
  124. computed: {
  125. ...mapState({
  126. myId: state => state.userId,
  127. userInfo: state => state.group.userInfo,
  128. blockList: state => state.group.blockList,
  129. adminList: state => state.group.adminList,
  130. members: state => state.group.members
  131. }),
  132. isAdmin () {
  133. return this.adminList && this.adminList.some(id => id == this.myId)
  134. },
  135. repealStr () {
  136. if (this.repealMsg) {
  137. if (this.from == this.userId) {
  138. return `${this.type == 'me' ? '你' : this.name}撤回了一条消息`
  139. } else {
  140. let admin = this.members[this.from]
  141. let adminName = '管理员'
  142. if (admin) {
  143. adminName = admin.nick_name
  144. }
  145. return `${adminName}撤回了${this.name}的一条消息`
  146. }
  147. } else {
  148. return ''
  149. }
  150. }
  151. },
  152. mounted () {
  153. if (this.msg_type == 1) {
  154. let rect = /_size([0-9]+)x([0-9]+)/.exec(this.content)
  155. if (rect) {
  156. let originalWidth = parseInt(rect[1])
  157. let originalHeight = parseInt(rect[2])
  158. let holderWidth = (document.body.offsetWidth - 35) * 0.84
  159. let scaleX = originalWidth > holderWidth ? holderWidth / originalWidth : 1
  160. let scaleY = originalHeight > 250 ? 250 / originalHeight : 1
  161. let scale = Math.min(scaleX, scaleY)
  162. this.width = (scale * originalWidth) + 'px'
  163. this.height = (scale * originalHeight) + 'px'
  164. }
  165. }
  166. },
  167. created () {
  168. },
  169. methods: {
  170. ...mapMutations([
  171. 'updateChatInputFocus',
  172. 'reSendChatItem'
  173. ]),
  174. ...mapActions([
  175. 'doRepealPersonMsg',
  176. 'doRepealGroupMsg',
  177. 'doBlockUser',
  178. 'doUnBlockUser',
  179. 'doPinMsg',
  180. 'doSendMsg',
  181. 'doSendFile'
  182. ]),
  183. imgLoad (e) {
  184. this.width = 'auto'
  185. this.height = 'auto'
  186. },
  187. hideToolbar (event) {
  188. if (this.showToolbar !== false) {
  189. this.showToolbar = false
  190. document.body.removeEventListener('click', this.hideToolbar, false)
  191. // document.body.removeEventListener('touchstart', this.hideToolbar, false)
  192. this.interactive = 0
  193. }
  194. },
  195. onToolBtn (event) {
  196. console.log(event)
  197. // if (this.interactive == 2) {
  198. // event.preventDefault()
  199. // return
  200. // }
  201. if (this.showToolbar) {
  202. this.hideToolbar(event)
  203. return
  204. }
  205. let winWidth = window.innerWidth
  206. let winHeight = window.innerHeight
  207. let clientX, clientY
  208. if (event instanceof MouseEvent) {
  209. this.interactive = 1
  210. clientX = event.clientX
  211. clientY = event.clientY
  212. this.toolBarX = event.layerX + 20
  213. this.toolBarY = event.layerY
  214. this.$nextTick(() => {
  215. document.body.addEventListener('click', this.hideToolbar, false)
  216. // document.body.addEventListener('touchstart', this.hideToolbar, false)
  217. })
  218. } else {
  219. this.interactive = 2
  220. let touch = event.touches[0] || event.changedTouches[0]
  221. clientX = touch.clientX
  222. clientY = touch.clientY
  223. let rect = event.target.getBoundingClientRect()
  224. this.toolBarX = touch.pageX + 20 - rect.left
  225. this.toolBarY = touch.pageY - rect.top
  226. // 校正局部坐标
  227. let target = event.target
  228. while (!target.classList.contains('bubble')) {
  229. this.toolBarX += target.offsetLeft
  230. target = target.parentNode
  231. }
  232. }
  233. if (clientX > winWidth * 0.66) {
  234. this.toolBarX = this.toolBarX - 120
  235. }
  236. this.toolBarX += 'px'
  237. if (clientY > winHeight / 2) {
  238. this.toolBarBottom = this.toolBarY + 'px'
  239. this.toolBarY = 'auto'
  240. } else {
  241. this.toolBarBottom = 'auto'
  242. this.toolBarY += 'px'
  243. }
  244. this.showToolbar = true
  245. this.block = this.blockList.some(id => id == this.userId)
  246. this.revokeTimeAllow = Date.now() - parseInt(this.timestamp) < 1e3 * 60 * 3
  247. this.revoke = this.type === 'me' && this.revokeTimeAllow
  248. },
  249. onTapToolBtn (event) {
  250. clearTimeout(this.longtapTimer)
  251. this.longtapTimer = setTimeout(() => {
  252. this.onToolBtn(event)
  253. }, 1000)
  254. },
  255. clearTapToolBtn () {
  256. clearTimeout(this.longtapTimer)
  257. this.$nextTick(() => {
  258. document.body.addEventListener('click', this.hideToolbar, false)
  259. })
  260. },
  261. handleQuote () {
  262. let { name, content } = this
  263. let quoteStr = `「${name}:${content}」\n- - - - - - - - - - - - - - -\n`
  264. this.$emit('quoteMsg', quoteStr)
  265. this.$nextTick(() => {
  266. this.updateChatInputFocus(true)
  267. })
  268. },
  269. handleCopy () {
  270. this.$copyText(this.content).then(
  271. e => {
  272. this.updateChatInputFocus(true)
  273. },
  274. e => {
  275. console.log('Can not copy')
  276. }
  277. )
  278. },
  279. handleShare () {
  280. this.$showInvite(this.content)
  281. },
  282. handleDel () {
  283. this.$emit('deleteMsg', this.hash)
  284. },
  285. handlePingMsg () {
  286. this.doPinMsg({ hash: this.hash })
  287. },
  288. handleRevoke () {
  289. if (this.isPrivate) {
  290. this.doRepealPersonMsg({ hash: this.hash })
  291. } else {
  292. this.doRepealGroupMsg({ hash: this.hash })
  293. }
  294. },
  295. handleBlock () {
  296. if (this.block) {
  297. this.doUnBlockUser({ id: this.userId })
  298. } else {
  299. this.doBlockUser({ id: this.userId })
  300. }
  301. },
  302. reSend () {
  303. if (this.msg_type == 0 || this.msg_type == 4) {
  304. let opt = {
  305. type: 0,
  306. msg: this.content,
  307. createTime: this.createTime
  308. }
  309. this.reSendChatItem({ createTime: this.createTime })
  310. this.doSendMsg(opt)
  311. } else {
  312. let opt = {
  313. res: this.res,
  314. createTime: this.createTime
  315. }
  316. this.reSendChatItem({ createTime: this.createTime })
  317. this.doSendFile(opt)
  318. }
  319. }
  320. },
  321. filters: {
  322. formatTimestamp (val) {
  323. if (!val) return ''
  324. return dayjs(val * 1).format('HH:mm')
  325. }
  326. }
  327. }
  328. </script>
  329. <style lang="scss" scoped>
  330. @import './style.scss';
  331. </style>