微信小程序-模仿绘制聊天界面

参考文章

1、小程序模仿微信聊天界面
2、微信小程序实现仿微信聊天界面(各种细节处理)
3、微信小程序之页面中关于聊天框三角形的制作和使用
4、仿微信聊天记录时间显示

5、微信小程序-同时获取麦克风、相机权限、获取多个权限
6、【uni-app】模仿微信实现简易发送/取发语音功能

7、微信小程序实现wxml中数据保留小数或取整

前言

代码参考自上述文章,样式和功能上根据自己需要做了一些改动以及删减,灰常感谢上述博主大大。ps:软键盘弹出还未进行测试。
消息交互的实现使用openfire,这里代码不做展示。

-----------------------2022/07/21修改-添加时间显示
-----------------------2022/07/22修改-发送按钮、空白消息提示
-----------------------2022/07/26修改-图片、语音消息

效果图

整体效果:
在这里插入图片描述

发送语音时(丑了点哈哈哈哈哈):

在这里插入图片描述
点击加号图标时:

在这里插入图片描述

代码

1、wxml

<wxs module="filters" src="../../../../utils/addmul.wxs"></wxs>
<view>
  <view >
    <scroll-view scroll-y scroll-into-view='{
     
     {toView}}'  style='height: { 
        { 
        scrollHeight}};' refresher-enabled="true" bindrefresherrefresh="loadMore" refresher-triggered="{
     
     {triggered}}">
      <view class='scrollMsg' >
        <block wx:key="key" wx:for='{
     
     {msgList}}' wx:for-index="index">
            <!-- 时间显示,时间间隔为5分钟(5分钟内的消息不必再显示时间) -->
            <view class="showTime" wx:if="{
     
     {item.showTime !== null}}">
              {
   
   {item.showTime}}
            </view>

            <!-- 单个消息1 客服发出(左) -->
            <view class="server" wx:if="{
     
     {item.jid == 'server'}}" id='msg-{
     
     {index}}'>
              <view class="serverIcon">
                <image src='{
     
     {head_img}}'></image>
              </view>
              <view class="serverContent">
                <view class="Angle">
                </view>
                <view class="Data">
                  <view class="leftMsg" wx:if="{
     
     {item.type == '1' }}">{
   
   {item.msg}}</view>
                  
                  <view class="leftMsg" wx:if="{
     
     {item.type == '2' }}">
                      <image src="{
     
     {item.msg}}" class="image" catchtap="picture" data-src="{
     
     {item.msg}}"></image>
                  </view>
                  
                  <view class="leftMsg" wx:if="{
     
     {item.type == '3' }}">
                      <view bindtap='playVoice' data-item="{
     
     {item}}" data-index="{
     
     {index}}">
                        <image style='height:32rpx;width:32rpx;'
                          src="{
     
     {imgs.yyxx}}" mode="aspectFit"></image>
                          {
   
   {filters.toFix(item.duration / 1000)}}"
                      </view>
                    </view>
                </view>
            </view>
            </view>

            <!-- 单个消息2 用户发出(右) -->
            <view class="customer" wx:else id='msg-{
     
     {index}}'>
                <!-- 发起方的聊天框 -->
                <view class="customerContent">
                  <view class="Data">
                  
                    <view class="rightMsg" wx:if="{
     
     {item.type == '1' }}">{
   
   {item.msg}}</view>
                    
                    <view class="rightMsg" wx:if="{
     
     {item.type == '2' }}">
                      <image class="image" src="{
     
     {item.msg}}" catchtap="picture" data-src="{
     
     {item.msg}}"></image>
                    </view>
                    
                    <view class="rightMsg" wx:if="{
     
     {item.type == '3' }}">
                      <view bindtap='playVoice' data-item="{
     
     {item}}" data-index="{
     
     {index}}">
                        {
   
   {filters.toFix(item.duration / 1000)}}"
                        <image style='height:32rpx;width:32rpx;margin-right:28rpx;'
                          src="{
     
     {imgs.yyxx}}" mode="aspectFit"></image>
                      </view>
                    </view>
                  </view>
                  <view class="AngleRight">
              </view>
            </view>
              <!-- 发起方的头像 -->
              <view class="serverIcon">
                <image  src='{
     
     {head_img}}'></image>
              </view>
            </view>
        </block>
      </view>
    </scroll-view>
  </view>

  <!-- 底部键盘、语音、加号 -->
  <view class='inputRoom' style="bottom: { 
        { 
        inputBottom  + 'px'}}">
    <image src='{
     
     {!voice ? imgs.icon_yy : imgs.xjp}}' catchtap="addSpeakMsg"  mode='widthFix'></image>
    <input wx:if="{
     
     {!voice}}" bindconfirm='sendClick' adjust-position='{
     
     {false}}' value="{
     
     {inputVal}}" confirm-type='send' bindfocus='focus' bindblur='blur' bindinput="getInputVal" maxlength="100"></input>
    <view wx:else class="touch" bindtouchstart="touchdown"  bindtouchend="touchup" bindtouchmove="touchmove">长按 说话</view>
    <image src='{
     
     {imgs.icon_gdgn}}' mode='widthFix' catchtap="addOtherFormatMsg"></image>
  </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 class="voice-mask" wx:if="{
     
     {mask}}">
	<!--语音条 -->
	<view class="voice-bar {
     
     {needCancel ? 'voiceDel' : ''}}">
		<image src="{
     
     {imgs.sb_c}}" class="voice-volume {
     
     {needCancel ? 'voiceDel' : ''}}"></image>
	</view>
	<!-- 底部区域 -->
	<view class="voice-send">
		<!-- 取消图标 -->
		<view class="voice-middle-wrapper">
			<!-- 取消 -->
			<view class="voice-left-wrapper">
				<view class="voice-middle-inner close {
     
     {needCancel ? 'bigger' : ''}}">
					<image src="{
     
     {imgs.voiceCancel}}" class="close-icon"></image>
				</view>
			</view>
			<view class="send-tip {
     
     {needCancel ? sendTipNone:''}}">{
   
   {sendtip}}</view>
		</view>	
		<!-- 底部语音显示 -->
		<view class="mask-bottom">
			<image src="{
     
     {imgs.ht}}"></image>
		</view>
	</view>	
