AI聊天交互页面(uniapp+vue3+zhipu)
一 简述
AI生成的聊天界面,使用流式输出
接口springboot提供
二 注意
1.流式地址,需要全地址,需要手动获取baseurl拼接
// 读取baseUrl
import config from '@/config.js';
const baseURL = config.baseUrl;
2.往响应式数组中,加响应式对象,实现流式输出
3.若流式请求方式app,小程序端报异步错误,可以参考如下链接,换请求方式
https://www.cnblogs.com/qxAndWorld/p/19342756
三 代码
<template> <view class="container"> <!-- 聊天头部 --> <view class="header"> <view class="header-content"> <text class="header-title">AI 助手</text> <text class="header-subtitle">随时为您解答疑问</text> </view> </view> <!-- 聊天消息区域 --> <scroll-view class="chat-container" scroll-y="true" :scroll-top="scrollTop" :scroll-with-animation="true" @scrolltoupper="onScrollToUpper" upper-threshold="50"> <view class="messages-wrapper"> <view v-for="(message, index) in messages" :key="index" class="message-item" :class="{ 'user-message': message.role === 'user', 'ai-message': message.role === 'ai' }"> <view class="avatar-container"> <view class="avatar" :class="{ 'user-avatar': message.role === 'user', 'ai-avatar': message.role === 'ai' }"> {{ message.role === 'user' ? 'U' : 'AI' }} </view> </view> <view class="message-content"> <view class="message-bubble" :class="{ 'user-bubble': message.role === 'user', 'ai-bubble': message.role === 'ai' }"> <text class="message-text">{{ message.content }}</text> </view> <text class="message-time">{{ formatTime(message.timestamp) }}</text> </view> </view> <!-- 加载状态 --> <view v-if="loading" class="loading-container"> <view class="typing-indicator"> <view class="dot"></view> <view class="dot"></view> <view class="dot"></view> </view> </view> </view> </scroll-view> <!-- 输入区域 --> <view class="input-container"> <view class="input-wrapper"> <textarea class="input-field" placeholder="请输入您的问题..." v-model="inputText" :auto-height="true" :maxlength="500" @confirm="sendMessage" @keyboardheightchange="onKeyboardChange" /> <button class="send-button" :disabled="!inputText.trim() || loading" @click="sendMessage"> <text class="send-text">发送</text> </button> </view> </view> </view> </template> <script setup> import { ref, reactive, nextTick } from 'vue'; import { fetchEventSource } from '@microsoft/fetch-event-source'; // 读取baseUrl import config from '@/config.js'; const baseURL = config.baseUrl; // 响应式数据 const inputText = ref('') const loading = ref(false) const scrollTop = ref(0) const keyboardHeight = ref(0) // 消息数组 const messages = reactive([{ role: 'ai', content: '您好!我是AI助手,很高兴为您服务。请问有什么可以帮助您的吗?', timestamp: new Date() }]) // 发送消息 const sendMessage = async () => { if (!inputText.value.trim() || loading.value) return const userMessage = { role: 'user', content: inputText.value.trim(), timestamp: new Date() } messages.push(userMessage) inputText.value = '' // 滚动到底部 await nextTick() scrollToBottom() // 显示加载状态 loading.value = true try { // 模拟AI回复(实际项目中这里应该是API调用) setTimeout(() => { const aiResponse = reactive({ role: 'ai', content: "", timestamp: new Date() }) messages.push(aiResponse) // 获取AI回复(模拟) fetchEventSource(baseURL + '/ai/callChatAiflux', { method: 'POST', headers: { 'Content-Type': 'application/json', // 如果需要认证,可加: // 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ "text": userMessage.content }), // 收到新事件 onmessage(event) { if (event.data) { // const data = JSON.parse(event.data) // 假设后端每次返回 { content: "部分文本" } // airetmsg.value = airetmsg.value + event.data; aiResponse.content = aiResponse.content + event.data; // console.log(aiResponse.content); // console.log(event.data) } }, // 连接打开 onopen(response) { if (response.ok) { console.log('SSE 连接已建立') } else { throw new Error(`连接失败: ${response.status}`) } }, // 错误处理 onerror(err) { console.error('SSE 错误:', err) // 可选:自动重连逻辑(默认会重连) throw err; // 抛出错误将停止重连 }, // 流结束(服务器关闭连接) onclose() { console.log('SSE 连接已关闭') } }) loading.value = false // 再次滚动到底部 nextTick(() => { scrollToBottom() }) }, 1500) } catch (error) { console.error('发送消息失败:', error) loading.value = false // 添加错误提示 messages.push({ role: 'ai', content: '抱歉,网络出现问题,请稍后重试。', timestamp: new Date() }) } } // 格式化时间 function formatTime(date) { const now = new Date() const diff = now - date const minutes = Math.floor(diff / 60000) if (minutes < 1) return '刚刚' if (minutes < 60) return `${minutes}分钟前` const hours = Math.floor(minutes / 60) if (hours < 24) return `${hours}小时前` return `${Math.floor(hours / 24)}天前` } // 滚动到底部 function scrollToBottom() { nextTick(() => { scrollTop.value = 999999 }) } // 监听键盘变化 function onKeyboardChange(e) { keyboardHeight.value = e.detail.height } // 上拉加载更多(模拟) function onScrollToUpper() { // 这里可以实现加载历史消息的逻辑 console.log('滚动到顶部') } </script> <style lang="scss"> .container { height: 100vh; display: flex; flex-direction: column; background-color: #f5f5f5; } .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40rpx 30rpx; color: white; .header-content { display: flex; flex-direction: column; align-items: center; } .header-title { font-size: 36rpx; font-weight: bold; margin-bottom: 10rpx; } .header-subtitle { font-size: 28rpx; opacity: 0.9; } } .chat-container { flex: 1; padding: 30rpx; box-sizing: border-box; .messages-wrapper { min-height: 100%; } } .message-item { display: flex; margin-bottom: 40rpx; animation: slideIn 0.3s ease-out; &.user-message { flex-direction: row-reverse; .message-content { align-items: flex-end; } } &.ai-message { .message-content { align-items: flex-start; } } } @keyframes slideIn { from { opacity: 0; transform: translateY(20rpx); } to { opacity: 1; transform: translateY(0); } } .avatar-container { display: flex; align-items: flex-start; margin-right: 20rpx; .user-message & { margin-right: 0; margin-left: 20rpx; } } .avatar { width: 80rpx; height: 80rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; font-weight: bold; color: white; &.user-avatar { background: linear-gradient(135deg, #ff6b6b, #ee5a24); } &.ai-avatar { background: linear-gradient(135deg, #4ecdc4, #44a08d); } } .message-content { display: flex; flex-direction: column; flex: 1; } .message-bubble { max-width: 70%; padding: 25rpx 30rpx; border-radius: 30rpx; position: relative; margin-bottom: 15rpx; &::after { content: ''; position: absolute; width: 0; height: 0; border-style: solid; } &.user-bubble { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; margin-left: auto; border-bottom-right-radius: 10rpx; &::after { top: 20rpx; right: -20rpx; border-width: 10rpx 0 10rpx 20rpx; border-color: transparent transparent transparent #667eea; } } &.ai-bubble { background: white; color: #333; border-bottom-left-radius: 10rpx; &::after { top: 20rpx; left: -20rpx; border-width: 10rpx 20rpx 10rpx 0; border-color: transparent #ffffff transparent transparent; } } } .message-text { line-height: 1.6; font-size: 30rpx; word-wrap: break-word; word-break: break-all; } .message-time { font-size: 24rpx; color: #999; text-align: right; } .loading-container { display: flex; justify-content: center; padding: 30rpx 0; } .typing-indicator { display: flex; align-items: center; padding: 20rpx 30rpx; background: white; border-radius: 30rpx; border-bottom-left-radius: 10rpx; .dot { width: 12rpx; height: 12rpx; background: #999; border-radius: 50%; margin: 0 6rpx; animation: bounce 1.4s infinite both; &:nth-child(2) { animation-delay: 0.2s; } &:nth-child(3) { animation-delay: 0.4s; } } } @keyframes bounce { 0%, 80%, 100% { transform: scale(0.8); opacity: 0.6; } 40% { transform: scale(1.2); opacity: 1; } } .input-container { padding: 30rpx; background: white; border-top: 1rpx solid #eee; } .input-wrapper { display: flex; align-items: flex-end; gap: 20rpx; } .input-field { flex: 1; min-height: 80rpx; max-height: 200rpx; padding: 20rpx; border: 2rpx solid #e0e0e0; border-radius: 20rpx; font-size: 30rpx; background: #fafafa; } .send-button { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 20rpx; padding: 25rpx 40rpx; font-size: 30rpx; font-weight: bold; &:not(:disabled) { opacity: 1; } &:disabled { opacity: 0.6; } } .send-text { color: white; } /* 在不同平台上的特殊样式 */ /* #ifdef MP-WEIXIN */ .container { padding-bottom: env(safe-area-inset-bottom); } /* #endif */ /* #ifdef H5 */ .container { padding-bottom: 0; } /* #endif */ </style>

浙公网安备 33010602011771号