在 UniApp 中实现流式 AI 聊天功能
介绍
在现代 Web 开发中,流式 API 响应能够显著提升用户体验,尤其是在与 AI 聊天接口进行交互时。本文将介绍如何在 UniApp 中使用 Fetch API 实现一个流式响应的 AI 聊天功能,包括实时更新聊天内容和滚动到底部的功能。
实现
用 Markdown 格式展示 AI 输出的内容
<!-- index.vue -->
<view v-else class="bot-message" :key="'bot-msg-' + index"><view class="avatar-container"><imageclass="message-avatar"src="/static/images/icon_robot.png"/></view><view class="message-content bot-bubble"><u-loading-iconv-if="isSendLoading &&index == chatMessages.length - 1 &&!getText(message.content)"></u-loading-icon><text v-else>// 用Markdown格式展示<AiMarkdownViewerclass="message-text":content="message.content"/><!-- {{ message.content || '服务异常,请重试' }} --></text></view>
</view><!-- AiMarkdownViewer.vue -->
// 使用showdown插件
<template><viewclass="markdown-container"v-html="parsedContent"ref="markdownContainer"></view>
</template><script>
import showdown from 'showdown'export default {name: 'AiMarkdownViewer',props: {content: {type: String,required: true,},},data() {return {converter: new showdown.Converter({tables: true,tasklists: true,simplifiedAutoLink: true,strikethrough: true,extensions: [this.tableEnhancement()],}),}},computed: {parsedContent() {if (this.content) {const processed = this.content.replace(/^```markdown\n([\s\S]*?)\n```$/gm, // 添加m标志处理多行(match, content) => {return content})return this.converter.makeHtml(processed)} else {return '微警灌云还在学习中,请您咨询当地派出所'}},},methods: {tableEnhancement() {return {type: 'output',filter: (text) => {// 为表格添加容器和样式类return text.replace(/<table>/g,'<div class="table-wrapper"><table class="data-table">').replace(/<\/table>/g, '</table></div>').replace(/<td>/g, '<td class="data-cell">').replace(/<th>/g, '<th class="header-cell">')},}},},
}
</script>
<style scoped lang="scss">
.markdown-container {margin: 0 auto;// padding: 20px;width: 100%;font-size: 26rpx;line-height: 1.7;color: #374151;white-space: normal;
}
/deep/ {.table-wrapper {overflow-x: auto;margin: 1em 0;border: 1px solid #ebeef5;border-radius: 4px;box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);.data-table {width: 100%;min-width: 600px;border-collapse: collapse;font-size: 28rpx;line-height: 1.5;.header-cell {background-color: #f8f9fa;color: #606266;font-weight: 600;padding: 12px 16px;border-bottom: 2px solid #ebeef5;white-space: nowrap;}.data-cell {padding: 12px 16px;border-bottom: 1px solid #ebeef5;color: #606266;min-width: 80px;&:empty::before {content: ' ';display: inline-block;width: 1px;}}tr:hover {background-color: #f5f7fa;}tr:nth-child(even) {background-color: #fafafa;}td:first-child,th:first-child {border-left: 1px solid #ebeef5;}td:last-child,th:last-child {border-right: 1px solid #ebeef5;}}}
}
/deep/ {h1,h2,h3,h4,h5,h6 {margin: 1em 0;font-weight: 600;color: #1f2937;&:not(h1) {border-bottom: 0.5px solid #e5e7eb;padding-bottom: 0.4em;}}h1 {font-size: 1.5em;}h2 {font-size: 1.4em;}h3 {font-size: 1.3em;}h4 {font-size: 1.2em;}h5 {font-size: 1.1em;}h6 {font-size: 1em;}// 列表样式ul,ol {margin: 0.8em 0;padding-left: 1.5em;li {margin: 0.4em 0;&::marker {color: #6b7280;}}}// 分割线hr {border: 0;height: 0.5px;background: #e5e7eb;margin: 1.5em 0;}// 代码块pre {background-color: #f9fafb;border-radius: 8px;padding: 1em;margin: 1.2em 0;border: 0.5px solid #e5e7eb;code {font-family: 'JetBrains Mono', Consolas, monospace;font-size: 13px;color: #374151;line-height: 1.6;}}// 行内代码code:not(pre code) {background-color: #f3f4f6;padding: 0.2em 0.4em;border-radius: 4px;font-size: 0.9em;color: #dc2626;}// 引用块blockquote {border-left: 3px solid #e5e7eb;margin: 1em 0;padding: 0.5em 1em;color: #4b5563;background-color: #f8fafc;border-radius: 0 4px 4px 0;}// 链接a {color: #3b82f6;text-decoration: none;&:hover {text-decoration: underline;}}
}
/deep/ {pre {code {display: block;width: 100%;overflow-x: auto;}}
}
</style>
我们需要使用 Fetch API 向 AI 聊天服务发送请求,并读取其流式响应。以下是实现的关键代码段。
async getAIChat(params, aiMessage) { // 使用 Fetch API 进行流式请求 const response = await fetch(`${config.aiBaseUrl}/api/v1/chat/completions`, { method: 'POST', headers: { Authorization: `Bearer ${config.apikey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(params), }); // 检测响应状态 if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || '请求失败'); } this.isSendLoading = false; // 更新加载状态 // 使用 response.body.getReader() 开始逐块读取流式响应,使用 UTF-8 编码解码数据。const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let done = false; // 用于流式读取数据 while (!done) { const { done: readerDone, value } = await reader.read(); done = readerDone; if (value) { // 逐块解码并拼接 const chunkText = decoder.decode(value, { stream: !done }); // 解析接收到的 JSON 数据 const lines = chunkText.split('\n'); // 按行分割 lines.forEach((line) => { if (line.startsWith('data:')) { const jsonString = line.substring(5).trim(); try { const jsonData = JSON.parse(jsonString); // 提取内容部分 const content = jsonData.choices?.[0]?.delta?.content || ''; if (content) { aiMessage.content += content; // 更新聊天内容 // 清除开头的换行符 if (aiMessage.content.startsWith('\n')) { aiMessage.content = aiMessage.content.slice(1); } // 刷新页面渲染 this.$forceUpdate(); // 确保视图更新 // 滚动到底部 this.$nextTick(() => { this.scrollToBottom(); // 调用滚动到底部的方法 }); } } catch (error) { console.error('Error parsing JSON:', error); } } }); } }
}