uniapp-vue3对话聊天实现模型流式输出
时间: 2025-07-04 07:40:36 浏览: 2
<think>我们正在使用UniApp和Vue3来实现一个对话聊天功能,并支持模型的流式输出。根据引用的资料,我们可以结合流式API和Markdown渲染来实现。引用[1]提到了在UniApp中使用FetchAPI实现流式响应,引用[3]提供了一个Vue3+TS(uniapp)的聊天页面实现,其中使用了scroll-view来实现自动滚动到底部。主要步骤:1.使用fetch或uni.request来请求支持流式输出的API(如SSE或分块传输)。2.处理流式数据,逐步更新聊天内容。3.使用Markdown渲染组件来展示AI返回的内容(可以引用使用第三方组件或自定义)。4.实现聊天界面,包括消息列表和输入框,并确保新消息出现时自动滚动到底部。注意:由于UniApp的环境,我们需要使用UniApp提供的API(如uni.request)或者使用H5的FetchAPI(在App端可能需要使用条件编译,因为App端不支持EventSource,但H5支持)。或者考虑使用WebSocket,但这里使用SSE(Server-SentEvents)在H5端较为简单。但是,引用[1]中提到的“FetchAPI”可能指的是H5的fetch。为了跨平台兼容性,我们可以这样处理:-在H5端使用EventSource(SSE)或fetch的ReadableStream。-在非H5端(如App和小程序)则可能需要使用WebSocket或者分块传输并自己处理流(非标准流式,因为uni.request不支持流式,但可以接收分块数据,通过不断请求或长连接实现)。然而,考虑到实现的简便性,我们可以先实现H5端的流式输出,使用SSE(EventSource)。但注意小程序不支持EventSource,因此可能需要条件编译。由于项目是UniApp,我们也可以使用uni.request来接收流式数据,但uni.request不支持流式响应。所以替代方案:-使用WebSocket:全端支持,但需要服务器支持WebSocket。-使用分块请求:服务器按块返回数据,客户端通过轮询或长连接方式接收,但这样实现复杂。不过,引用[1]提到使用FetchAPI实现流式响应,在H5端我们可以使用原生的fetch,并利用ReadableStream来处理流。在非H5端,我们可能需要使用WebSocket。鉴于这个示例主要是为了演示,我们将以H5端为例,使用fetch和ReadableStream。同时,为了兼容非H5端,我们可能会提供不同的实现(条件编译),但本例仅提供H5实现。另外,引用[3]中提供了如何自动滚动到底部的实现,使用scroll-view和scroll-into-view。下面我们编写一个示例代码:注意:本示例只包含关键部分,并假设在H5环境下运行。首先,在Vue3的setup语法中:1.定义数据:消息列表(messages),当前输入内容(inputValue)。2.定义方法:发送消息(sendMessage),处理流式响应(handleStreamResponse)。3.使用scroll-view并设置scroll-into-view为最后一条消息的id,以实现自动滚动。我们将使用`marked`(一个Markdown解析库)来解析AI返回的Markdown文本。注意:在uni-app中引入第三方库可能需要考虑包体积,或者使用HBuilderX的插件市场中的Markdown渲染组件。但为了简化,我们暂时不引入marked,而是将Markdown文本作为纯文本显示,之后再替换为Markdown渲染组件。步骤:1.安装事件流处理的库(可选,也可以自己处理),这里我们使用`eventsource`(但注意EventSource是浏览器自带,在H5可直接使用)或者使用fetch的ReadableStream。2.由于我们使用fetch的ReadableStream,因此我们将分块读取数据并逐步更新消息内容。3.为了滚动到底部,每次更新消息后都需要滚动到最新消息。具体代码:我们将创建一个Vue组件。</think>
以下是一个基于 UniApp 和 Vue3 的对话聊天功能实现示例,支持流式输出和自动滚动效果:
```vue
<script setup>
import { ref, reactive, nextTick } from 'vue'
// 消息数据结构
const messages = ref([
{ id: 1, text: '你好!有什么我可以帮助的?', isUser: false }
])
const inputText = ref('')
const loading = ref(false)
// 创建滚动视图引用
const scrollView = ref(null)
const lastMsgId = ref('msg0')
// 流式获取AI回复
async function getAIResponse() {
loading.value = true
// 添加占位消息
const newMsgId = `msg${messages.value.length}`
messages.value.push({
id: newMsgId,
text: '',
isUser: false
})
try {
// 使用Fetch API实现流式读取
const response = await fetch('https://your-api-endpoint.com/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: inputText.value })
})
if (!response.body) throw new Error('无响应体')
const reader = response.body.getReader()
let decoder = new TextDecoder()
// 流式数据处理
while (true) {
const { done, value } = await reader.read()
if (done) break
// 更新消息内容
const chunk = decoder.decode(value, { stream: true })
messages.value[messages.value.length - 1].text += chunk
// 触发滚动更新
lastMsgId.value = newMsgId
await nextTick()
}
} catch (error) {
console.error('请求失败:', error)
messages.value[messages.value.length - 1].text = '请求出错,请重试'
} finally {
loading.value = false
inputText.value = ''
}
}
// 发送消息
function sendMessage() {
if (!inputText.value.trim() || loading.value) return
messages.value.push({
id: `msg${messages.value.length}`,
text: inputText.value,
isUser: true
})
getAIResponse()
lastMsgId.value = `msg${messages.value.length - 1}`
}
</script>
<template>
<view class="container">
<!-- 消息列表 -->
<scroll-view
ref="scrollView"
scroll-y
:scroll-into-view="lastMsgId"
:scroll-with-animation="true"
class="message-container"
>
<view
v-for="(msg, index) in messages"
:key="msg.id"
:id="`msg${index}`"
class="message"
:class="{'user-msg': msg.isUser, 'ai-msg': !msg.isUser}"
>
<!-- 使用markdown渲染组件展示内容 -->
<markdown-view :content="msg.text" v-if="!msg.isUser"/>
<text v-else>{{ msg.text }}</text>
</view>
<view v-if="loading" class="loading-indicator">思考中...</view>
</scroll-view>
<!-- 输入区域 -->
<view class="input-area">
<input
v-model="inputText"
placeholder="输入消息..."
@confirm="sendMessage"
/>
<button @click="sendMessage" :disabled="loading">发送</button>
</view>
</view>
</template>
<style>
.message-container {
height: 80vh;
padding: 20rpx;
}
.user-msg {
text-align: right;
background-color: #e3f2fd;
margin: 10rpx 0;
padding: 15rpx;
border-radius: 10rpx;
}
.ai-msg {
text-align: left;
background-color: #f5f5f5;
margin: 10rpx 0;
padding: 15rpx;
border-radius: 10rpx;
}
.input-area {
display: flex;
padding: 20rpx;
border-top: 1px solid #eee;
}
input {
flex: 1;
margin-right: 20rpx;
border: 1px solid #ddd;
padding: 10rpx;
}
.loading-indicator {
padding: 20rpx;
text-align: center;
color: #888;
}
</style>
```
### 关键实现点解析:
1. **流式处理机制**:
- 使用 `fetch API + ReadableStream` 处理流式响应[^1]
- 通过 `TextDecoder` 实时解析数据块
- 使用 `while` 循环持续读取数据流
2. **自动滚动优化**:
- 利用 `scroll-view` 的 `scroll-into-view` 属性
- 动态更新 `lastMsgId` 跟踪最新消息[^3]
- 配合 `nextTick()` 确保DOM更新后执行滚动
3. **数据结构设计**:
- 区分用户消息和AI消息(使用 `isUser` 标识)
- 为每条消息创建唯一ID用于锚点定位
4. **响应式交互**:
- 添加 `loading` 状态防止重复提交
- 输入区域禁用控制与反馈
### 使用说明:
1. 安装 `markdown-it` 渲染组件(通过npm安装)
```bash
npm install markdown-it
```
2. 创建 `markdown-view` 组件用于内容渲染:
```vue
<script setup>
import { ref, computed } from 'vue'
import MarkdownIt from 'markdown-it'
const props = defineProps(['content'])
const md = new MarkdownIt()
const htmlContent = computed(() => md.render(props.content))
</script>
<template>
<rich-text :nodes="htmlContent" />
</template>
```
### 注意事项:
1. 小程序端需使用 `uni.request` 替代 `fetch`
2. iOS平台滚动动画需配置 `scroll-with-animation`
3. 实际API替换为您的后端服务地址
4. 复杂富文本建议使用 `uParse` 组件库
阅读全文
相关推荐















