• 使用wx.request的enableChunked: true,实现流式传输,通过定时器实现聊天交互效果

  • UI框架使用vant-weapp

重点问题:

1.在流式请求时因为延迟显示打字效果,导致流式数据返回后处理顺序错乱,我们需要一个缓冲队列来保存这些数据

2.输入每个字的时候需要调用页面滚动来展示数据,这时候,需要做节流处理,否则会出现scrollTop已经赋值了,但是dom还没加载完成,导致滚动不到位

1.主聊天界面

流式请求

// pages/chatGptDemo/chatGptDemo.js
import chatRequest from './chatRequest'
import { CDN_PREFIX } from '../../utils/key'
import Dialog from '../../miniprogram/miniprogram_npm/@vant/weapp/dialog/dialog';
import { formatTime} from'../../utils/util';
import local from '../../utils/local';
import {apiGetChatList,apiDeleteDreamChat,apiGetChatUserInfo} from '../../http/api'
const { navigation } = require("../../utils/navigation");
const app = getApp()
const {
    events,
    userInfo,
} = app.globalData
//自己维护消息队列 重点******
let isProcessing = false;
let queue = [];
Page({

  /**
   * 页面的初始数据
   */
  data: {
    navigationBarHeight: 0,
    scrollTop: 0,
    inputValue: "",
    valueTemp: "",
    isFirstSend: false,
    valueList: [],
    inputDisabled:false,
    focus:false,
    enableAnimation:true,
    throttleScroll:true,
    // 分页
    triggered: false, //向上刷新load
    page: 0,
    page_size: 10,
    lastPageId:0,//锚链接
    showViewIdName:'',
    //解梦模板
    dreamModuleVisable: false,
    dreamModuleAnimate:false,
    //进度条
    progress:{
      total_num:20,
      remain_num:0,
      user_type:'free',
      perce:0,
    }
  },
  //显示欢迎语
  showWelcomeMsg(){
    let list = []
    let nickName = userInfo.nickname ? userInfo.nickname+',':''
    let msg = `你好,${nickName}我是布咕,可以帮你解梦,也可以陪你聊天,你想聊些什么吗?`
      list.push({
        type: 'left',
        value: msg
      })
    setTimeout(()=>{
      this.setData({
        valueList:list
      })
    },500)
  },

  //滚到到指定ID
  autoScrollId(){
    this.setData({
      showViewIdName:'item'+this.data.lastPageId
    },()=>{
      this.setData({
        enableAnimation:true
      })
    })
  },

  //滚动条至最底部
  autoScroll() {
    if (!this.data.throttleScroll) return;
    this.data.throttleScroll = false;
    setTimeout(() => {
      this.data.throttleScroll = true;
    }, 500);
      var that = this
      let query = wx.createSelectorQuery()
      query.select('.chat-value').boundingClientRect(res => {
          that.setData({
            scrollTop: res.height*10,
          })
          
      })
      query.exec(res => {})
   
    
  },
  //清除历史记录
  onclear() {
    Dialog.confirm({
      title: '删除聊天记录',
      message: '确定要删除聊天记录吗?\n删除后不可查看历史聊天记录',
      confirmButtonText:'删除'
    })
      .then(async() => {
       const res = await apiDeleteDreamChat()
  
       if(res.code==200){
        local.setSync('welcomeTime',JSON.stringify(0))
         this.setData({
          valueList: [],
          valueTemp: '',
          page:0
        });
       }
        
      })
      .catch(() => {
        // on cancel
      });

   
  },
  //没有机会弹窗
  onBalance(){
    Dialog.confirm({
      title: '提示',
      message: '免费使用次数已用完\n点击前往获取更多次数',
      confirmButtonText:'获取更多'
    })
      .then(() => {
        this.routeToCenter()
      })
      .catch(() => {
        // on cancel
      });
  },
  //点击重新加载
  resendOutputMsg() {
    this.resendInputMsg()
  },
  //点击重新发送
  resendInputMsg() {
    let list = this.data.valueList
    let lastItem = list.reduce((prev, curr) => {
      if (curr.type === 'right') {
        return curr
      }
      return prev
    }, {})
    list.splice(list.length-2,1)
  
    if (lastItem.value) {
      this.setData({
        inputValue: lastItem.value,
        valueList: list
      }, () => {
        this.onSearchBaidu()
      })
    }

  },
  clickInputView(){
    this.setData({
      focus:true
    })
  },
 
    //创建发送消息
    createSendMsg() {
      let list = this.data.valueList
      list.push({
        type: 'right',
        value: this.data.inputValue,
        time:formatTime(new Date(),'yyyy-MM-dd HH:mm:ss')
      })
      list =this.addShowTime(list)

      this.setData({
        valueList: list,
        inputValue: '',
      },()=>{
        this.autoScroll()
        setTimeout(()=>{
          this.createLoadingMsg()
        },500)
      })
    },
    //创建回复空消息
    createLoadingMsg(){
      let list = this.data.valueList
      list.push({
        type: 'left',
        value: ''
      })
      this.setData({
        valueList: list,
      },()=>{
        this.autoScroll()
      })

    },
   //创建回复消息
   async sendBotMsg(data) {
    await new Promise((resolve, reject) => {
     let str = data.content
     let is_end = data.is_end
     const contentCharArr = Array.from(str);
     this.showText(0, contentCharArr,is_end,resolve);
    });
     
  },
  //打字效果
  showText(key = 0, value,is_end,resolve) {
    if (key >= value.length) {
      if(is_end){
        this.setData({
          inputDisabled:false
        })
      }
      resolve();
      return;
    }
    this.setData({
      valueTemp: this.data.valueTemp + value[key]
    }, () => {
      let itemlist = this.data.valueList
      let index = this.data.valueList.length - 1
      let tempStr = itemlist[index]
      tempStr.value = this.data.valueTemp
      
      console.log( this.data.valueTemp)
      this.setData({
        [`valueList[${index}]`]: tempStr,
      }) 
      setTimeout(() => {
        /* 递归渲染内容 */
       this.showText(key + 1, value,is_end,resolve);
       this.autoScroll()
      }, 50);
    })
  },
   //清空所有的异常消息
   onClearErrMsg() {
    let array = this.data.valueList
    let errorCodes = [1, 2]
    let list = array.filter(item => {
      return !errorCodes.includes(item.errType)
    })

    this.setData({
      valueList: list
    })
  },
  //模板解梦
  templateDream(e){
    if(e.detail){
      this.setData({
        inputValue:e.detail
      },()=>{
        wx.showLoading({
          title: '正在整合信息',
          mask:true
        })
        setTimeout(()=>{
          this.onSearchBaidu()
          wx.hideLoading()
        },500)
        
      })
    }
  },
  //发送消息
  onSearchBaidu() {
    if(this.data.progress.remain_num<1){
      //次数用完
      this.onBalance()
      return
    }
    this.getDecCount()

    let inputValue = this.data.inputValue.trim()
    
    if(!inputValue){
      wx.showToast({
        title: '请输入内容',
        icon:'none'
      })
      return
    }
    let welcomeTime = new Date().getTime()
    local.setSync('welcomeTime',JSON.stringify(welcomeTime))

    this.onClearErrMsg()
    this.setData({
      isFirstSend: true,
      inputDisabled:true,
      valueTemp: ''
    })
    let params = {
      content: inputValue
    }
    this.createSendMsg()
    setTimeout(()=>{
      this.streamingRequests(params)
    },1000)
  },
  //处理消息队列 难点******
   processQueue() {
    if(queue.length === 0) {
      return;
    }  
    const res = queue.shift();
    return this.doProcess(res).then(() => {
      return this.processQueue();  
    });
  },
  
  async doProcess(res) {
    return new Promise(async  resolve => {
      let str = this.Uint8ArrayToStringBaidu(res.data)
      let arr = str.split(':dream-answer')
      for(let item of arr) {
        if(item) {
          try {
            console.debug(JSON.parse(item))
            let {code,data,msg} = JSON.parse(item)
            if (code == 200) {
              await this.sendBotMsg(data)
            } else {
              this.handleError(code,msg)
            }
          } catch (error) {
            console.error(error)
            this.setData({
               inputDisabled:false,
            })
            console.log('报错了')
            this.handleError(500,'数据错误,请联系客服')
          }
        }
      }
  
      resolve();
    }) 
  },
  //流式请求
  streamingRequests(params){
    //重点 流处理的时候可能会丢失数据。我们需要一个缓冲队列来保存这些数据
    const requestTask = chatRequest(params, '/api/dreamChat/crateStreamChat')
    requestTask.onChunkReceived( (res) => {
      queue.push(res);
      if(!isProcessing) {
        isProcessing = true;
        this.processQueue().then(() => {
          isProcessing = false; 
        });
      }
    })
  },
  //流式解析数据
  Uint8ArrayToStringBaidu(fileData) {
    const str = decodeURIComponent(escape(String.fromCharCode.apply(null, new Uint8Array(fileData))))
    return str
  },
  //错误信息处理
  handleError(code, msg) {
    if (!this.data.isFirstSend) {
      this.getChatUserInfo()
      return
    }
    if (code == 422) {
     //报错重新发送
     let itemlist = this.data.valueList
     //errType 0 正常  1重新发送  2重新加载
     itemlist[this.data.valueList.length - 1].errType =2
      this.setData({
        valueList: itemlist,
        isFirstSend: false,
      }, () => {
        this.autoScroll()
      })
    }else if(code==423){
      //敏感消息报错
       let itemlist = this.data.valueList
      itemlist =  itemlist.slice(0, -2);
      this.setData({
        valueList: itemlist,
        isFirstSend: false
      }, () => {
        this.autoScroll()
      })
    } else {
      //报错重新发送
      let itemlist = this.data.valueList
      itemlist =  this.removeLastLeft(itemlist)
      //errType 0 正常  1重新发送  2重新加载
      itemlist[this.data.valueList.length - 1].errType = 1
      this.setData({
        valueList: itemlist,
        isFirstSend: false
      }, () => {
        this.autoScroll()
      })
    }
    wx.showToast({
      title: msg,
      icon: 'none'
    })
    this.setData({
      inputDisabled:false,
   })
  },
  //报错删除最后一个回复
  removeLastLeft(arr) {
    let lastIndex = -1;
    for (let i = arr.length - 1; i >= 0; i--) {
      if (arr[i].type === 'left') {
        lastIndex = i;
        break; 
      }
    }
    if (lastIndex !== -1) {
      arr.splice(lastIndex, 1);
    }
    return arr;
  },
  //手动减次数
  getDecCount(){
    let progress = this.data.progress
    if(progress.user_type!=='mouth'){
      let user_type = progress.user_type
      let total_num = progress.total_num ||0
      let remain_num = progress.remain_num - 1 || 0
      let perce = Math.round(remain_num * 100 / total_num)
      let obj = {
        total_num,
        remain_num,
        user_type,
        perce,
      }
      this.setData({
        progress:obj
      })
    }
  },
  //下拉加载
 async bindrefresherrefresh() {
    this.data.page++
    await this.getChatHistoryList()
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    let navigationBarHeight = getApp().globalNavbarInfo.navigationBarHeight
    this.setData({
      navigationBarHeight,
    })
     // 监听微信登录成功
     events.on('on-launch-executed', this, async res => {
      this.initialData()
      this.getChatUserInfo()
    })
    this.initialData()
  },
   initialData(){
 
   let welcome = local.getSync('welcomeTime')
   if(!welcome){
     this.showWelcomeMsg()
   }else{
    let welcomeTime = Number(JSON.parse(local.getSync('welcomeTime')))
    let dayTime = new Date().getTime();
    if(dayTime - welcomeTime < 24*60*60*1000){
      this.data.page=1
      this.getChatHistoryList()
    }else{
      this.showWelcomeMsg()
    }
   }
  },
  //用户信息
  async getChatUserInfo(){
 
    const {data,code} = await apiGetChatUserInfo()
    if(code!==200)return

    let user_type = 'free'//免费用户
    let total_num = 20//总次数
    let remain_num =0 //剩余次数
    let card_status=data.card_status//会员卡状态
    if(card_status==1){
      //会员用户
      if(data.once_card && data.once_card.remain_num>0){
        //次卡用户
        user_type ='once'
        total_num = data.once_card.total_num
        remain_num = data.once_card.remain_num
      }
      if(data.month_card && data.month_card.remain_num>0){
        //月卡用户
        user_type ='mouth'
        total_num = data.month_card.total_num
        remain_num = data.month_card.remain_num
      }
    }else{
      remain_num =data.dream_free
    }
    let perce = Math.round(remain_num * 100 / total_num)
    let progress = {
      total_num,
      remain_num,
      user_type,
      perce,
    }
    this.setData({
      progress
    })
 
    
  },
  //聊天记录
  async getChatHistoryList() {
    let page = this.data.page
      let params = {
        page,
        size: this.data.page_size,
      }
      const res = await apiGetChatList(params)
      if (res.code == 200) {
        let result =[ ]
        
        if(res.data.data&&res.data.data.length>0){
           result = res.data.data.map(message => {
            return {
              id:message.id,
              value: message.content,
              time: message.created_at,
              type: message.user_type === 1 ? 'right' : 'left'
            };
          });
        }
        let resData = [];
        if (page == 1) {
          resData = this.data.valueList;
        } else {
          if(this.data.valueList[0] &&this.data.valueList[0].id){
            this.data.lastPageId =  this.data.valueList[0].id
          }
          resData = this.data.valueList
        }
        if (result.length == 0) {
          page -= 1 
          if (page == 0){
            page = 1
          } 
          //首次不提示
          wx.showToast({
            title: '没有更多了~',
            icon: "none"
          })
          this.setData({
            triggered: false,
            page
          })
          return
        }
        resData = result.reverse().concat(resData);
         const listWithShowTime = this.addShowTime(resData)
        this.setData({
          page,
          valueList: listWithShowTime,
          triggered: false,
        },()=>{
          if (this.data.lastPageId!==0){
            // 向上分页锚链接
            this.setData({
              enableAnimation:false
            },()=>{
              this.autoScrollId()
            })
          }else{
            this.autoScroll()
          }
        })
      }
   
  },
  //时间转换
  addShowTime(list) {
    let prevTime = null
    list.forEach(item => {
      const currTime = new Date(item.time).getTime()
      if (prevTime) {
        const diff = (currTime - prevTime) / (1000 * 60 * 60)
        if (diff > 1) {
          item.showTime = true
        } else {
          item.showTime = false
        }
      } else {
        item.showTime = true
      }
      prevTime = currTime
    })
    return list
  },

  //打开解梦模板
  openDreamModule() {
    this.setData({
      dreamModuleVisable: true,
      dreamModuleAnimate:true,
    })
  },
  //关闭解梦模板
  closeDreamModule() {
    this.setData({
      dreamModuleVisable: false
    },()=>{
      setTimeout(()=>{
        this.setData({
          dreamModuleAnimate:false,
        })
      },300)
    })
  },
  routeToCenter(){
    navigation.push({
      url: '/pages/member/member',
    })
  },

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

  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow() {
    this.getChatUserInfo()
  },

  /**
   * 生命周期函数--监听页面隐藏
   */
  onHide() {

  },

  /**
   * 生命周期函数--监听页面卸载
   */
  onUnload() {
    events.remove('on-launch-executed', this)
  },

  /**
   * 页面相关事件处理函数--监听用户下拉动作
   */
  onPullDownRefresh() {

  },

  /**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom() {

  },

  /**
   * 用户点击右上角分享
   */
  onShareAppMessage() {
   
    let imageUrl = `${CDN_PREFIX}dream_share_img.png`
    return {
      title: '聊天解梦样样精通,快来试试吧',
      path: `/pages/chatDream/chatDream`,
      imageUrl:imageUrl
    }
  }
})

