【易售小程序项目】私聊功能uniapp界面实现 (买家、卖家 沟通商品信息)【后端基于若依管理系统开发】

简介: 【易售小程序项目】私聊功能uniapp界面实现 (买家、卖家 沟通商品信息)【后端基于若依管理系统开发】

效果显示

WebSocket连接

使用全局变量

本小程序在用户浏览首页的时候创建WebSocket连接,并将连接获得的WebSocket对象存储到全局变量中,方便其他页面来使用WebSocket

首先在项目的main.js文件中声明全局变量socket

Vue.prototype.$socket = null

对全局变量进行赋值

Vue.prototype.$socket = this.$socket;

后续如果需要使用全局变量,直接使用this.$socket即可

WebSocket连接细节

下面的代码中有一个headbeat方法,该方法主要用来定时给WebSocket服务器发送一个信号,告诉WebSocket服务器当前客户端还处于连接状态。当心跳停止的时候(比如客户端断网),后端服务就会将用户信息从连接中移除

/**
* 创建websocket连接
 */
initWebsocket() {
  // console.log("this.socket:" + JSON.stringify(this.$socket))
  // this.$socket == null,刚刚进入首页,还没有建立过websocket连接
  // this.$socket.readyState==0 表示正在连接当中
  // this.$socket.readyState==1 表示处于连接状态
  // this.$socket.readyState==2 表示连接正在关闭
  // this.$socket.readyState==3 表示连接已经关闭
  if (this.$socket == null || (this.$socket.readyState != 1 && this.$socket.readyState != 0)) {
    this.$socket = uni.connectSocket({
      url: "ws://10.23.17.146:8085/websocket/" + uni.getStorageSync("curUser").userName,
      success(res) {
        console.log('WebSocket连接成功', res);
      },
    })
    // console.log("this.socket:" + this.$socket)
    // 监听WebSocket连接打开事件
    this.$socket.onOpen((res) => {
      console.log("websocket连接成功")
      Vue.prototype.$socket = this.$socket;
      // 连接成功,开启心跳
      this.headbeat();
    });
    // 连接异常
    this.$socket.onError((res) => {
      console.log("websocket连接出现异常");
      // 重连
      this.reconnect();
    })
    // 连接断开
    this.$socket.onClose((res) => {
      console.log("websocket连接关闭");
      // 重连
      this.reconnect();
    })
  }
},
/**
 * 重新连接
 */
reconnect() {
  console.log("重连");
  // 防止重复连接
  if (this.lockReconnect == true) {
    return;
  }
  // 锁定,防止重复连接
  this.lockReconnect = true;
  // 间隔一秒再重连,避免后台服务出错时,客户端连接太频繁
  setTimeout(() => {
    this.initWebsocket();
  }, 1000)
  // 连接完成,设置为false
  this.lockReconnect = false;
},
// 开启心跳
headbeat() {
  console.log("websocket心跳");
  var that = this;
  setTimeout(function() {
    if (that.$socket.readyState == 1) {
      // websocket已经连接成功
      that.$socket.send({
        data: JSON.stringify({
          status: "ping"
        })
      })
      // 调用启动下一轮的心跳
      that.headbeat();
    } else {
      // websocket还没有连接成功,重连
      that.reconnect();
    }
  }, that.heartbeatTime);
},

最近和自己聊天的用户信息

界面效果

界面代码

<template>
  <view class="container">
    <scroll-view @scrolltolower="getMoreChatUserVo">
      <view v-for="(chatUserVo,index) in chatUserVoList" :key="index" @click="trunToChat(chatUserVo)">
        <view style="height: 10px;"></view>
        <view class="chatUserVoItem">
          <view style="display: flex;align-items: center;">
            <uni-badge class="uni-badge-left-margin" :text="chatUserVo.unReadChatNum" absolute="rightTop"
              size="small">
              <u--image :showLoading="true" :src="chatUserVo.userAvatar" width="50px" height="50px"
                :fade="true" duration="450">
                <view slot="error" style="font-size: 24rpx;">加载失败</view>
              </u--image>
            </uni-badge>
          </view>
          <view style="margin: 10rpx;"></view>
          <view
            style="line-height: 20px;width: 100%;display: flex;justify-content: space-between;flex-direction: column;">
            <view style="display: flex; justify-content: space-between;">
              <view>
                <view class="nickname">{{chatUserVo.userNickname}}
                </view>
                <view class="content">{{chatUserVo.lastChatContent}}</view>
              </view>
              <view class="date">{{formatDateToString(chatUserVo.lastChatDate)}}</view>
            </view>
            <!-- <view style="height: 10px;"></view> -->
            <u-line></u-line>
          </view>
        </view>
      </view>
    </scroll-view>
  </view>
