chatMiniHandle.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729
  1. import msgItem from '@/components/msgItem'
  2. import emoji from '@/components/emoji'
  3. import chatAt from '@/components/chatAt'
  4. import atMe from '@/components/chatAt/atme'
  5. import chatPin from '@/components/chatPin'
  6. import toolbar from '@/components/chatInput/toolbar'
  7. import { mapActions, mapState, mapMutations } from 'vuex'
  8. import { getMiniWsUrl } from '@/util/contract.js'
  9. import { isMobile, scrollMsgIntoView, lazyloadImage } from '@/util/util.js'
  10. import WsManager from '@/util/wsManager.js'
  11. import { Message } from 'element-ui'
  12. import ImageMin from '@/util/imageMin.js'
  13. import { chatAtMixin, chatInputMixin, changeLangMixin } from '@/mixins'
  14. import { accountLoginMixin } from '@/mixins/login'
  15. import { emojiList } from '@/util/emoji'
  16. import api from '@/api'
  17. import loginBox from '@/components/login/loginBox'
  18. export default {
  19. name: 'chatMini',
  20. mixins: [accountLoginMixin, chatAtMixin, chatInputMixin, changeLangMixin],
  21. components: {
  22. msgItem,
  23. emoji,
  24. chatAt,
  25. chatPin,
  26. atMe,
  27. toolbar,
  28. loginBox
  29. },
  30. props: {
  31. width: {
  32. type: Number,
  33. default: 274
  34. },
  35. height: {
  36. type: Number,
  37. default: 390
  38. },
  39. show: {
  40. type: Boolean,
  41. default: false
  42. },
  43. groupId: [Number, String]
  44. },
  45. computed: {
  46. ...mapState([
  47. 'account',
  48. 'group',
  49. 'userId',
  50. 'userInfo'
  51. ]),
  52. ...mapState({
  53. chatInputFocus: state => state.group.chatInputFocus,
  54. blockList: state => state.group.blockList,
  55. pinMsg: state => state.group.pinMsg,
  56. atList: state => state.group.atList,
  57. chatList: state => state.group.chatList,
  58. unreadNums: state => state.group.unreadNums,
  59. sessionList: state => state.chat.sessionList,
  60. isJoin: state => state.group.isJoin
  61. }),
  62. emojiMap () {
  63. var emojiMap = {}
  64. for (let i in emojiList) {
  65. let arr = emojiList[i]
  66. arr.forEach(v => {
  67. let names = JSON.stringify(v.names)
  68. let emoji = v.surrogates
  69. emojiMap[names] = emoji
  70. })
  71. }
  72. return emojiMap
  73. },
  74. linkToCreator () {
  75. let { creator, userId } = this.group
  76. let sessionId = creator > userId ? `${userId}-${creator}` : `${creator}-${userId}`
  77. return `${location.origin}/#/pm/${sessionId}`
  78. },
  79. isAdmin () {
  80. return (this.group.adminList && this.group.adminList.some(id => id == this.userId)) || this.group.creator == this.userId
  81. }
  82. },
  83. data () {
  84. return {
  85. loginBoxVisible: false,
  86. isLoadingRoom: true,
  87. isMobile: isMobile(),
  88. showChat: !!this.show, // 显示聊天窗
  89. showEmoji: false, // 显示表情选择框
  90. showMenuExtra: false, // 显示左上角菜单
  91. showLoginBtn: true, // 显示登录按钮
  92. lockMore: false,
  93. isScrollToView: false,
  94. lockEnd: false,
  95. loading: false,
  96. unreadCounts: 0, // 未读消息数
  97. inputMsg: '', // 用户输入的内容
  98. atInd: 0, // @人索引
  99. inputHeight: 18,
  100. enableScroll: false, // 记录滚动条是否激活的状态
  101. isBottom: true,
  102. toolShow: false,
  103. chatImageArrSel: null,
  104. serverUnRead: 0, // 客服未读
  105. personUnRead: 0 // 私聊未读
  106. }
  107. },
  108. watch: {
  109. inputMsg (val, newval) {
  110. let ele = this.$refs.chatInput
  111. this.inputHeight = 'auto'
  112. this.$nextTick(() => {
  113. this.inputHeight = Math.max(18, Math.min(ele.scrollHeight, 75)) + 'px'
  114. })
  115. },
  116. chatList (val) {
  117. let lastVal = val[val.length - 1]
  118. if ((lastVal && lastVal.msg_type == 4) || this.isBottom) {
  119. // 自己发的红包自动滚动到底部
  120. this.$nextTick(this.resizeToBottom)
  121. }
  122. },
  123. chatInputFocus (val, newval) {
  124. if (this.showLoginBtn) return
  125. let ele = this.$refs.chatInput
  126. if (val) {
  127. if (document.activeElement !== ele) {
  128. this.placeEnd(ele)
  129. ele.focus()
  130. }
  131. } else {
  132. if (document.activeElement === ele) {
  133. ele.blur()
  134. }
  135. }
  136. }
  137. },
  138. async mounted () {
  139. // 设置groupId
  140. this.initGroup({
  141. userId: this.userId,
  142. groupId: this.groupId,
  143. useCache: false
  144. })
  145. // 检查登录态
  146. let isLogin = await this.checkLocalLogin()
  147. if (isLogin) {
  148. this.showLoginBtn = false
  149. await this.getUserInfo()
  150. }
  151. this.$nextTick(this.initChat)
  152. this.$nextTick(this.initMiniSocket)
  153. document.getElementById('app').addEventListener('contextmenu', e => e.preventDefault())
  154. document.addEventListener('paste', this.initPaste)
  155. document.addEventListener('drop', this.initDrop)
  156. document.addEventListener('dragover', this.initDragOver)
  157. document.body.addEventListener('click', () => {
  158. this.showEmoji = false
  159. this.showMenuExtra = false
  160. this.toolShow = false
  161. })
  162. },
  163. beforeDestroy () {
  164. document.removeEventListener('paste', this.initPaste)
  165. document.removeEventListener('drop', this.initDrop)
  166. document.removeEventListener('dragover', this.initDragOver)
  167. },
  168. methods: {
  169. ...mapMutations([
  170. 'initGroup',
  171. 'setUserId',
  172. 'setToken',
  173. 'addChatItem',
  174. 'deleteChatItem',
  175. 'updateChatInputFocus',
  176. 'updateGroupBlockList',
  177. 'updateMembers',
  178. 'updateGroupPinMsg',
  179. 'repealChatItem',
  180. 'removeAtListLast',
  181. 'clearAtList',
  182. 'initState',
  183. 'addPacketItem',
  184. 'addPacketTip',
  185. 'addUnreadNums',
  186. 'resetUnreadNums',
  187. 'addPinChatItem'
  188. ]),
  189. ...mapActions([
  190. 'getUserInfo',
  191. 'setAccount',
  192. 'doGameLogin',
  193. 'doScatterLogout',
  194. 'getGroupInfo',
  195. 'getNewMsgFromDb',
  196. 'getNewMsg',
  197. 'getHistoryMsg',
  198. 'doSendMsg',
  199. 'doSendFile',
  200. 'updateSessionLastmsg'
  201. ]),
  202. async joinGroup () {
  203. this.isLoadingRoom = true
  204. await this.$store.dispatch('joinGroup')
  205. this.isLoadingRoom = false
  206. },
  207. handleMoreClick () {
  208. this.showEmoji = false
  209. this.toolShow = !this.toolShow
  210. this.checkNeedToBottom()
  211. },
  212. handleEmojiClick () {
  213. this.toolShow = false
  214. this.showEmoji = !this.showEmoji
  215. this.checkNeedToBottom()
  216. },
  217. checkNeedToBottom () {
  218. if (!this.isBottom) return
  219. this.$nextTick(() => {
  220. this.resizeToBottom()
  221. })
  222. },
  223. async initMiniLoginCallback () {
  224. await this.initMiniSocket()
  225. this.showLoginBtn = false
  226. this.initGroup({
  227. userId: this.userId,
  228. groupId: this.groupId,
  229. useCache: false
  230. })
  231. await this.getUserInfo()
  232. this.loginBoxVisible = false
  233. },
  234. // 连接socket
  235. initMiniSocket () {
  236. if (!window.WebSocket) {
  237. console.log('Error: WebSocket is not supported .')
  238. return
  239. }
  240. let host = getMiniWsUrl() + `?group_id=${this.groupId}`
  241. if (this.socket) {
  242. this.socket.destroy()
  243. this.socket = null
  244. }
  245. this.socket = new WsManager(host, {
  246. autoConnect: true, // 自动连接
  247. reconnection: true, // 断开自动重连
  248. reconnectionDelay: 2000 // 重连间隔时间,单位秒
  249. })
  250. this.socket.on('open', res => {})
  251. this.socket.on('message', (data) => {
  252. data = JSON.parse(data)
  253. if (data.channel.match('chat:group')) {
  254. if (data.data.type === 'msg') {
  255. this.getNewMsg({ newMsg: true })
  256. if (data.data.from != this.userId) {
  257. // 未读消息数+1
  258. if (!this.showChat) {
  259. if (this.unreadCounts === 0) {
  260. this.postResize(130, 50)
  261. }
  262. this.unreadCounts++
  263. } else {
  264. this.addUnreadNums()
  265. }
  266. }
  267. }
  268. if (data.data.type === 'repeal') {
  269. this.repealChatItem(data.data)
  270. }
  271. if (data.data.type === 'block') {
  272. this.updateGroupBlockList({
  273. type: 'add',
  274. id: data.data.to
  275. })
  276. }
  277. if (data.data.type === 'unblock') {
  278. this.updateGroupBlockList({
  279. type: 'delete',
  280. id: data.data.to
  281. })
  282. }
  283. if (data.data.type === 'join') {
  284. this.updateMembers(data.data.user_info)
  285. }
  286. if (data.data.type === 'pin_msg') {
  287. this.updateGroupPinMsg(data.data.pinMsg)
  288. }
  289. if (data.data.type === 'unpin_msg') {
  290. this.updateGroupPinMsg(null)
  291. }
  292. if (data.data.type === 'new_redpack') {
  293. this.addPacketItem(data.data)
  294. if (data.data.from == this.userId) {
  295. this.$nextTick(this.resizeToBottom)
  296. }
  297. }
  298. if (data.data.type === 'grab_redpack') {
  299. if (data.data.from == this.userId || data.data.to == this.userId) {
  300. this.addPacketTip(data.data)
  301. }
  302. }
  303. }
  304. })
  305. },
  306. /**
  307. * 聊天群初始化处理
  308. * 先后调用 group/info, group/msg
  309. */
  310. async initChat () {
  311. this.handleToggleChat(this.show)
  312. this.isLoadingRoom = true
  313. await this.getGroupInfo()
  314. await this.getNewMsgFromDb()
  315. await this.getNewMsg()
  316. this.isLoadingRoom = false
  317. // 有登录态才要请求
  318. if (!this.showLoginBtn) {
  319. await this.getPmUnRead()
  320. }
  321. if (this.show) {
  322. this.$nextTick(this.resizeToBottom)
  323. }
  324. this.chatImageArrSel = this.$refs.scrollWrap.getElementsByTagName('img')
  325. },
  326. getPmUnRead () {
  327. api.session.getMiniUnRead().then(({ data }) => {
  328. this.personUnRead = data.data['0']
  329. this.serverUnRead = data.data[this.groupId] || 0
  330. })
  331. },
  332. /**
  333. * 清空私聊未读
  334. * @param {type} 1.客服2.私聊
  335. **/
  336. clearPmUnread (type) {
  337. if (type == 1) this.serverUnRead = 0
  338. else this.personUnRead = 0
  339. },
  340. async handleLogout () {
  341. this.doScatterLogout()
  342. this.showLoginBtn = true
  343. if (self !== top) {
  344. localStorage.removeItem('account')
  345. this.postMessager.send({
  346. action: 'meechat:logout'
  347. })
  348. }
  349. // 初始vuex数据
  350. this.$store.commit('setUserInfo', null)
  351. // this.$store.commit('initChatData')
  352. // this.$store.commit('initGroupData')
  353. // 注销后,刷新页面
  354. location.replace(location.href.replace('show=false', 'show=true'))
  355. },
  356. async handleLogout2 () {
  357. this.doScatterLogout()
  358. this.showLoginBtn = true
  359. },
  360. /**
  361. * 登录处理
  362. */
  363. async handleLogin () {
  364. // 设置登录按钮状态
  365. this.loginBoxVisible = true
  366. },
  367. /**
  368. * 聊天窗体滚动到底部
  369. */
  370. resizeToBottom () {
  371. this.$refs.scrollWrap.scrollTop = this.$refs.msgWrap.offsetHeight
  372. this.resetUnreadNums()
  373. this.isBottom = true
  374. lazyloadImage({
  375. wrap: this.$refs.scrollWrap,
  376. imageArr: this.chatImageArrSel,
  377. derection: 'up'
  378. })
  379. },
  380. /**
  381. * @des 点击,查看未读消息
  382. * 直接滚动到聊天列表底部
  383. */
  384. doSetRead () {
  385. this.resizeToBottom()
  386. },
  387. /**
  388. * 添加表情
  389. */
  390. addEmoji (value) {
  391. this.inputMsg += value
  392. },
  393. /**
  394. * @des 聊天窗体滚动事件处理集
  395. */
  396. handleScroll (e) {
  397. this.enableScroll = true
  398. e.target.focus()
  399. // 防止切换房间时触发滚动处理
  400. if (!this.group.chatList.length) {
  401. return
  402. }
  403. // 防止滚动到置顶消息触发滚动
  404. // if (this.isScrollToView) {
  405. // return
  406. // }
  407. let msgWrap = this.$refs.msgWrap
  408. let totalHeight = msgWrap.offsetHeight
  409. let scrollTop = e.target.scrollTop
  410. if (scrollTop === 0 && !this.lockMore) {
  411. if (this.group.endHash !== null) {
  412. this.lockMore = true
  413. this.getHistoryMsg().then((res) => {
  414. if (res === 'end') {
  415. this.lockEnd = true
  416. } else {
  417. let scrollBottom = totalHeight - scrollTop
  418. this.$nextTick(() => {
  419. e.target.scrollTop = msgWrap.offsetHeight - scrollBottom
  420. this.ps && this.ps.update()
  421. setTimeout(() => {
  422. this.lockMore = false
  423. }, 800)
  424. })
  425. }
  426. })
  427. }
  428. }
  429. // 滚动到底部清空未读消息状态
  430. if (scrollTop + e.target.offsetHeight > totalHeight) {
  431. this.isBottom = true
  432. if (this.unreadNums) {
  433. this.resetUnreadNums()
  434. }
  435. } else {
  436. this.isBottom = false
  437. }
  438. lazyloadImage({
  439. wrap: this.$refs.scrollWrap,
  440. imageArr: this.chatImageArrSel,
  441. derection: 'up'
  442. })
  443. },
  444. /**
  445. * @des 处理消息发送
  446. */
  447. async handleSend (e) {
  448. // 判断是否被禁言
  449. if (this.blockList.some(id => id == this.userId)) {
  450. Message({
  451. message: this.$t('chat.youAreBan'),
  452. type: 'error'
  453. })
  454. return
  455. }
  456. // 替换emoji字符串
  457. let _inputMsg = this.inputMsg
  458. let parts = _inputMsg.match(/\["[a-z0-9A-Z_]+"\]/g)
  459. for (let k in parts) {
  460. let emoji = this.emojiMap[parts[k]]
  461. if (emoji) {
  462. _inputMsg = _inputMsg.replace(parts[k], emoji)
  463. }
  464. }
  465. let text = _inputMsg.trim()
  466. if (text.length === 0) {
  467. Message({
  468. message: this.$t('chat.cannotBeEmpty'),
  469. type: 'warning'
  470. })
  471. return
  472. }
  473. let opt = {
  474. type: 0,
  475. msg: text
  476. }
  477. // 清空输入框
  478. this.inputMsg = ''
  479. // 用户不是第一次发言
  480. if (this.group.members[this.userId]) {
  481. let createTime = Date.now()
  482. this.addChatItem({
  483. from: this.userId,
  484. content: text,
  485. hash: `${createTime}`,
  486. timestamp: createTime,
  487. createTime,
  488. msg_type: '0',
  489. loading: true
  490. })
  491. opt.createTime = createTime
  492. await this.doSendMsg(opt)
  493. } else {
  494. // 发言后,才滚动底部
  495. await this.doSendMsg(opt)
  496. }
  497. // 滚到底部
  498. this.$nextTick(function () {
  499. this.resizeToBottom()
  500. })
  501. e.preventDefault()
  502. return false
  503. },
  504. placeEnd (el) {
  505. var range = document.createRange()
  506. range.selectNodeContents(el)
  507. range.collapse(false)
  508. var sel = window.getSelection()
  509. sel.removeAllRanges()
  510. sel.addRange(range)
  511. },
  512. initDrop (e) {
  513. e.preventDefault()
  514. let files = Array.from(e.dataTransfer.files)
  515. files.forEach(file => this.handleFile(file))
  516. },
  517. initDragOver (e) {
  518. e.preventDefault()
  519. },
  520. initPaste (event) {
  521. var items = (event.clipboardData || window.clipboardData).items
  522. if (items && items.length) {
  523. Array.from(items).forEach(item => {
  524. let file = item.getAsFile()
  525. if (file) {
  526. this.handleFile(file)
  527. }
  528. })
  529. }
  530. },
  531. /**
  532. * 文件预处理
  533. * @return {Object} data 预处理文件信息
  534. * @param {Number} data.type
  535. * @param {File} data.res
  536. */
  537. async preHandleFile (file) {
  538. let type = file.type
  539. let size = file.size
  540. if (type.match('video')) {
  541. return size > 3 * 1024 * 1024 ? Promise.reject(new Error(file)) : Promise.resolve({
  542. type: 2,
  543. res: file
  544. })
  545. } else if (type.match('audio')) {
  546. return size > 2 * 1024 * 1024 ? Promise.reject(new Error(file)) : Promise.resolve({
  547. type: 3,
  548. res: file
  549. })
  550. } else if (type.match('image')) {
  551. let image = await new ImageMin({
  552. file: file,
  553. maxSize: 1024 * 1024
  554. })
  555. return {
  556. type: 1,
  557. preview: image.base64,
  558. res: image.res
  559. }
  560. }
  561. },
  562. /**
  563. * @des 处理文件发送
  564. */
  565. async handleFile (e) {
  566. let inputfile
  567. if (e.constructor === File) {
  568. inputfile = e
  569. } else {
  570. inputfile = e.target.files[0]
  571. }
  572. try {
  573. let fileInfo = await this.preHandleFile(inputfile)
  574. let opt = { res: fileInfo.res }
  575. if (this.group.members[this.userId]) {
  576. let createTime = Date.now()
  577. this.addChatItem({
  578. content: fileInfo.preview || '',
  579. from: this.userId,
  580. hash: `${createTime}`,
  581. msg_type: fileInfo.type,
  582. timestamp: createTime,
  583. res: fileInfo.res,
  584. loading: true,
  585. createTime
  586. })
  587. opt.createTime = createTime
  588. }
  589. this.doSendFile(opt)
  590. this.toolShow = false
  591. setTimeout(() => {
  592. this.$refs.toolbar.resetInput()
  593. this.resizeToBottom()
  594. }, 100)
  595. } catch (error) {
  596. Message({
  597. message: this.$t('chat.maxUploadTips'),
  598. type: 'warning'
  599. })
  600. }
  601. },
  602. /**
  603. * @des 控制聊天窗口开关
  604. */
  605. handleToggleChat (flag) {
  606. if (flag) {
  607. this.showChat = true
  608. this.unreadCounts = 0
  609. this.$nextTick(() => {
  610. this.postResize(this.width, this.height + 16)
  611. this.resizeToBottom()
  612. })
  613. } else {
  614. this.showChat = false
  615. this.$nextTick(() => {
  616. this.postResize(56, 50)
  617. })
  618. }
  619. },
  620. postResize (width, height) {
  621. let request = {
  622. action: 'meechat:resize',
  623. data: {
  624. ch: height,
  625. cw: width
  626. }
  627. }
  628. return window.parent.postMessage(request, '*')
  629. },
  630. /**
  631. * @des 引用某条消息
  632. */
  633. quoteMsg (msg) {
  634. this.inputMsg = msg
  635. },
  636. /**
  637. * @des 某条消息被删除
  638. */
  639. deleteMsg (hash) {
  640. this.deleteChatItem(hash)
  641. },
  642. pinMsgClose () {
  643. this.pinMsg.visible = false
  644. },
  645. scrollToView () {
  646. if (!this.pinMsg) return
  647. let hash = this.pinMsg.hash
  648. let index = this.group.chatList.findIndex(item => item.hash === hash)
  649. if (index < 0) {
  650. this.addPinChatItem(this.pinMsg)
  651. index = 0
  652. }
  653. let node = this.$refs.msgWrap.getElementsByClassName('msg-item')[index]
  654. let toOffsetTop = index >= 0 ? node.offsetTop - (this.pinMsg ? 40 : 10) : node.offsetTop
  655. scrollMsgIntoView(
  656. this.$refs.scrollWrap, toOffsetTop, node
  657. )
  658. // 防止加载更多
  659. this.isScrollToView = true
  660. setTimeout(() => {
  661. this.isScrollToView = false
  662. lazyloadImage({
  663. wrap: this.$refs.scrollWrap,
  664. imageArr: this.chatImageArrSel,
  665. derection: 'up'
  666. })
  667. }, 2000)
  668. },
  669. scrollToMsg (index) {
  670. let hash = this.atList[index].hash
  671. let eleIndex = this.group.chatList.findIndex(item => item.hash === hash)
  672. if (eleIndex >= 0) {
  673. let node = this.$refs.msgWrap.querySelectorAll('.msg-item').item(eleIndex)
  674. scrollMsgIntoView(this.$refs.scrollWrap, node.offsetTop - (this.pinMsg ? 40 : 10), node)
  675. }
  676. this.removeAtListLast()
  677. }
  678. }
  679. }