JS

// pages/chatGptDemo/chatGptDemo.js
import chatRequest from './chatRequest'
import { CDN_PREFIX } from '../../utils/key'
import Dialog from '../../miniprogram/miniprogram_npm/@vant/weapp/dialog/dialog';
import { formatTime} from'../../utils/util';
import local from '../../utils/local';
import {apiGetChatList,apiDeleteDreamChat,apiGetChatUserInfo} from '../../http/api'
const { navigation } = require("../../utils/navigation");
const app = getApp()
const {
    events,
    userInfo,
} = app.globalData
Page({

  /**
   * 页面的初始数据
   */
  data: {
    navigationBarHeight: 0,
    scrollTop: 0,
    inputValue: "",
    valueTemp: "",
    isFirstSend: false,
    valueList: [],
    inputDisabled:false,
    focus:false,
    enableAnimation:true,
    // 分页
    triggered: false, //向上刷新load
    page: 1,
    page_size: 10,
    lastPageId:0,//锚链接
    showViewIdName:'',
    //解梦模板
    dreamModuleVisable: false,
    dreamModuleAnimate:false,
    //进度条
    progress:{
      total_num:20,
      remain_num:0,
      user_type:'free',
      perce:0,
    }
  },
  //显示欢迎语
  showWelcomeMsg(){
    let list = []
    let nickName = userInfo.nickname ? userInfo.nickname+',':''
    let msg = `你好,${nickName}我是小羊,可以帮你解梦,也可以陪你聊天,你想聊些什么吗?`
      list.push({
        type: 'left',
        value: msg
      })
    setTimeout(()=>{
      this.setData({
        valueList:list
      })
    },500)
  },

  //滚到到指定ID
  autoScrollId(){
    this.setData({
      showViewIdName:'item'+this.data.lastPageId
    },()=>{
      this.setData({
        enableAnimation:true
      })
    })
  },
  //滚动条至最底部
  autoScroll() {
    var that = this
    let query = wx.createSelectorQuery()
    query.select('.value-scroll').boundingClientRect(res => {
      //延迟滚动,否则不生效
        that.setData({
        scrollTop: res.height*100
        })
    })
    query.exec(res => {})
  },
  //清除历史记录
  onclear() {
    Dialog.confirm({
      title: '删除聊天记录',
      message: '确定要删除聊天记录吗?\n删除后不可查看历史聊天记录',
      confirmButtonText:'删除'
    })
      .then(async() => {
       const res = await apiDeleteDreamChat()
  
       if(res.code==200){
        local.setSync('welcomeTime',JSON.stringify(0))
         this.setData({
          valueList: [],
          valueTemp: '',
          page:0
        });
       }
        
      })
      .catch(() => {
        // on cancel
      });

   
  },
  //没有机会弹窗
  onBalance(){
    Dialog.confirm({
      title: '提示',
      message: '免费使用次数已用完\n点击前往获取更多次数',
      confirmButtonText:'获取更多'
    })
      .then(() => {
        this.routeToCenter()
      })
      .catch(() => {
        // on cancel
      });
  },
  //点击重新加载
  resendOutputMsg() {
    this.resendInputMsg()
  },
  //点击重新发送
  resendInputMsg() {
    let list = this.data.valueList
    let lastItem = list.reduce((prev, curr) => {
      if (curr.type === 'right') {
        return curr
      }
      return prev
    }, {})
    list.splice(list.length-2,1)
  
    if (lastItem.value) {
      this.setData({
        inputValue: lastItem.value,
        valueList: list
      }, () => {
        this.onSearchBaidu()
      })
    }

  },
  clickInputView(){
    this.setData({
      focus:true
    })
  },
 
    //创建发送消息
    createSendMsg() {
      let list = this.data.valueList
      list.push({
        type: 'right',
        value: this.data.inputValue,
        time:formatTime(new Date(),'yyyy-MM-dd HH:mm:ss')
      })
      list =this.addShowTime(list)
      console.log(list)
      this.setData({
        valueList: list,
        inputValue: '',
      },()=>{
        this.autoScroll()
        setTimeout(()=>{
          this.createLoadingMsg()
        },500)
      })
    },
    //创建回复空消息
    createLoadingMsg(){
      let list = this.data.valueList
      list.push({
        type: 'left',
        value: ''
      })
      this.setData({
        valueList: list,
      },()=>{
        this.autoScroll()
      })

    },
   //创建回复消息
   sendBotMsg(data) {
     let str = data.content
    this.setData({
      valueTemp: this.data.valueTemp + str
    }, () => {
      let itemlist = this.data.valueList
      itemlist[this.data.valueList.length - 1].value = this.data.valueTemp
      this.setData({
        valueList: itemlist,
      }, () => {
        this.autoScroll()
        if(data.is_end){
          this.setData({
            inputDisabled:false
          })
        }
      })
    })
  },
   //清空所有的异常消息
   onClearErrMsg() {
    let array = this.data.valueList
    let errorCodes = [1, 2]
    let list = array.filter(item => {
      return !errorCodes.includes(item.errType)
    })

    this.setData({
      valueList: list
    })
  },
  //模板解梦
  templateDream(e){
    if(e.detail){
      this.setData({
        inputValue:e.detail
      },()=>{
        wx.showLoading({
          title: '正在整合信息',
          mask:true
        })
        setTimeout(()=>{
          this.onSearchBaidu()
          wx.hideLoading()
        },500)
        
      })
    }
  },
  //发送消息
  onSearchBaidu() {
    if(this.data.progress.remain_num<1){
      //次数用完
      this.onBalance()
      return
    }
    this.getDecCount()

    let inputValue = this.data.inputValue.trim()
    
    if(!inputValue){
      wx.showToast({
        title: '请输入内容',
        icon:'none'
      })
      return
    }
    let welcomeTime = new Date().getTime()
    local.setSync('welcomeTime',JSON.stringify(welcomeTime))

    this.onClearErrMsg()
    this.setData({
      isFirstSend: true,
      inputDisabled:true,
      valueTemp: ''
    })
    let params = {
      content: inputValue
    }
    this.createSendMsg()
    setTimeout(()=>{
      this.streamingRequests(params)
    },1000)
  },
  //流式请求
  streamingRequests(params){
    const requestTask = chatRequest(params, '/api/dreamChat/crateStreamChat')
  
    requestTask.onChunkReceived((res) => {
      
      let str = this.Uint8ArrayToStringBaidu(res.data)
      let arr = str.split(':dream-answer')
      arr.map(item => {
        if (item) {
          console.log(item)
          try {
            console.log(JSON.parse(item))
            let {code,data,msg} = JSON.parse(item)
            if (code == 200) {
              this.sendBotMsg(data)
            } else {
              this.handleError(code,msg)
            }
          } catch (error) {
            console.error(error)
            this.setData({
               inputDisabled:false,
            })
            console.log('报错了')
            this.handleError(500,'数据错误,请联系客服')
          }
        }
      })
    })
  },
  //流式解析数据
  Uint8ArrayToStringBaidu(fileData) {
    const str = decodeURIComponent(escape(String.fromCharCode.apply(null, new Uint8Array(fileData))))
    return str
  },
  //错误信息处理
  handleError(code, msg) {
    if (!this.data.isFirstSend) {
      this.getChatUserInfo()
      return
    }
    if (code == 422) {
     //报错重新发送
     let itemlist = this.data.valueList
     //errType 0 正常  1重新发送  2重新加载
     itemlist[this.data.valueList.length - 1].errType =2
      this.setData({
        valueList: itemlist,
        isFirstSend: false,
      }, () => {
        this.autoScroll()
      })
    }else if(code==423){
      //敏感消息报错
       let itemlist = this.data.valueList
      itemlist =  itemlist.slice(0, -2);
      this.setData({
        valueList: itemlist,
        isFirstSend: false
      }, () => {
        this.autoScroll()
      })
    } else {
      //报错重新发送
      let itemlist = this.data.valueList
      itemlist =  this.removeLastLeft(itemlist)
      //errType 0 正常  1重新发送  2重新加载
      itemlist[this.data.valueList.length - 1].errType = 1
      this.setData({
        valueList: itemlist,
        isFirstSend: false
      }, () => {
        this.autoScroll()
      })
    }
    wx.showToast({
      title: msg,
      icon: 'none'
    })
    this.setData({
      inputDisabled:false,
   })
  },
  //报错删除最后一个回复
  removeLastLeft(arr) {
    let lastIndex = -1;
    for (let i = arr.length - 1; i >= 0; i--) {
      if (arr[i].type === 'left') {
        lastIndex = i;
        break; 
      }
    }
    if (lastIndex !== -1) {
      arr.splice(lastIndex, 1);
    }
    return arr;
  },
  //手动减次数
  getDecCount(){
    let progress = this.data.progress
    if(progress.user_type!=='mouth'){
      let user_type = progress.user_type
      let total_num = progress.total_num ||0
      let remain_num = progress.remain_num - 1 || 0
      let perce = Math.round(remain_num * 100 / total_num)
      let obj = {
        total_num,
        remain_num,
        user_type,
        perce,
      }
      this.setData({
        progress:obj
      })
    }
  },
  //下拉加载
 async bindrefresherrefresh() {
    this.data.page++
    await this.getChatHistoryList()
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    let navigationBarHeight = getApp().globalNavbarInfo.navigationBarHeight
    this.setData({
      navigationBarHeight,
    })
     // 监听微信登录成功
     events.on('on-launch-executed', this, async res => {
      this.initialData()
      this.getChatUserInfo()
    })
    this.initialData()
  },
   initialData(){
 
   let welcome = local.getSync('welcomeTime')
   if(!welcome){
     this.showWelcomeMsg()
   }else{
    let welcomeTime = Number(JSON.parse(local.getSync('welcomeTime')))
    let dayTime = new Date().getTime();
    if(dayTime - welcomeTime < 24*60*60*1000){
      this.getChatHistoryList()
      
    }else{
      this.showWelcomeMsg()
    }
   }
  },
  //用户信息
  async getChatUserInfo(){
 
    const {data,code} = await apiGetChatUserInfo()
    if(code!==200)return

    let user_type = 'free'//免费用户
    let total_num = 20//总次数
    let remain_num =0 //剩余次数
    let card_status=data.card_status//会员卡状态
    if(card_status==1){
      //会员用户
      if(data.once_card && data.once_card.remain_num>0){
        //次卡用户
        user_type ='once'
        total_num = data.once_card.total_num
        remain_num = data.once_card.remain_num
      }
      if(data.month_card && data.month_card.remain_num>0){
        //月卡用户
        user_type ='mouth'
        total_num = data.month_card.total_num
        remain_num = data.month_card.remain_num
      }
    }else{
      remain_num =data.dream_free
    }
    let perce = Math.round(remain_num * 100 / total_num)
    let progress = {
      total_num,
      remain_num,
      user_type,
      perce,
    }
    this.setData({
      progress
    })
 
    
  },
  //聊天记录
  async getChatHistoryList() {
      let params = {
        page: this.data.page,
        size: this.data.page_size,
      }
      const res = await apiGetChatList(params)
      if (res.code == 200) {
        let result =[ ]
        if(res.data.data&&res.data.data.length>0){
           result = res.data.data.map(message => {
            return {
              id:message.id,
              value: message.content,
              time: message.created_at,
              type: message.user_type === 1 ? 'right' : 'left'
            };
          });
        }
        let resData = [];
        if (this.data.page == 1) {
          resData = [];
        } else {
          if(this.data.valueList[0] &&this.data.valueList[0].id){
            this.data.lastPageId =  this.data.valueList[0].id
          }
          resData = this.data.valueList
        }
        let page = this.data.page;
        if (result.length == 0) {
          page = this.data.page - 1;
          if (page == 0){
            page = 1
          } else{
            //首次不提示
              wx.showToast({
              title: '没有更多了~',
              icon: "none"
            })
            this.setData({
              triggered: false,
            })
            return
          }
        }
        resData = result.reverse().concat(resData);
         const listWithShowTime = this.addShowTime(resData)
        this.setData({
          page,
          valueList: listWithShowTime,
          triggered: false,
        },()=>{
          if (this.data.lastPageId!==0){
            // 向上分页锚链接
            this.setData({
              enableAnimation:false
            },()=>{
              this.autoScrollId()
            })
            
          }else{
            this.autoScroll()
          }
        })
      }
   
  },
  //时间转换
  addShowTime(list) {
    let prevTime = null
    list.forEach(item => {
      const currTime = new Date(item.time).getTime()
      if (prevTime) {
        const diff = (currTime - prevTime) / (1000 * 60 * 60)
        if (diff > 1) {
          item.showTime = true
        } else {
          item.showTime = false
        }
      } else {
        item.showTime = true
      }
      prevTime = currTime
    })
    return list
  },

  //打开解梦模板
  openDreamModule() {
    this.setData({
      dreamModuleVisable: true,
      dreamModuleAnimate:true,
    })
  },
  //关闭解梦模板
  closeDreamModule() {
    this.setData({
      dreamModuleVisable: false
    },()=>{
      setTimeout(()=>{
        this.setData({
          dreamModuleAnimate:false,
        })
      },300)
    })
  },
  routeToCenter(){
    navigation.push({
      url: '/pages/member/member',
    })
  },

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

  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow() {
    this.getChatUserInfo()
  },

  /**
   * 生命周期函数--监听页面隐藏
   */
  onHide() {

  },

  /**
   * 生命周期函数--监听页面卸载
   */
  onUnload() {
    events.remove('on-launch-executed', this)
  },

  /**
   * 页面相关事件处理函数--监听用户下拉动作
   */
  onPullDownRefresh() {

  },

  /**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom() {

  },

  /**
   * 用户点击右上角分享
   */
  onShareAppMessage() {
   
    let imageUrl = `${CDN_PREFIX}dream_share_img.png`
    return {
      title: '聊天解梦样样精通,快来试试吧',
      path: `/pages/chatDream/chatDream`,
      imageUrl:imageUrl
    }
  }
})

