人工智能+GPT微信小程序聊天机器人(deepSeek)

一 . 项目功能:

1.智能问答(实时聊天+流畅打字机效果+自动滚动)

2.语音输入

3.停止生成(中断请求)、重新生成

4.复制功能、分页功能

二 . 效果展示:

三 . 技术分析:

1. RequestTask请求: 小程序中wx.request 默认不支持流式数据请求。但是设置enableChunked为true,可以在onChunkReceived中分块接收数据,实现响应内容实时输出。停止请求时,调用requestTask.abort()即可。

javascript 复制代码
data: {
   requestTask: null as WechatMiniprogram.RequestTask | null
},

//发送请求
requestFn(){
   var that = this;
   const requestTask = wx.request({
      url: `https://XXXX?message=${inputStr}`,
      responseType: "arraybuffer",
      method: "GET",
      enableChunked: true,
    });

    //接口响应成功,但此时无内容响应,相当于fetchEventSource的open阶段
    requestTask.onHeadersReceived(function (res: any) {});

    //持续内容响应,相当于fetchEventSource的onmessage阶段
    requestTask.onChunkReceived(function (r) {});

    that.setData({
      requestTask: requestTask,
    });
}

 //停止生成
stopFn() {
  if (this.data.requestTask) {
     this.data.requestTask.abort();
     this.setData({
        requestTask: null, 
     });
  }
}

**2. marked将markdown转为wxml:**接口响应的数据是markdown格式,需要借助marked第三方包转化为wxml,然后通过rich-text渲染。

typescript 复制代码
import { marked } from "../../../miniprogram_npm/marked/lib/marked.esm";

// 使用marked将markdown格式转化为Html格式
markdownToHtml(markdown: string) {
   return marked(markdown)
}

<rich-text nodes="{{item.message[item.answerIndex]}}"></rich-text>

3. wx.setClipboardData复制功能: 使用微信小程序官网提供的API setClipboardData实现。

php 复制代码
copyFn(copyText: string) {
    wx.setClipboardData({
      data: copyText,
      success: function () {
        wx.showToast({
          title: "复制成功",
          icon: "none",
        });
      },
      fail: function () {
        wx.showToast({
          title: "复制失败",
          icon: "none",
        });
      },
    });
  },

**4. recordRecoManager微信语音识别功能:**使用微信小程序官方提供的插件,使用步骤如下:

javascript 复制代码
1、在微信公众平台设置---第三方设置---插件管理添加同声传译插件

2、注册插件
在app.json中注册插件
  "plugins": {
    "WechatSI": {
       "version": "0.3.5",
       "provider": "wx069ba97219f66d99"
     }
   },

3、在页面中引入插件并获取语音识别管理器
//引入微信同声传译插件
const plugin = requirePlugin('WechatSI');
//获取全局唯一的语音识别管理器recordRecoManager
const manager = plugin.getRecordRecognitionManager();

4、识别语音 -- 初始化
 lifetimes: {
    ready() {
      let that = this;
      //识别结束事件
      manager.onStop = function (res) {
           that.setData({
              question: that.data.question + res.result || "",
           });
      };
      // 正常开始录音识别时会调用此事件
      manager.onStart = function (res) {};
      //识别错误事件
      manager.onError = function (res) {
        wx.showToast({
          title: "没有听清,请重试~",
          icon: "none",
        });
      };
    },
  },

5、绑定语音识别事件
speakFn() {
     if (!this.data.isSpeaking) {
        manager.start({ duration: 60000, lang: "zh_CN" });
     } else {
        manager.stop();
     }
}

5. 置顶/置底/自动滚动功能: 在scroll-view标签监听滚动事件bindscroll,当scrollHeight - scrollTop - offsetHeight < 20时,说明滚动到屏幕底部,显示向上滚动按钮,并通过设置scrollTop=0实现置顶功能;当scrollTop < 5时,说明到顶部,显示向下滚动按钮,并通过设置scrollTop=999999(设置一个足够大的值,确保滚动到底部)实现置底功能;同时在enableChunked持续接收消息中设置scrollTop=999999实现触底自动滚动。

javascript 复制代码
index.wxml页面:

