index.vue 12 KB

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