chatMiniHandle.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  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. 'addChatItem',
  177. 'updateGroupBlockList',
  178. 'updateMembers',
  179. 'updateGroupPinMsg',
  180. 'repealChatItem',
  181. 'addPacketItem',
  182. 'addPacketTip',
  183. 'addUnreadNums',
  184. 'resetUnreadNums',
  185. 'setLogining'
  186. ]),
  187. ...mapActions([
  188. 'getUserInfo',
  189. 'doScatterLogout',
  190. 'getGroupInfo',
  191. 'getNewMsgFromDb',
  192. 'getNewMsg',
  193. 'getHistoryMsg',
  194. 'doSendMsg',
  195. 'doSendFile'
  196. ]),
  197. handleMoreClick () {
  198. this.showEmoji = false
  199. this.toolShow = !this.toolShow
  200. this.checkNeedToBottom()
  201. },
  202. handleEmojiClick () {
  203. this.toolShow = false
  204. this.showEmoji = !this.showEmoji
  205. this.checkNeedToBottom()
  206. },
  207. checkNeedToBottom () {
  208. if (!this.isBottom) return
  209. this.$nextTick(() => {
  210. this.resizeToBottom()
  211. })
  212. },
  213. async initMiniLoginCallback () {
  214. // await this.initMiniSocket()
  215. // this.showLoginBtn = false
  216. // this.initGroup({
  217. // userId: this.userId,
  218. // groupId: this.groupId,
  219. // useCache: false
  220. // })
  221. // await this.getUserInfo()
  222. // this.loginBoxVisible = false
  223. location.reload()
  224. },
  225. // 连接socket
  226. initMiniSocket () {
  227. if (!window.WebSocket) {
  228. console.log('Error: WebSocket is not supported .')
  229. return
  230. }
  231. let host = getMiniWsUrl() + `?group_id=${this.groupId}`
  232. if (this.socket) {
  233. this.socket.destroy()
  234. this.socket = null
  235. }
  236. this.socket = new WsManager(host, {
  237. autoConnect: true, // 自动连接
  238. reconnection: true, // 断开自动重连
  239. reconnectionDelay: 2000 // 重连间隔时间,单位秒
  240. })
  241. this.socket.on('open', res => {})
  242. this.socket.on('message', (data) => {
  243. data = JSON.parse(data)
  244. if (data.channel.match('chat:group')) {
  245. if (data.data.type === 'msg') {
  246. this.getNewMsg({ newMsg: true })
  247. if (data.data.from != this.userId) {
  248. // 未读消息数+1
  249. if (!this.showChat) {
  250. if (this.unreadCounts === 0) {
  251. this.postResize(130, 50)
  252. }
  253. this.unreadCounts++
  254. } else {
  255. this.addUnreadNums()
  256. }
  257. }
  258. }
  259. if (data.data.type === 'repeal') {
  260. this.repealChatItem(data.data)
  261. }
  262. if (data.data.type === 'block') {
  263. this.updateGroupBlockList({
  264. type: 'add',
  265. id: data.data.to
  266. })
  267. }
  268. if (data.data.type === 'unblock') {
  269. this.updateGroupBlockList({
  270. type: 'delete',
  271. id: data.data.to
  272. })
  273. }
  274. if (data.data.type === 'join') {
  275. this.updateMembers(data.data.user_info)
  276. }
  277. if (data.data.type === 'pin_msg') {
  278. this.updateGroupPinMsg(data.data.pinMsg)
  279. }
  280. if (data.data.type === 'unpin_msg') {
  281. this.updateGroupPinMsg(null)
  282. }
  283. if (data.data.type === 'new_redpack') {
  284. this.addPacketItem(data.data)
  285. if (data.data.from == this.userId) {
  286. this.$nextTick(this.resizeToBottom)
  287. }
  288. }
  289. if (data.data.type === 'grab_redpack') {
  290. if (data.data.from == this.userId || data.data.to == this.userId) {
  291. this.addPacketTip(data.data)
  292. }
  293. }
  294. }
  295. })
  296. },
  297. /**
  298. * 聊天群初始化处理
  299. * 先后调用 group/info, group/msg
  300. */
  301. async initChat () {
  302. this.handleToggleChat(this.show)
  303. this.isLoadingRoom = true
  304. await this.getGroupInfo()
  305. await this.getNewMsgFromDb()
  306. await this.getNewMsg()
  307. this.isLoadingRoom = false
  308. // 有登录态才要请求
  309. if (!this.showLoginBtn) {
  310. await this.getPmUnRead()
  311. }
  312. if (this.show) {
  313. this.$nextTick(this.resizeToBottom)
  314. }
  315. this.chatImageArrSel = this.$refs.scrollWrap.getElementsByTagName('img')
  316. },
  317. getPmUnRead () {
  318. api.session.getMiniUnRead().then(({ data }) => {
  319. this.personUnRead = data.data['0']
  320. this.serverUnRead = data.data[this.groupId] || 0
  321. })
  322. },
  323. /**
  324. * 清空私聊未读
  325. * @param {type} 1.客服2.私聊
  326. **/
  327. clearPmUnread (type) {
  328. if (type == 1) this.serverUnRead = 0
  329. else this.personUnRead = 0
  330. },
  331. async handleLogout () {
  332. this.doScatterLogout()
  333. this.showLoginBtn = true
  334. if (self !== top) {
  335. localStorage.removeItem('account')
  336. this.postMessager.send({
  337. action: 'meechat:logout'
  338. })
  339. }
  340. // 初始vuex数据
  341. this.$store.commit('setUserInfo', null)
  342. // this.$store.commit('initChatData')
  343. // this.$store.commit('initGroupData')
  344. // 注销后,刷新页面
  345. location.replace(location.href.replace('show=false', 'show=true'))
  346. },
  347. async handleLogout2 () {
  348. this.doScatterLogout()
  349. this.showLoginBtn = true
  350. },
  351. /**
  352. * 登录处理
  353. */
  354. async handleLogin () {
  355. // 设置登录按钮状态
  356. this.loginBoxVisible = true
  357. },
  358. /**
  359. * 聊天窗体滚动到底部
  360. */
  361. resizeToBottom () {
  362. this.$refs.scrollWrap.scrollTop = this.$refs.msgWrap.offsetHeight
  363. this.resetUnreadNums()
  364. this.isBottom = true
  365. lazyloadImage({
  366. wrap: this.$refs.scrollWrap,
  367. imageArr: this.chatImageArrSel,
  368. derection: 'up'
  369. })
  370. },
  371. /**
  372. * @des 点击,查看未读消息
  373. * 直接滚动到聊天列表底部
  374. */
  375. doSetRead () {
  376. this.resizeToBottom()
  377. },
  378. /**
  379. * 添加表情
  380. */
  381. addEmoji (value) {
  382. this.inputMsg += value
  383. },
  384. /**
  385. * @des 聊天窗体滚动事件处理集
  386. */
  387. handleScroll (e) {
  388. this.enableScroll = true
  389. e.target.focus()
  390. // 防止切换房间时触发滚动处理
  391. if (!this.group.chatList.length) {
  392. return
  393. }
  394. // 防止滚动到置顶消息触发滚动
  395. // if (this.isScrollToView) {
  396. // return
  397. // }
  398. let msgWrap = this.$refs.msgWrap
  399. let totalHeight = msgWrap.offsetHeight
  400. let scrollTop = e.target.scrollTop
  401. if (scrollTop === 0 && !this.lockMore) {
  402. if (this.group.endHash !== null) {
  403. this.lockMore = true
  404. this.getHistoryMsg().then((res) => {
  405. if (res === 'end') {
  406. this.lockEnd = true
  407. } else {
  408. let scrollBottom = totalHeight - scrollTop
  409. this.$nextTick(() => {
  410. e.target.scrollTop = msgWrap.offsetHeight - scrollBottom
  411. this.ps && this.ps.update()
  412. setTimeout(() => {
  413. this.lockMore = false
  414. }, 800)
  415. })
  416. }
  417. })
  418. }
  419. }
  420. // 滚动到底部清空未读消息状态
  421. if (scrollTop + e.target.offsetHeight > totalHeight) {
  422. this.isBottom = true
  423. if (this.unreadNums) {
  424. this.resetUnreadNums()
  425. }
  426. } else {
  427. this.isBottom = false
  428. }
  429. lazyloadImage({
  430. wrap: this.$refs.scrollWrap,
  431. imageArr: this.chatImageArrSel,
  432. derection: 'up'
  433. })
  434. },
  435. /**
  436. * @des 处理消息发送
  437. */
  438. async handleSend (e) {
  439. // 判断是否被禁言
  440. if (this.blockList.some(id => id == this.userId)) {
  441. Message({
  442. message: this.$t('chat.youAreBan'),
  443. type: 'error'
  444. })
  445. return
  446. }
  447. // 替换emoji字符串
  448. let _inputMsg = this.inputMsg
  449. let parts = _inputMsg.match(/\["[a-z0-9A-Z_]+"\]/g)
  450. for (let k in parts) {
  451. let emoji = this.emojiMap[parts[k]]
  452. if (emoji) {
  453. _inputMsg = _inputMsg.replace(parts[k], emoji)
  454. }
  455. }
  456. let text = _inputMsg.trim()
  457. if (text.length === 0) {
  458. Message({
  459. message: this.$t('chat.cannotBeEmpty'),
  460. type: 'warning'
  461. })
  462. return
  463. }
  464. let opt = {
  465. type: 0,
  466. msg: text
  467. }
  468. // 清空输入框
  469. this.inputMsg = ''
  470. // 用户不是第一次发言
  471. if (this.group.members[this.userId]) {
  472. let createTime = Date.now()
  473. this.addChatItem({
  474. from: this.userId,
  475. content: text,
  476. hash: `${createTime}`,
  477. timestamp: createTime,
  478. createTime,
  479. msg_type: '0',
  480. loading: true
  481. })
  482. opt.createTime = createTime
  483. await this.doSendMsg(opt)
  484. } else {
  485. // 发言后,才滚动底部
  486. await this.doSendMsg(opt)
  487. }
  488. // 滚到底部
  489. this.$nextTick(function () {
  490. this.resizeToBottom()
  491. })
  492. e.preventDefault()
  493. return false
  494. },
  495. placeEnd (el) {
  496. var range = document.createRange()
  497. range.selectNodeContents(el)
  498. range.collapse(false)
  499. var sel = window.getSelection()
  500. sel.removeAllRanges()
  501. sel.addRange(range)
  502. },
  503. initDrop (e) {
  504. e.preventDefault()
  505. let files = Array.from(e.dataTransfer.files)
  506. files.forEach(file => this.handleFile(file))
  507. },
  508. initDragOver (e) {
  509. e.preventDefault()
  510. },
  511. initPaste (event) {
  512. var items = (event.clipboardData || window.clipboardData).items
  513. if (items && items.length) {
  514. Array.from(items).forEach(item => {
  515. let file = item.getAsFile()
  516. if (file) {
  517. this.handleFile(file)
  518. }
  519. })
  520. }
  521. },
  522. /**
  523. * 文件预处理
  524. * @return {Object} data 预处理文件信息
  525. * @param {Number} data.type
  526. * @param {File} data.res
  527. */
  528. async preHandleFile (file) {
  529. let type = file.type
  530. let size = file.size
  531. if (type.match('video')) {
  532. return size > 3 * 1024 * 1024 ? Promise.reject(new Error(file)) : Promise.resolve({
  533. type: 2,
  534. res: file
  535. })
  536. } else if (type.match('audio')) {
  537. return size > 2 * 1024 * 1024 ? Promise.reject(new Error(file)) : Promise.resolve({
  538. type: 3,
  539. res: file
  540. })
  541. } else if (type.match('image')) {
  542. let image = await new ImageMin({
  543. file: file,
  544. maxSize: 1024 * 1024
  545. })
  546. return {
  547. type: 1,
  548. preview: image.base64,
  549. res: image.res
  550. }
  551. }
  552. },
  553. /**
  554. * @des 处理文件发送
  555. */
  556. async handleFile (e) {
  557. let inputfile
  558. if (e.constructor === File) {
  559. inputfile = e
  560. } else {
  561. inputfile = e.target.files[0]
  562. }
  563. try {
  564. let fileInfo = await this.preHandleFile(inputfile)
  565. let opt = { res: fileInfo.res }
  566. if (this.group.members[this.userId]) {
  567. let createTime = Date.now()
  568. this.addChatItem({
  569. content: fileInfo.preview || '',
  570. from: this.userId,
  571. hash: `${createTime}`,
  572. msg_type: fileInfo.type,
  573. timestamp: createTime,
  574. res: fileInfo.res,
  575. loading: true,
  576. createTime
  577. })
  578. opt.createTime = createTime
  579. }
  580. this.doSendFile(opt)
  581. this.toolShow = false
  582. setTimeout(() => {
  583. this.$refs.toolbar.resetInput()
  584. this.resizeToBottom()
  585. }, 100)
  586. } catch (error) {
  587. Message({
  588. message: this.$t('chat.maxUploadTips'),
  589. type: 'warning'
  590. })
  591. }
  592. },
  593. /**
  594. * @des 控制聊天窗口开关
  595. */
  596. handleToggleChat (flag) {
  597. if (flag) {
  598. this.showChat = true
  599. this.unreadCounts = 0
  600. this.$nextTick(() => {
  601. this.postResize(this.width, this.height + 16)
  602. this.resizeToBottom()
  603. })
  604. } else {
  605. this.showChat = false
  606. this.$nextTick(() => {
  607. this.postResize(56, 50)
  608. })
  609. }
  610. },
  611. postResize (width, height) {
  612. let request = {
  613. action: 'meechat:resize',
  614. data: {
  615. ch: height,
  616. cw: width
  617. }
  618. }
  619. return window.parent.postMessage(request, '*')
  620. },
  621. /**
  622. * @des 引用某条消息
  623. */
  624. quoteMsg (msg) {
  625. this.inputMsg = msg
  626. },
  627. hideLoginBox () {
  628. this.loginBoxVisible = false
  629. this.setLogining(false)
  630. }
  631. }
  632. }