WeChat applet - imitate drawing chat interface

reference article

1. The mini-program imitates the WeChat chat interface
2. The WeChat mini-program realizes the imitation of the WeChat chat interface (processing of various details)
3. The creation and use of the chat box triangle in the WeChat mini-program page
4. The imitation of the WeChat chat record time display

5. WeChat applet - obtain microphone and camera permissions at the same time, and obtain multiple permissions
6. [uni-app] imitates WeChat to realize simple sending/retrieving voice functions

7. Wechat applet implements data retention in wxml to keep decimals or round up

foreword

The code is referenced from the above article, and some changes and deletions have been made in terms of style and function according to my own needs. I am very grateful to the above-mentioned bloggers. ps: The soft keyboard popup has not been tested yet.
The implementation of message interaction uses openfire, and the code is not shown here.

-----------------------2022/07/21 Modification - add time display
----------------- ------2022/07/22 revision - send button, blank message prompt
-----------------------2022/07/26 revision - picture ,Voice messages

renderings

overall effect:
insert image description here

When sending voice (it's ugly hahahaha):

insert image description here
When clicking the plus icon:

insert image description here

the code

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
  }

To be added later

Pause the voice and start playing from the original paused position

Guess you like

Origin blog.csdn.net/qq_42622871/article/details/125677698