<scroll-view id="scrollElement" scroll-y scroll-top="{{scrollTop}}" bindscroll="onScroll" 
   bindscrolltoupper="handleScrollToUpper" bindscrolltolower="handleScrollToLower">
</scroll-view>

index.ts页面:

 //滚动事件
onScroll(e: any) {
    const query = wx.createSelectorQuery();
    query.select("#scrollElement").boundingClientRect();
    query.exec((res) => {
      if (res && res[0]) {
        const offsetHeight = res[0].height; // 获取元素的高度(相当于 offsetHeight)
        if (e.detail.scrollHeight - e.detail.scrollTop - offsetHeight < 20) {
          this.setData({
            isBottom: true,
            scrollBottomShow: false,
          });
        } else {
          this.setData({
            isBottom: false,
            scrollBottomShow: true,
          });
        }
      }
    });
    if (e.detail.scrollTop < 5) {
      this.setData({
        scrollTopShow: false,
      });
    } else {
      this.setData({
        scrollTopShow: true,
      });
    }
  },

scrollTopFn() {
    this.setData({
      scrollTop: 0,
      scrollTopShow: false,
    });
},

scrollBottomFn() {
    this.setData({
      scrollTop: 999999,    // 设置一个足够大的值,确保滚动到底部
      scrollBottomShow: false, 
    });
},

requestFn(){
  ......
  requestTask.onChunkReceived(function (r) {
  ......
  that.setData({
      scrollTop: 999999, // 设置一个足够大的值,确保滚动到底部
   });
 })
}

四 . 疑难点及解决方案:

1.RequestTask.abort()中断请求时,在微信开发者工具中不生效,在真机中可正常使用,建议该功能直接在真机上调试;

2.requestTask.onChunkReceived有可能同时接收两条信息,所以需将响应数据转化为数组,然后再进行下一步处理:

javascript 复制代码
//判断是否json字符串
const isJSON = (str) => {
  if (typeof str == 'string') {
    try {
      var obj = JSON.parse(str);
      if (typeof obj == 'object' && obj) {
        return true;
      } else {
        return false;
      }
    } catch (e) {
      return false;
    }
  }

}
//request响应数据处理
export const arrayBufferToString = (buffer) => {
  const decoder = new TextDecoder("utf-8"); 
  let text = decoder.decode(buffer)
  let arr = text.split("data:")
  let newArr = []
  arr.forEach(item => {
    if (item && isJSON(item)) {
      newArr.push(JSON.parse(item))
    }
  })
  return newArr
}

3.在分包中引入marked第三方包报错,原因分析:有时即使按照官方文档操作仍然会出现找不到模块的情况,特别是涉及到不同类型的 JavaScript 模块标准 (ESM vs CJS) 。可考虑采用兼容性的写法导入模块,比如直接引用 dist 版本下的 UMD 格式的 js 文件而不是 src 中源码形式的 esm 或 cjs 文件2。解决方案:将import marked from "../../../miniprogram_npm/marked";改为import { marked } from "../../../miniprogram_npm/marked/lib/marked.esm";

4.使用marked转换markdown为wxml时,部分样式丢失,如table表格丢失行列线及背景色,可手动补充,代码如下:

css 复制代码
markdownToHtml(markdown: string) {
    return marked(markdown)
      .replaceAll(
        "<table>",
        '<table style="width: 100%; border-collapse: collapse; margin: 10px 0;">'
      )
      .replaceAll(
        "<th>",
        '<th style="border: 1px solid #ddd; padding: 8px; text-align: left; background-color: #f2f2f2; font-weight: bold;">'
      )
      .replaceAll(
        "<td>",
        '<td style="border: 1px solid #ddd; padding: 8px; text-align: left;">'
      );
 }

五 . 完整代码:

report.wxml:

