安装marked
npm install marked --save
<template>
<view class="container">
<!-- 对话列表 -->
<scroll-view class="chat-list" scroll-y="true" :scroll-into-view="scrollToId">
<view v-for="(item, index) in messages" :key="index" :id="'msg' + index" class="message-wrap"
:class="[item.role === 'assistant' ? 'left' : 'right']">
<view class="message-box">
<rich-text class="message-content" :nodes="parseMarkdown(item.content)"
v-if="item.role === 'assistant'"></rich-text>
<text v-else class="message-content">{{ item.content }}</text>
</view>
</view>
</scroll-view>
<!-- 输入框 -->
<!-- <view class="input-area">
<input
class="input"
v-model="inputText"
placeholder="请输入问题"
@confirm="sendQuestion"
/>
<button class="send-btn" @click="sendQuestion">发送</button>
</view> -->
</view>
</template>
<script>
import marked from 'marked';
export default {
data() {
return {
messages: [], // 消息列表
// inputText: '请根据填写信息帮我制定一份个人专属营养品补充方案', // 输入内容
inputText: uni.getStorageSync('AIContent'),
scrollToId: '', // 自动滚动锚点
isResponding: false, // 是否正在响应中
ai_storage: '',
};
},
onLoad() {
const {
keys
} = uni.getStorageInfoSync()
if (!keys.includes('AIContent')) {
console.log("不存在")
uni.redirectTo({
url: '/activity/wenjuan/wenjuan',
fail: function(err) { // 失败回调函数
console.error('Failed to redirect:', err); // 在控制台输出错误信息
},
success() {
console.log("success")
}
});
return
}
this.sendQuestion()
this.ai()
},
methods: {
ai() {
const aiContent = uni.getStorageSync('AIContent');
if (aiContent !== null | aiContent !== undefined) {
uni.redirectTo({
url: 'activity/wenjuan/wenjuan'
})
}
// 若没有配置环境变量,请用百炼API Key将下行替换为:const apiKey = "sk-xxx";
const apiKey = "sk-111111111111111111111";
// 设置请求头
let that = this;
that.requestTask = uni.request({
url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
method: 'POST',
header: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
},
enableChunked: true, // 开启流传输
data: {
// 模型列表:https://help.aliyun.com/zh/model-studio/getting-started/models
"model": "qwen-plus", //deepseek-r1 qwen-plus qwen-max qwen-turbo deepseek-v3
"messages": [{
"role": "user",
"content": uni.getStorageSync('AIContent')
}],
"stream": true,
// "stream_options": [{
// "include_usage": true
// }]
},
success: (res) => {
// console.log(res.data.choices[0].message.content);
console.log('success Response:', res);
// console.log("结束")
},
fail: (err) => {
console.error('Request failed:', err);
}
})
that.requestTask.onChunkReceived((res) => {
const mockAnswer = this.decode(res);
const answerIndex = this.messages.length - 1;
for (let i = 0; i < mockAnswer.length; i++) {
new Promise(resolve => setTimeout(resolve, 30));
this.messages[answerIndex].content += mockAnswer[i];
if (i % 5 === 0) this.scrollToBottom();
}
});
},
decode(res) {
const text = this.decodeUTF8(res.data);
const lines = text.split('\n');
let result = '';
for (let line of lines) {
if (line.startsWith('data: ')) {
const jsonData = line.slice(6).trim();
if (jsonData === '[DONE]') return result;
const cleanedData = jsonData.replace(/[\u0000-\u001F\u007F-\u009F]/g, '');
try {
const parsedData = JSON.parse(cleanedData);
result += parsedData.choices[0].delta.content || '';
} catch (e) {
console.error('解析失败:', e);
}
}
}
return result;
},
decodeUTF8(data) {
const uint8Array = new Uint8Array(data);
let string = '';
for (let i = 0; i < uint8Array.length; i++) {
string += String.fromCharCode(uint8Array[i]);
}
return decodeURIComponent(escape(string));
},
// 发送问题
async sendQuestion() {
if (!this.inputText.trim() || this.isResponding) return;
// 添加用户消息
this.messages.push({
role: 'user',
content: this.inputText.trim()
});
const question = this.inputText;
this.inputText = '';
this.scrollToBottom();
// 添加助手消息
this.messages.push({
role: 'assistant',
content: '',
isStreaming: true
});
// 模拟流式响应
this.isResponding = true;
// await this.mockStreamResponse(question);
this.isResponding = false;
},
// 模拟流式响应
async mockStreamResponse(ai_answer) {
const mockAnswer = ai_answer
const answerIndex = this.messages.length - 1;
for (let i = 0; i < mockAnswer.length; i++) {
await new Promise(resolve => setTimeout(resolve, 30));
this.messages[answerIndex].content += mockAnswer[i];
if (i % 5 === 0) this.scrollToBottom();
}
},
// 解析Markdown
parseMarkdown(content) {
return marked.parse(content)
.replace(/<a href/g, '<a style="color: #007AFF; text-decoration: none" href')
.replace(/<code>/g, '<code style="background: #f5f5f5; padding: 2px 4px; border-radius: 4px">')
.replace(/<pre>/g,
'<pre style="background: #f5f5f5; padding: 10px; border-radius: 8px; overflow: auto">');
},
// 滚动到底部
scrollToBottom() {
const lastIndex = this.messages.length - 1;
this.scrollToId = 'msg' + lastIndex;
}
}
};
</script>
<style scoped>
.container {
flex: 1;
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
.chat-list {
flex: 1;
padding: 20rpx;
}
.message-wrap {
margin: 20rpx 0;
display: flex;
}
.message-wrap.left {
justify-content: flex-start;
}
.message-wrap.right {
justify-content: flex-end;
}
.message-box {
max-width: 70%;
padding: 20rpx;
border-radius: 12rpx;
background-color: #fff;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.message-wrap.right .message-box {
background-color: #007AFF;
margin-right: 30rpx;
padding-right: 30rpx;
}
.message-content {
font-size: 28rpx;
line-height: 1.5;
color: #333;
}
.message-wrap.right .message-content {
color: #fff;
}
.input-area {
padding: 20rpx;
background-color: #fff;
display: flex;
align-items: center;
border-top: 1rpx solid #eee;
}
.input {
flex: 1;
height: 80rpx;
padding: 0 20rpx;
background-color: #f5f5f5;
border-radius: 40rpx;
margin-right: 20rpx;
}
.send-btn {
width: 140rpx;
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
background-color: #007AFF;
color: #fff;
border-radius: 40rpx;
}
</style>