uni-app 72聊天类封装(七)-完善发送消息状态

简介: uni-app 72聊天类封装(七)-完善发送消息状态


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 => {
        // 发送成功
        message.id = res.id 
        message.sendStatus = 'success'
        result(res);
      }).catch(err => {
        // 发送失败
        message.sendStatus = 'fail'
        // 断线重连提示
        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">&#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: 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')">&#xe605;</text></free-icon-button>
        <!-- 扩展菜单 -->
        <free-icon-button @click="openActionOrEmoticon('action')"><text class="iconfont font-lg">&#xe603;</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
        })
        // 渲染到页面
        let index = this.list.length
        this.list.push(message);
        // 发送到服务端
        this.chat.send(message).then(res=>{
          console.log(res);
          // 发送成功
          this.list[index] = res.id,
          this.list[index].sendStatus = 'success'
        }).catch(err=>{
          // 发送失败
          this.list[index].sendStatus = 'fail'
          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>

感谢大家观看,我们下次见

目录
相关文章
|
1月前
uni-app 77聊天类封装(十三)-断线重连提示
uni-app 77聊天类封装(十三)-断线重连提示
26 0
|
1月前
|
开发框架 移动开发 JavaScript
uni-app 68 egg.js发送消息接口开发-单聊(一)
uni-app 68 egg.js发送消息接口开发-单聊(一)
|
1月前
|
前端开发 API 数据处理
uni-app 封装api请求
uni-app 封装api请求
15 0
|
1月前
uni-app 81聊天类封装(十五)-读取会话功能
uni-app 81聊天类封装(十五)-读取会话功能
17 1
|
1月前
uni-app 79聊天类封装(十四)-处理接收消息
uni-app 79聊天类封装(十四)-处理接收消息
15 2
|
1月前
|
API 数据安全/隐私保护 iOS开发
利用uni-app 开发的iOS app 发布到App Store全流程
利用uni-app 开发的iOS app 发布到App Store全流程
89 3
|
1月前
|
Android开发 开发者 UED
个人开发 App 成功上架手机应用市场的关键步骤
个人开发 App 成功上架手机应用市场的关键步骤
|
1月前
|
开发工具 数据安全/隐私保护 Android开发
【教程】APP 开发后如何上架?
【教程】APP 开发后如何上架?
|
1月前
|
API
uni-app 146朋友圈列表api开发
uni-app 146朋友圈列表api开发
18 0
|
1月前
|
Java Android开发 开发者
【Uniapp开发】APP的真机调试指南,从开发到上架全过程
【Uniapp开发】APP的真机调试指南,从开发到上架全过程
36 3