xml 复制代码
<!--pages/shiye/subpages/report/report.wxml-->
<view class="page-container">
  <navigation-bar back="{{false}}" color="#000" background="#fff">
    <view slot="left" class="left-btn">
      <image class="back btn" src="../../../static/images/shiye/back.png" mode="" bind:tap="backFn" />
      <image class="menu btn" src="../../../static/images/shiye/menu1.png" mode="" bind:tap="setMenu" />
    </view>
    <view slot="center" class="navigation-center">{{titleStr}}</view>
  </navigation-bar>
  <scroll-view class="report-container" id="scrollElement" scroll-y scroll-top="{{scrollTop}}" bindscroll="onScroll" bindscrolltoupper="handleScrollToUpper" bindscrolltolower="handleScrollToLower">
    <view wx:for="{{chatList}}" wx:key="index" class="chat-list-container">
      <view class="{{item.type === 'user' ?'chat-item user':'chat-item ai'}}">
        <view wx:if="{{item.type === 'user'}}" class="question">
          <view class="message" wx:if="{{item.message && item.message.length > 0}}">{{ item.message[0] }}</view>
        </view>
        <view wx:else="{{item.type === 'ai'}}">
          <view wx:if="{{item.message.length > 0 ||item.isLoading}}" class="answer-container">
            <view class="answer">
              <view class="answer-message">
                <view wx:if="{{item.isLoading}}">
                  <rich-text nodes="{{currentHTML}}" class="markdown-body"></rich-text>
                </view>
                <view v-else>
                  <rich-text nodes="{{item.message[item.answerIndex]}}" class="markdown-body"></rich-text>
                  <!-- <view wx:if="{{item.reportUrl}}">
                    <button bind:tap="downloadReport" data-url="{{item.reportUrl}}" class="download-btn">下载尽调报告</button>
                  </view> -->
                </view>
              </view>
            </view>
            <view class="btn-container">
              <view class="page-container" wx:if="{{item.message.length > 1}}">
                <view class="pre-page" bind:tap="preFn" data-index="{{index}}" data-answerIndex="{{item.answerIndex}}">{{lt}}</view>
                <view> {{ item.answerIndex + 1 }} / {{ item.message.length}}</view>
                <view class="next-page" bind:tap="nextFn" data-index="{{index}}" data-answerIndex="{{item.answerIndex}}">{{gt}}</view>
              </view>
              <view class="tool-container" wx:if="{{!item.isLoading}}">
                <image wx:if="{{index === chatList.length - 1 && preInputValue}}" class="tool-img refresh" src="../../../static/images/shiye/refresh.png" alt="" bind:tap="regenerateFn" />
                <view class="line" wx:if="{{index === chatList.length - 1 && preInputValue}}" />
                <image class="tool-img copy" src="../../../static/images/shiye/copy.png" alt="" bind:tap="copyFn" data-index="{{index}}" data-answerIndex="{{item.answerIndex}}" />
              </view>
            </view>
          </view>
        </view>
      </view>
    </view>
    <view wx:if="{{loadingStatus}}" class="thinking">正在思考 !</view>
  </scroll-view>
  <view class="input-box">
    <view class="stop-container">
      <view>
        <view class="stop" bind:tap="stopFn" wx:if="{{(!loadingStatus)&&isLoading}}">
          <view style="padding-top: 3px">
            <image class="stop-img" src="../../../static/images/shiye/stop.png" alt="" />
          </view>
          <view>停止生成</view>
        </view>
      </view>
      <view class="preNext-btn" wx:if="{{!isLoading}}">
        <view class="scroll" bind:tap="scrollTopFn" wx:if="{{scrollTopShow}}">

          <image class="scroll-img" src="../../../static/images/shiye/scroll-top.png" alt="" />
        </view>
        <view class="scroll" bind:tap="scrollBottomFn" wx:if="{{scrollBottomShow}}">
          <image class="scroll-img" src="../../../static/images/shiye/scroll-bottom.png" alt="" />
        </view>
      </view>
    </view>
    <input-textarea isloading="{{isLoading}}" inputValue="{{inputValue}}" bind:send="sendFn" bind:onChanged="onChanged"></input-textarea>
    <view class="tip">内容由AI生成,仅供参考。更专业解答可<text class="service" bind:tap="gotoService">咨询客服</text></view>
  </view>
  <menu-popup visible="{{visible}}"></menu-popup>
</view>

report.ts:

