主文件 chat.nvue
<template> <view> <!-- 导航栏 --> <free-nav-bar title="呵呵呵呵" :noreadnum="1" showBack> <free-icon-button slot="right"><text class="iconfont font-md"></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: 75rpx;" :adjust-position="false" v-model="text" @click="onInputClick" /> </view> <!-- 表情 --> <free-icon-button><text class="iconfont font-lg" @click="openActionOrEmoticon('emoticon')"></text></free-icon-button> <template v-if="text.length === 0"> <!-- 扩展菜单 --> <free-icon-button @click="openActionOrEmoticon('action')"><text class="iconfont font-lg"></text></free-icon-button> </template> <template v-else> <!-- 发送按钮 --> <view class="main-bg-color rounded flex align-center justify-center mr-2 px-2 pt-4" style="height: 70rpx;" @click="send('text')">发送 </view> </template> </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 { mapState,mapMutations } from 'vuex'; export default { components: { freeNavBar, freeIconButton, freeChatItem, freePopup }, watch:{ mode(newValue,oldValue){ if(newValue !== 'action' && newValue !== 'emoticon'){ this.$refs.action.hide(); } if(newValue !== 'text'){ uni.hideKeyboard() } } }, // 生命周期 mounted() { // 获取任务栏高度 // #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 }) } }); }, computed: { ...mapState({ RECORD:state=>state.audio.RECORD, RecordTime:state=>state.audio.RecordTime }), // 所有信息的图片地址 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:'audio', // 扩展菜单列表 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: [{ 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, menus: [{ name: '复制', event: "" }, { name: '发送给朋友', event: "" }, { name: '收藏', event: "" }, { name: '删除', event: "" }, { name: '多选', event: "" }, { name: '撤回', event: "removeChatItem" }], } }, created(){ // 初始化 this.__init(); }, 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={}) { 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]; if (chatItem[lastIndex]) { domModule.scrollToElement(last, {}); } }, // 操作菜单方法分发 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); } } } </script> <style> </style>
插件 free-nav-bar.vue free-icon-button.vue free-chat-item.vue free-popup.vue
free-nav-bar.vue
<template> <view class="flex align-center justify-center" hover-class="bg-hover-light" @click="$emit('click')" style="height: 90rpx;width: 90rpx;"> <slot></slot> </view> </template> <script> export default { props: { icon: { type: String, default: '' }, }, mounted() { // #ifdef APP-PLUS-NVUE // 加载公共图标库 const domModule = weex.requireModule('dom') domModule.addRule('fontFace', { 'fontFamily': "iconfont", 'src': "url('/static/font_1365296_2ijcbdrmsg.ttf')" }); // #endif } } </script> <style> </style>
free-icon-button.vue
<template> <view class="flex align-center justify-center" hover-class="bg-hover-light" @click="$emit('click')" style="height: 90rpx;width: 90rpx;"> <slot></slot> </view> </template> <script> export default { props: { icon: { type: String, default: '' }, }, mounted() { // #ifdef APP-PLUS-NVUE // 加载公共图标库 const domModule = weex.requireModule('dom') domModule.addRule('fontFace', { 'fontFamily': "iconfont", 'src': "url('/static/font_1365296_2ijcbdrmsg.ttf')" }); // #endif } } </script> <style> </style>
free-chat-item.vue
<template> <view @longpress="long"> <!-- 时间显示 --> <view v-if="showTime" class="flex align-center justify-center pb-4 pt-2"> <text class="font-sm text-light-muted">{{showTime}}</text> </view> <!-- 撤回消息 --> <view class="flex align-center justify-center pb-4 pt-1 chat-animate" v-if="item.isremove"> <text class="font-sm text-light-muted">你撤回了一条消息</text> </view> <!-- 气泡 --> <view v-else class="flex align-start position-relative mb-3" :class="!isself ? 'justify-start' : 'justify-end'"> <!-- 头像 --> <template v-if="!isself"> <free-avatar size="75" :src="item.avatar"></free-avatar> <view class="iconfont font-md position-absolute chat-left-icon bg-light"><text class="iconfont font-md text-white"></text></view> <!-- 聊天气泡 --> <div class="p-2 rounded" :class="labelClass" style="max-width:500rpx;"> <!-- 文字 --> <text v-if="item.type === 'text'" class="font-md">{{item.data}}</text> <!-- 表情包||图片 --> <free-image @click="preview(item.data)" v-if="item.type==='emoticon' || item.type==='image'" :src="item.data"></free-image> <!-- 音频 --> <view v-if="item.type === 'audio'" class="flex align-center" @click="openAudio(item.data)"> <image :src="audioPlaying ?'/static/images/audio/audio/play.gif' : '/static/images/audio/audio/audio3.png'" style="width: 50rpx;height: 50rpx;" class="font"></image> <text class="font">{{item.options.time + '"'}}</text> </view> </div> </template> <template v-if="isself"> <div class="p-2 rounded" :class="labelClass" style="max-width:500rpx;" :style="labelStyle"> <!-- 文字 --> <text v-if="item.type === 'text'" class="font-md">{{item.data}}</text> <!-- 表情包||图片 --> <free-image @click="preview(item.data)" v-if="item.type==='emoticon' || item.type==='image'" :src="item.data"></free-image> <!-- 音频 --> <view v-if="item.type === 'audio'" class="flex align-center" @click="openAudio(item.data)"> <image :src="audioPlaying ?'/static/images/audio/audio/play.gif' : '/static/images/audio/audio/audio3.png'" style="width: 50rpx;height: 50rpx;" class="mx-1"></image> <text class="font">{{item.options.time + '"'}}</text> </view> <!-- 视频 --> <view v-if="item.type === 'video'" class="position-relative rounded"> <free-image :src="item.options.poster" @click="openVideo" imageClass="rounded" :maxWidth="350" :maxHeight="300" @load="loadPoster"></free-image> <text class="iconfont text-white position-absolute" style="font-size: 80rpx;width: 80rpx;height: 80rpx;" :style="posterTextStyle"></text> </view> </div> <view class="iconfont font-md position-absolute" :class="(item.type==='text')?'chat-right-icon' : '' "> <text class="text-chat-item iconfont"></text></view> <free-avatar size="75" :src="item.avatar"></free-avatar> </template> </view> </view> </template> <script> import freeAvatar from "@/components/free-ui/free-avatar.vue"; import $T from '@/common/free-lib/time.js'; import freeImage from './free-image.vue'; import { mapState,mapActions } from 'vuex'; export default { components: { freeAvatar, freeImage }, props: { item: Object, index: Number, // 上一条消息的时间戳 pretime: [Number, String] }, data(){ return { innerAudioContext:null, audioPlaying:false, // 默认封面宽高 poster:{ w:100, h:100 } } }, mounted() { // 注册全局事件 if(this.item.type === 'audio'){ this.audioOn(this.onPlayAudio) } // 监听是否撤回消息 // #ifdef APP-PLUS-NVUE this.$watch('item.isremove', (newVal, oldVal) => { if (newVal) { const animation = weex.requireModule('animation'); this.$nextTick(() => { animation.transition(this.$refs.isremove, { styles: { opacity: 1 }, duration: 100, //ms timingFunction: 'ease', }, () => { console.log('动画执行结束'); }) }) } }) // #endif }, computed: { ...mapState({ ceshi:state=>state.audio.ceshi }), labelStyle(){ if(this.item.type === 'audio'){ let time = this.item.options.time || 0; let width = parseInt(time) / (60/500); width = width < 150 ? 150 : width; return `width:${width}rpx;`; } }, // 视频封面图标 posterTextStyle(){ let w = (this.poster.w-uni.upx2px(80))/2; let h = (this.poster.h-uni.upx2px(80))/2; return `left:${w}px;top:${h}px;`; }, // 是否是本人 isself() { // 获取本人id(假设拿到了) let id = 1; return this.item.user_id === id; }, // 显示时间 showTime() { return $T.getChatTime(this.item.create_time, this.pretime); }, // 是否需要气泡样式 hasLabelClass() { return this.item.type === 'text' || this.item.type === 'audio'; }, // 气泡打样式 labelClass() { let lable = this.hasLabelClass ? 'bg-chat-item mr-3' : 'mr-3'; return this.isself ? lable : 'bg-white ml-3'; } }, // 组件销毁 destroyed() { if(this.item.type === 'audio'){ this.audioOff(this.onPlayAudio) } // 销毁 if(this.innerAudioContext != null){ this.innerAudioContext.destroy(); this.innerAudioContext = null; } }, methods: { ...mapActions(['audioOn','audioEmit','audioOff']), // 加载封面 loadPoster(e){ this.poster.w = e.w; this.poster.h = e.h; }, // 打开视频 openVideo(){ uni.navigateTo({ url:'/pages/chat/video/video?url='+this.item.data, }) }, // 监听音频播放全局事件 onPlayAudio(index){ if(this.innerAudioContext){ if(this.index !== index){ this.innerAudioContext.pause(); } } }, // 播放音频 openAudio(url){ // 通知其他音频停止 this.audioEmit(this.index); if(this.innerAudioContext==null){ this.innerAudioContext = uni.createInnerAudioContext(); this.innerAudioContext.autoplay = true; this.innerAudioContext.src = url; this.innerAudioContext.play(); // 监听播放 this.innerAudioContext.onPlay(()=>{ this.audioPlaying = true }) // 监听暂停 this.innerAudioContext.onPause(()=>{ this.audioPlaying = false }) // 监听停止 this.innerAudioContext.onStop(()=>{ this.audioPlaying = false }) // 监听错误 this.innerAudioContext.onError(()=>{ this.audioPlaying = false }) }else{ this.innerAudioContext.pause(); this.innerAudioContext.play(); } }, // 预览图片 preview(url) { this.$emit('preview',url); }, // 长按事件 long(e) { let x = 0, y = 0; // #ifdef APP-PLUS-NVUE if (Array.isArray(e.changedTouches) && e.changedTouches.length > 0) { x = e.changedTouches[0].screenX; y = e.changedTouches[0].screenY; } // #endif // #ifdef MP x = e.detail.x; y = e.detail.y; // #endif this.$emit('long', { x, y, index: this.index }); } } } </script> <style> .chat-left-icon { left: 80rpx; top: 20rpx; } .chat-right-icon { right: 80rpx; top: 20rpx; } .chat-animate { /* #ifndef APP-PLUS-NVUE */ opacity: 0; /* #endif */ } </style>
free-popup.vue
<template> <div style="z-index:9999;overflow:hidden;" v-if="status"> <!-- 蒙版 --> <view v-if="mask" class="position-fixed top-0 left-0 right-0 bottom-0" :style="getMaskColor" @click="hide"></view> <!-- 弹出框内容 --> <div ref="popup" class="position-fixed free-animated" :class="getBodyClass" :style="getBodyStyle"> <slot></slot> </div> </div> </template> <script> // #ifdef APP-PLUS-NVUE const animation = weex.requireModule('animation'); // #endif export default { props: { // 是否开启蒙版颜色 maskColor: { type: Boolean, default: false }, // 是否开启蒙版 mask:{ type:Boolean, default:true }, // 是否处于底部 bottom:{ type:Boolean, default:false }, // 弹出层内容高度 bodyHeight:{ type:Number, default:0 }, // 弹出层内容宽度 bodyWidth:{ type:Number, default:0 }, bodyBgColor:{ type:String, default:'bg-white' }, transformOrigin:{ type:String, default:'left top' }, // tabbar高度 tabbarHeight:{ type:Number, default:0 } }, data() { return { status: false, x:-1, y:1, maxX:0, maxY:0 } }, mounted() { try { const res = uni.getSystemInfoSync(); this.maxX = res.windowWidth - uni.upx2px(this.bodyWidth) this.maxY = res.windowHeight - uni.upx2px(this.bodyHeight) - uni.upx2px(this.tabbarHeight) } catch (e) { // error } }, computed: { getMaskColor() { let i = this.maskColor ? 0.5 : 0 return `background-color: rgba(0,0,0,${i});` }, getBodyClass(){ if(this.center){ return 'left-0 right-0 bottom-0 top-0 flex align-center justify-center'; } let bottom = this.bottom ? 'left-0 right-0 bottom-0' : 'rounded border' return `${this.bodyBgColor} ${bottom}`; }, getBodyStyle(){ let left = this.x > -1 ? `left:${this.x}px;` : '' let top = this.y > -1 ? `top:${this.y}px;` : '' return left + top } }, methods:{ show(x=-1,y=-1){ if(this.status){ return; } this.x = (x > this.maxX) ? this.maxX : x; this.y = (y > this.maxY) ? this.maxY : y; this.status = true // #ifdef APP-PLUS-NVUE this.$nextTick(()=>{ animation.transition(this.$refs.popup,{ styles:{ transform:'scale(1,1)', transformOrigin:this.transformOrigin, opacity:1 }, duration:100, //ms timingFunction:'ease', },()=>{ console.log('动画执行结束'); }) }) // #endif }, hide(){ this.$emit('hide'); // #ifdef APP-PLUS-NVUE this.$nextTick(()=>{ animation.transition(this.$refs.popup,{ styles:{ transform:'scale(0,0)', transformOrigin:this.transformOrigin, opacity:0 }, duration:100, //ms timingFunction:'ease', },()=>{ this.status = false; }) }) // #endif } } } </script> <style scoped> .free-animated{ /* #ifdef APP-PLUS-NVUE */ /* transform: scale(0,0); opacity: 0; */ /* #endif */ } .z-index{ /* #ifndef APP-NVUE */ z-index: 9999; /* #endif */ } </style>
还有我们在chat.nvue中引入了vuex
/store/index.js
import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); import audio from '@/store/modules/audio.js'; export default new Vuex.Store({ modules:{ audio } })
/store/modules/audio.js
export default{ state:{ // 存放全局事件 events:[], RECORD:null, RecordTime:0, RECORDTIMER:null, sendVoice:null }, mutations:{ // 初始化录音管理器 initRECORD(state){ state.RECORD = uni.getRecorderManager(); // 监听录音开始 state.RECORD.onStart(()=>{ state.RecordTime = 0; state.RECORDTIMER = setInterval(()=>{ state.RecordTime++ },1000); }) // 监听录音结束 state.RECORD.onStop((e)=>{ if(state.RECORDTIMER){ clearInterval(state.RECORDTIMER) state.RECORDTIMER = null } // if(!state.unRecord){ // this.send('audio',e.tempFilePath,{time:state.RecordTime}); // } // 执行发送 if(typeof state.sendVoice === 'function'){ state.sendVoice(e.tempFilePath) } }) }, // 注册发送音频事件 regSendVoiceEvent(state,event){ state.sendVoice = event }, // 注册全局事件 regEvent(state,event){ console.log('注册事件') state.events.push(event) }, // 执行全局事件 doEvent(state,params){ state.events.forEach(e=>{ console.log('执行事件'+state.events.length) e(params) }) }, // 注销事件 removeEvent(state,event){ let index = state.events.findIndex(item=>{ return item === event }) if(index !== -1){ state.events.splice(index,1); } } }, actions:{ // 分发注册全局事件 audioOn({commit},event){ commit('regEvent',event); }, // 分发执行全局事件 audioEmit({commit},params){ commit('doEvent',params); }, // 分发注销全局事件 audioOff({commit},event){ commit('removeEvent',event); } } }
页面是如下所示
感谢大家观看,我们下期再见。