</template>
<script>
  import {
    listChatUserVo
  } from "@/api/market/chat.js";
  import {
    listChat
  } from "@/api/market/chat.js"
  export default {
    data() {
      return {
        chatUserVoList: [],
        page: {
          pageNum: 1,
          pageSize: 15
        },
      }
    },
    created() {
    },
    methods: {
      /**
       * 滑动到底部,自动加载新一页的数据
       */
      getMoreChatUserVo() {
        this.page.pageNum++;
        this.listChatUserVo();
      },
      listChatUserVo() {
        listChatUserVo(this.page).then(res => {
          // console.log("res:"+JSON.stringify(res.rows))
          // this.chatUserVoList = res.rows;
          for (var i = 0; i < res.rows.length; i++) {
            this.chatUserVoList.push(res.rows[i]);
          }
        })
      },
      /**
       * 格式化日期
       * @param {Object} date
       */
      formatDateToString(dateStr) {
        let date = new Date(dateStr);
        // 今天的日期
        let curDate = new Date();
        if (date.getFullYear() == curDate.getFullYear() && date.getMonth() == curDate.getMonth() && date
          .getDate() == curDate.getDate()) {
          // 如果和今天的年月日都一样,那就只显示时间
          return this.toDoubleNum(date.getHours()) + ":" + this.toDoubleNum(date.getMinutes());
        } else {
          // 如果年份一样,就只显示月日
          return (curDate.getFullYear() == date.getFullYear() ? "" : (date.getFullYear() + "-")) + this
            .toDoubleNum((
              date
              .getMonth() + 1)) +
            "-" +
            this.toDoubleNum(date.getDate());
        }
      },
      /**
       * 如果传入的数字是两位数,直接返回;
       * 否则前面拼接一个0
       * @param {Object} num
       */
      toDoubleNum(num) {
        if (num >= 10) {
          return num;
        } else {
          return "0" + num;
        }
      },
      /**
       * 转到私聊页面
       */
      trunToChat(chatUserVo) {
        let you = {
          avatar: chatUserVo.userAvatar,
          nickname: chatUserVo.userNickname,
          username: chatUserVo.userName
        }
        uni.navigateTo({
          url: "/pages/chat/chat?you=" + encodeURIComponent(JSON.stringify(you))
        })
      },
      /**
       * 接收消息
       */
      receiveMessage() {
        this.$socket.onMessage((response) => {
          // console.log("接收消息:" + response.data);
          let message = JSON.parse(response.data);
          // 收到消息,将未读消息数量加一
          for (var i = 0; i < this.chatUserVoList.length; i++) {
            if (this.chatUserVoList[i].userName == message.from) {
              this.chatUserVoList[i].unReadChatNum++;
              // 显示对方发送的最新消息
              listChat(message.from, {
                pageNum: 1,
                pageSize: 1
              }).then(res => {
                this.chatUserVoList[i].lastChatContent = res.rows[0].content
              });
              break;
            }
          }
        })
      },
    },
    onLoad(e) {
      this.receiveMessage();
    },
    onShow: function() {
      this.chatUserVoList = [];
      this.listChatUserVo();
    },
  }
</script>
<style lang="scss">
  .container {
    padding: 20rpx;
    .chatUserVoItem {
      display: flex;
      margin: 0 5px;
      .nickname {
        font-weight: 700;
      }
      .content {
        color: #A7A7A7;
        font-size: 14px;
        /* 让消息只显示1行,超出的文字内容使用...来代替 */
        overflow: hidden;
        text-overflow: ellipsis;
        display: -webkit-box;
        -webkit-line-clamp: 1;
        -webkit-box-orient: vertical;
      }
      .date {
        color: #A7A7A7;
        font-size: 12px;
      }
    }
    // .uni-badge-left-margin {
    //  margin-left: 10px;
    // }
  }