WXML

<!--pages/chatGptDemo/chatGptDemo.wxml-->
<wxs src="/wxs/cdn.wxs" module="cdn" />
<nav-bar id="nav-bar" customBackground="#fff" textStyle='black' pageTitle='小羊'></nav-bar>

<view class="gpt-contgainer" style="padding-top:{{navigationBarHeight}}px;">
  <scroll-view  class="value-scroll"  scroll-y="true"	

  scroll-into-view-within-extent="{{true}}" 
  scroll-into-view="{{showViewIdName}}"
  scroll-top="{{scrollTop}}"  scroll-with-animation="{{enableAnimation}}"  enhanced show-scrollbar="{{false}}"  bounces="{{false}}"  enable-passive="{{true}}" 

   refresher-enabled bindrefresherrefresh="bindrefresherrefresh" enable-flex refresher-triggered="{{triggered}}">
    <view class="chat-value">
      <view wx:key="index" class="valueItem" id="item{{item.id}}" wx:for="{{valueList}}">
        <!-- <van-transition custom-class="valueItem" show="{{ item  }}"  name="slide-up"> -->
        <view class="itemTime" wx:if="{{item.showTime&&item.time}}">{{item.time}}</view>
        <view wx:if="{{item.type=='right'}}" class="itemRight">
        <text user-select="{{true}}" selectable="{{true}}">{{item.value}}</text>
        </view>
        <view wx:if="{{item.type=='left'}}" class="itemLeft">
          <text user-select="{{true}}" selectable="{{true}}">{{item.value||'正在输入中...'}}</text>
        </view>
        <view wx:if="{{item.errType && item.errType==1}}" bind:tap="resendInputMsg" class="resend-input-msg">
          <view class="err-icon-box">
            <van-icon custom-class="err-icon" name="fail" />
          </view>
          发送失败 点击 <text style="color: #9B5617;">重新发送</text>
        </view>
        <view class="resend-output-msg" bind:tap="resendOutputMsg" wx:if="{{item.errType && item.errType==2}}">发送失败 点击<text style="color: #9B5617;">重新加载</text>
        </view>
        <!-- </van-transition> -->
      </view>
    </view>

  </scroll-view>
  <!-- 进度条 -->
  <view class="header-box" style="top: {{navigationBarHeight}}px;">
    <view class="progress-bar-box">
      <view class="top-box">
        <view wx:if="{{progress.user_type=='free'}}" class="top-title">
          <text>剩余免费次数</text>
          <text style="color: #9B5617;font-weight: 600;"> {{progress.remain_num}} </text>
          <text>次</text>
        </view>
        <view wx:if="{{progress.user_type=='mouth'}}" class="top-title">
          <text>剩余天数</text>
          <text style="color: #9B5617;font-weight: 600;"> {{progress.remain_num}} </text> 
          <text>天</text>
        </view>
        <view wx:if="{{progress.user_type=='once'}}" class="top-title">
          <text>剩余次数</text>
          <text style="color: #9B5617;font-weight: 600;"> {{progress.remain_num}} </text>
          <text>次</text>
        </view>
        <auth-btn>
        <van-button bind:click="routeToCenter" custom-class="header-btn-style" round>{{progress.user_type=='free'?'获取更多':'会员中心'}}</van-button>
        </auth-btn>
      </view>
      <view class="bottom-box">
        <view class="progress-bg-box">
          <van-progress show-pivot="{{false}}" percentage="{{progress.perce}}" color="linear-gradient(90deg, #FFB079 0%, #FFB0FC 46.25%, #938AFF 100%);" custom-class="progress-bar-style" />
        </view>
        <view class="progress-text">{{progress.remain_num}}/{{progress.total_num}}</view>
        
      </view>
    </view>
  </view>
  <!-- 输入框 -->
  <view class="footer-box">
    <auth-btn>
    <view class="module-style">
      <van-button bind:click="openDreamModule" custom-class="module-btn-style" icon="{{cdn.image('dream_module_icon.png')}}" round>
     
      解梦大师
      </van-button>
    </view>
    <view class="group-style" >
      <view class="clear-icon-box">
        <van-icon custom-class="clear-icon" bind:click="onclear" name="{{cdn.image('chat_delete_icon.png')}}" />
      </view>
      <view bind:tap="clickInputView">
        <van-field show-confirm-bar="{{false}}" bind:focus="autoScroll" focus="{{focus}}" bind:click-input="clickInputView" custom-class="custom-input-style" input-class="input-style" model:value="{{ inputValue }}" label="" clearable type="textarea" placeholder="请输入内容" autosize border="{{ false }}" />
      </view>
     
      <van-button bind:click="onSearchBaidu" disabled="{{inputDisabled}}" custom-class="send-btn-style" round>发送</van-button>
    </view>
  </auth-btn> 
  </view>