</view>

2、wxss

page {
    
    
    background-color: #f1f1f1;
  }
  
  .inputRoom {
    
    
    width: 100vw;
    height: 60px;
    border-top: 1px solid #EEE;
    background-color: #fff;
    position: fixed;
    bottom: 0;
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    z-index: 20;
    padding: 0 2vw;
  }
  .inputRoom image{
    
    
    width: 7vw;
  }
  
  input {
    
    
    width: 70vw;
    height: 45%;
    background-color: #F2F2F2;
    border-radius: 2px;
    padding: 4px;
    font-size: 28rpx;
    color: #444;
  }
  .touch{
    
    
    width: 72vw;
    height: 60%;
    text-align: center;
    background-color: #F2F2F2;
    border-radius: 2px;
    padding: 4px;
    font-size: 28rpx;
    color: #444;
  }

  .leftMsg {
    
    
    padding: 2vh 2.5vw;
    background-color: #fff;
    border-radius: 10rpx;
    z-index: 10;
    font-size: 14px;
    color: #3B3B3B;
    line-height: 20px;
    font-weight: 400;
  }
  
  .rightMsg {
    
    
    font-size: 14px;
    line-height: 20px;
    padding: 2vh 2.5vw;
    background-color: #149C89;
    border-radius: 10rpx;
    z-index: 10;
    color: #FDFDFD;
    font-weight: 400;
  }
  .Angle {
    
    
    display:flex;
    width:0;
    height:0;
    border-width:10px;
    border-style:solid;
    border-color:transparent #fff transparent transparent;
  }

  .AngleRight {
    
    
    display:flex;
    width:0;
    height:0;
    border-width:10px;
    border-style:solid;
    border-color:transparent transparent transparent #149C89 ;
  }
  .showTime{
    
    
    display: flex;
    justify-content: center;
    color:#AEAEAE;
    font-size: 14px;
    padding: 1vh 0;
  }
  .server{
    
    
    display: flex; 
    padding: 2vh 11vw 2vh 2vw;
    flex-direction: row; 
    justify-content: flex-start; 
    align-items: center;
  }
  .serverIcon{
    
    
    width: 10vw; 
    height: 10vw;
  }
  .serverIcon image{
    
    
    width: 100%;
     height: 100%;
  }
  .serverContent{
    
    
    width: 71vw; 
    height: auto;  
    display: flex; 
    justify-content: flex-start; 
    align-items: center; 
    z-index: 9;
  }
  .customer{
    
    
    display: flex; 
    justify-content: flex-end; 
    padding: 1vh 2vw 1vh 11vw;
    align-items: center;
  }
  .customerContent{
    
    
    width: 71vw; 
    height: auto;  
    display: flex; 
    justify-content: flex-end;
    align-items: center; 
    z-index: 9;
  }

  .chat-camera{
    
    
    width: 100%;
    height: 100px;
    float: left;
    overflow: hidden;
    background-color: #EDEDED;
    overflow-y: auto;
    margin-top: 60px;
  }
  .camera-feature{
    
    
    margin: 5% 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;
  }
  .image{
    
    
    max-width: 71vw;
    max-height: 71vh;
  }
  /* 语音录制弹窗 */