</style>

最近的聊天内容太长

当最近的一条聊天内容太长的时候,页面不太美观,缺少整齐的感觉

解决的方式非常简单,只需要添加以下样式即可

.content {
  /* 让消息只显示1行,超出的文字内容使用...来代替 */
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
}

日期时间显示

本文显示日期时间的时候,遵循以下原则:

  • 如果上次聊天时间的年月日和今天一致,那就只显示时间,即显示 时:分
  • 如果上次聊天时间的年份和今年一致,那就只显示月-日
  • 如果上面的条件都不满足,就显示年-月-日

在显示月、日、时、分的时候,如果数字是一位数字,就在前面补一个零,具体操作如方法toDoubleNum

/**
 * 格式化日期
 * @param {Object} date
 */
formatDateToString(dateStr) {
  let date = new Date(dateStr);
  // 今天的日期
  let curDate = new Date();
  if (date.getFullYear() == curDate.getFullYear() && date.getMonth() == curDate.getMonth() && date
    .getDate() == curDate.getDate()) {
    // 如果和今天的年月日都一样,那就只显示时间
    return this.toDoubleNum(date.getHours()) + ":" + this.toDoubleNum(date.getMinutes());
  } else {
    // 如果年份一样,就只显示月日
    return (curDate.getFullYear() == date.getFullYear() ? "" : (date.getFullYear() + "-")) + this
      .toDoubleNum((
        date
        .getMonth() + 1)) +
      "-" +
      this.toDoubleNum(date.getDate());
  }
},
/**
* 如果传入的数字是两位数,直接返回;
 * 否则前面拼接一个0
 * @param {Object} num
 */
toDoubleNum(num) {
  if (num >= 10) {
    return num;
  } else {
    return "0" + num;
  }
},

未读消息数量显示

未读消息数量显示使用角标组件,即uni-badge,使用该组件需要下载安装插件,下载链接,下载之前需要看广告,哈哈哈,当然有钱可以不看

显示效果如下图

<uni-badge class="uni-badge-left-margin" :text="chatUserVo.unReadChatNum" absolute="rightTop"
  size="small">
  <u--image :showLoading="true" :src="chatUserVo.userAvatar" width="50px" height="50px"
    :fade="true" duration="450">
    <view slot="error" style="font-size: 24rpx;">加载失败</view>
  </u--image>
</uni-badge>

私聊界面

界面展示

【微信公众平台模拟的手机界面】

【手机端,键盘呼出之后的聊天区域】

代码实现

<template>
  <view style="height:100vh;">
    <!-- @scrolltoupper:上滑到顶部执行事件,此处用来加载历史消息 -->
    <!-- scroll-with-animation="true" 设置滚动条位置的时候使用动画过渡,让动作更加自然 -->
    <scroll-view :scroll-into-view="scrollToView" scroll-y="true" class="messageListScrollView"
      :style="{height:scrollViewHeight}" @scrolltoupper="getHistoryChat()"
      :scroll-with-animation="!isFirstListChat" ref="chatScrollView">
      <view v-for="(message,index) in messageList" :key="message.id" :id="`message`+message.id"
        style="width: 750rpx;min-height: 60px;">
        <view style="height: 10px;"></view>
        <view v-if="message.type==0" class="messageItemLeft">
          <view style="width: 8px;"></view>
          <u--image :showLoading="true" :src="you.avatar" width="50px" height="50px" radius="3"></u--image>
          <view style="width: 7px;"></view>
          <view class="messageContent left">
            {{message.content}}
          </view>
        </view>
        <view v-if="message.type==1" class="messageItemRight">
          <view class="messageContent right">
            {{message.content}}
          </view>
          <view style="width: 7px;"></view>
          <u--image :showLoading="true" :src="me.avatar" width="50px" height="50px" radius="3"></u--image>
          <view style="width: 8px;"></view>
        </view>
      </view>
    </scroll-view>
    <view class="messageSend">
      <view class="messageInput">
        <u--textarea v-model="messageInput" placeholder="请输入消息内容" autoHeight>
        </u--textarea>
      </view>
      <view style="width:5px"></view>
      <view class="commmitButton" @click="send()">发 送</view>
    </view>
  </view>
