一 . 项目功能:
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;
}
}
}
}