</view>

<!-- 解梦模板 -->
<dream-module wx:if="{{dreamModuleAnimate}}" bind:templateDream="templateDream" navigationBarHeight="{{navigationBarHeight}}" bind:closeDreamModule="closeDreamModule" dreamModuleVisable="{{dreamModuleVisable}}"></dream-module>
<!-- dialog弹窗 -->
<van-dialog id="van-dialog" confirm-button-class="alert-confirm-button" cancle-button-class="alert-cancle-button" custom-class="alert-class" />

WXSS

.gpt-contgainer {
  background: #F7F7F7;
  height: 100vh;
  overflow: hidden;
  padding-bottom: calc(env(safe-area-inset-bottom) + 125rpx);
  box-sizing: border-box;

}




/*---------顶部进度条---------*/
.header-box {
  position: fixed;
  height: 126rpx;
  width: 100%;
  background: #fff;
  filter: drop-shadow(0rpx 2rpx 20rpx rgba(0, 0, 0, 0.05));
  display: flex;
  justify-content: center;
}

.progress-bar-box {
  width: 100%;
  box-sizing: border-box;
  padding: 24rpx 40rpx 0;
}

.progress-bar-box .top-box {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
}

.progress-bar-box .bottom-box {
  display: flex;
  flex-direction: row;
  align-items: center;
  margin-top: 4rpx;
}

