chatMiniHandle.js 18 KB

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