</template>
<script>
  import {
    getUserProfileVo
  } from "@/api/user";
  import {
    listChat
  } from "@/api/market/chat.js"
  let socket;
  export default {
    data() {
      return {
        webSocketUrl: "",
        socket: null,
        messageInput: '',
        // 我自己的信息
        me: {},
        // 对方信息
        you: {},
        scrollViewHeight: undefined,
        messageList: [],
        // 底部滑动到哪里
        scrollToView: '',
        page: {
          pageNum: 1,
          pageSize: 15
        },
        isFirstListChat: true,
        loadHistory: false,
        // 消息总条数
        total: 0,
      }
    },
    created() {
      this.me = uni.getStorageSync("curUser");
    },
    beforeDestroy() {
      console.log("执行销毁方法");
      this.endChat();
    },
    onLoad(e) {
      // 设置初始高度
      this.scrollViewHeight = `calc(100vh - 20px - 44px)`;
      this.you = JSON.parse(decodeURIComponent(e.you));
      uni.setNavigationBarTitle({
        title: this.you.nickname,
      })
      this.startChat();
      this.listChat();
      this.receiveMessage();
    },
    onReady() {
      // 监听键盘高度变化,以便设置输入框的高度
      uni.onKeyboardHeightChange(res => {
        let keyBoardHeight = res.height;
        console.log("keyBoardHeight:" + keyBoardHeight);
        this.scrollViewHeight = `calc(100vh - 20px - 44px - ${keyBoardHeight}px)`;
        this.scrollToView = '';
        setTimeout(() => {
          this.scrollToView = 'message' + this.messageList[this
            .messageList.length - 1].id;
        }, 150)
      })
    },
    methods: {
      /**
       * 发送消息
       */
      send() {
        if (this.messageInput != '') {
          let message = {
            from: this.me.userName,
            to: this.you.username,
            text: this.messageInput
          }
          // console.log("this.socket.send:" + this.$socket)
          // 将组装好的json发送给服务端,由服务端进行转发
          this.$socket.send({
            data: JSON.stringify(message)
          });
          this.total++;
          let newMessage = {
            // code: this.messageList.length,
            type: 1,
            content: this.messageInput
          };
          this.messageList.push(newMessage);
          this.messageInput = '';
          this.toBottom();
        }
      },
      /**
       * 开始聊天
       */
      startChat() {
        let message = {
          from: this.me.userName,
          to: this.you.username,
          text: "",
          status: "start"
        }
        // 告诉服务端要开始聊天了
        this.$socket.send({
          data: JSON.stringify(message)
        });
      },
      /**
       * 结束聊天
       */
      endChat() {
        let message = {
          from: this.me.userName,
          to: this.you.username,
          text: "",
          status: "end"
        }
        // 告诉服务端要结束聊天了
        this.$socket.send({
          data: JSON.stringify(message)
        });
      },
      /**
       * 接收消息
       */
      receiveMessage() {
        this.$socket.onMessage((response) => {
          // console.log("接收消息:" + response.data);
          let message = JSON.parse(response.data);
          let newMessage = {
            // code: this.messageList.length,
            type: 0,
            content: message.text
          };
          this.messageList.push(newMessage);
          this.total++;
          // 让scroll-view自动滚动到最新的数据那里
          // this.$nextTick(() => {
          //  // 滑动到聊天区域最底部
          //  this.scrollToView = 'message' + this.messageList[this
          //    .messageList.length - 1].id;
          // });
          this.toBottom();
        })
      },
      /**
       * 查询对方和自己最近的聊天数据
       */
      listChat() {
        return new Promise((resolve, reject) => {
          listChat(this.you.username, this.page).then(res => {
            for (var i = 0; i < res.rows.length; i++) {
              this.total = res.total;
              if (res.rows[i].fromWho == this.me.userName) {
                res.rows[i].type = 1;
              } else {
                res.rows[i].type = 0;
              }
              // 将消息放到数组的首位
              this.messageList.unshift(res.rows[i]);
            }
            if (this.isFirstListChat == true) {
              // this.$nextTick(function() {
              //  // 滑动到聊天区域最底部
              //  this.scrollToView = 'message' + this.messageList[this
              //    .messageList.length - 1].id;
              // })
              this.toBottom();
              this.isFirstListChat = false;
            }
            resolve();
          })
        })
      },
      /**
       * 滑到最顶端,分页加一,拉取更早的数据
       */
      getHistoryChat() {
        // console.log("获取历史消息")
        this.loadHistory = true;
        if (this.messageList.length < this.total) {
          // 当目前的消息条数小于消息总量的时候,才去查历史消息
          this.page.pageNum++;
          this.listChat().then(() => {})
        }
      },
      /**
       * 滑动到聊天区域最底部
       */
      toBottom() {
        // 让scroll-view自动滚动到最新的数据那里
        this.scrollToView = '';
        setTimeout(() => {
          // 滑动到聊天区域最底部
          this.scrollToView = 'message' + this.messageList[this
            .messageList.length - 1].id;
        }, 150)
      }
    }
  }