php 复制代码
// pages/shiye/subpages/report/report.ts
import type { ChatList } from "../../../../typings/types/shiye/index";
import { arrayBufferToString } from "../../../utils/baseutf";
import { marked } from "../../../miniprogram_npm/marked/lib/marked.esm";
Page({
  /**
   * 页面的初始数据
   */
  data: {
    visible: false, //设置菜单弹框是否显示
    inputValue: "", //当前输入框输入的问题
    chatList: [] as ChatList,
    contentItems: "", ////当前正在输出的数据流(markdown格式)
    isRegenerate: false, //是否重新生成
    preInputValue: "", //上一次查询输入内容,用于重新生成使用
    isLoading: false, //节流loading
    loadingStatus: false, //加载状态显示loading,当loadingStatus为true,不用展示"正在思考"状态
    requestTask: null as WechatMiniprogram.RequestTask | null,
    scrollTopShow: false,
    scrollBottomShow: false,
    lt: "<",
    gt: ">",
    copyIndex: 0, //复制的索引(chatList中的index)
    copyText: "",
    currentHTML: "",
    scrollTop: 0,
    isRolling: false, //鼠标滚轮是否滚动
    isBottom: true, //滚动参数
    titleStr: "尽调报告",
  },
  //滚动事件
  onScroll(e: any) {
    const query = wx.createSelectorQuery();
    // 选择元素
    query.select("#scrollElement").boundingClientRect();

    // 执行查询
    query.exec((res) => {
      if (res && res[0]) {
        const offsetHeight = res[0].height; // 获取元素的高度(相当于 offsetHeight)
        if (e.detail.scrollHeight - e.detail.scrollTop - offsetHeight < 20) {
          this.setData({
            isBottom: true,
            scrollBottomShow: false,
          });
        } else {
          this.setData({
            isBottom: false,
            scrollBottomShow: true,
          });
        }
      }
    });
    if (e.detail.scrollTop < 5) {
      this.setData({
        scrollTopShow: false,
      });
    } else {
      this.setData({
        scrollTopShow: true,
      });
    }
  },
  //下载尽调报告
  downloadReport(e: any) {
    const url = e.target.dataset.url;
    // const url =
    //   "https://files.bczcc.com/prompt/尽调报告_江苏省建筑工程集团有限公司_55_20250208_170139.docx";
    wx.downloadFile({
      url: url,
      success: function (res) {
        if (res.statusCode === 200) {
          const filePath = res.tempFilePath;
          wx.openDocument({
            filePath: filePath,
            showMenu: true, //默认是false,为true的时候,右上角有三个点
            success: function () {
              console.log("打开文档成功");
            },
            fail: function (err) {
              console.log("打开文档失败", err);
            },
          });
        }
      },
      fail: function (err) {
        console.log("下载文件失败", err);
      },
    });
  },
  //复制
  copyFn(e: any) {
    const index = e.target.dataset.index;
    const answerIndex = e.target.dataset.answerindex;
    this.setData({
      copyIndex: index,
      copyText: this.data.chatList[index].downMessage[answerIndex],
    });
    wx.setClipboardData({
      data: this.data.copyText,
      success: function () {
        wx.showToast({
          title: "复制成功",
          icon: "none",
        });
      },
      fail: function () {
        wx.showToast({
          title: "复制失败",
          icon: "none",
        });
      },
    });
  },
  //重新生成
  regenerateFn() {
    this.setData({
      isRegenerate: true,
    });
    this.requestFn();
  },
  //上一页
  preFn(e: any) {
    const index = e.target.dataset.index;
    const answerIndex = e.target.dataset.answerindex;
    if (this.data.isLoading)
      return wx.showToast({
        title: "正在生成内容,请勿切换。",
        icon: "none",
      });
    const curAnswerIndex = "chatList[" + index + "].answerIndex";
    if (answerIndex === 0) {
      this.setData({
        [curAnswerIndex]: this.data.chatList[index].message.length - 1,
      });
    } else {
      this.setData({
        [curAnswerIndex]: this.data.chatList[index].answerIndex - 1,
      });
    }
  },
  //下一页
  nextFn(e: any) {
    const index = e.target.dataset.index;
    const answerIndex = e.target.dataset.answerindex;
    if (this.data.isLoading)
      return wx.showToast({
        title: "正在生成内容,请勿切换。",
        icon: "none",
      });
    const curAnswerIndex = "chatList[" + index + "].answerIndex";
    if (answerIndex === this.data.chatList[index].message.length - 1) {
      this.setData({
        [curAnswerIndex]: 0,
      });
    } else {
      this.setData({
        [curAnswerIndex]: this.data.chatList[index].answerIndex + 1,
      });
    }
  },
  onChanged(e: any) {
    this.setData({ inputValue: e.detail });
  },
  //停止生成
  stopFn() {
    if (this.data.requestTask) {
      this.data.requestTask.abort();
      this.setData({
        requestTask: null, // 清空 requestTask
      });
      const lastIndex = this.data.chatList.length - 1;
      const lastItem = this.data.chatList[lastIndex];

      if (this.data.isRegenerate) {
        //重新生成
        const messageLast =
          "chatList[" +
          lastIndex +
          "].message[" +
          (lastItem.message.length - 1 + "]");
        const downMessageLast =
          "chatList[" +
          lastIndex +
          "].downMessage[" +
          (lastItem.message.length - 1 + "]");
        const answerIndex = "chatList[" + lastIndex + "].answerIndex";
        this.setData({
          [messageLast]:
            this.data.currentHTML +
            "<div style='font-size:16px;margin-top:10px;margin-bottom:15px'>停止生成</div>",
          [downMessageLast]:
            this.data.contentItems + "\n" + "\n" + "停止生成" + "\n",
          [answerIndex]: lastItem.message.length - 1,
        });
      } else {
        //第一次生成
        const curLastIndex = this.data.chatList.length - 1;
        const lastMessage = "chatList[" + curLastIndex + "].message";
        const lastDownMessage = "chatList[" + curLastIndex + "].downMessage";
        const lastIsLoading = "chatList[" + curLastIndex + "].isLoading";
        this.setData({
          [lastMessage]: [
            this.data.currentHTML +
              "<div style='font-size:16px;margin-top:10px;margin-bottom:15px'>停止生成</div>",
          ],
          [lastDownMessage]: [
            this.data.contentItems + "\n" + "\n" + "停止生成" + "\n",
          ],
          [lastIsLoading]: false,
        });
      }
    }
  },
  //置顶
  handleScrollToUpper() {
    this.setData({
      scrollBottomShow: true,
    });
  },
  //置底
  handleScrollToLower() {
    this.setData({
      scrollTopShow: true,
    });
  },
  scrollTopFn() {
    this.setData({
      scrollTop: 0,
      scrollTopShow: false,
    });
  },
  scrollBottomFn() {
    this.setData({
      scrollTop: 999999,
      scrollBottomShow: false, // 设置一个足够大的值,确保滚动到底部
    });
  },
  //设置菜单
  setMenu() {
    this.setData({ visible: true });
  },
  //返回
  backFn() {
    wx.navigateBack();
  },
  //跳转人工客服
  gotoService() {
    wx.navigateTo({
      url: "/pages/mine/subpages/contactus/contactus",
    });
  },
  // 使用 marked
  markdownToHtml(markdown: string) {
    return marked(markdown)
      .replaceAll(
        "<table>",
        '<table style="width: 100%; border-collapse: collapse; margin: 10px 0;">'
      )
      .replaceAll(
        "<th>",
        '<th style="border: 1px solid #ddd; padding: 8px; text-align: left; background-color: #f2f2f2; font-weight: bold;">'
      )
      .replaceAll(
        "<td>",
        '<td style="border: 1px solid #ddd; padding: 8px; text-align: left;">'
      );
  },
  //重置操作,接口响应成功后重置数据
  resetFn() {
    const lastIsLoading =
      "chatList[" + (this.data.chatList.length - 1) + "].isLoading";
    this.setData({
      [lastIsLoading]: false,
      contentItems: "",
      isLoading: false,
      loadingStatus: false,
    });
  },
  requestFn() {
    var that = this;
    //先判断inputStr有没有值,isRegenerate表示是否重新生成
    const inputStr = that.data.isRegenerate
      ? that.data.preInputValue
      : that.data.inputValue;
    if (!inputStr)
      return wx.showToast({
        title: "请输入要查询的问题。",
        icon: "none",
      });
    if (that.data.isLoading)
      return wx.showToast({
        title: "正在生成内容,请稍后。",
        icon: "none",
      });
    that.setData({ isLoading: true });
    if (!that.data.isRegenerate) {
      let curChatList = that.data.chatList;
      //第一次生成
      that.setData({
        chatList: curChatList.concat([
          {
            type: "user",
            message: [inputStr],
            answerIndex: 0,
            isLoading: false,
            downMessage: [inputStr],
          },
        ]),
      });
      that.setData({
        scrollTop: 999999, // 设置一个足够大的值,确保滚动到底部
        titleStr: that.data.chatList[0].message[0],
      });
    }
    that.setData({ loadingStatus: true });
    const requestTask = wx.request({
      url: `https://xxxx?message=${inputStr}`,
      responseType: "arraybuffer",
      method: "GET",
      enableChunked: true,
      //接口响应结束后执行
      success: () => {
        that.resetFn();
      },
      //响应失败后执行
      fail: () => {
        that.resetFn();
      },
      //接口响应结束后执行,晚于success阶段
      complete: () => {
        that.resetFn();
      },
    });
    //接口响应成功,但此时无内容响应,相当于fetchEventSource的open阶段
    requestTask.onHeadersReceived(function (res: any) {
      if (res.statusCode !== 200)
        return wx.showToast({
          title: "服务器忙,请稍后再试。",
          icon: "none",
        });
      //判断是否重新生成,如果重新生成向最后chatList最后一项中的message中追加,如果不是重新生成向chatList中追加一条
      if (that.data.isRegenerate) {
        const lastIndex = that.data.chatList.length - 1;
        const lastMessage = "chatList[" + lastIndex + "].message";
        const lastDownMessage = "chatList[" + lastIndex + "].downMessage";
        const lastAnswerIndex = "chatList[" + lastIndex + "].answerIndex";
        that.setData({
          [lastMessage]: that.data.chatList[lastIndex].message.concat([""]),
          [lastDownMessage]: that.data.chatList[lastIndex].downMessage.concat([
            "",
          ]),
          [lastAnswerIndex]: that.data.chatList[lastIndex].answerIndex + 1,
        });
      } else {
        let curChatList = that.data.chatList.concat([
          {
            type: "ai",
            message: [],
            answerIndex: 0,
            isLoading: true,
            downMessage: [],
            reportUrl: "",
          },
        ]);
        that.setData({
          chatList: curChatList,
          preInputValue: that.data.inputValue,
        });
      }
      const lastIsLoading =
        "chatList[" + (that.data.chatList.length - 1) + "].isLoading";
      that.setData({
        inputValue: "",
        isLoading: true,
        // loadingStatus: false,
        [lastIsLoading]: true,
      });
    });
    //持续内容响应,相当于fetchEventSource的onmessage阶段
    requestTask.onChunkReceived(function (r) {
      that.setData({
        loadingStatus: false,
      });
      // onChunkReceived 有可能同时接收两条信息,所以需将响应数据转化为数组
      let arr = arrayBufferToString(r.data);
      arr.forEach((item) => {
        if (item) {
          const content = item.content;
          that.setData(
            {
              contentItems: that.data.contentItems + content,
            },
            () => {
              if (that.data.isBottom) {
                that.setData({
                  scrollTop: 999999, // 设置一个足够大的值,确保滚动到底部
                });
              }
              if (that.data.contentItems) {
                if (that.data.contentItems.includes("</sy_think>")) {
                  const list = that.data.contentItems.split("</sy_think>");
                  const thinkStr = `
                      <h4 style="margin-bottom:5px"> 师爷模型深度思考中...</h4>
                      <div style="color: gray;font-size:14px;padding-left:10px;margin-bottom:10px;line-height:25px;border-left:1px solid #e5e5e5">${list[0].replace(
                        "<sy_think>",
                        ""
                      )}</div>
                     `;
                  that.setData({
                    currentHTML: thinkStr + that.markdownToHtml(list[1]),
                  });
                } else {
                  const thinkStr = `
                      <h4 style="margin-bottom:5px"> 师爷模型深度思考中...</h4>
                      <div style="color: gray;font-size:14px;padding-left:10px;margin-bottom:10px;line-height:25px;border-left:1px solid #e5e5e5">${that.data.contentItems.replace(
                        "<sy_think>",
                        ""
                      )}</div>
                      `;
                  that.setData({
                    currentHTML: thinkStr,
                  });
                }
              } else {
                that.setData({
                  currentHTML: "",
                });
              }
            }
          );
          if (item.status === "end") {
            const lastIndex = that.data.chatList.length - 1;
            const lastItem = that.data.chatList[lastIndex];
            if (that.data.isRegenerate) {
              //重新生成
              const messageLast =
                "chatList[" +
                lastIndex +
                "].message[" +
                (lastItem.message.length - 1 + "]");
              const downMessageLast =
                "chatList[" +
                lastIndex +
                "].downMessage[" +
                (lastItem.message.length - 1 + "]");
              const answerIndex = "chatList[" + lastIndex + "].answerIndex";
              that.setData({
                [messageLast]: that.data.currentHTML,
                [downMessageLast]: that.data.contentItems,
                [answerIndex]: lastItem.message.length - 1,
              });
            } else {
              //第一次生成
              const curLastIndex = that.data.chatList.length - 1;
              const lastMessage = "chatList[" + curLastIndex + "].message";
              const lastDownMessage =
                "chatList[" + curLastIndex + "].downMessage";
              const lastIsLoading = "chatList[" + curLastIndex + "].isLoading";
              that.setData({
                [lastMessage]: [that.data.currentHTML],
                [lastDownMessage]: [that.data.contentItems],
                [lastIsLoading]: false,
              });
            }
            // if (item.reportUrl) {
            //   const lastReportUrl = "chatList[" + lastIndex + "].reportUrl";
            //   that.setData({
            //     [lastReportUrl]: item.reportUrl,
            //   });
            // }
            that.resetFn();
          }
        }
      });
    });
    that.setData({
      requestTask: requestTask,
    });
  },
  //发送问题
  sendFn() {
    this.setData({
      isRegenerate: false,
    });
    this.requestFn();
  },
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options: any) {
    if (options && options.question) {
      this.setData({
        inputValue: options.question,
      });
      this.requestFn();
    }
  },

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

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

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

  /**
   * 生命周期函数--监听页面卸载
   */
  onUnload() {},

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

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

  /**
   * 用户点击右上角分享
   */
  onShareAppMessage() {},
});

