chat.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. import { mapActions, mapState, mapMutations } from 'vuex'
  2. import { scrollMsgIntoView, lazyloadImage, getMeechatType, getUserOpt, setUserOpt } from '@/util/util.js'
  3. import { emojiList } from '@/util/emoji'
  4. import { Message } from 'element-ui'
  5. import ImageMin from '@/util/imageMin.js'
  6. // 三端公共chat
  7. export const chatCommonMixin = {
  8. watch: {
  9. isRefreshImg (val) {
  10. if (!val) return
  11. this.$nextTick(() => {
  12. lazyloadImage({
  13. wrap: this.$refs.scrollWrap,
  14. imageArr: this.chatImageArrSel,
  15. derection: 'up'
  16. })
  17. })
  18. this.setIsRefreshImg(false)
  19. }
  20. },
  21. computed: {
  22. ...mapState({
  23. isRefreshImg: state => state.group.isRefreshImg
  24. })
  25. },
  26. methods: {
  27. ...mapMutations([
  28. 'addPinChatItem',
  29. 'setIsRefreshImg',
  30. 'deleteChatItem',
  31. 'removeAtListLast'
  32. ]),
  33. pinMsgClose () {
  34. this.pinMsg.visible = false
  35. },
  36. /**
  37. * @des 某条消息被删除
  38. */
  39. deleteMsg (hash) {
  40. this.deleteChatItem(hash)
  41. },
  42. scrollToMsg (index) {
  43. let hash = this.atList[index].hash
  44. let eleIndex = this.group.chatList.findIndex(item => item.hash === hash)
  45. if (eleIndex >= 0) {
  46. let node = this.$refs.msgWrap.getElementsByClassName('msg-item-common').item(eleIndex)
  47. scrollMsgIntoView(
  48. this.$refs.scrollWrap,
  49. node.offsetTop - (this.pinMsg ? 40 : 10),
  50. node
  51. )
  52. }
  53. this.removeAtListLast()
  54. },
  55. scrollToView () {
  56. if (!this.pinMsg) return
  57. let hash = this.pinMsg.hash
  58. let index = this.group.chatList.findIndex(item => item.hash === hash && item.msg_type == 0)
  59. if (index < 0) {
  60. this.addPinChatItem(this.pinMsg)
  61. index = 0
  62. }
  63. this.$nextTick(() => {
  64. let node = this.$refs.msgWrap.getElementsByClassName('msg-item-common')[index]
  65. let toOffsetTop = index >= 0 ? node.offsetTop : 0
  66. let lazy = function () {
  67. lazyloadImage({
  68. wrap: this.$refs.scrollWrap,
  69. imageArr: this.chatImageArrSel,
  70. derection: 'up'
  71. })
  72. }
  73. scrollMsgIntoView(
  74. this.$refs.scrollWrap, toOffsetTop, node, lazy.bind(this)
  75. )
  76. // 防止加载更多
  77. this.isScrollToView = true
  78. setTimeout(() => {
  79. this.isScrollToView = false
  80. }, 2000)
  81. })
  82. },
  83. async joinGroup () {
  84. this.isLoadingRoom = true
  85. await this.$store.dispatch('joinGroup')
  86. this.isLoadingRoom = false
  87. }
  88. }
  89. }
  90. // 聊天mixin 用于chatRoom组件
  91. export const chatMixin = {
  92. mixins: [chatCommonMixin],
  93. watch: {
  94. '$route' (val) {
  95. this.bdHiden = true
  96. // 切换房间
  97. this.groupSet = false
  98. this.lockMore = false
  99. this.lockEnd = false
  100. this.enableScroll = false
  101. this.initRoom()
  102. },
  103. chatList (val) {
  104. let lastVal = val[val.length - 1]
  105. if ((lastVal && lastVal.msg_type == 4) || this.isBottom) {
  106. // 自己发的红包自动滚动到底部
  107. this.$nextTick(this.resizeToBottom)
  108. }
  109. },
  110. isJoinGroup (val) {
  111. if (val == 1) setTimeout(this.resizeToBottom.bind(this), 100)
  112. }
  113. },
  114. data () {
  115. return {
  116. isLoadingRoom: true,
  117. groupSet: false,
  118. lockMore: false,
  119. lockEnd: false,
  120. enableScroll: false, // 记录滚动条是否激活的状态
  121. isBottom: true,
  122. scrollHeight: 100, // 滚动条高度
  123. isScrollToView: false,
  124. isShowGroudMgr: false, // 是否显示群管理
  125. chatImageArrSel: null, // 图片组
  126. meechatType: getMeechatType()
  127. }
  128. },
  129. computed: {
  130. ...mapState(['curSession', 'group', 'chat', 'userId', 'userInfo']),
  131. ...mapState({
  132. creator: state => state.group.creator,
  133. isJoin: state => state.group.isJoin,
  134. pinMsg: state => state.group.pinMsg,
  135. atList: state => state.group.atList,
  136. unreadNums: state => state.group.unreadNums,
  137. chatList: state => state.group.chatList,
  138. members: state => state.group.members,
  139. sessionId: state => state.curSession,
  140. sessionInfo: state => state.group.sessionInfo
  141. }),
  142. isExist () {
  143. // 是否存在在会话列表中
  144. let sessionList = this.chat.sessionList
  145. if (sessionList && sessionList.length) {
  146. return sessionList.some(e => {
  147. return e.session_id == this.sessionId
  148. })
  149. } else {
  150. return true
  151. }
  152. },
  153. isAdmin () {
  154. return (this.group.adminList && this.group.adminList.some(id => id == this.userId)) || this.group.creator == this.userId
  155. },
  156. isPrivate () {
  157. return this.$store.getters.isPrivate
  158. },
  159. isCreator () {
  160. return this.userId == this.creator
  161. },
  162. isJoinGroup () {
  163. if (this.group && this.group.groupId) {
  164. return this.isJoin ? 1 : 0
  165. } else {
  166. return 1
  167. }
  168. },
  169. linkToCreator () {
  170. let { creator, userId } = this.group
  171. let sessionId = creator > userId ? `${userId}-${creator}` : `${creator}-${userId}`
  172. return `${location.origin}/#/pm/${sessionId}`
  173. }
  174. },
  175. mounted () {
  176. this.initRoom()
  177. document.getElementById('app').addEventListener('contextmenu', e => e.preventDefault())
  178. this.chatImageArrSel = this.$refs.scrollWrap.getElementsByTagName('img')
  179. // this.$nextTick(() => {
  180. // this.$refs.msgWrap.style.height = 'auto'
  181. // })
  182. },
  183. methods: {
  184. ...mapMutations([
  185. 'initGroup',
  186. 'setUserId',
  187. 'setToken',
  188. 'resetUnreadNums',
  189. 'addChatItem',
  190. 'initState',
  191. 'clearAtList',
  192. 'clearHash',
  193. 'setSessionItemUnread',
  194. 'clearChatList',
  195. 'changeSessionId',
  196. 'updateMembers'
  197. ]),
  198. ...mapActions([
  199. 'setAccount',
  200. 'getGroupInfo',
  201. 'getUserInfo',
  202. 'getNewMsgFromDb',
  203. 'getNewMsg',
  204. 'getHistoryMsg',
  205. 'doSendMsg',
  206. 'getPrivateNewMsgFromDb',
  207. 'getPrivateNewMsg',
  208. 'getPrivateHistoryMsg',
  209. 'doSendPrivateMsg'
  210. ]),
  211. async initRoom () {
  212. if (!this.userInfo) {
  213. await this.getUserInfo()
  214. }
  215. if (!this.userInfo) return
  216. this.changeSessionId(this.$route.params.id)
  217. this.clearHash()
  218. this.clearChatList()
  219. this.initState()
  220. if (this.isExist) {
  221. // 把会话列表的消息数设置为0
  222. this.$store.commit('setSessionItemUnread', {
  223. session_id: this.curSession,
  224. unread: 0,
  225. curSession: this.curSession
  226. })
  227. }
  228. if (this.isPrivate) await this.initPersonChat()
  229. else await this.initGroupChat()
  230. },
  231. /**
  232. * @des 私聊初始化处理
  233. */
  234. async initPersonChat () {
  235. let flag = await this.getPrivateNewMsgFromDb()
  236. if (!flag) {
  237. // 如果indexDB没数据时,才需要loading
  238. this.showLoadingRoom(true)
  239. }
  240. let data = await this.getPrivateNewMsg()
  241. this.showLoadingRoom(false)
  242. this.$nextTick(() => {
  243. this.resizeToBottom()
  244. this.bdHiden = false
  245. })
  246. // 没消息时member添加成员
  247. if (!this.members[this.userId]) {
  248. this.updateMembers({ [this.userId]: this.userInfo })
  249. }
  250. // 房间名
  251. let userIds = this.curSession.split('-')
  252. let otherId = userIds[0] != this.userId ? userIds[0] : userIds[1]
  253. let otherInfo = data.data.userMap[otherId]
  254. if (otherInfo) {
  255. this.$store.commit('updateGroup', {
  256. key: 'privateName',
  257. data: otherInfo.nick_name
  258. })
  259. // 不存在会话,则添加会话
  260. if (!this.isExist) {
  261. let obj = {
  262. cover_photo: otherInfo.cover_photo,
  263. is_group: '0',
  264. name: otherInfo.nick_name,
  265. session_id: this.sessionId
  266. }
  267. this.$store.commit('addSessionItem', obj)
  268. }
  269. }
  270. return data
  271. },
  272. /**
  273. * @des 聊天群初始化处理
  274. */
  275. async initGroupChat () {
  276. this.initGroup({
  277. userId: this.userId,
  278. groupId: this.sessionId,
  279. useCache: false
  280. })
  281. this.isShowGroudMgr = false
  282. this.getGroupInfo()
  283. let flag = await this.getNewMsgFromDb()
  284. // 如果indexDB没数据时,才需要loading
  285. if (!flag) this.showLoadingRoom(true)
  286. await this.getNewMsg()
  287. this.showLoadingRoom(false)
  288. this.$nextTick(() => {
  289. this.resizeToBottom()
  290. this.bdHiden = false
  291. lazyloadImage({
  292. wrap: this.$refs.scrollWrap,
  293. imageArr: this.chatImageArrSel,
  294. derection: 'up'
  295. })
  296. })
  297. setTimeout(() => {
  298. // 不存在会话,则添加会话
  299. if (!this.isExist) {
  300. // 获取对方信息
  301. if (this.group) {
  302. let obj = {
  303. cover_photo: this.group.coverPhoto,
  304. is_group: '1',
  305. name: this.group.groupName,
  306. session_id: this.sessionId
  307. }
  308. this.$store.commit('addSessionItem', obj)
  309. }
  310. }
  311. }, 1000)
  312. },
  313. /**
  314. * @des 滚动事件监听
  315. */
  316. initScrollEvent () {},
  317. /**
  318. * @des 聊天窗体滚动事件处理集
  319. */
  320. async handleScroll (e) {
  321. // 防止切换房间时触发滚动处理
  322. if (!this.group.chatList.length) {
  323. return
  324. }
  325. // 防止滚动到置顶消息触发滚动
  326. // if (this.isScrollToView) {
  327. // return
  328. // }
  329. // 激活滚动条
  330. this.enableScroll = true
  331. let totalHeight = this.$refs.msgWrap.offsetHeight - 16
  332. let scrollTop = e.target.scrollTop
  333. // 差不多滚动到顶部
  334. if (scrollTop == 0 && !this.lockMore) {
  335. if (this.group.endHash !== null) {
  336. this.lockMore = true
  337. let res
  338. if (this.isPrivate) {
  339. res = await this.getPrivateHistoryMsg()
  340. } else {
  341. res = await this.getHistoryMsg()
  342. }
  343. if (res === 'end') {
  344. this.lockEnd = true
  345. } else {
  346. let scrollBottom = totalHeight - scrollTop
  347. this.$nextTick(() => {
  348. e.target.scrollTop =
  349. this.$refs.msgWrap.offsetHeight - scrollBottom
  350. setTimeout(() => {
  351. this.lockMore = false
  352. }, 800)
  353. })
  354. }
  355. }
  356. }
  357. // 滚动到底部清空未读消息状态
  358. if (scrollTop + e.target.offsetHeight > totalHeight) {
  359. this.isBottom = true
  360. if (this.group.unreadNums) {
  361. this.resetUnreadNums()
  362. }
  363. } else {
  364. this.isBottom = false
  365. }
  366. lazyloadImage({
  367. wrap: this.$refs.scrollWrap,
  368. imageArr: this.chatImageArrSel,
  369. derection: 'up'
  370. })
  371. },
  372. /**
  373. * @des 聊天窗体滚动到底部
  374. */
  375. resizeToBottom () {
  376. if (!this.$refs.msgWrap) return
  377. this.$refs.msgWrap.style.height = 'auto'
  378. this.$refs.scrollWrap.scrollTop = this.$refs.msgWrap.offsetHeight
  379. this.resetUnreadNums()
  380. lazyloadImage({
  381. wrap: this.$refs.scrollWrap,
  382. imageArr: this.chatImageArrSel,
  383. derection: 'up'
  384. }, 200)
  385. },
  386. /**
  387. * @des 点击,查看未读消息
  388. * 直接滚动到聊天列表底部
  389. */
  390. doSetRead () {
  391. this.resizeToBottom()
  392. },
  393. /**
  394. * @des 引用某条消息
  395. */
  396. quoteMsg (msg) {
  397. this.$refs.inputArea.inputMsg = msg
  398. },
  399. // 群管理
  400. showGroudMgr (flag) {
  401. this.isShowGroudMgr = flag == 1
  402. },
  403. // 关闭表情,文件栏
  404. initEmojiAndTool () {
  405. this.emojiShow = false
  406. this.toolShow = false
  407. },
  408. showLoadingRoom (flag) {
  409. this.isLoadingRoom = flag
  410. }
  411. },
  412. beforeDestroy () {
  413. document.body.removeEventListener('click', this.initEmojiAndTool)
  414. }
  415. }
  416. // 聊天输入框mixin
  417. export const inputMixin = {
  418. computed: {
  419. ...mapState(['group', 'userId']),
  420. ...mapState({
  421. chatInputFocus: state => state.group.chatInputFocus,
  422. blockList: state => state.group.blockList
  423. }),
  424. isPrivate () {
  425. return this.$store.getters.isPrivate
  426. },
  427. emojiMap () {
  428. var emojiMap = {}
  429. for (let i in emojiList) {
  430. let arr = emojiList[i]
  431. arr.forEach(v => {
  432. let names = JSON.stringify(v.names)
  433. let emoji = v.surrogates
  434. emojiMap[names] = emoji
  435. })
  436. }
  437. return emojiMap
  438. }
  439. },
  440. data () {
  441. return {
  442. filePreviewShow: false, // 是否显示文件预览
  443. emojiShow: false, // 是否显示emoji
  444. fileInfo: null, // 当前上传文件
  445. inputMsg: '',
  446. atInd: 0,
  447. meechatType: getMeechatType()
  448. }
  449. },
  450. mounted () {
  451. },
  452. methods: {
  453. ...mapMutations(['updateChatInputFocus', 'addChatItem']),
  454. ...mapActions(['doSendMsg', 'doSendFile', 'doSendPrivateMsg']),
  455. addEmoji (val) {
  456. this.inputMsg += val
  457. if (this.meechatType == 'pc') {
  458. this.emojiShow = false
  459. this.$refs.chatInput.focus()
  460. }
  461. },
  462. closeFilePreview () {
  463. this.fileInfo = null
  464. this.filePreviewShow = false
  465. this.emptyFileForm()
  466. },
  467. showFilePreview (fileInfo) {
  468. if (!fileInfo) return
  469. this.fileInfo = fileInfo
  470. this.filePreviewShow = true
  471. },
  472. /**
  473. * @des 处理消息发送
  474. */
  475. handleInput () {
  476. this.inputMsg = this.inputMsg.substring(0, 5000)
  477. },
  478. /**
  479. * @des 处理消息发送
  480. */
  481. async handleSend (e) {
  482. // 判断是否被禁言
  483. if (this.blockList.some(id => id == this.userId)) {
  484. Message({
  485. message: '您已被禁言',
  486. type: 'error'
  487. })
  488. return
  489. }
  490. // 替换emoji字符串
  491. let _inputMsg = this.inputMsg
  492. let parts = _inputMsg.match(/\["[a-z0-9A-Z_]+"\]/g)
  493. for (let k in parts) {
  494. let emoji = this.emojiMap[parts[k]]
  495. if (emoji) {
  496. _inputMsg = _inputMsg.replace(parts[k], emoji)
  497. }
  498. }
  499. let text = _inputMsg.trim().substring(0, 5000)
  500. if (text.length === 0) {
  501. Message({
  502. message: '聊天内容不能为空',
  503. type: 'warning'
  504. })
  505. return
  506. }
  507. let opt = {
  508. type: 0,
  509. msg: text
  510. }
  511. // 用户不是第一次发言
  512. if (this.group.members[this.userId]) {
  513. let createTime = Date.now()
  514. let lastShowMsgUid = getUserOpt('lastShowMsgUid') || 0
  515. this.addChatItem({
  516. from: this.userId,
  517. content: text,
  518. hash: `${createTime}`,
  519. timestamp: createTime,
  520. createTime,
  521. msg_type: '0',
  522. loading: true,
  523. isShowFullInfo: this.userId != lastShowMsgUid
  524. })
  525. if (this.userId != lastShowMsgUid)lastShowMsgUid = this.userId
  526. setUserOpt('lastShowMsgUid', lastShowMsgUid)
  527. opt.createTime = createTime
  528. }
  529. this.inputMsg = ''
  530. let data = this.isPrivate ? await this.doSendPrivateMsg(opt) : await this.doSendMsg(opt)
  531. // // 发送成功后,才加
  532. this.$store.commit('setSessionItemUnread', {
  533. session_id: this.curSession,
  534. unread: 0,
  535. curSession: this.curSession,
  536. cont: text,
  537. timestamp: data.timestamp
  538. })
  539. // 滚到底部
  540. this.$nextTick(function () {
  541. this.resizeToBottom ? this.resizeToBottom() : this.$emit('toBottom')
  542. lazyloadImage({
  543. wrap: this.$refs.scrollWrap,
  544. imageArr: this.chatImageArrSel,
  545. derection: 'up'
  546. })
  547. })
  548. e.preventDefault()
  549. return false
  550. },
  551. /**
  552. * 文件预处理
  553. * @return {Object} data 预处理文件信息
  554. * @param {Number} data.type
  555. * @param {File} data.res
  556. */
  557. async preHandleFile (file) {
  558. let type = file.type
  559. let size = file.size
  560. if (type.match('video')) {
  561. return size > 3 * 1024 * 1024
  562. ? Promise.reject(new Error(file))
  563. : Promise.resolve({
  564. type: 2,
  565. res: file,
  566. preview: window.webkitURL.createObjectURL(file)
  567. })
  568. } else if (type.match('audio')) {
  569. return size > 2 * 1024 * 1024
  570. ? Promise.reject(new Error(file))
  571. : Promise.resolve({
  572. type: 3,
  573. res: file,
  574. preview: window.webkitURL.createObjectURL(file)
  575. })
  576. } else if (type.match('image')) {
  577. let image = await new ImageMin({
  578. file: file,
  579. maxSize: 1024 * 1024
  580. })
  581. return {
  582. type: 1,
  583. preview: image.base64,
  584. res: image.res
  585. }
  586. }
  587. },
  588. /**
  589. * @des 处理文件发送
  590. */
  591. async handleFile (e) {
  592. let inputfile
  593. if (e.constructor === File) {
  594. inputfile = e
  595. } else {
  596. inputfile = e.target.files[0]
  597. }
  598. try {
  599. let fileInfo = await this.preHandleFile(inputfile)
  600. if (this.meechatType == 'pc') this.showFilePreview(fileInfo)
  601. else this.handleFileSend(fileInfo)
  602. } catch (error) {
  603. Message({
  604. message: '上传文件大小限制:音频2M以内,视频3M以内',
  605. type: 'warning'
  606. })
  607. }
  608. },
  609. // 发送文件消息
  610. async handleFileSend (fileInfo) {
  611. this.filePreviewShow = false
  612. let opt = { res: fileInfo.res }
  613. let createTime = Date.now()
  614. this.addChatItem({
  615. content: fileInfo.preview || '',
  616. from: this.userId,
  617. hash: `${createTime}`,
  618. msg_type: fileInfo.type,
  619. timestamp: createTime,
  620. res: fileInfo.res,
  621. loading: true,
  622. createTime
  623. })
  624. opt.createTime = createTime
  625. await this.doSendFile(opt)
  626. setTimeout(() => {
  627. this.emptyFileForm()
  628. this.$refs.toolbar && this.$refs.toolbar.resetInput()
  629. this.resizeToBottom ? this.resizeToBottom() : this.$emit('toBottom')
  630. }, 100)
  631. },
  632. // 初始文件表单
  633. emptyFileForm () {
  634. if (this.$refs.inputFile) {
  635. this.$refs.inputFile.value = null
  636. }
  637. if (this.$refs.inputFile1) {
  638. this.$refs.inputFile1.value = null
  639. }
  640. if (this.$refs.inputFile2) {
  641. this.$refs.inputFile2.value = null
  642. }
  643. if (this.$refs.inputFile3) {
  644. this.$refs.inputFile3.value = null
  645. }
  646. }
  647. }
  648. }