.voice-mask{
    
    
  position:fixed;
  top:0;
  right:0;
  bottom:60px;
  left:0;
  /* display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center; */
  background-color: rgba(0,0,0,0.8);
}
.voice-bar{
    
    
  position: absolute;
  left:50%;
  top: 50%;
  width: 45%;
  transform: translate(-50%,-30%);
  /* width: 230rpx; */
  height:150rpx;
  background-color:#51ff50;
  border-radius: 26rpx;
  margin-bottom: 220rpx;
}
.voiceDel{
    
    
  left:80rpx;
  top: 52%;
  width: 170rpx !important;
  transform: translateX(0%);
  transform: translateY(-30%);
  background-color: red;
}
.voice-volume{
    
    
  position: relative;
  top: 50%;
  left: 50%;
  transform: translate(-50%,-50%);
  width: 50%;
  height: 77%;
}
.volumeDel{
    
    
  width: 80rpx;
}
.trangle-bottom{
    
    
  position: absolute;
  bottom: -38rpx;
  left:50%;
  transform: translateX(-50%);
  border-width: 20rpx;
  border-style: solid;
  border-color: #51FF50 transparent transparent transparent;
}
.trangleDel{
    
    
  border-color: red transparent transparent transparent;
}
.voice-send{
    
    
  position: absolute;
  bottom: 0;
  width: 100%;
}
.voice-middle-wrapper{
    
    
  width: 100%;
  display: flex;
  position:relative;
  justify-content: space-between;
  align-items: flex-end;
  margin-bottom: 40rpx;
}
.voice-left-wrapper{
    
    
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-end;
}
.cancel-del{
    
    
  display:none;
}
.delTip{
    
    
  display:block;
  color:#bfbfbf;
  margin: 0 22rpx 18rpx 0;
}
.voice-middle-inner{
    
    
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: rgba(0,0,0,0.2);
  width: 140rpx;
  height: 140rpx;
  border-radius: 50%;
}
.close{
    
    
  transform: rotate(350deg);
  margin-left: 80rpx;
}	
.bigger{
    
    
  width: 170rpx;
  height: 170rpx;
}
.to-text{
    
    
  transform: rotate(10deg);
  margin-right: 80rpx;
}
.close-icon{
    
    
  width: 80rpx;
  height: 80rpx;	
}
.wen{
    
    
  font-size: 40rpx;
  color:#bfbfbf;
}
.send-tip{
    
    
  position: absolute;
  left: 50%;
  bottom:0rpx;
  transform: translate(-50%,36%);
  color:#bfbfbf;
}
.sendTipNone{
    
    
  display: none;
}
.mask-bottom{
    
    
  position: relative;
  width: 100%;
  height:190rpx;
  border-top: #BABABB 8rpx solid;
  border-radius: 300rpx 300rpx 0 0;
  background-image: linear-gradient(#949794,#e1e3e1);
}
.mask-bottom image{
    
    
  position: absolute;
  width: 60rpx;
  height: 60rpx;
  top: 0;
  right:0;
  bottom: 0;
  left: 0;
  margin: auto;
}
  

3、.json

{
    
    
  "usingComponents": {
    
    }
}

4、.js

const app = getApp();
Page({
    
    

  /**
   * 页面的初始数据
   */
  data: {
    
    
    //图标路径
    imgs:{
    
    
      icon_yy:"/icon_yy.png",
      icon_gdgn:"/icon_gdgn.png",
      ht: "ht.png",
      sb: "sb.png",
      xjp:"xjp.png",
      yyxx:"yyxx.png",
      voiceCancel:"voiceCancel.png",
      sb_c:"sb_c.png"
    },
    //对方头像,可从上个页面获取过来
    head_img:"/icon_gdgn.png",
    //输入
    inputVal : '',
    //下拉加载状态
    triggered: true,
    //记录前一条信息的时间戳-用于时间转换
    prevFirst: '',
    //记录当前信息列表的第一条信息的时间戳,用于下次查询
    curTopTimeStamp:'',

    //一次查询几条信息
    pagenum:10,
    //触发上拉操作+1
    index:0,
    
    msgList : [{
    
    
        msgid:'001',
        //发送方id
        jid: 'server',
        //接收方
        tojid: 'customer',
        timestamp:'1658136237',
        msg: '你喜欢看明星大侦探吗?',
        type: '1',
        isread:'1',
      },
      {
    
    
        msgid:'002',
        //发送方
        jid: 'customer',
        //接收方
        tojid: 'server',
        timestamp:'1658136357',
        msg: '喜欢的,你呢?',
        type: '1',
        isread:'1',
      },{
    
    
        msgid:'003',
        //发送方
        jid: 'server',
        //接收方
        tojid: 'customer',
        timestamp:'1658136657',
        msg: '我也喜欢的,你喜欢里面的谁呢',
        type: '1',
        isread:'1',
      },
      {
    
    
        msgid:'004',
        //发送方
        jid: 'server',
        //接收方
        tojid: 'customer',
        timestamp:'1658309457',
        msg: '你怎么不说话了?',
        type: '1',
        isread:'1',
      },
      {
    
    
        msgid:'005',
        //发送方
        jid: 'customer',
        //接收方
        tojid: 'server',
        timestamp:'1658481572',
        msg: '不想说不想说话不想说话不想说话不想说话不想说话不想说话不想说话不想说话不想说话不想说话不想说话不想说话不想说话不想说话不想说话不想说话不想说话不想说话不想说话不想说话不想说话不想说话不想说话不想说话不想说话话',
        type: '1',
        isread:'1',
      },
    ],

    //---高度信息----
    scrollHeight: 'calc(100vh - 60px)',
    inputBottom: 0,
    keyHeight : 0,
    windowHeight : 0,
    windowWidth : 0,
    //功能框高度
    featureHeight:0,

    toView : '',

    //点击加号
    camera: false,
    //点击语音
    voice:false,
    //是否正在说话
    isSpeaking : false,
    recorderManager: null, //manager
    innerAudioContext: null, //音频播放manager
    sendtip: '松 开 发 送', // 录音过程中提示
    //播放语音中
    isPlaying:false,
    palyingMsgData: null, //记录正在播放的音频对象
    //遮罩层
    mask:false,
    //定义录音是否发送
    isClock:true,
    //需要取消(但是还没有取消)
    needCancel:false,

    //记录“取消发送”图标坐标位置,用于判断是否想要取消发送
    top:'',
    left:'',
    right:'',
    bottom:'',

    // 功能 -图标集合
    feature:[
      {
    
     src: 'camera.png', name: '相册' }
    ],
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    
    
    const that = this;
    that.setData({
    
    
        windowWidth:wx.getSystemInfoSync().windowWidth,
        windowHeight:wx.getSystemInfoSync().windowHeight,
        //对方账号
        tojid: 'server',
        //该页导航栏标题从上一页传递过来
        name:decodeURIComponent(options.name),
        //对方头像从上一页传递过来
        head_img:decodeURIComponent(options.head_img),
    },(res)=>{
    
    
      //页面切换,更换页面标题
      wx.setNavigationBarTitle({
    
    
         title: that.data.name 
      });
      //后续考虑每次退出页面时,将信息存入缓存?记录最早一条的时间戳,下次查询从这个时间戳开始查询
      
      //此处可调用接口获取已有的信息。
      that.getMsgList();

      //初始化音频相关
      that.initVoiceConfig();
    })
  },

  //调用接口查询信息列表
  getMsgList(){
    
    
	//根据自己需要写取信息的逻辑,此处先使用默认消息
	var msgList = that.data.msgList;
	dealMsg(msgList)
  },

  //处理信息并保存渲染
  dealMsg(msgList){
    
    
    const that = this;
    //需要对信息集合进行处理-时间的显示与否
    for (var i = 0; i < msgList.length; i++){
    
    
        let list = msgList[i];
        let showTime = this.msgTimeFormat(list.timestamp,i);
        list['showTime'] = showTime;
    }
    that.setData({
    
    
        msgList:msgList,
        toView:'msg-' + (that.data.msgList.length - 1),
    })
  },

  //上拉触发事件
  loadMore(){
    
    
	//根据实际业务写上拉触发的时间
  },

  /**
   * 生命周期函数--监听页面初次渲染完成
   */
  onReady: function (options) {
    
    
  
 },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow() {
    
    
    app.pageBindOpEvent(this.onConnect, this.onMessage);
  },

  //获取聚焦
  focus(e){
    
    
    const that = this;
    let keyHeight = e.detail.height;
    that.setData({
    
    
        camera : false,
        scrollHeight: (that.data.windowHeight - keyHeight - 60) + 'px',
        keyHeight: keyHeight
    });
    that.setData({
    
    
        toView: 'msg-' + (that.data.msgList.length - 1),
        inputBottom: keyHeight
    })
  },

  //失去聚焦(软键盘消失)
  blur(e) {
    
    
    const that = this;
    that.setData({
    
    
      scrollHeight: 'calc(100vh - 60px)',
      inputBottom: 0
    })
    that.setData({
    
    
      toView: 'msg-' + (that.data.msgList.length - 1)
    })
  },

  //获取输入内容
  getInputVal: function(e) {
    
    
    this.setData({
    
    
      inputVal: e.detail.value
    })
  },

  //发送点击监听
  sendClick: function(e) {
    
    
    const that = this;
    let value = that.data.inputVal;
    let msgList = that.data.msgList;
    if(value && !value.replace(/\s+/g, '').length == 0){
    
    
       //限制输入,为空或空格时不发送
        // 塞时间
        let timestamp = Date.parse(new Date());
        let showTime = this.msgTimeFormat(timestamp,that.data.msgList.length)
        msgList.push(
            {
    
    
              msgid:'010',
              //发送方
              jid: 'customer',
              //接收方
              tojid: 'server',
              timestamp: timestamp,
              msg: value,
              type: '1',
              isread:'1',
              showTime:showTime
            }
        )
    }else{
    
    
      //提示
      wx.showToast({
    
    
        title: '发送消息为空!',
        icon:'none'
      })
    }
    that.setData({
    
    
      msgList : msgList,
      inputVal : '',
      toView:'msg-' + (that.data.msgList.length - 1),
    });
  },

    /**
    * 聊天时间 格式化
    * 规则:
    *  1. 每五分钟为一个跨度
    *  2. 今天显示,小时:分钟,例如:11:12
    *  3. 昨天显示,昨天 小时:分钟 例如:昨天 11:12
    *  4. 日期差大于一天显示,年月日 小时:分钟 例如:2021年9月30日 11:12
    * @param timestamp,index 
    * @returns {string|null}
  */
  msgTimeFormat(timestamp, index) {
    
    
    const that = this;
    //时间戳转变为时间
    let date = timestamp.toString().length == 13 ? new Date(parseInt(timestamp)) : new Date(parseInt(timestamp * 1000));
    let time = '';
    //第一条消息
    if (0 == index){
    
    
        that.setData({
    
    
          prevFirst : timestamp
        })
        let prev = new Date(date);
        let next = new Date();
        let day = next.getDate() - prev.getDate();
        day = day >= 0 ? day : -(day);
        if (day > 1) {
    
    
            //时间间隔大于一天,显示YYYY年MM月DD日 HH:mm
            time = this.dateFormatChina(new Date(that.data.prevFirst.toString().length == 13 ? new Date(parseInt(that.data.prevFirst)) : new Date(parseInt(that.data.prevFirst * 1000))));
        } else if (day === 1) {
    
    
            time = '昨天 ' + prev.getHours() + ":" + this.timeAppendZero(prev);
        } else {
    
    
            time = prev.getHours() + ":" + this.timeAppendZero(prev);
        }
        return time;
    }
    
    let prev = new Date(that.data.prevFirst.toString().length == 13 ? new Date(parseInt(that.data.prevFirst)) : new Date(parseInt(that.data.prevFirst * 1000)));
    let next = new Date(date);

    let day = Math.floor( (next-prev) / (24*60*60*1000) );
    let minutes = Math.floor((next-prev) / (1000 * 60));
    let dayT = new Date().getDate() - next.getDate();
    let yesterdayFlag = dayT === 1 || dayT === -1;
    let todayFlag = dayT === 0;

    /*
      下标越界标志
      未越界且分钟差大于5,将当前消息日期作为比较值并替换prevFirst,并根据规则格式化
      越界则表示下标走到了最后一位,将其作为要显示的日期赋值给prev,并根据规则格式化
     */
    let indexOutFlag = that.data.msgList.length !== (index + 1);
    if (indexOutFlag && minutes > 5) {
    
    
      that.setData({
    
    
        prevFirst : timestamp
      })
      if (!todayFlag && !yesterdayFlag) {
    
    
        return this.dateFormatChina(next);
      } else {
    
    
          prev = new Date(date);
          if (yesterdayFlag) {
    
    
              return '昨天 ' + prev.getHours() + ":" + this.timeAppendZero(prev);
          }
      }
    } else {
    
    
        prev = new Date(date);
    }

    if (yesterdayFlag && minutes >= 5) {
    
    
        return '昨天 ' + prev.getHours() + ":" + this.timeAppendZero(prev);
    } else if (todayFlag && minutes >= 5) {
    
    
        return prev.getHours() + ":" + this.timeAppendZero(prev);
    }
    return null;
  },
  dateFormatChina(date) {
    
    
    return date.getFullYear() + "年" + (date.getMonth()+1) + "月" + date.getDate() + "日 " + date.getHours() + ":" + this.timeAppendZero(date);
  },
  timeAppendZero(time) {
    
    
    return time.getMinutes().toString().length === 1 ? '0' + time.getMinutes() : time.getMinutes();
  },

  //点击加号
  addOtherFormatMsg() {
    
    
    const that = this;
    that.setData({
    
    
      camera: !that.data.camera,
      voice:false,
      isSpeaking:false
    })
    that.setData({
    
    
      inputBottom: that.data.camera == true ? that.data.inputBottom + 100 : 0,
      scrollHeight: that.data.camera == true ? 'calc(100vh - 160px)' : 'calc(100vh - 60px)',
    })
    that.setData({
    
    
      toView: 'msg-' + (that.data.msgList.length - 1),
    })
  },

  //功能页-
  featch(e){
    
    
    const that = this
    let index = e.currentTarget.dataset.index
    if(index == 0){
    
    
      //相册-选择图片
      that.upload();
    }
  },

  //上传图片
  upload:function(e){
    
    
    const that = this
    let msgList = that.data.msgList;
    // 微信选择图片
    wx.chooseImage({
    
    
      count: 3, // 最多一次性选择图片的数量 默认9
      // sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有
      // sourceType: ['album'], //, 'camera' 可以指定来源是相册还是相机,默认二者都有
      success: function (res) {
    
    
        //时间戳转换为时间
        // 判断时间戳是否为13位数,如果不是则*1000,时间戳只有13位数(带毫秒)和10(不带毫秒)位数的
        let timeTamp = Date.parse((new Date()));
        let showTime = that.msgTimeFormat(timeTamp,msgList.length);

        let tempFilePathLists = res.tempFilePaths;
        for (var i = 0; i < tempFilePathLists.length; i++){
    
    
          msgList.push(
            {
    
    
              msgid:"00" + Math.random(),
              //发送方
              jid: "customer",
              //接收方
              tojid: 'server',
              timestamp: timeTamp,
              msg: tempFilePathLists[i],
              type: '2',
              isread:'0',
              showTime:showTime
            }
          )
          
		//在此,图片的路径先使用微信临时文件,后续需要上传至服务器

        that.setData({
    
    
          msgList : msgList,
          toView:'msg-' + (that.data.msgList.length - 1),
          camera:false,
          scrollHeight:"calc(100vh - 60px)",
          inputBottom:0
        });
      },
      failed: function (res) {
    
    
        wx.showToast({
    
    
          title: '图片选择失败,请重试',
          icon: 'none'
        })
      },
      complete: function (res) {
    
    
      }
    });
  },

    /**
   * 点击看大图
   */
  picture:function(e){
    
    
    let src = e.currentTarget.dataset.src;
    wx.previewImage({
    
    
      current: src,
      urls: [src]
    })
  },

   /**
   * 初始化语音录制和播放的配置数据
   */
  initVoiceConfig() {
    
    
    const recorderManager = wx.getRecorderManager(); // 录音manager
    var msgList = this.data.msgList;
    recorderManager.onStart(() => {
    
    
      console.log('start')
    })
    recorderManager.onPause(() => {
    
    
      console.log('pause')
    })
    recorderManager.onStop((res) => {
    
    
      console.log('stop')
      // 录音时间小于一秒钟,提示录音时间过短
      if (res.duration < 1000) {
    
    
        wx.showToast({
    
    
          title: '说话时间太短',
          icon: 'error'
        });
        return;
      }
      // 防止出现录音结束了,录音弹框没有消失的问题
      clearInterval(this.timer);
      this.setData({
    
    
        isSpeaking: false,
        sendtip: '松 开 发 送'
      });

      var that = this;
      //封装消息
      if (that.data.isClock) {
    
    
        //时间戳转换为时间
        // 判断时间戳是否为13位数,如果不是则*1000,时间戳只有13位数(带毫秒)和10(不带毫秒)位数的
        let timeTamp = Date.parse((new Date()));
        let showTime = that.msgTimeFormat(timeTamp,msgList.length);
        msgList.push(
          {
    
    
            msgid:"00" + Math.random(),
            //发送方
            jid: "customer",
            //接收方
            tojid: 'server',
            timestamp: timeTamp,
            msg: res.tempFilePath,
            duration: res.duration,
            type: '3',
            isread:'0',
            showTime:showTime
          }
        );

        that.setData({
    
    
          msgList : msgList,
          toView:'msg-' + (that.data.msgList.length - 1)
        })
      }
      
     //在此,语音的路径先使用微信临时文件,后续需要上传至服务器
     
    });
    recorderManager.onFrameRecorded((res) => {
    
    
      const {
    
    
        frameBuffer
      } = res
      console.log('frameBuffer.byteLength', frameBuffer.byteLength)
    });
    this.data.recorderManager = recorderManager;

    //音频播放manager
    const innerAudioContext = wx.createInnerAudioContext();
    innerAudioContext.onPlay(() => {
    
    
      console.log('开始播放');
    });

    innerAudioContext.onEnded(() => {
    
    
      console.log('音频自然播放结束');
      this.setData({
    
    
        palyingMsgData: null
      });
    });

    innerAudioContext.onStop((res) => {
    
    
      console.log("音频播放停止");
    });

    innerAudioContext.onError((res) => {
    
    
      console.log("音频播放失败" + res.errCode + "---errMsg=" + res.errMsg);
      this.setData({
    
    
        palyingMsgData: null
      });
      wx.showToast({
    
    
        title: '音频播放失败',
        icon: 'error'
      });
    });
    this.data.innerAudioContext = innerAudioContext;
  },

  //点击语音图标
  addSpeakMsg(){
    
    
    const that = this;
    //检查麦克风权限
    that.checkAuthorize().then((result) => {
    
    
      that.setData({
    
    
        camera:false,
        voice:!that.data.voice,
        scrollHeight:'calc(100vh - 60px)',
        inputBottom:0
      })
      that.setData({
    
    
        toView: 'msg-' + (that.data.msgList.length - 1),
      })
    }).catch((error) => {
    
    
     
    })

  },

  //按下说话
  touchdown: function (e) {
    
    
    console.log("手指按下")
    const query = wx.createSelectorQuery();
    
    var that= this;
    that.setData({
    
    
      isSpeaking: true,
      mask:true
    },(res)=>{
    
    
      //记录“取消发送”元素位置
      if (that.data.mask) {
    
    
        query.select('.close-icon').boundingClientRect()
        query.exec(function (res) {
    
    
          that.setData({
    
    
            top:res[0].top,
            left:res[0].left,
            right:res[0].right,
            bottom:res[0].bottom,
          })
       })
      }
      that.startVoice();
      speaking.call(that);
    });
  },
  
   /**
   * 录音:手指滑动,录音不发送
   */
  touchmove: function(e){
    
    
    const that = this;
    let needCancel = false;
    let sendtip = '松 开 发 送';
    //判断当前触摸位置是否处于“取消发送”元素内
    if(e.touches[0].pageX >= that.data.left && e.touches[0].pageX <= that.data.right && e.touches[0].pageY >= that.data.top && e.touches[0].pageY <= that.data.bottom){
    
    
      needCancel = true;
      sendtip = '松 开 取 消';
    } 
    that.setData({
    
    
      needCancel:needCancel,
      sendtip: sendtip
    })
  },

  /**
   * 录音:手指抬起,录音结束
   */
  touchup: function (e) {
    
    
    console.log("手指抬起");
    const that = this;
    this.setData({
    
    
      isSpeaking: false,
      isClock:!that.data.needCancel,
      mask:false,
    },(res)=>{
    
    
      this.handleStopVoice(this);
    })
  },

  //开始录音
  startVoice: function () {
    
    
    console.log("startVoice----");
    // 如果此时正在播放语音,则停止
    this.handleStopPlayVoice(this);
    const options = {
    
    
      duration: 61000, //默认最长播放时长60秒 
      // sampleRate: 44100,
      // numberOfChannels: 1,
      // encodeBitRate: 192000,
    };
    
    if (this.data.isSpeaking) {
    
    
      this.data.recorderManager.start(options);
    }else{
    
    
      wx.showToast({
    
    
        title: '说话时间太短',
        icon: 'error'
      });
      return;
    }
  },

   /**
   * 结束录音以及处理相关逻辑
   */
  handleStopVoice: function (that) {
    
    
    that.stopVoice();
    clearInterval(that.timer);
    that.setData({
    
    
      needCancel:false
    });
  },

  /**
   * 结束录音
   */
  stopVoice: function () {
    
    
    console.log("stopVoice----");
    this.data.recorderManager.stop();
  },

  /**
   * 播放音频
   */
  playVoice: function (e) {
    
    
    var that = this;
    var mData = e.currentTarget.dataset.item;
    var index = e.currentTarget.dataset.index;
    // 如果点击的是正在播放的语音,则停止语音播放
    if (that.data.palyingMsgData != null && that.data.palyingMsgData == mData.msg) {
    
    
      that.handleStopPlayVoice(that);
      return false;
    }
    //  如果点击的是未在播放的语音,播放之前先停掉别的语音播放
    that.stopPlayVoice();
    that.setData({
    
    
      palyingMsgData: mData.msg
    });
    //播放
    var voiceUrl = mData.msg;
    that.data.innerAudioContext.src = voiceUrl;
    that.data.innerAudioContext.play();
  },

  /**
   * 停止音频播放
   */
  stopPlayVoice: function () {
    
    
    console.log('stopPlayVoice----');
    this.data.innerAudioContext.stop();
  },

  /**
   * 停止语音播放以及处理相关逻辑
   */
  handleStopPlayVoice: function (that) {
    
    
    if (that.data.palyingMsgData != null) {
    
    
      // 停止语音播放
      that.stopPlayVoice();
    }
  },

  //检查授权-麦克风权限
  checkDeviceAuthorize: function () {
    
    
    return new Promise((resolve, reject) => {
    
    
      wx.getSetting({
    
    
        success:(res)=>{
    
    
          let auth = res.authSetting['scope.record']
          if (auth === true) {
    
     // 用户已经同意授权
            resolve()
          }
          else if (auth === undefined) {
    
    // 首次发起授权
            wx.authorize({
    
    
              scope: 'scope.record',
              success() {
    
    
                resolve()
              },
              fail(res) {
    
    
              }
            })
          }
          else if (auth === false) {
    
     // 非首次发起授权,用户拒绝过 => 弹出提示对话框
            wx.showModal({
    
    
              title: '授权提示',
              content: '请前往设置页打开麦克风',
              success: (tipRes) => {
    
    
                if (tipRes.confirm) {
    
    
                  wx.openSetting({
    
    
                    success: (settingRes) => {
    
    
                      if (settingRes.authSetting['scope.record']) {
    
    
                        resolve()
                      }
                    },
                  })
                }
              }
            })
          }
        },
        })
      })
  },

  // 页面从前台变为后台时执行
  onHide: function () {
    
    
    app.pageunBindOpEvent();
  },

    /**
   * 生命周期函数--监听页面卸载
   */
  onUnload() {
    
    
    const that = this;
    app.pageunBindOpEvent();
    that.data.innerAudioContext.destroy();//销毁这个实例
  },


})

/**
 * 麦克风帧动画
 */
function speaking() {
    
    
  var that = this;
  var delayTime = 1000;
  var MAX_DURATION = 60000;
  var COUNTDOWN_DURATION = 50000;
  //话筒帧动画
  var duration = 0;
  that.timer = setInterval(function () {
    
    
    duration = duration + delayTime;
    console.log("duration==" + duration);
    //倒计时提示-10秒
    if (duration > COUNTDOWN_DURATION) {
    
    
      var djs = parseInt((MAX_DURATION - duration) / 1000);
      that.setData({
    
    
        sendtip: '录音倒计时:' + djs + 's'
      });
    }
    if (duration >= MAX_DURATION) {
    
    
      that.handleStopVoice(that);
    }
  }, delayTime);
}

5、addmul.wxs

var filters = {
    toFix2: function (value) {
      return parseFloat(value).toFixed(2)//此处2为保留两位小数
    },
    toFix1: function (value) {
      return parseFloat(value).toFixed(1)//此处1为保留一位小数
    },
    toFix: function (value) {
      return parseFloat(value).toFixed(0)//此处0为取整数
    }
 }
  module.exports = {
    toFix2: filters.toFix2,
    toFix1: filters.toFix1,
    toFix: filters.toFix
  }

后续待补充

语音暂停,从原来暂停的位置开始播放

猜你喜欢

转载自blog.csdn.net/qq_42622871/article/details/125677698