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

这篇具有很好参考价值的文章主要介绍了微信小程序-模仿绘制聊天界面。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

参考文章

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
  }

后续待补充

语音暂停,从原来暂停的位置开始播放文章来源地址https://www.toymoban.com/news/detail-401540.html

到了这里,关于微信小程序-模仿绘制聊天界面的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 微信小程序 云开发 聊天功能

    。功能要求为:一对一聊天,可以发送文字,图片,语音,文件,视频,含消息列表页。 暑假没事干来写篇博客复盘一下。框架和样式部分就是采用了colorUI 的组件,没啥好说的,这方面我也不会  这是聊天内容部分 浅浅地小讲一下 scroll-into-view string 值应为某子元素id(id不

    2024年02月09日
    浏览(44)
  • 【微信开发】微信小程序实现实时聊天功能

    最近在做一个项目,需要运用到实时聊天功能,分享一下。      分为: 1.界面如何布局以及细节; 2.如何实现实时更新; 3.全部代码展示;         一、界面如何布局以及细节:         1.说到底,聊天界面就是循环一个数组,每一行为一个单位(item),循环(wx.for)我

    2024年02月04日
    浏览(53)
  • 微信小程序之在线客服(即时聊天)

    一、即时聊天 使用小程序给我们提供的在线聊天功能,在界面中只能通过按钮开放能力来打开。 注意:此功能只能在手机端中去使用,在模拟器中无法进行。 二、小程序页面中的代码展示 上诉button按钮需要绑定open-type=“contact” 这个参数即可实现客服功能。 注意:模拟器

    2024年02月11日
    浏览(52)
  • 微信小程序 WebSocket 通信 —— 在线聊天

             * 源码已经上传到资源处,需要的话点击跳转下载 |  源码下载         在Node栏目就讲到了Socket通信的内容,使用Node实现Socke通信,还使用两个流行的WebSocket 库,ws 和 socket.io,在小程序中的WebSocket接口和HTML5的WebSocket基本相同,可以实现浏览器与服务器之间的

    2024年02月01日
    浏览(45)
  • 微信小程序05---聊天室的搭建

    wss://showme.myhope365.com/websocketChat?username=password=courseId=nickName=avatar= 参数名 说明 示例 username 用户名 可以使用用户loginname password 密码 随便设置,这个后台开放连接 courseId 分组id 小组id nickName 昵称 用户昵称 avatar 头像

    2024年02月12日
    浏览(37)
  • 【uniapp】微信小程序分享到聊天,朋友圈

    ​​​​​manifest.json的app模块配置开启Share微信分享,填入appId。  2.示例  3.界面分享按钮:button open-type=\\\"share\\\"分享/button

    2024年02月14日
    浏览(56)
  • uniapp微信小程序 选择聊天记录文件上传

    目录 精简版总结 示例 容易踩的坑 1、页面刷新问题 2、extension问题 单文件 多个文件 PS:files和filePath/name只能二选一组 此处用xlsx文件作实例,选择聊天记录中的xlsx文件上传到指定接口中。 因为某些微信版本extension可能不生效,或者又想要对提交的文件名做校验,建议参考我

    2024年02月09日
    浏览(82)
  • 微信小程序 | 基于小程序+Java+WebSocket实现实时聊天功能

    此文主要实现在小程序内聊天对话功能,使用Java作为后端语言进行支持,界面友好,开发简单。 2.1、注册微信公众平台账号。 2.2、下载安装IntelliJ IDEA(后端语言开发工具),Mysql数据库,微信Web开发者工具。 1.创建maven project 先创建一个名为SpringBootDemo的项目,选择【New Proje

    2024年02月02日
    浏览(52)
  • SpringBoot实现Websocket聊天交友微信小程序(一)

    记录一下我开发一个交友微信小程序并且上线运营的心得体会。 2022年10月1日上线的,到目前终于实现每天收益300左右。 界面比较简洁,功能有动态,动态可以选择话题,相册,相册可以设置看广告解锁,私信,私信可以发图片,发语音。还有会员功能。前端用的uniapp,UI框

    2024年02月03日
    浏览(44)
  • 微信小程序WebSocket实现stream流式聊天对话功能

    要在微信小程序实现聊天对话功能,回话是流式应答,这里使用了WebSocket技术。WebSocket大家应该都很熟悉,使用wx.connectSocket就可以了。这里可能需要注意下的是流式应答,后端如何发送,前端如何接收。直接上代码: 可以扫码体验: 后端关键代码: 小程序ts代码: 可以扫码

    2024年04月10日
    浏览(82)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包