微信小程序(十六)小程序仿微信聊天页面及功能

7,006 阅读7分钟

后期打算在小程序中添加即时聊天的功能,但是目前这个还没有考虑好以一种什么样的形势去实现,先接入一个腾讯AI提供的免费闲聊接口。先做一个大概的页面及功能。

腾讯AI地址:

ai.qq.com/doc/nlpchat…

有兴趣,可以试一试,当然,这种免费且不限次数的机器人肯定不会太聪明,期望值别太高。

我这里实现的效果是,可以发文字,表情,图片以及语音,当然如果你还想要更多的功能,就需要自己去拓展了。

最后的效果:

在这里插入图片描述

其余的就不多介绍了,感兴趣的同学可以自行研究下代码:

Aichat.wxml

<!-- 顶部title及返回键 -->
<view class="index" style="height:{{globalData.CustomBar}}px;padding-top: {{globalData.StatusBar}}px;">
  <!-- 如果正在输入title变正在输入中 -->
  <image catchtap="fanhui" mode="widthFix" class="index-img" src="/images/leftrow.png"></image><text>{{news?'正在输入中...':openid}}</text>
</view>
<view class="chat-index" style="margin-top:{{globalData.CustomBar}}px;height:calc(100vh - {{globalData.CustomBar}}px);">
  <view wx:if="{{playing}}" style="position: fixed;top: 75px;" class="chat-cbins">正在播放语音</view>
  <!-- 聊天列表 -->
  <scroll-view bindtouchstart="scrollstart" style="height: calc(100% - {{chatheight?chatheight:'50'}}px);{{emojis || camera?'transition:all 0.5s;':'transition:all 0.1s;'}}" scroll-with-animation="{{animation}}" class="scroll-view" scroll-y="{{true}}" scroll-into-view="{{curr}}">
    <!-- 循环消息列表 -->
    <view wx:for="{{newsList}}" wx:key="index" class="news {{item.openid == openid?'contrary':''}}" id="jump{{index}}">
      <!-- 用户头像 -->
      <view class="mews-image">
        <image src="{{item.logo}}" mode='aspectFill'></image>
      </view>
      <!-- 用户消息,消息分四种类型,语音,表情,图片,文字 -->
      <!-- 当用户标识等于系统默认用户标识的时候 -->
      <view class="news-centent {{item.openid == openid?'centent-right':'centent-left'}}" >
        <view class="{{item.openid == openid?'trianright':'trianleft'}}"></view>
        <text selectable="{{true}}" wx:if="{{item.news_type == 'text'}}" class="{{item.openid == openid?'background_right':'background_left'}}" >{{item.news_centent}}</text>
        <image class="news-images" catchtap="picture" data-src="{{item.news_centent.src}}" style="{{item.news_centent.width <= 100?'width:'+item.news_centent.width*2+'px;height:'+item.news_centent.height*2+'px':''}}{{item.news_centent.width > 100 && item.news_centent.width < 400?'width:130px;height:'+item.news_centent.height/(item.news_centent.width / 130)+'px':''}}{{item.news_centent.width > 400 && item.news_centent.width <= 600?'width:'+item.news_centent.width/2.5+'px;height:'+item.news_centent.height/2.5+'px':''}}{{item.news_centent.width > 600 && item.news_centent.width <= 800?'width:'+item.news_centent.width/4+'px;height:'+item.news_centent.height/4+'px':''}}{{item.news_centent.width > 800?'width:150px;height:'+item.news_centent.height/(item.news_centent.width / 150)+'px':''}};" wx:if="{{item.news_type == 'image'}}" src="{{item.news_centent.src}}"></image>
        <view class="news-voice" wx:if="{{item.news_type == 'voice'}}" bindtap="{{cuindex == index?'suspend':'play'}}" data-voice="{{item.news_centent.voice}}" data-index="{{index}}" > 
          <text wx:if="{{item.openid == openid}}">{{item.news_centent.dimen < '1'?'1':item.news_centent.dimen}}"</text>
          <image src="/images/{{item.openid == openid?'voice-right':'voice-left'}}.png"></image>
          <text wx:if="{{item.openid != openid}}">{{item.news_centent.dimen < '1'?item.news_centent.dimen:'1'}}"</text>
        </view>
      </view>
    </view>
  </scroll-view>
  <!-- 底部功能栏 -->
  <view class="chat-bottom" id="chat-height">
    <!-- 点击语音功能 -->
    <view class="chat-centent">
      <view class="chat-pression">
        <image wx:if="{{!voice}}" catchtap="speak" src="/images/voice.png"></image>
        <image wx:if="{{voice}}" catchtap="speak" src="/images/keyboard.png"></image>
      </view>
      <view class="chat-tencvi" style="{{sendout?'width:67%;':''}}">
        <textarea adjust-position="{{false}}" bindfocus="getkey" bindblur="getblur" wx:if="{{!voice}}" bindinput="moninput" value="{{news}}" maxlength="-1" auto-height />
        <view  bindtouchstart="touchdown" bindtouchend="touchup" wx:if="{{voice}}">按住 说话</view>
      </view>
      <view class="chat-pression">
        <image wx:if="{{!emojis}}" catchtap="emoji" src="/images/emotion.png"></image>
        <image wx:if="{{emojis}}" catchtap="emoji" src="/images/keyboard.png"></image>
      </view>
      <view class="chat-pression" wx:if="{{!sendout}}">
        <image catchtap="camerax" src="/images/plus.png"></image>
      </view>
      <view class="chat-news" catchtap="message" style="{{sendout?'transition:all 0.5s;width:15%;':'width:0;position: absolute;right: 0;visibility: hidden;'}}">
        <view>发送</view>
      </view>
    </view>
    <!-- 点击表情图标 -->
    <view class="chat-cube" wx:if="{{emojis}}">
      <view class="cube-title">所有表情</view>
      <view class="cube-centent">
        <view catchtap="expression" data-item="{{item}}" wx:for="{{pression}}" wx:key="index">{{item}}</view>
      </view>
    </view>
    <!-- 点击加号图标 -->
    <view class="chat-camera" wx:if="{{camera}}">
      <view wx:for="{{feature}}" wx:key="index" class="camera-feature" catchtap="featch" data-index="{{index}}">
        <view class="feature-src">
          <image src="{{item.src}}"></image>
        </view>
        <view class="feature-text">{{item.name}}</view>
      </view>
    </view>
  </view>
