import msgItem from '@/components/msgItem' import emoji from '@/components/emoji' import chatAt from '@/components/chatAt' import atMe from '@/components/chatAt/atme' import chatPin from '@/components/chatPin' import toolbar from '@/components/chatInput/toolbar' import { mapActions, mapState, mapMutations } from 'vuex' import { getMiniWsUrl } from '@/util/contract.js' import { isMobile, scrollMsgIntoView, lazyloadImage } from '@/util/util.js' import WsManager from '@/util/wsManager.js' import { Message } from 'element-ui' import ImageMin from '@/util/imageMin.js' import { chatAtMixin, chatInputMixin, changeLangMixin } from '@/mixins' import { accountLoginMixin } from '@/mixins/login' import { emojiList } from '@/util/emoji' import api from '@/api' import loginBox from '@/components/login/loginBox' export default { name: 'chatMini', mixins: [accountLoginMixin, chatAtMixin, chatInputMixin, changeLangMixin], components: { msgItem, emoji, chatAt, chatPin, atMe, toolbar, loginBox }, props: { width: { type: Number, default: 274 }, height: { type: Number, default: 390 }, show: { type: Boolean, default: false }, groupId: [Number, String] }, computed: { ...mapState([ 'account', 'group', 'userId', 'userInfo' ]), ...mapState({ chatInputFocus: state => state.group.chatInputFocus, blockList: state => state.group.blockList, pinMsg: state => state.group.pinMsg, atList: state => state.group.atList, chatList: state => state.group.chatList, unreadNums: state => state.group.unreadNums, sessionList: state => state.chat.sessionList, isJoin: state => state.group.isJoin }), emojiMap () { var emojiMap = {} for (let i in emojiList) { let arr = emojiList[i] arr.forEach(v => { let names = JSON.stringify(v.names) let emoji = v.surrogates emojiMap[names] = emoji }) } return emojiMap }, linkToCreator () { let { creator, userId } = this.group let sessionId = creator > userId ? `${userId}-${creator}` : `${creator}-${userId}` return `${location.origin}/#/pm/${sessionId}` }, isAdmin () { return (this.group.adminList && this.group.adminList.some(id => id == this.userId)) || this.group.creator == this.userId } }, data () { return { loginBoxVisible: false, isLoadingRoom: true, isMobile: isMobile(), showChat: !!this.show, // 显示聊天窗 showEmoji: false, // 显示表情选择框 showMenuExtra: false, // 显示左上角菜单 showLoginBtn: true, // 显示登录按钮 lockMore: false, isScrollToView: false, lockEnd: false, loading: false, unreadCounts: 0, // 未读消息数 inputMsg: '', // 用户输入的内容 atInd: 0, // @人索引 inputHeight: 18, enableScroll: false, // 记录滚动条是否激活的状态 isBottom: true, toolShow: false, chatImageArrSel: null, serverUnRead: 0, // 客服未读 personUnRead: 0 // 私聊未读 } }, watch: { inputMsg (val, newval) { let ele = this.$refs.chatInput this.inputHeight = 'auto' this.$nextTick(() => { this.inputHeight = Math.max(18, Math.min(ele.scrollHeight, 75)) + 'px' }) }, chatList (val) { let lastVal = val[val.length - 1] if ((lastVal && lastVal.msg_type == 4) || this.isBottom) { // 自己发的红包自动滚动到底部 this.$nextTick(this.resizeToBottom) } }, chatInputFocus (val, newval) { if (this.showLoginBtn) return let ele = this.$refs.chatInput if (val) { if (document.activeElement !== ele) { this.placeEnd(ele) ele.focus() } } else { if (document.activeElement === ele) { ele.blur() } } } }, async mounted () { // 设置groupId this.initGroup({ userId: this.userId, groupId: this.groupId, useCache: false }) // 检查登录态 let isLogin = await this.checkLocalLogin() if (isLogin) { this.showLoginBtn = false await this.getUserInfo() } this.$nextTick(this.initChat) this.$nextTick(this.initMiniSocket) document.getElementById('app').addEventListener('contextmenu', e => e.preventDefault()) document.addEventListener('paste', this.initPaste) document.addEventListener('drop', this.initDrop) document.addEventListener('dragover', this.initDragOver) document.body.addEventListener('click', () => { this.showEmoji = false this.showMenuExtra = false this.toolShow = false }) }, beforeDestroy () { document.removeEventListener('paste', this.initPaste) document.removeEventListener('drop', this.initDrop) document.removeEventListener('dragover', this.initDragOver) }, methods: { ...mapMutations([ 'initGroup', 'setUserId', 'setToken', 'addChatItem', 'deleteChatItem', 'updateChatInputFocus', 'updateGroupBlockList', 'updateMembers', 'updateGroupPinMsg', 'repealChatItem', 'removeAtListLast', 'clearAtList', 'initState', 'addPacketItem', 'addPacketTip', 'addUnreadNums', 'resetUnreadNums', 'addPinChatItem' ]), ...mapActions([ 'getUserInfo', 'setAccount', 'doGameLogin', 'doScatterLogout', 'getGroupInfo', 'getNewMsgFromDb', 'getNewMsg', 'getHistoryMsg', 'doSendMsg', 'doSendFile', 'updateSessionLastmsg' ]), async joinGroup () { this.isLoadingRoom = true await this.$store.dispatch('joinGroup') this.isLoadingRoom = false }, handleMoreClick () { this.showEmoji = false this.toolShow = !this.toolShow this.checkNeedToBottom() }, handleEmojiClick () { this.toolShow = false this.showEmoji = !this.showEmoji this.checkNeedToBottom() }, checkNeedToBottom () { if (!this.isBottom) return this.$nextTick(() => { this.resizeToBottom() }) }, async initMiniLoginCallback () { await this.initMiniSocket() this.showLoginBtn = false this.initGroup({ userId: this.userId, groupId: this.groupId, useCache: false }) await this.getUserInfo() this.loginBoxVisible = false }, // 连接socket initMiniSocket () { if (!window.WebSocket) { console.log('Error: WebSocket is not supported .') return } let host = getMiniWsUrl() + `?group_id=${this.groupId}` if (this.socket) { this.socket.destroy() this.socket = null } this.socket = new WsManager(host, { autoConnect: true, // 自动连接 reconnection: true, // 断开自动重连 reconnectionDelay: 2000 // 重连间隔时间,单位秒 }) this.socket.on('open', res => {}) this.socket.on('message', (data) => { data = JSON.parse(data) if (data.channel.match('chat:group')) { if (data.data.type === 'msg') { this.getNewMsg({ newMsg: true }) if (data.data.from != this.userId) { // 未读消息数+1 if (!this.showChat) { if (this.unreadCounts === 0) { this.postResize(130, 50) } this.unreadCounts++ } else { this.addUnreadNums() } } } if (data.data.type === 'repeal') { this.repealChatItem(data.data) } if (data.data.type === 'block') { this.updateGroupBlockList({ type: 'add', id: data.data.to }) } if (data.data.type === 'unblock') { this.updateGroupBlockList({ type: 'delete', id: data.data.to }) } if (data.data.type === 'join') { this.updateMembers(data.data.user_info) } if (data.data.type === 'pin_msg') { this.updateGroupPinMsg(data.data.pinMsg) } if (data.data.type === 'unpin_msg') { this.updateGroupPinMsg(null) } if (data.data.type === 'new_redpack') { this.addPacketItem(data.data) if (data.data.from == this.userId) { this.$nextTick(this.resizeToBottom) } } if (data.data.type === 'grab_redpack') { if (data.data.from == this.userId || data.data.to == this.userId) { this.addPacketTip(data.data) } } } }) }, /** * 聊天群初始化处理 * 先后调用 group/info, group/msg */ async initChat () { this.handleToggleChat(this.show) this.isLoadingRoom = true await this.getGroupInfo() await this.getNewMsgFromDb() await this.getNewMsg() this.isLoadingRoom = false // 有登录态才要请求 if (!this.showLoginBtn) { await this.getPmUnRead() } if (this.show) { this.$nextTick(this.resizeToBottom) } this.chatImageArrSel = this.$refs.scrollWrap.getElementsByTagName('img') }, getPmUnRead () { api.session.getMiniUnRead().then(({ data }) => { this.personUnRead = data.data['0'] this.serverUnRead = data.data[this.groupId] || 0 }) }, /** * 清空私聊未读 * @param {type} 1.客服2.私聊 **/ clearPmUnread (type) { if (type == 1) this.serverUnRead = 0 else this.personUnRead = 0 }, async handleLogout () { this.doScatterLogout() this.showLoginBtn = true if (self !== top) { localStorage.removeItem('account') this.postMessager.send({ action: 'meechat:logout' }) } // 初始vuex数据 this.$store.commit('setUserInfo', null) // this.$store.commit('initChatData') // this.$store.commit('initGroupData') // 注销后,刷新页面 location.replace(location.href.replace('show=false', 'show=true')) }, async handleLogout2 () { this.doScatterLogout() this.showLoginBtn = true }, /** * 登录处理 */ async handleLogin () { // 设置登录按钮状态 this.loginBoxVisible = true }, /** * 聊天窗体滚动到底部 */ resizeToBottom () { this.$refs.scrollWrap.scrollTop = this.$refs.msgWrap.offsetHeight this.resetUnreadNums() this.isBottom = true lazyloadImage({ wrap: this.$refs.scrollWrap, imageArr: this.chatImageArrSel, derection: 'up' }) }, /** * @des 点击,查看未读消息 * 直接滚动到聊天列表底部 */ doSetRead () { this.resizeToBottom() }, /** * 添加表情 */ addEmoji (value) { this.inputMsg += value }, /** * @des 聊天窗体滚动事件处理集 */ handleScroll (e) { this.enableScroll = true e.target.focus() // 防止切换房间时触发滚动处理 if (!this.group.chatList.length) { return } // 防止滚动到置顶消息触发滚动 // if (this.isScrollToView) { // return // } let msgWrap = this.$refs.msgWrap let totalHeight = msgWrap.offsetHeight let scrollTop = e.target.scrollTop if (scrollTop === 0 && !this.lockMore) { if (this.group.endHash !== null) { this.lockMore = true this.getHistoryMsg().then((res) => { if (res === 'end') { this.lockEnd = true } else { let scrollBottom = totalHeight - scrollTop this.$nextTick(() => { e.target.scrollTop = msgWrap.offsetHeight - scrollBottom this.ps && this.ps.update() setTimeout(() => { this.lockMore = false }, 800) }) } }) } } // 滚动到底部清空未读消息状态 if (scrollTop + e.target.offsetHeight > totalHeight) { this.isBottom = true if (this.unreadNums) { this.resetUnreadNums() } } else { this.isBottom = false } lazyloadImage({ wrap: this.$refs.scrollWrap, imageArr: this.chatImageArrSel, derection: 'up' }) }, /** * @des 处理消息发送 */ async handleSend (e) { // 判断是否被禁言 if (this.blockList.some(id => id == this.userId)) { Message({ message: this.$t('chat.youAreBan'), type: 'error' }) return } // 替换emoji字符串 let _inputMsg = this.inputMsg let parts = _inputMsg.match(/\["[a-z0-9A-Z_]+"\]/g) for (let k in parts) { let emoji = this.emojiMap[parts[k]] if (emoji) { _inputMsg = _inputMsg.replace(parts[k], emoji) } } let text = _inputMsg.trim() if (text.length === 0) { Message({ message: this.$t('chat.cannotBeEmpty'), type: 'warning' }) return } let opt = { type: 0, msg: text } // 清空输入框 this.inputMsg = '' // 用户不是第一次发言 if (this.group.members[this.userId]) { let createTime = Date.now() this.addChatItem({ from: this.userId, content: text, hash: `${createTime}`, timestamp: createTime, createTime, msg_type: '0', loading: true }) opt.createTime = createTime await this.doSendMsg(opt) } else { // 发言后,才滚动底部 await this.doSendMsg(opt) } // 滚到底部 this.$nextTick(function () { this.resizeToBottom() }) e.preventDefault() return false }, placeEnd (el) { var range = document.createRange() range.selectNodeContents(el) range.collapse(false) var sel = window.getSelection() sel.removeAllRanges() sel.addRange(range) }, initDrop (e) { e.preventDefault() let files = Array.from(e.dataTransfer.files) files.forEach(file => this.handleFile(file)) }, initDragOver (e) { e.preventDefault() }, initPaste (event) { var items = (event.clipboardData || window.clipboardData).items if (items && items.length) { Array.from(items).forEach(item => { let file = item.getAsFile() if (file) { this.handleFile(file) } }) } }, /** * 文件预处理 * @return {Object} data 预处理文件信息 * @param {Number} data.type * @param {File} data.res */ async preHandleFile (file) { let type = file.type let size = file.size if (type.match('video')) { return size > 3 * 1024 * 1024 ? Promise.reject(new Error(file)) : Promise.resolve({ type: 2, res: file }) } else if (type.match('audio')) { return size > 2 * 1024 * 1024 ? Promise.reject(new Error(file)) : Promise.resolve({ type: 3, res: file }) } else if (type.match('image')) { let image = await new ImageMin({ file: file, maxSize: 1024 * 1024 }) return { type: 1, preview: image.base64, res: image.res } } }, /** * @des 处理文件发送 */ async handleFile (e) { let inputfile if (e.constructor === File) { inputfile = e } else { inputfile = e.target.files[0] } try { let fileInfo = await this.preHandleFile(inputfile) let opt = { res: fileInfo.res } if (this.group.members[this.userId]) { let createTime = Date.now() this.addChatItem({ content: fileInfo.preview || '', from: this.userId, hash: `${createTime}`, msg_type: fileInfo.type, timestamp: createTime, res: fileInfo.res, loading: true, createTime }) opt.createTime = createTime } this.doSendFile(opt) this.toolShow = false setTimeout(() => { this.$refs.toolbar.resetInput() this.resizeToBottom() }, 100) } catch (error) { Message({ message: this.$t('chat.maxUploadTips'), type: 'warning' }) } }, /** * @des 控制聊天窗口开关 */ handleToggleChat (flag) { if (flag) { this.showChat = true this.unreadCounts = 0 this.$nextTick(() => { this.postResize(this.width, this.height + 16) this.resizeToBottom() }) } else { this.showChat = false this.$nextTick(() => { this.postResize(56, 50) }) } }, postResize (width, height) { let request = { action: 'meechat:resize', data: { ch: height, cw: width } } return window.parent.postMessage(request, '*') }, /** * @des 引用某条消息 */ quoteMsg (msg) { this.inputMsg = msg }, /** * @des 某条消息被删除 */ deleteMsg (hash) { this.deleteChatItem(hash) }, pinMsgClose () { this.pinMsg.visible = false }, scrollToView () { if (!this.pinMsg) return let hash = this.pinMsg.hash let index = this.group.chatList.findIndex(item => item.hash === hash) if (index < 0) { this.addPinChatItem(this.pinMsg) index = 0 } let node = this.$refs.msgWrap.getElementsByClassName('msg-item')[index] let toOffsetTop = index >= 0 ? node.offsetTop - (this.pinMsg ? 40 : 10) : node.offsetTop scrollMsgIntoView( this.$refs.scrollWrap, toOffsetTop, node ) // 防止加载更多 this.isScrollToView = true setTimeout(() => { this.isScrollToView = false lazyloadImage({ wrap: this.$refs.scrollWrap, imageArr: this.chatImageArrSel, derection: 'up' }) }, 2000) }, scrollToMsg (index) { let hash = this.atList[index].hash let eleIndex = this.group.chatList.findIndex(item => item.hash === hash) if (eleIndex >= 0) { let node = this.$refs.msgWrap.querySelectorAll('.msg-item').item(eleIndex) scrollMsgIntoView(this.$refs.scrollWrap, node.offsetTop - (this.pinMsg ? 40 : 10), node) } this.removeAtListLast() } } }