</script>
<style lang="scss">
  .messageListScrollView {
    background: #F5F5F5;
    overflow: auto;
    .messageItemLeft {
      display: flex;
      align-items: flex-start;
      justify-content: flex-start;
      .messageContent {
        max-width: calc(750rpx - 10px - 50px - 15px - 10px - 50px - 15px);
        padding: 10px;
        // margin-top: 10px;
        border-radius: 7px;
        font-family: sans-serif;
        // padding: 10px;
        // 让view只包裹文字
        width: auto;
        // display: inline-block !important;
        // display: inline;
        // 解决英文字符串、数字不换行的问题
        word-break: break-all;
        word-wrap: break-word;
      }
    }
    .messageItemRight {
      display: flex;
      align-items: flex-start;
      justify-content: flex-end;
      .messageContent {
        max-width: calc(750rpx - 10px - 50px - 15px - 10px - 50px - 15px);
        padding: 10px;
        // margin-top: 10px;
        border-radius: 7px;
        font-family: sans-serif;
        // padding: 10px;
        // 让view只包裹文字
        width: auto;
        // display: inline-block !important;
        // display: inline;
        // 解决长英文字符串、数字不换行的问题
        word-wrap: break-word;
      }
    }
    .right {
      background-color: #94EA68;
    }
    .left {
      background-color: #ffffff;
    }
  }
  .messageSend {
    display: flex;
    background: #ffffff;
    padding-top: 5px;
    padding-bottom: 15px;
    .messageInput {
      border: 1px #EBEDF0 solid;
      border-radius: 5px;
      width: calc(750rpx - 65px);
      margin-left: 5px;
    }
    .commmitButton {
      height: 38px;
      border-radius: 5px;
      width: 50px;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #ffffff;
      background: #3C9CFF;
    }
  }
</style>

英文长串不换行问题

这个问题属于是整串英文被以为是一个单词了,所以没有换行,看下面的句子,英文单词可以比较短的,所以会自动换行

解决这个问题只需要添加下面的css即可

// 解决长英文字符串、数字不换行的问题
word-wrap: break-word;

下面是添加之后的效果

聊天区域自动滑动到底部

在聊天的时候,无论是发送一条新的消息,还是接收到一条新的消息,聊天区域都需要自动滑动到最新的消息那里。本文使用scroll-view组件来包裹显示聊天消息,在scroll-view组件中,可以通过给scroll-into-view属性赋值来指定聊天区域所显示到的位置。使用时需要注意如下问题:

  • 需要先给每一条消息设置一个id属性,id属性存储的内容不能以数字开头,因此本文在id之间拼接了一个字符串’message’
  • scroll-view需要被设置好高度,本文通过绑定一个变量来设置高度,如:style="{height:scrollViewHeight}",因为手机端使用小程序打字时键盘呼出会影响聊天区域的高度