</view>
<!-- 加载图标 -->
<view class="animation" wx:if="{{alcur}}">
  <image src="/images/recording.gif"></image>
</view>

Aichat.js

var app = getApp()
Page({
  data: {
    // 全局变量,控制全局显示样式
    globalData:{
      StatusBar:'',
      Custom:'',
      CustomBar:'',
    },
    // 消息列表(存在缓存中)
    newsList:[],
    // 用户唯一标识
    openid:'',
    // 头像
    figureurl_wx:'',
    // 是否显示加载图标
    animation:false,
    // 是否是发送声音
    voice:false,
    // 是否发送完成
    sendout:false,
    // 是否发送照片
    camera:false,
    // 新消息
    news:'',
    // 表情数组
    pression:[],
    // 是否是发送表情
    emojis:false,
    // 聊天列表高度
    curra:0,
    // 功能相册图标
    feature:[
      { src: '/images/album.png', name: '相册' }
    ],
    // 软键盘是否关闭
    alcur:false,
    // 视野(软键盘高度)
    vision:'',
    // 定时器对象
    time:'',
    // 期间
    duration:'',
    // 是否被正在播放录音
    playing:false,
    // 
    cuindex:'',
    // ai回答
    aiAnswer:'',
  
  },
  /**
   * 自定义导航栏,返回上一级
   */
  fanhui:function(e){
    // 清除聊天缓存
    wx.removeStorage({
      key: 'newsList',
      success: function(res) {
         
      },
    });
    wx.navigateBack({
      delta: 1
    });
  },
  /**
   * 监听页面底部输入框
   */
  cuinbut:function(e){
    let that = this
    setTimeout(function () {
      wx.createSelectorQuery().select('#chat-height').boundingClientRect(function (rect) {
        that.setData({
          // 底部输入框的高度
          chatheight: parseFloat(rect.height) + parseFloat(that.data.curra),
          // 当前最后一条信息
          curr: 'jump' + JSON.stringify(that.data.newsList.length - 1)
        })
      }).exec()
    }, 100);
    console.log(wx.getStorageSync('newsList'));
  },
  /**
   * 生命周期----显示页面
   */
  onShow:function(e){
    // 页面赋值,最新一条数据索引标识
    this.setData({
      curr: 'jump' + JSON.stringify(this.data.newsList.length-1)
    })
  },
  /**
   * 生命周期加载页面
   */
  onLoad: function (options) {
    let that = this;
    that.getUseridFromStorage();
    // 获取系统信息
    wx.getSystemInfo({
      success: e => {
        that.data.globalData.StatusBar = e.statusBarHeight;
        let capsule = wx.getMenuButtonBoundingClientRect();
        if (capsule) {
          that.data.globalData.Custom = capsule;
          that.data.globalData.CustomBar = capsule.bottom + capsule.top - (e.statusBarHeight + 15);
        } else {
          that.data.globalData.CustomBar = e.statusBarHeight + 50;
        }
      }
    });
    that.data.figureurl_wx = options.figureurl_wx;
    that.setData({
      // 用户唯一标识(这里是用户名)
      openid:options.openid,
      // 全局变量
      globalData: that.data.globalData,
      // 聊天记录存储在缓存中
      newsList: wx.getStorageSync('newsList'),
      // 表情列表
      pression: app.emoji.pression,
      // 录音
      vision: wx.getRecorderManager(),
    });
  },
    /**
   * 从缓存中获取用户信息
   */
  getUseridFromStorage:function()
  {
    var self = this;
    // 从缓存中获取用户id
    wx.getStorage({
      key: 'userinfo',
      success (res) {
        self.data.user_id = res.data.id;
        self.data.figureurl_wx = res.data.figureurl_wx;
        self.data.openid = res.data.nickname;
      }
    });
  },
  /**
   * 失去焦点事件
   */
  getkey:function(e){
    // 这个setdata的值没有用,wxml中无应用
    this.setData({
      curra: e.detail.height,
      emojis: false,
      camera: false
    })
    this.cuinbut();
  },
/**
 * 获取焦点事件
 */
  getblur:function(e){
    this.setData({
      curra: 0,
      emojis: false,
      camera: false
    })
    this.cuinbut();
  },
  /**
   * 获取input输入的消息
   */
  moninput:function(e){
    let that = this
    that.cuinbut();
    this.setData({
      sendout: e.detail.value?true:false,
      news: e.detail.value,
    })
  },
  /**
   * 滚动条开始滚动,收起表情列表以及功能列表
   */
  scrollstart:function(e){
    this.setData({
      emojis: false,
      camera: false,
    })
    this.cuinbut();
  },
  /**
   * 点击语音图标时(收起功能栏)
   */
  speak:function(e){
    this.setData({
      voice: !this.data.voice,
      emojis:false,
      camera:false,
    })
    this.cuinbut();
  },
  /**
   * 点击表情栏时(收起功能栏)
   */
  emoji:function(e){
    this.setData({
      emojis: !this.data.emojis,
      voice:false,
      camera:false,
    })
    this.cuinbut();
  },
  /**
   * 点击功能栏时,收起声音和表情 
   */
  camerax:function(e){
    this.setData({
      camera: !this.data.camera,
      voice: false,
      emojis: false,
    })
    this.cuinbut();
  },
  /**
   * 上传图片
   */
  upload:function(e){
    const that = this
    // 微信选择图片
    wx.chooseImage({
      count: 1,
      success: function (res) {
        // 这是临时文件路径
        const src = res.tempFilePaths[0];
        // 获取图片信息
        wx.getImageInfo({
          src,
          success: function (res) {
            const { width, height } = res
            const newChatList = [...that.data.newsList,{
              news_type: 'image',
              news_centent: { src, width, height },
              openid: that.data.openid,
              logo:that.data.figureurl_wx
            }];
            // 用户消息发送完成,调用接口,等待AI回话
            that.aiChat('');
            that.setData({
              animation: true,
              newsList: newChatList,
            });
            // 同步设置缓存
            wx.setStorageSync('newsList', newChatList);
            that.cuinbut();
            // 用户消息发送完成,调用接口,等待AI回话
            that.aiChat();
          }
        })
      }
    })
  },
  /**
   * 点击看大图
   */
  picture:function(e){
    let src = e.currentTarget.dataset.src;
    wx.previewImage({
      current: src,
      urls: [src]
    })
  },
  /**
   * 发送消息接口
   */
  message:function(e){
    let that = this
    const newChatList = [...that.data.newsList, {
      news_type: 'text',
      news_centent: that.data.news,
      openid: that.data.openid,
      logo:that.data.figureurl_wx
    }];
    // 用户消息发送完成,调用接口,等待AI回话
    that.aiChat(that.data.news);
  
    that.setData({
      animation: true,
      newsList: newChatList,
      sendout:false,
      news: '',
    })
    wx.setStorageSync('newsList', newChatList);
    that.cuinbut();
  },
  /**
   * 选择表情
   */
  expression:function(e){
    let item = e.currentTarget.dataset.item
    this.setData({
      sendout: true,
      news: this.data.news+=item,
    })
  },
  /**
   * 点击功能图标
   */
  featch:function(e){
    let that = this
    let index = e.currentTarget.dataset.index
    if(index == 0){
      that.upload();
    }
  },
  /**
   * 按住说话
   */
  touchdown: function (e) {
    let that = this
    wx.stopVoice();
    that.setData({
      alcur: true,
      playing: false,
      cuindex: ''
    })
    that.data.duration = 0
    that.data.time = setInterval(function () {
      that.setData({
        duration: that.data.duration+1
      })
    },1000);
    // 开始录音
    wx.startRecord({
      success: function (res) {
        const newChatList = [...that.data.newsList, {
          news_type: 'voice',
          news_centent: { voice: res.tempFilePath, dimen: that.data.duration},
          openid: that.data.openid,
          logo:that.data.figureurl_wx
        }];
        // 用户消息发送完成,调用接口,等待AI回话
        that.aiChat('');
        that.setData({
          newsList: newChatList,
        })
        wx.setStorageSync('newsList', newChatList);
        that.cuinbut();
        // 用户消息发送完成,调用接口,等待AI回话
        that.aiChat();
      },
      fail:function(e){
        that.touchup();
      }
    });
    console.log(wx.getStorageSync('newsList'));
  },
  /**
   * 录音完成
   */
  touchup: function (e) {
    let that = this
    wx.stopRecord()
    clearInterval(that.data.time)
    that.setData({
      alcur:false
    })
  },
  /**
   * 播放录音
   */
  play: function (e) {
    console.log('123456');
    let that = this
    let voice = e.currentTarget.dataset.voice
    var index = e.currentTarget.dataset.index
    if (!that.data.playing){
      that.setData({
        playing:true,
        cuindex:index
      })
      wx.playVoice({
        filePath: voice,
        success: function () {
          that.setData({
            playing: false,
            cuindex:''
          })
        },
      })
    }
  },
  /**
   * 暂停录音
   */
  suspend:function(e){
    wx.stopVoice();
    this.setData({
      playing: false,
      cuindex: ''
    })
  },
  /**
   * 聊天接口
   * @param str 用户发的消息
   */
  aiChat:function(str)
  {
    var self = this;
    // 请求后台接口获取文章列表
    wx.request({
      // 请求连接
      url: 'https://guanchao.site/index/im/tencentAI',
      // 请求所需要的的参数
      data: {
        'str':str
      },
      success(result){
        self.data.aiAnswer = result.data.data.answer;
        if(result.data.msg != 'ok')
        {
          self.data.aiAnswer = "你的问题太深奥了,我没有办法回答~";
        }
        const newChatList = [...self.data.newsList, {
          news_type: 'text',
          news_centent: self.data.aiAnswer,
          openid: 'ai',
          logo:'https://guanchao.site/uploads/atricle/5c0a66cac42cb.jpeg'
        }];
        self.setData({
          animation: true,
          newsList: newChatList,
          sendout:false,
          news: '',
        })
        wx.setStorageSync('newsList', newChatList);
        self.cuinbut();
      }
    });
  },
  clicks:function(){
    console.log('12345');
  }
})

