chat.js
import $U from "./util.js"; import $H from './request.js'; class chat { constructor(arg) { this.url = arg.url this.isOnline = false this.socket = null // 获取当前用户相关信息 let user = $U.getStorage('user'); this.user = user ? JSON.parse(user) : {}, // 初始化聊天对象 this.TO = false; // 连接和监听 if (this.user.token) { this.connectSocket() } } // 连接socket connectSocket() { console.log(this.user); this.socket = uni.connectSocket({ url: this.url + '?token=' + this.user.token, complete: () => {} }) // 监听连接成功 this.socket.onOpen(() => this.onOpen()) // 监听接收信息 this.socket.onMessage((res) => this.onMessage(res)) // 监听断开 this.socket.onClose(() => this.onClose()) // 监听错误 this.socket.onError(() => this.onError()) } // 监听打开 onOpen() { // 用户状态上线 this.isOnline = true; console.log('socket连接成功'); // 获取用户离线消息 } // 监听关闭 onClose() { // 用户下线 this.isOnline = false; this.socket = null; console.log('socket连接关闭'); } // 监听消息 onMessage(data) { console.log('监听消息', data); } // 监听连接错误 onError() { // 用户下线 this.isOnline = false; this.socket = null; console.log('socket连接错误'); } // 关闭连接 close() { this.socket.close() } // 创建聊天对象 createChatObject(detail) { this.TO = detail; console.log('创建聊天对象', this.TO) } // 销毁聊天对象 destoryChatObject() { this.TO = false } // 组织发送信息格式 formatSendData(params) { return { id: 0, // 唯一id,后端生成,用于撤回指定消息 from_avatar: this.user.avatar, // 发送者头像 from_name: this.user.nickname || this.user.username, // 发送者昵称 from_id: this.user.id, // 发送者id to_id: params.to_id || this.TO.id, // 接收人/群 id to_name: params.to_name || this.TO.name, // 接收人/群 名称 to_avatar: params.to_avatar || this.TO.avatar, // 接收人/群 头像 chat_type: params.chat_type || this.TO.chat_type, // 接收类型 type: params.type, // 消息类型 data: params.data, // 消息内容 options: params.options ? params.options : {}, // 其他参数 create_time: (new Date()).getTime(), // 创建时间 isremove: 0, // 是否撤回 sendStatus: params.sendStatus ? params.sendStatus : "pending" // 发送状态,success发送成功,fail发送失败,pending发送中 } } // 发送信息 send(message) { return new Promise((result, reject) => { // 添加消息历史记录 let { data, k } = this.addChatDetail(params, true); // 置顶消息记录 // 验证是否上线 $H.post('/chat/send', { to_id: this.TO.id, type: message.type, chat_type: this.TO.chat_type, data: message.data, }).then(res => { // 发送成功 result(res); }).catch(err => { // 发送失败 // 断线重连提示 result(err); }); }) } } export default chat
chat.vue
<template> <view> <!-- 导航栏 --> <free-nav-bar title="呵呵呵呵" :noreadnum="1" showBack> <free-icon-button slot="right"><text class="iconfont font-md" @click="openChat"></text></free-icon-button> </free-nav-bar> <!-- 聊天内容区域 --> <scroll-view scroll-y="true" class="bg-light position-fixed left-0 right-0" style="bottom: 105rpx;" :style="chatBodyBottom" :show-scrollbar="false"> <block v-for="(item,index) in list" :key="index"> <!-- 聊天信息列表组件 --> <free-chat-item :item="item" :index="index" :pretime="index>0 ? list[index-1].create_time : 0" @long="long" ref="chatItem" @preview="previewImage"></free-chat-item> </block> <!-- 右边 --> <!-- <view class="flex align-start justify-end position-relative"> <div class="bg-chat-item p-2 rounded mr-3" style="max-width:500rpx;"> <text class="font-md">你好你好你好你好你好你好你好你好你好你好你好</text> </div> <view class="iconfont font-md position-absolute chat-right-icon"><text class="text-chat-item iconfont font-md"></text></view> <free-avatar size="75" src="/static/images/demo/demo6.jpg"></free-avatar> </view> --> </scroll-view> <!-- 扩展菜单 --> <free-popup ref="action" bottom transformOrigin="center bottom" @hide="keyBoardHeight = 0" :mask="false"> <view style="height: 580rpx;" class="border-top border-light-secondary"> <swiper :indicator-dots="emoticonOrActionList.length>1" style="height:510rpx;"> <swiper-item class="row" v-for="(item,index) in emoticonOrActionList" :key="index" > <view class="col-3 flex flex-column align-center justify-center" style="height: 255rpx;" v-for="(item2,index2) in item" :key="index2" @click="actionEvent(item2)"> <image :src="item2.icon" mode="widthFix" style="width: 100rpx;height: 100rpx;"></image> <text class="font-sm text-muted mt-2">{{item2.name}}</text> </view> </swiper-item> </swiper> </view> </free-popup> <!-- 弹出层 --> <free-popup ref="extend" maskColor bottom :bodyWidth="240" :bodyHeight="geMenusHeight" :tabbarHeight="105"> <view class="flex flex-column" style="width:240rpx;" :style="getMenusStyle"> <view v-for="(item,index) in menusList" :key="index" class="flex-1 flex align-center" hover-class="bg-light" @click="clickEvent(item.event)"> <text class="font-md pl-3">{{item.name}}</text> </view> </view> </free-popup> <!-- #ifdef APP-PLUS-NVUE --> <div class="position-fixed top-0 right-0 left-0 bottom-0" v-if="mode==='action' || mode==='emoticon'" @click="clickPage" :style="'bottom:'+maskBottom+'px;'"></div> <!-- #endif --> <!-- 底部输入框 --> <view class="position-fixed left-0 right-0 border-top flex align-center" style="background-color: #F7F7F6;height: 105rpx;" :style="'bottom:'+keyBoardHeight+'px;'"> <free-icon-button @click="changeVoiceOrText"> <block v-if="mode === 'audio'"> <text class="iconfont font-lg"></text> </block> <block v-else> <text class="iconfont font-lg"></text> </block> </free-icon-button> <view class="flex-1"> <view v-if="mode==='audio'" class="rounded flex align-center justify-center" style="height: 80rpx;" :class="isRecording?'bg-hover-light':'bg-white'" @touchstart="voiceTouchStart" @touchend="voiceTouchEnd" @touchmove="voiceTouchMove" @touchcancel="voiceTouchCancel"> <text class="font">{{isRecording ? '松开 结束' : '按住 说话'}}</text> </view> <textarea v-else class="bg-white rounded p-1 font-md" style="height: 50rpx;max-width: 500rpx;" :adjust-position="false" v-model="text" @click="onInputClick" /> </view> <template v-if="text.length === 0"> <!-- 表情 --> <free-icon-button><text class="iconfont font-lg" @click="openActionOrEmoticon('emoticon')"></text></free-icon-button> <!-- 扩展菜单 --> <free-icon-button @click="openActionOrEmoticon('action')"><text class="iconfont font-lg"></text></free-icon-button> </template> <view v-else class="flex-shrink"> <!-- 发送按钮 --> <!-- <view class="main-bg-color rounded flex align-center justify-center mr-2 px-2 pt-4" style="height: 70rpx;" @click="send('text')"> 发送 </view> --> <free-main-button name="发送" @click="send('text')"></free-main-button> </view> </view> <!-- 录音提示 --> <view v-if="isRecording" class="position-fixed top-0 left-0 right-0 flex align-center justify-center" style="bottom: 105rpx;"> <view class="rounded flex flex-column align-center justify-center" style="width: 360rpx;height: 360rpx;background-color: rgba(0,0,0,0.5);"> <image src="/static/images/audio/audio/recording.gif" style="width: 150rpx;height: 150rpx;"></image> <text class="font text-white mt-3">{{unRecord?'松开手指,取消发送':'手指上滑,取消发送'}}</text> </view> </view> </view> </template> <script> // #ifdef APP-NVUE const domModule = weex.requireModule('dom'); // #endif import freeNavBar from '@/components/free-ui/free-nav-bar.vue'; import freeIconButton from '@/components/free-ui/free-icon-button.vue'; import freeChatItem from '@/components/free-ui/free-chat-item.vue'; import freePopup from '@/components/free-ui/free-popup.vue'; import freeMainButton from '@/components/free-ui/free-main-button.vue'; import { mapState,mapMutations } from 'vuex'; import auth from '@/common/mixin/auth.js'; export default { mixins:[auth], components: { freeNavBar, freeIconButton, freeChatItem, freePopup, freeMainButton }, watch:{ mode(newValue,oldValue){ if(newValue !== 'action' && newValue !== 'emoticon'){ this.$refs.action.hide(); } if(newValue !== 'text'){ uni.hideKeyboard() } } }, // 生命周期 mounted() { this.statusBarHeight = 0; // 获取任务栏高度 // #ifdef APP-PLUS-NVUE this.statusBarHeight = plus.navigator.getStatusbarHeight() // #endif this.navBarHeight = this.statusBarHeight + uni.upx2px(90) // 监听键盘高度变化 uni.onKeyboardHeightChange((res) => { if(this.mode !== 'action' && this.mode !== 'emoticon'){ this.keyBoardHeight = res.height; } if (this.keyBoardHeight>0) { this.pageToBottom() } }) // 注册发送音频事件 this.regSendVoiceEvent((url)=>{ if(!this.unRecord){ // 发送 this.send('audio',url,{ time:this.RecordTime }) } }); }, onLoad(e) { if(!e.params){ return this.backToast(); } // 初始化 this.__init(); this.detail = JSON.parse(e.params) console.log(this.detail) // 创建聊天对象 this.chat.createChatObject(this.detail) // 获取历史记录 // 监听接收聊天信息 }, destroyed() { // 销毁聊天对象 this.chat.destoryChatObject(); }, computed: { ...mapState({ RECORD:state=>state.audio.RECORD, RecordTime:state=>state.audio.RecordTime, chat:state=>state.user.chat }), // 所有信息的图片地址 imageList(){ var arr = []; this.list.forEach((item)=>{ if(item.type === 'emoticon' || item.type === 'image'){ arr.push(item.data) } }) return arr; }, // 获取蒙版的位置 maskBottom(){ return this.keyBoardHeight + uni.upx2px(105) }, // 动态获取菜单高度 geMenusHeight() { let H = 100; return this.menus.length * H; }, // 获取菜单的样式 getMenusStyle() { return `height:${this.geMenusHeight}rpx;`; }, // 是否是本人 isDoSelf() { // 获取本人id(假设拿到了) // let id = 1; // let user_id = this.propIndex > -1 ? this.list[this.propIndex].user_id : 0; // return user_id === id; }, // 获取操作菜单 menusList() { return this.menus.filter(v => { if (v.name === '撤回' && this.isDoSelf) { return false; } else { return true; } }) }, // 聊天区域bottom chatBodyBottom() { return `bottom:${uni.upx2px(105) + this.keyBoardHeight}px;top:${this.navBarHeight}px`; }, // 获取操作或者表情列表 emoticonOrActionList(){ if(this.mode === 'emoticon' || this.mode === 'action'){ return this[this.mode+'List']; }else{ return []; } } }, data() { return { // 模式 text输入文字 emoticon表情 action操作 audio音频 mode:'text', // 扩展菜单列表 actionList: [ [{ name: "相册", icon: "/static/images/extends/pic.png", event: "uploadImage" }, { name: "拍摄", icon: "/static/images/extends/video.png", event: "uploadVideo" }, { name: "收藏", icon: "/static/images/extends/shoucan.png", event: "" }, { name: "名片", icon: "/static/images/extends/man.png", event: "" }, { name: "语音通话", icon: "/static/images/extends/phone.png", event: "" }, { name: "位置", icon: "/static/images/extends/path.png", event: "" } ] ], // 表情包 emoticonList:[ [{ name: "沮丧", icon: "/static/images/emoticon/5497/0.gif", event: "" }, { name: "沮丧", icon: "/static/images/emoticon/5497/0.gif", event: "" }] ], // 输入文字 text: "", // 音频录制中 isRecording:false, // 取消录音 unRecord:false, // 当前气泡索引 keyBoardHeight: 0, propIndex: 1, navBarHeight: 0, list:[], // list: [{ // avatar: "/static/images/demo/demo6.jpg", // nickname: "昵称", // user_id: 1, // type: "text", // image,audio,video,file,share // data: "你好你好你好你好你好你好你好你好你好你好你好", // create_time: 1628233521, // isremove: false // }, { // avatar: "/static/images/demo/demo6.jpg", // nickname: "昵称", // user_id: 2, // type: "text", // image,audio,video,file,share // data: "11你好你好你好你好你好你好你好你好你好你好你好", // create_time: 1628234896, // isremove: false // }, // { // avatar: "/static/images/demo/demo6.jpg", // nickname: "昵称", // user_id: 1, // type: "text", // image,audio,video,file,share // data: "你好你好你好你好你好你好你好你好你好你好你好", // create_time: 1628233521, // isremove: false // },{ // avatar: "/static/images/demo/demo6.jpg", // nickname: "昵称", // user_id: 2, // type: "text", // image,audio,video,file,share // data: "11你好你好你好你好你好你好你好你好你好你好你好", // create_time: 1628234896, // isremove: false // }, // { // avatar: "/static/images/demo/demo6.jpg", // nickname: "昵称", // user_id: 1, // type: "audio", // image,audio,video,file,share // data: "/static/images/audio/audio/1.mp3", // options:{ // time:10 // }, // create_time: 1628234896, // isremove: false // }, // { // avatar: "/static/images/demo/demo6.jpg", // nickname: "昵称", // user_id: 2, // type: "audio", // image,audio,video,file,share // options:{ // time:20 // }, // data: "/static/images/audio/audio/2.mp3", // create_time: 1628234896, // isremove: false // }, // { // avatar: "/static/images/demo/demo6.jpg", // nickname: "昵称", // user_id: 1, // type: "audio", // image,audio,video,file,share // data: "/static/images/audio/audio/3.mp3", // options:{ // time:60 // }, // create_time: 1628234896, // isremove: false // }, // { // avatar: "/static/images/demo/demo6.jpg", // nickname: "昵称", // user_id: 1, // type: "video", // image,audio,video,file,share // data: "/static/video/demo.mp4", // options:{ // poster:"/static/video/demo.jpg" // }, // create_time: 1628234896, // isremove: false // } // ], RecordingStartY:0, detail:{ id:0, name:'', avatar:'', chat_type:'user' }, menus: [{ name: '复制', event: "" }, { name: '发送给朋友', event: "" }, { name: '收藏', event: "" }, { name: '删除', event: "" }, { name: '多选', event: "" }, { name: '撤回', event: "removeChatItem" }], } }, methods: { ...mapMutations(['regSendVoiceEvent']), __init(){ var total = 24; var page = Math.ceil(total/8); var arr = []; for (var i = 0;i<page;i++) { var start = i*8; arr[i] = []; for(var j=0;j<=8;j++){ arr[i].push({ name:'表情'+(start+j), icon:'/static/images/emoticon/5497/'+(start+j)+'.gif', event:'sendEmoticon' }) } } this.emoticonList = arr; }, // 点击区域 clickPage(){ // 隐藏操作菜单 this.$refs.action.hide(); this.mode = 'text'; }, // 输入框聚焦 onInputClick(event){ this.mode = 'text'; }, // 事件分发 actionEvent(e){ switch(e.event){ case 'uploadImage': uni.chooseImage({ count: 9, //默认9 success:(res)=>{ res.tempFilePaths.forEach((item)=>{ // 发送到服务器 // 渲染到页面 this.send('image',item); }) } }); break; case 'sendEmoticon': // 发送表情包 this.send('emoticon',e.icon); break; case 'uploadVideo': // 发送短视频 uni.chooseVideo({ maxDuration:10, success: (res) => { // 渲染页面 this.send('video',res.tempFilePath); // 发送到服务端(获取视频封面,返回url) // 修改本地发送状态 } }) break; } }, // 打开扩展菜单或者表情包 openActionOrEmoticon(model = 'action') { this.mode = model; this.$refs.action.show(); uni.hideKeyboard(); this.keyBoardHeight = uni.upx2px(580); }, // 发送 send(type,data='',options={}) { // 组织数据格式 switch (type) { case 'text': data = this.text; break; case 'image': data = data; break; case 'audio': break; case 'video': break; case 'file': break; case 'share': break; default: obj.data=data; break; } let message = this.chat.formatSendData({ type, data, options }) // 渲染到页面 this.list.push(message); // 发送到服务端 this.chat.send(message).then(res=>{ console.log(res) }).catch(err=>{ console.log(err); }) // 发送文字成功,清空输入框 if(type==='text'){ this.text = ""; } // var time = (new Date()).getTime(); // var obj = { // avatar: "/static/images/demo/demo6.jpg", // nickname: "昵称", // user_id: 1, // type: type, // image,audio,video,emoticon // data: data, // create_time: time, // options:options, // isremove: false // }; // switch (type) { // case 'text': // obj.data = this.text; // this.text = ""; // break; // case 'image': // obj.data = data; // break; // case 'audio': // break; // case 'video': // break; // case 'file': // break; // case 'share': // break; // default: // obj.data=data; // break; // } // this.list.push(obj); // // 置于底部 // var pageToBottomTimer = setTimeout(()=>{ // this.pageToBottom(); // clearTimeout(pageToBottomTimer); // },200); }, // 长按消息气泡 long(e) { this.propIndex = e.index; this.$refs.extend.show(e.x, e.y); }, // 回到底部 pageToBottom() { let chatItem = this.$refs.chatItem; let lastIndex = chatItem.length > 0 ? chatItem.length - 1 : 0; let last = chatItem[lastIndex]; // #ifdef APP-NVUE if (chatItem[lastIndex]) { domModule.scrollToElement(last, {}); } // #endif }, // 操作菜单方法分发 clickEvent(event) { switch (event) { case 'removeChatItem': // 撤回消息 // 拿到当前被操作的信息 this.list[this.propIndex].isremove = true; break; default: break; } // 关闭菜单 this.$refs.extend.hide(); }, // 预览图片 previewImage(url){ // 预览图片 uni.previewImage({ current:url, urls:this.imageList, indicator:"default", longPressActions: { itemList: ['发送给朋友', '保存图片', '收藏'], success: function(data) { console.log('选中了第' + (data.tapIndex + 1) + '个按钮,第' + (data.index + 1) + '张图片'); }, fail: function(err) { console.log(err.errMsg); } } }); }, // 切换音频录制喝文本输入 changeVoiceOrText(){ this.mode = this.mode !== 'audio' ? 'audio' : 'text'; }, // 录音相关 // 录音开始 voiceTouchStart(e){ this.isRecording = true; this.RecordingStartY = e.changedTouches[0].screenY; this.unRecord = false; // 开始录音 this.RECORD.start({ format:'mp3' }) }, // 录音结束 voiceTouchEnd(){ this.isRecording = false; // 停止录音 this.RECORD.stop(); }, // 录音打断 voiceTouchCancel(){ this.isRecording = false; this.unRecord = true; // 停止录音 this.RECORD.stop(); }, voiceTouchMove(e){ var Y = Math.abs(e.changedTouches[0].screenY-this.RecordingStartY); this.unRecord = (Y>=80); }, // 打卡聊天信息 openChat(){ uni.navigateTo({ url:'/pages/chat/chat-set/chat-set' }) } } } </script> <style> </style>