后续通过给scrollToView设置不同的值即可控制聊天区域的滑动,比如每接收到一条新的消息,就调用toBottom方法,该方法通过设置scrollToView为'message' + this.messageList[this.messageList.lengthh - 1].id将聊天区域滑动到最新的消息处。需要注意的是,在进行该值的设置之前,需要延迟一段时间,否则滑动可能不成功,本文延迟150ms,读者也可以探索不同的值,该值不能太大或者太小。


通过设置scroll-view的属性scroll-with-animation的值为true,可以让消息区域在滑动的时候使用动画过渡,这样滑动更加自然。


键盘呼出,聊天区域收缩,聊天区域滑动到底部

当键盘呼出时,需要将聊天区域的高度减去键盘的高度。同时将scrollToView赋值为最后一条消息的id。需要注意的是,在设置scrollToView之前,需要先将scrollToView设置为空字符串,否则滑动效果可能不成功

onReady() {
  // 监听键盘高度变化,以便设置输入框的高度
  uni.onKeyboardHeightChange(res => {
    let keyBoardHeight = res.height;
    console.log("keyBoardHeight:" + keyBoardHeight);
    this.scrollViewHeight = `calc(100vh - 20px - 44px - ${keyBoardHeight}px)`;
    this.scrollToView = '';
    setTimeout(() => {
      this.scrollToView = 'message' + this.messageList[this
        .messageList.length - 1].id;
    }, 150)
  })
},

通知WebSocket服务器哪两个用户开始聊天

为了便于后端在存储聊天数据的时候辨别消息是否为已读状态。比如,在小王开始聊天之前,需要先告诉后端:“小王要开始和小明聊天了”,如果正好小明也告诉后端:“我要和小王聊天了”,那小王发出去的消息就会被设置为已读状态,因为他们两个此时此刻正在同时和对方聊天,那小王发出去的消息就默认被小明看到了,因此设置为已读状态

/**
* 开始聊天
*/
startChat() {
  let message = {
    from: this.me.userName,
    to: this.you.username,
    text: "",
    status: "start"
  }
  // 告诉服务端要开始聊天了
  this.$socket.send({
    data: JSON.stringify(message)
  });
},
/**
 * 结束聊天
 */
endChat() {
  let message = {
    from: this.me.userName,
    to: this.you.username,
    text: "",
    status: "end"
  }
  // 告诉服务端要结束聊天了
  this.$socket.send({
    data: JSON.stringify(message)
  });
},
目录
相关文章
预约按摩小程序开发,为什么很多上门按摩平台根本招聘不到优秀技师?
上门按摩平台面临招不到优秀技师的问题,主要原因是平台众多,技师选择多样。为解决此问题,平台可引入技师等级制度,根据订单数量和好评率划分高、低等级技师。高等级技师可享受70%-90%的高提成及首页推荐,这不仅能激励技师的积极性,还能帮助平台筛选出优质技师,提升服务质量和口碑,形成良性循环。
|
16天前
|
小程序 Android开发
|
6天前
|
设计模式 开发框架 JavaScript
基于.NET8 + Vue/UniApp前后端分离的快速开发框架,开箱即用!
基于.NET8 + Vue/UniApp前后端分离的快速开发框架,开箱即用!
|
5天前
|
小程序 云计算 Android开发
发者社区 云计算 文章 正文 小程序开发与公众号用户关联推送消息(九)
发者社区 云计算 文章 正文 小程序开发与公众号用户关联推送消息(九)
22 3
|
10天前
|
小程序 云计算 开发者
|
11天前
|
小程序
|
12天前
|
小程序 数据安全/隐私保护
|
11天前
|
小程序
|
17天前
|
人工智能 小程序
【一步步开发AI运动小程序】五、帧图像人体识别
随着AI技术的发展,阿里体育等公司推出的AI运动APP,如“乐动力”和“天天跳绳”,使云上运动会、线上健身等概念广受欢迎。本文将引导您从零开始开发一个AI运动小程序,使用“云智AI运动识别小程序插件”。文章分为四部分:初始化人体识别功能、调用人体识别功能、人体识别结果处理以及识别结果旋转矫正。下篇将继续介绍人体骨骼图绘制。
|
15天前
|
小程序