Aichat.json

{
  "usingComponents": {},
  "navigationBarTitleText": "AI聊天",
  "navigationStyle": "custom"
}

Aichat.wxss

page{
  background-color: #EDEDED;
}
.index{
  position:fixed;
  width: 100%;
  float: left;
  height: 64px;
  padding: 20rpx 220rpx 0 0;
  box-shadow: 0rpx 0rpx 0rpx;
  min-height: 0px;
  display: flex;align-items: center;
  background-color:#fff;
  z-index:9999;
  border-bottom: 1px solid #f7f7f7;
  top: 0;
}
.index-img{
  width: 34rpx;
  margin-left: 20rpx;
}
.index>text{
  margin-left: 20rpx;
  font-size: 28rpx;
  font-weight: bold;
}
.chat-index{
  /* padding-top: 30px; */
  width: 100%;
  float: left;
  overflow: hidden;
}
.scroll-view{
  width: 100%;
  float: left;
  overflow: hidden;
  background-color: #EDEDED;
}
.contrary{
  display:flex;
  flex-direction:row-reverse;
}
.news{
  width: 94%;
  float: left;
  overflow: hidden;
  padding: 0 3% 0 3%;
  margin-bottom: 26rpx;
}
.news:first-child{
  margin-top: 60rpx
}
.mews-image{
  width: 70rpx;
  height: 70rpx;
  float: left;
}
.mews-image>image{
  width: 100%;
  height: 100%;
  border-radius: 10rpx;
}
.news-centent{
  width: 70%;
  float: left;
  overflow: hidden;
  position: relative;
}
.centent-right{
  margin-right: 10rpx;
  padding: 0 15rpx 0 0;
}
.centent-left{
  margin-left: 10rpx;
  padding: 0 0 0 15rpx;
}
.trianright{
  width:0;
  height:0;
  border-top:15rpx solid transparent;
  border-bottom:15rpx solid transparent;
  border-left:15rpx solid #FFFFFF;
  position: absolute;
  right: 0;
  top: 20rpx;
}
.trianleft{
  width:0;
  height:0;
  border-top:15rpx solid transparent;
  border-bottom:15rpx solid transparent;
  border-right:15rpx solid #95EB69;
  position: absolute;
  left: 0;
  top: 20rpx;
}
.centent-right>text{
  float: right;
}
.centent-right>.news-images{
  float: right;
}
.centent-right>.news-voice{
  float: right;
}
.centent-left>text{
  float: left;
}
.centent-left>.news-images{
  float: left;
}
.centent-left>.news-voice{
  float: left;
}
.news-centent>text{
  word-wrap:break-word;
  word-break:normal; 
  padding: 16.5rpx;
  /* background-color: #95EB69; */
  color: #000200;
  border-radius: 8rpx;
  font-size: 28rpx;
  max-width: 100%;
  line-height: 42rpx;
}
.news-centent>.news-images{
  max-width: calc(100%);
  padding: 16.5rpx;
  background-color: #95EB69;
  border-radius: 8rpx;
}
.news-centent>.news-voice{
  height: 70rpx;
  background-color: #95EB69;
  border-radius: 8rpx;
  padding: 0 10rpx;
  display: flex;
  align-items: center;
}
.centent-left>.news-voice>text{
  padding: 0 30rpx 0 15rpx;
  font-size: 26rpx;
  font-weight: bold;
}
.centent-right>.news-voice>text{
  padding: 0 15rpx 0 30rpx;
  font-size: 26rpx;
  font-weight: bold;
}
.centent-left>.news-voice>image{
  float: left;
}
.centent-right>.news-voice>image{
  float: right;
}
.news-voice>image{
  width: 50rpx;
  height: 42rpx;
}
.chat-bottom{
  width: 100%;
  float: left;
  overflow: hidden;
  background-color: #F7F7F7;
  transition:all 0.5s;
}
.chat-centent{
  width: 100%;
  float: left;
  overflow: hidden;
  position: relative;
  padding: 0 1.5%;
}
.chat-pression{
  width: 7%;
  height: 100rpx;
  display: flex;
  align-items: center;
  text-align: center;
  float: left;
}
.chat-pression>image{
  width: 45rpx;
  height: 45rpx;
  margin: 0 auto;
}
.chat-tencvi{
  width: 73%;
  padding: 0 1%;
  float: left;
  display: flex;
  align-items: center;
  transition:all 0.5s;
}
.chat-tencvi>textarea{ 
  background-color: #fff;
  border-radius: 8rpx;
  width: 100%;
  padding: 3%;
  font-size: 28rpx;
  margin: 20rpx 0;
  max-height: 180rpx;
}
.chat-tencvi>view{
  width: 100%;
  padding: 3%;
  margin: 20rpx 0;
  background-color: #fff;
  border-radius: 8rpx;
  font-size: 26rpx;
  text-align: center;
}
.chat-news{
  height: 100rpx;
  float: right;
  display: flex;
  align-items: center;
  
}
.chat-news>view{
  width: 80%;
  background-color: #07C160;
  color: #fff;
  border-radius: 8rpx;
  height: 60rpx;
  line-height: 60rpx;
  font-size:26rpx;
  text-align: center;
}
.chat-cube{
  width: 100%;
  float: left;
  overflow: hidden;
  height: 400rpx;
  background-color: #EDEDED;
  border-top: 1px solid #ddd;
  padding:2% 0 8% 0;
  overflow-y: auto;
}
.cube-title{
  width: 100%;
  float: left;
  font-size: 24rpx;
  padding:0 3.5%;
  position: relative;
  top: 10rpx;
}
.cube-centent{
  width: 100%;
  float: left;
  overflow: hidden;
}
.cube-centent>view{
  margin-top: 30rpx;
  width: 12.5%;
  float: left;
  text-align: center;
}
.chat-camera{
  width: 100%;
  float: left;
  overflow: hidden;
  padding-bottom: 60rpx;
  background-color: #EDEDED;
  border-top: 1px solid #ddd;
  overflow-y: auto;
}
.camera-feature{
  margin: 60rpx 0 0 5%;
  width: 18.75%;
  float: left;
  overflow: hidden;
  text-align: center;
  font-size: 20rpx;
}
.feature-src{
  background-color: #fff;
  border-radius: 15rpx;
  float: left;
  width: 80rpx;
  height: 80rpx;
  margin: 0 calc(50% - 40rpx);
  text-align: center;
}
.feature-src>image{
  width: 40rpx;
  height: 40rpx;
  margin:20rpx;
}
.feature-text{
  width: 100%;
  float: left;
  margin-top: 10rpx;
}
.animation{
  z-index: 10;
  position: fixed;
  width:300rpx;
  height: 300rpx; 
  border-radius: 50%;
  margin: calc((100vh - 300rpx)/2) calc((100% - 300rpx)/2);
}
.animation>image{
  width: 100%;
  height: 100%;
  border-radius: 50%;
}
.animation::before {
  content: "";
  display: block;
  background-color: #ccc;
  filter: blur(10rpx);
  position: absolute;
  width: 100%;
  height: 100%;
  top: 10rpx;
  left: 10rpx;
  z-index: -1;
  opacity: 0.4;
  transform-origin: 0 0;
  border-radius: inherit;
  transform: scale(1, 1);
}
.chat-cbins{
  z-index: 10;
  width: 100%;
  background-color: rgba(26, 26, 26, 0.6);
  color: #fff;
  padding: 3%;
  font-size: 22rpx;
}
.background_left{
  background-color: #95EB69;
}
.background_right{
  background-color: #FFFFFF;
}

代码基本上就是这些了,其实没有多少东西,当然,就这么几行代码实现出来的效果肯定是有bug的,这个不要有太高的期望。

有好的建议,请在下方输入你的评论。

欢迎访问个人博客:guanchao.site

欢迎访问我的小程序:打开微信->发现->小程序->搜索“时间里的”