.progress-bar-style {
  height: 6rpx !important;
  width: 430rpx !important;
  border-radius: 10rpx !important;
  overflow: hidden !important;
  box-sizing: border-box !important;
  background: #F2F2F2 !important;
}


.progress-bar-box .top-box .top-title {
  color: #333;
  font-size: 28rpx;
  line-height: 39rpx;
}

.progress-bg-box {
  padding: 4rpx;
  width: 434rpx;
  border-radius: 10rpx;
  background: #F2F2F2;

}

.progress-text {
  font-size: 22rpx;
  margin-left: 4rpx;
  line-height: 31rpx;
  color: #333
}

.progress-icon-box {

  background: #C9DCFD;
  width: 48rpx;
  height: 48rpx;
  border-radius: 50%;
  margin-left: -8rpx;
}


.header-btn-style {
  padding: 0 !important;
  width: 144rpx !important;
  height: 48rpx !important;
  font-size: 24rpx !important;
  color: #fff !important;
  border-radius: 100rpx;
  font-weight: 600 !important;
  background: linear-gradient(180deg, #FFDF8E 0%, #FF7B45 100%), linear-gradient(180deg, #FFE4A0 0%, #FFA985 100%) !important;
  border: none !important;
  box-shadow: 1rpx -1rpx 3rpx 0rpx rgba(255, 255, 255, 0.80) inset !important;
}

/*-------------底部输入框-------------------*/

.footer-box {
  position: fixed;
  bottom: 0;
  width: 100%;
  padding-bottom: env(safe-area-inset-bottom);
  left: 50%;
  transform: translateX(-50%);
  z-index: 1;
  box-sizing: border-box;
  background: #fff;
  box-shadow: 0rpx -4rpx 20rpx 0rpx rgba(0, 0, 0, 0.05);
}

.group-style {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  background: #fff;
  padding: 24rpx;

}

.input-style {
  font-size: 30rpx !important;
  color: #333 !important;
  box-sizing: border-box;
  max-height: 234rpx;
}

.group-style .van-field__body--textarea {
  padding: 0 !important;
}

.custom-input-style {
  width: 486rpx !important;
  border-radius: 64rpx !important;
  padding: 16rpx 32rpx !important;
  background: #f7f8fa !important;
}
.clear-icon-box{
  background: #f7f7f7;
  border-radius: 50%;
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  width: 48rpx;
  height: 48rpx;
}
.clear-icon {
  font-size: 33rpx !important;
}
.footer-box-module .van-field__body--textarea{
  padding: 0 !important;
  overflow:visible !important;
}
.footer-box-module .van-cell{
  --cell-font-size:30rpx;
  --cell-line-height:30rpx;
  --cell-text-color:#333;
}
.footer-box-module .van-field__control--textarea{
  --field-text-area-min-height:45rpx;
}

.send-btn-style {
  padding: 0 !important;
  width: 120rpx !important;
  height: 64rpx !important;
  border: none !important;
  border-radius: 100rpx !important;
  background: linear-gradient(180deg, #FFDF8E 0%, #FF7B45 100%), linear-gradient(180deg, #FFE4A0 0%, #FFA985 100%) !important;
  box-shadow: 1rpx -1rpx 3rpx 0rpx rgba(255, 255, 255, 0.80) inset !important;
  color: #fff !important;
  font-size: 28rpx !important;
  font-weight: 600 !important;
  line-height: 44rpx !important;
}

.module-style {
  position: absolute;
  top: -96rpx;
  left: 0;
  display: flex;
  flex-direction: row;
  align-items: center;
  padding: 0rpx 24rpx;
}
.van-button__icon{
  padding: 0 !important;
  font-size: 46rpx !important;
}

.module-btn-style {
  display: flex;
  flex-direction: row !important;
  align-items: center !important;
  padding: 0 !important;
  width: 213rpx !important;
  height: 64rpx !important;
  font-size: 28rpx !important;
  color: #697AF6 !important;
  border-radius: 32rpx !important;
  border: 2rpx solid #697AF6 !important;
  font-weight: 600 !important;
  background: #F7F7F7
}


/*--------------聊天内容-----------------*/
.value-scroll {
  width: 100%;
  height: 100%;
  flex-grow: 1;
  margin-bottom: 0;
  /* margin-top: 24rpx; */
  border-radius: 10rpx;
  overflow: hidden;
  box-sizing: border-box;
  padding-top: 126rpx;
  
  /* padding-top: calc(env(safe-area-inset-bottom) + 100rpx); */
}

.chat-value {
  padding: 0 32rpx;
  padding-bottom:235rpx ;
}


.valueItem {
  display: grid;
  justify-items: center; 
  width: 100%;
}
.itemTime{
  width: 100%;
  color: #999;
  text-align: center;
  font-size: 24rpx;
  line-height: 34rpx;
  margin-top: 32rpx;
}

.itemRight {
  justify-self: end; 
  margin-top: 32rpx;
  max-width: 654rpx;
  padding: 32rpx;
  font-size: 30rpx;
  line-height: 45rpx;
  color: #333;
  background-color: #FFF7E9;
  border-radius: 64rpx 16rpx 64rpx 64rpx;
  box-sizing: border-box;
 
  word-break:break-all;
}

.itemLeft {
  justify-self: start; 
  margin-top: 32rpx;
  max-width: 654rpx;
  padding: 32rpx;
  font-size: 30rpx;
  line-height: 45rpx;
  background-color: #fff;
  color: #333;
  border-radius: 16rpx 64rpx 64rpx 64rpx;
  box-sizing: border-box;

  word-break:break-all
}

.resend-input-msg{
  display: flex;
  flex-direction: row;
  justify-content: flex-end;
  align-items: center;
  margin-top: 16rpx;
  font-size: 24rpx;
  line-height: 34rpx;
  color: #333;
  width: 100%;
  padding: 0 32rpx;
  box-sizing: border-box;
}
.resend-output-msg{
  display: flex;
  flex-direction: row;
  justify-content: flex-start;
  align-items: center;
  margin-top: 16rpx;
  font-size: 24rpx;
  line-height: 34rpx;
  color: #333;
  width: 100%;
  padding: 0 32rpx;
  box-sizing: border-box;
}

.resend-input-msg .err-icon-box{
  display: flex;
  align-items: center;
  justify-content: center;
  width: 33rpx;
  height: 33rpx;
  border-radius: 50%;
  background: #F84F60;
  margin-right: 8rpx;
}
.resend-input-msg .err-icon{
font-size: 28rpx !important;
color: #fff !important;
}

/* 弹窗 */
.alert-class{
  border-radius: 16rpx !important;
  width: auto !important;
  padding: 0 !important;
  box-sizing: border-box !important;
  --dialog-header-padding-top:48rpx;

}
.alert-class .van-dialog__header{
  font-weight: 600!important;
  line-height: 56rpx !important;
  font-size: 36rpx !important;
  color: rgba(0, 0, 0, 0.85) !important;
}
.alert-class .van-dialog__message{
  padding: 24rpx 48rpx 48rpx !important;
  text-align: center !important;
  color: rgba(0, 0, 0, 0.65) !important;
  font-size: 28rpx !important;
  line-height: 44rpx !important;
}
.alert-confirm-button{
  color: #AF8603 !important;
font-size: 36rpx !important;
padding: 22rpx 0 !important;

}
.alert-cancle-button{
color: rgba(0, 0, 0, 0.65)!important;
font-size: 36rpx !important;
padding: 22rpx 0 !important;
}

JSON配置文件

{
    "usingComponents": {
      "van-search": "../../miniprogram/miniprogram_npm/@vant/weapp/search/index",
      "van-field": "../../miniprogram/miniprogram_npm/@vant/weapp/field/index",
      "van-icon": "../../miniprogram/miniprogram_npm/@vant/weapp/icon/index",
      "van-button": "../../miniprogram/miniprogram_npm/@vant/weapp/button/index",
      "van-progress": "../../miniprogram/miniprogram_npm/@vant/weapp/progress/index",
      "nav-bar":"../../components/navBar/navBar",
      "dream-module":"./components/dreamModule/dreamModule",
      "van-dialog": "../../miniprogram/miniprogram_npm/@vant/weapp/dialog/index",
      "van-transition": "../../../../miniprogram/miniprogram_npm/@vant/weapp/transition/index",
      "auth-btn": "../../components/authBtn/authBtn"
    },
    "navigationStyle": "custom"
    
}

2.弹窗模板聊天组件

JS

// pages/chatGptDemo/components/dreamModule/dreamModule.js
Component({
  options: { styleIsolation: 'apply-shared' },
  /**
   * 组件的属性列表
   */
  properties: {
    dreamModuleVisable: {
      type: Boolean,
      value: false
    },
    navigationBarHeight: {
      type: Number,
      value: 0
    }
  },

  /**
   * 组件的初始数据
   */
  data: {
    dreamList: [],
    questionList: [],
    inputValue: '',
    round: 0, //轮数
    scrollTop: 0,
    inputDisabled:true
  },
  lifetimes: {
    created() {
      
    },
    attached() {
     
      this.getQuestionList()
    },
    ready() {
      this.getStartModule()
    },
    moved() {
      
    },
    detached() {
      
    }
  },

  /**
   * 组件的方法列表
   */
  methods: {
    getQuestionList() {
      let list = [{
          value: '你好!我是解梦大师,接下来请你回答我4个问题,来帮助我更好的了解你的梦境!解梦过程中,请你尽量不要离开我和你的聊天室,避免我们的聊天内容丢失。如果过程中,你感觉有任何不适,可以提前结束我们的对话~下面,让我们开始吧!',
          type: 'left',
          answerType: 0 //回答问题 0 已回答 1 未回答 2已跳过
        },
        {
          value: '能否简单描述一下你做了什么梦?',
          type: 'left',
          example: '例如:梦到了吃火锅、梦到了老同学、梦到下了很大的雨等',
          answerType: 0 //回答问题 0 已回答 1 未回答 2已跳过
        },
        {
          value: '在这个梦里,你有什么特殊的情感体验吗?',
          type: 'left',
          example: '例如:感到开心、感到伤心、感到恐惧等',
          answerType: 1 //回答问题 0 已回答 1 未回答 2已跳过
        },
        {
          value: '有哪些和这个梦有关的事或人导致你做了这个梦吗?',
          type: 'left',
          example: '例如:白天看了很多关于飞机飞行的视频',
          answerType: 1 //回答问题 0 已回答 1 未回答 2已跳过
        },
        {
          value: '你解梦的目的是什么?',
          type: 'left',
          example: '例如:想要知道这个梦代表什么意思,是吉是凶,对我的生活会不会有什么影响',
          answerType: 0 //回答问题 0 已回答 1 未回答 2已跳过
        }
      ]
      this.setData({
        questionList: list
      })
    },
    //加载模板
    getStartModule() {
      let questionList = this.data.questionList
      const typeQuestions = questionList.slice(0, 2)
      // 计数器
      let count = 0
      // 用setInterval模拟插入
      const timer = setInterval(() => {
        let dreamList = this.data.dreamList
        // 获取一条新数据
        const newItem = typeQuestions[count]
        // 拼接到列表头部
        const list = [...dreamList, newItem]
        // 更新列表
        this.setData({
          round: this.data.round + 1,
          dreamList: list
        })
        count++
        // 如果数据插入完了,停止
        if (count >= typeQuestions.length) {
          this.setData({
            inputDisabled:false
          })
          clearInterval(timer)
        }
      }, 500) // 每隔0.5秒执行一次
     
    },
    //发送信息
    onSendMsg() {
      let inputValue = this.data.inputValue.trim()
      if(!inputValue){
        wx.showToast({
          title: '请输入内容',
          icon:'none'
        })
        return
      }

      this.setData({
        inputDisabled:true
      })
      let list = this.data.dreamList
      list[list.length - 1].answerType = 0 //已回答
      let cueType //1.梦境描述 2.情感体验 3.背景信息 4.解梦目的
      
      switch(this.data.round){
        case 2:
          cueType = 1;
          break;
        case 3:
          cueType = 2;
          break;
        case 4:
          cueType = 3;
          break;
        case 5:
          cueType = 4;
          break;
        default:
          cueType = 0;
          break;
      }
      list.push({
        type: 'right',
        value: inputValue,
        cueType,
      })
      this.setData({
        dreamList: list,
        inputValue: '',
      }, () => {
        if(this.data.round<5){
          //下一轮
          this.onReply()
        }else{
          //解梦
           this.composeMsg()
        }
      })
    },
    //开启下一轮问题
    onReply() {
      let round = this.data.round + 1
      let list = this.data.dreamList
      list.push(this.data.questionList[round - 1])
      setTimeout(() => {
        this.setData({
          dreamList: list,
          round,
        }, () => {
          this.setData({
            inputDisabled:false
          })
          this.autoScroll()
        })
      }, 500)
    },
    //跳过问题
    skipAnswer(){
      this.setData({
        inputDisabled:true
      })
      const list = this.data.dreamList
      list[list.length-1].answerType = 2//跳过
      this.setData({
        dreamList:list
      },()=>{
        this.onReply()
      })
    },
    onClearInput(){
      this.setData({
        inputValue:''
      })
    },
    //组合提示词
    composeMsg(){
      let list = this.data.dreamList
      let allSendMsg = list.filter(item => item.type === 'right')
      let tempMsg = '我梦见了'
      allSendMsg.map(item=>{
        if(item.cueType==1){
          tempMsg = tempMsg+`${item.value}。`
        }else if(item.cueType==2){
          tempMsg = tempMsg + `在这个梦里,我感受到了${item.value}。`
        }else if(item.cueType==3){
          tempMsg = tempMsg + `为了更好地理解我的梦境,我想提供一些背景信息。最近我经历了${item.value},这可能与我的梦境有关。`
        }else if(item.cueType==4){
          tempMsg = tempMsg + `通过解梦,我希望得到${item.value}。我希望解梦师可以帮助我解释梦境本身,或者提供对个人生活、情感或未来的指引。`
        }
      })
      this.triggerEvent('templateDream',tempMsg)
      this.closeDreamModule()
    },
    //关闭弹窗
    closeDreamModule() {
      this.triggerEvent('closeDreamModule')
    },

    //蒙层静止滚动
    preventTouchMove: function (e) {

    },
    //滚动条至最底部
    autoScroll() {
      var that = this
      let query = this.createSelectorQuery()
      query.select('.scroll-view-container').boundingClientRect(res => {
        //延迟滚动,否则不生效
        setTimeout(()=>{
          that.setData({
          scrollTop: res.height*100
          })
        },201)
        
      })
      query.exec(res => {})
    },

  }
})

WXML

<view style="--navigationBarHeight:{{navigationBarHeight}}px">
  <van-action-sheet z-index="99999" catchtouchmove="preventTouchMove" custom-class="sheet-custom-class" bind:close="closeDreamModule" show="{{ dreamModuleVisable }}">
  <view class="sheet-container">
    <view class="sheet-header" style="padding: 32rpx;">
      <text style="flex: 1;"></text>
      <text style="flex: 1;" class="sheet-title">解梦大师</text>
      <view style="flex: 1;" class="sheet-title-icon">
        <van-icon bind:click="closeDreamModule" custom-class="sheet-close-icon" name="cross" />
      </view>
    </view>
    <!-- 内容 -->
    <scroll-view class="scroll-box" scroll-y scroll-top="{{scrollTop}}" scroll-with-animation enhanced  bounces="{{false}}" enable-passive="{{true}}"  enable-flex  show-scrollbar="{{false}}" >
      <view class="scroll-view-container">
        <view wx:key="index" wx:for="{{dreamList}}">
          <van-transition custom-class="valueItem" show="{{ item  }}"  name="slide-up">
            <view wx:if="{{item.type=='right'}}" class="itemRight">
            <text user-select="{{true}}" selectable="{{true}}">{{item.value}}</text>
          </view>
          <view wx:if="{{item.type=='left'}}" class="itemLeft">
            <text user-select="{{true}}" selectable="{{true}}">{{item.value}}</text>
            <text class="example-text">{{item.example}}</text>
          </view>
         
          
          <view class="skip-answer-box">
            <van-button wx:if="{{item.answerType==1}}"  bind:click="skipAnswer" custom-class="skip-answer" round>跳过这个问题</van-button>
          </view>
          
          <view wx:if="{{item.answerType==2}}" class="skipped-answer">
            <view class="dot"></view>
            <text>已跳过该问题</text>
          </view>
        </van-transition>
        </view>
      </view>
      
    </scroll-view>
    <!-- 输入框 -->
    
    <view class="footer-box-module">
      <view class="group-style-module">
        <van-field custom-class="custom-input-style-module" 
        bind:focus="autoScroll"
        input-class="input-style-module" model:value="{{ inputValue }}" label="" clearable 
        bind:clear = "onClearInput"
        type="textarea" show-confirm-bar="{{false}}" disable-default-padding placeholder="请输入内容" autosize border="{{ false }}" />
        <van-button disabled="{{inputDisabled}}" bind:click="onSendMsg" custom-class="send-btn-style" round>{{round<5?'发送':'解梦'}}</van-button>
      </view>
    </view>
  </view>

</van-action-sheet>
</view>

WXSS

/* pages/chatGptDemo/components/dreamModule/dreamModule.wxss */
/* 动作面板 */
.sheet-custom-class {
  border-radius: 16rpx !important;
  max-height:calc(100% - var(--navigationBarHeight) - 126rpx)  !important;
  height: 100% !important;
  box-sizing: border-box;
  background: #fff !important;
  padding-bottom: 0rpx !important;  
}

.sheet-container{
  position: relative;
  flex-grow: 1;
  width: 100%;
  height: 100%;
 
}
.sheet-header{
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
  position: absolute;
  width: 100%;
  box-sizing: border-box;
}

.sheet-title {
  color: #333;
  text-align: center;
  font-family: PingFang SC;
  font-size: 36rpx;
  font-style: normal;
  font-weight: 600;
  line-height: 56rpx
}

.sheet-title-icon {
  text-align: right;
}
.sheet-close-icon{
  padding: 14rpx 0 14rpx 14rpx;
  font-size: 30rpx !important;
  color: #3333334D !important;
  font-weight: 600 !important;
}



.scroll-box {
  padding-top: 100rpx;
  width: 100%;
  height: 100%;
  flex-grow: 1;
  margin-bottom: 0;
  border-radius: 10rpx;
  overflow: hidden;
  box-sizing: border-box;
}
.scroll-view-container{
  box-sizing: border-box;
  padding-left: 32rpx;
  padding-right: 32rpx;
  padding-bottom: calc(env(safe-area-inset-bottom) + 300rpx);
}

/* 底部输入框 */
.footer-box-module {
  position: fixed;
  bottom: 0;
  width: 100%;
  padding-bottom: env(safe-area-inset-bottom);
  left: 50%;
  transform: translateX(-50%);
  z-index: 1;
  box-sizing: border-box;
  background: #fff;
  box-shadow: 0rpx -4rpx 20rpx 0rpx rgba(0, 0, 0, 0.05);
}

.group-style-module {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  background: #fff;
  padding: 24rpx;
  
}



.custom-input-style-module {
  display: flex;
  flex-direction: row;
  align-items: center;
  width: 530rpx !important;
  border-radius: 64rpx !important;
  padding: 16rpx 32rpx !important;
  background: #f7f8fa !important;
  box-sizing: border-box !important;
  overflow:visible !important;

}
.input-style-module {
  max-height: 234rpx;
 
}

.send-btn-style {
  padding: 0 !important;
  width: 120rpx !important;
  height: 64rpx !important;
  border: none !important;
  border-radius: 100rpx !important;
  background: linear-gradient(180deg, #C3CAFF 0%, #475DFF 100%) !important;
  box-shadow: 1rpx -1rpx 3rpx 0rpx rgba(255, 255, 255, 0.80) inset !important;
  color: #fff !important;
  font-size: 28rpx !important;
  font-weight: 600 !important;
  line-height: 44rpx !important;
}

/* 中间内容 */
.valueItem {
  display: grid;
  justify-items: center; 
  width: 100%;
}
.itemRight {
  justify-self: end; 
  margin-top: 32rpx;
  max-width: 654rpx;
  padding: 32rpx;
  font-size: 30rpx;
  line-height: 45rpx;
  color: #fff;
  background-color: #697AF6;
  border-radius: 64rpx 16rpx 64rpx 64rpx;
  box-sizing: border-box;
  word-wrap: break-word; 
}

.itemLeft {
  justify-self: start; 
  margin-top: 32rpx;
  max-width: 654rpx;
  padding: 32rpx;
  font-size: 30rpx;
  line-height: 45rpx;
  background-color:rgba(105, 122, 246, 0.10);
  color: #333;
  border-radius: 16rpx 64rpx 64rpx 64rpx;
  box-sizing: border-box;
  word-wrap: break-word; 
}

.example-text{
  color: #999;
}
.skip-answer-box{
  display: flex;
  flex-direction: row;
  justify-content: flex-end;
  width: 100%;
}
.skip-answer{
  width: 192rpx!important; 
height: 48rpx!important; 
  margin-top: 16rpx!important; 
  margin-right: 32rpx!important; 
  color: #697AF6 !important;
  padding: 2rpx 0 !important;
font-size: 24rpx !important;
font-weight: 600 !important; 
line-height: 44rpx !important;
border-radius: 32rpx !important;
border: 2rpx solid rgba(105, 122, 246, 0.40) !important;
background: rgba(255, 255, 255, 0.00)!important;
}


.skipped-answer{
  margin-top: 16rpx;
  margin-right: 32rpx;
  justify-self: end; 
  display: flex;
  flex-direction: row;
  align-items: center;
  color: #999;
  font-size: 24rpx;
  font-weight: 400;
  line-height: 34rpx; 
}

.skipped-answer .dot{
 
  width: 16rpx;
 height: 16rpx;
 background-color: rgba(105, 122, 246, 0.40);
 border-radius: 50%;
 margin-right: 8rpx;
}

JSON配置文件

{
    "component": true,
    "usingComponents": {
   
      "van-action-sheet": "../../../../miniprogram/miniprogram_npm/@vant/weapp/action-sheet/index",
      "van-field": "../../../../miniprogram/miniprogram_npm/@vant/weapp/field/index",
      "van-icon": "../../../../miniprogram/miniprogram_npm/@vant/weapp/icon/index",
      "van-button": "../../../../miniprogram/miniprogram_npm/@vant/weapp/button/index",
      "van-transition": "../../../../miniprogram/miniprogram_npm/@vant/weapp/transition/index"
    }
}

代码世界的构建师,现实生活的悠游者。