uni-app 8聊天页开发

简介: uni-app 8聊天页开发


主文件 chat.nvue

<template>
  <view>
    <!-- 导航栏 -->
    <free-nav-bar title="呵呵呵呵" :noreadnum="1" showBack>
      <free-icon-button slot="right"><text class="iconfont font-md">&#xe6fd;</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">&#xe640;</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">&#xe607;</text>
        </block>
        <block v-else>
          <text class="iconfont font-lg">&#xe606;</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')">&#xe605;</text></free-icon-button>
    
      <template v-if="text.length === 0">
        <!-- 扩展菜单 -->
        <free-icon-button @click="openActionOrEmoticon('action')"><text class="iconfont font-lg">&#xe603;</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">&#xe609;</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">&#xe737;</text>
           </view>
        </div>
        <view class="iconfont font-md position-absolute" :class="(item.type==='text')?'chat-right-icon' : '' ">
          <text class="text-chat-item iconfont">&#xe640;</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);
    }
  }
}

页面是如下所示

感谢大家观看,我们下期再见。

目录
相关文章
|
23天前
|
前端开发 安全 开发工具
【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
163 90
【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
2月前
|
Dart 前端开发
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
122 75
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
16天前
|
JavaScript 搜索推荐 Android开发
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
40 8
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
|
23天前
|
机器学习/深度学习 存储 人工智能
MNN-LLM App:在手机上离线运行大模型,阿里巴巴开源基于 MNN-LLM 框架开发的手机 AI 助手应用
MNN-LLM App 是阿里巴巴基于 MNN-LLM 框架开发的 Android 应用,支持多模态交互、多种主流模型选择、离线运行及性能优化。
1329 14
MNN-LLM App:在手机上离线运行大模型,阿里巴巴开源基于 MNN-LLM 框架开发的手机 AI 助手应用
|
1月前
|
前端开发 Java Shell
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
164 20
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
2月前
|
Dart 前端开发 容器
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
89 18
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
26天前
|
Dart 前端开发 Android开发
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
48 4
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
2月前
|
前端开发 Java 开发工具
【03】完整flutter的APP打包流程-以apk设置图标-包名-签名-APP名-打包流程为例—-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈 章节内容【03】
【03】完整flutter的APP打包流程-以apk设置图标-包名-签名-APP名-打包流程为例—-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈 章节内容【03】
135 18
【03】完整flutter的APP打包流程-以apk设置图标-包名-签名-APP名-打包流程为例—-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈 章节内容【03】
|
2月前
|
缓存 前端开发 Android开发
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
97 12
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
|
24天前
|
供应链 数据挖掘 API
1688APP 原数据 API 接口的开发、应用与收益
1688作为阿里巴巴旗下的B2B平台,汇聚海量供应商和商品资源。其APP原数据API接口为开发者提供获取商品详细信息的强大工具,涵盖商品标题、价格、图片等。通过注册开放平台账号、申请API权限并调用接口,开发者可构建比价工具、供应链管理及自动化上架工具等应用,提升用户体验与运营效率,创造新的商业模式。示例代码展示了如何使用Python调用API并解析返回结果。
92 8

热门文章

最新文章

  • 1
    MNN-LLM App:在手机上离线运行大模型,阿里巴巴开源基于 MNN-LLM 框架开发的手机 AI 助手应用
  • 2
    原生鸿蒙版小艺APP接入DeepSeek-R1,为HarmonyOS应用开发注入新活力
  • 3
    【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 4
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 5
    【Azure App Service】基于Linux创建的App Service是否可以主动升级内置的Nginx版本呢?
  • 6
    【Azure Function】Function App出现System.IO.FileNotFoundException异常
  • 7
    1688APP 原数据 API 接口的开发、应用与收益
  • 8
    PiliPala:开源项目真香,B站用户狂喜!这个开源APP竟能自定义主题+去广告?PiliPala隐藏功能大揭秘
  • 9
    APP-国内主流安卓商店-应用市场-鸿蒙商店上架之必备前提·全国公安安全信息评估报告如何申请-需要安全评估报告的资料是哪些-优雅草卓伊凡全程操作
  • 10
    语音app系统软件源码开发搭建新手启蒙篇