report.scss:

css 复制代码
/* pages/shiye/subpages/report/report.wxss */
.page-container {
  background-color: #fff;
  position: relative;
  .left-btn {
    width: 178rpx;
    height: 60rpx;
    border-radius: 60rpx;
    border: 2rpx solid #e3e6eb;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;

    .back {
      margin-right: 24rpx;
    }

    .btn {
      width: 40rpx;
      height: 40rpx;
    }
  }
  .navigation-center {
    width: calc(100vw - 400rpx);
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font-size: 34rpx;
    font-weight: 400;
  }

  .report-container {
    padding: 10rpx 0rpx;
    width: 100%;
    height: calc(100vh - 462rpx);
    box-sizing: border-box;
    box-sizing: border-box;
    .chat-list-container {
      margin: 0 30rpx;
      .chat-item {
        display: flex;

        .question {
          display: flex;
          margin-bottom: 40rpx;

          .message {
            padding: 20rpx 24rpx 16rpx 24rpx;
            border-radius: 32rpx 32rpx 0 32rpx;
            background-color: #4d80f0;
            font-size: 28rpx;
            color: #fff;
            font-weight: 400;
            line-height: 44rpx;
          }
        }

        .answer-container {
          margin-bottom: 90rpx;
          .answer {
            .answer-message {
              padding: 40rpx 20rpx;
              border-radius: 28rpx 28rpx 28rpx 0;
              min-width: 300rpx;
              background-color: #f6f6f6;
              .download-btn {
                color: #333;
                text-align: center;
                font-family: "PingFang SC";
                font-size: 28rpx;
                font-style: normal;
                font-weight: 400;
                line-height: 68rpx;
                height: 68rpx;
                margin-top: 40rpx;
                border-radius: 12rpx;
                border-color: transparent;
                .icon-img {
                  width: 34rpx;
                  height: 34rpx;
                  margin-right: 10rpx;
                }
                &:hover {
                  background: linear-gradient(
                    128deg,
                    #4672ff -1.27%,
                    #7daafc 109.62%
                  );
                  color: #fff;
                }
              }
            }
          }
          .btn-container {
            margin-top: 20rpx;
            display: flex;
            justify-content: flex-end;
            align-items: center;
            width: 100%;
            .page-container {
              color: #000;
              font-family: "PingFang SC";
              font-size: 28rpx;
              font-style: normal;
              font-weight: 400;
              margin-right: 20rpx;
              height: 80rpx;
              width: 150rpx;
              display: flex;
              flex-direction: row;
              justify-content: space-around;
              align-items: center;
              .pre-page {
                margin-right: 2rpx;
                cursor: pointer;
              }
              .next-page {
                margin-left: 2rpx;
                cursor: pointer;
              }
            }
            .tool-container {
              background-color: #f6f6f6;
              padding: 0 30rpx;
              height: 80rpx;
              border-radius: 40rpx;
              // min-width: 60rpx;
              text-align: center;
              display: flex;
              flex-direction: row;
              position: relative;
              .line {
                border-left: 2rpx solid #ddd;
                height: 38rpx;
                position: absolute;
                left: 98rpx;
                top: 23rpx;
              }
              .tool-img {
                width: 48rpx;
                height: 48rpx;
                margin-top: 16rpx;
                cursor: pointer;
              }
              .copy-acive {
                color: #5577ff;
              }
              .refresh {
                margin-right: 40rpx;
              }
            }
          }
        }
      }

      .user {
        flex-direction: row-reverse;
      }
    }

    .thinking {
      margin: 0 30rpx;
      max-width: 140rpx;
      background-color: #f6f6f6;
      color: #666;
      padding: 20rpx;
      font-family: "PingFang SC";
      font-size: 28rpx;
      font-style: normal;
      font-weight: 400;
      line-height: 48rpx;
      border-radius: 28rpx 28rpx 28rpx 0;
    }
    &::-webkit-scrollbar {
      display: none;
    }
  }

  .input-box {
    padding: 10rpx 30rpx 16rpx;
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    box-sizing: border-box;
    .stop-container {
      position: absolute;
      top: -20rpx;
      left: 30rpx;
      height: 47px;
      width: calc(100% - 60rpx);
      margin-bottom: 10px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      z-index: 100;
      .stop {
        width: 104px;
        height: 36px;
        border-radius: 18px;
        background-color: #f6f6f6;
        color: #5863ff;
        display: flex;
        justify-content: center;
        align-items: center;
        font-family: "PingFang SC";
        font-size: 14px;
        font-style: normal;
        font-weight: 400;
        .stop-img {
          width: 22px;
          height: 22px;
          margin-right: 5px;
        }
      }
      .preNext-btn {
        display: flex;
        .scroll {
          width: 32px;
          height: 32px;
          border-radius: 45px;
          background: #fff;
          margin-left: 10px;
          box-shadow: 0px 0px 8.857px 0px rgba(51, 117, 245, 0.25);
          .scroll-img {
            width: 16px;
            height: 16px;
            margin: 8px;
          }
        }
      }
    }
    .tip {
      margin: 24rpx 0 50rpx;
      color: #999;
      font-family: "PingFang SC";
      font-size: 20rpx;
      font-style: normal;
      font-weight: 400;
      line-height: 20rpx;
      text-align: center;

      .service {
        color: #3375f5;
      }
    }
  }
}
相关推荐
_r0bin_2 小时前
前端面试准备-7
开发语言·前端·javascript·fetch·跨域·class
IT瘾君2 小时前
JavaWeb:前端工程化-Vue
前端·javascript·vue.js
potender2 小时前
前端框架Vue
前端·vue.js·前端框架
站在风口的猪11083 小时前
《前端面试题:CSS预处理器(Sass、Less等)》
前端·css·html·less·css3·sass·html5
程序员的世界你不懂3 小时前
(9)-Fiddler抓包-Fiddler如何设置捕获Https会话
前端·https·fiddler
MoFe13 小时前
【.net core】天地图坐标转换为高德地图坐标(WGS84 坐标转 GCJ02 坐标)
java·前端·.netcore
去旅行、在路上4 小时前
chrome使用手机调试触屏web
前端·chrome
Aphasia3114 小时前
模式验证库——zod
前端·react.js
lexiangqicheng5 小时前
es6+和css3新增的特性有哪些
前端·es6·css3
拉不动的猪5 小时前
都25年啦,还有谁分不清双向绑定原理,响应式原理、v-model实现原理
前端·javascript·vue.js