Files
lanecarford_front/src/views/asistant/index.vue

326 lines
8.6 KiB
Vue
Raw Normal View History

2025-10-14 16:42:09 +08:00
<template>
<div class="asistant-container flex flex-column">
<div class="header">
2025-11-17 11:24:46 +08:00
<HeaderTitle hasSetting styleType="3" />
2025-10-14 16:42:09 +08:00
</div>
2025-10-15 15:46:01 +08:00
<div class="loading-container" v-if="isLoading">
<GenerateLoading />
2025-10-14 16:42:09 +08:00
</div>
2025-10-15 15:46:01 +08:00
<template v-else>
<div class="content flex-1" v-if="!isLoading">
2025-10-28 11:33:20 +08:00
<NoticeList
ref="noticeListRef"
:list="messageList"
:is-streaming="isStreaming"
:streaming-message="currentStreamingMessage"
/>
2025-10-15 15:46:01 +08:00
</div>
<div class="footer" v-if="!isLoading">
<InputArea @send-message="handleSendMessage" />
<div class="continue">
<button class="btn" @click="handleContinue">Continue</button>
2025-10-15 15:46:01 +08:00
</div>
</div>
</template>
2025-10-14 16:42:09 +08:00
</div>
</template>
<script setup lang="ts">
import HeaderTitle from '@/components/HeaderTitle.vue'
import NoticeList from './components/NoticeList.vue'
import InputArea from './components/InputArea.vue'
import GenerateLoading from './components/GenerateLoading.vue'
2025-11-03 15:52:20 +08:00
import { ref, onMounted, onUnmounted, onActivated } from 'vue'
2025-11-17 17:38:57 +08:00
import { useRouter, useRoute } from 'vue-router'
2025-10-30 15:00:44 +08:00
import { useUserInfoStore, useGenerateStore } from '@/stores'
2025-10-28 11:33:20 +08:00
import { streamChatAddress } from '@/api/workshop'
import { generateUUID } from '@/utils/tools'
2025-10-30 13:16:50 +08:00
import { showToast } from 'vant'
2025-10-24 17:37:15 +08:00
const router = useRouter()
2025-11-17 17:38:57 +08:00
const route = useRoute()
2025-10-30 14:22:24 +08:00
const generateStore = useGenerateStore()
2025-10-28 11:33:20 +08:00
const userInfoStore = useUserInfoStore()
2025-10-14 16:42:09 +08:00
2025-10-21 11:21:15 +08:00
defineOptions({
2025-10-24 17:37:15 +08:00
name: 'asistant'
2025-10-21 11:21:15 +08:00
})
2025-10-14 16:42:09 +08:00
// 定义NoticeList组件引用类型
interface NoticeListRef {
simulateSendMessage: () => void
2025-10-30 15:00:44 +08:00
scrollToBottom: () => void
2025-10-14 16:42:09 +08:00
}
// 定义消息类型
interface ChatMessage {
2025-10-28 11:33:20 +08:00
sessionId: string | number
type: string
content: string
2025-10-28 11:33:20 +08:00
timestamp: string
id?: string
self?: boolean
}
2025-10-14 16:42:09 +08:00
const isLoading = ref<boolean>(false)
const noticeListRef = ref<NoticeListRef | null>(null)
2025-10-28 11:33:20 +08:00
const messageList = ref<ChatMessage[]>([])
// 流式消息相关状态
const isStreaming = ref<boolean>(false)
const currentStreamingMessage = ref<ChatMessage | null>(null)
2025-11-03 15:52:20 +08:00
const sessionId = ref<string>('')
2025-11-17 17:38:57 +08:00
const sendPrefilledMessage = () => {
const { message, ...restQuery } = route.query
if (typeof message === 'string' && message.trim()) {
handleSendMessage(message)
router.replace({
path: route.path,
query: restQuery
})
}
}
2025-11-03 15:52:20 +08:00
onMounted(() => {
sessionId.value = Math.floor(Date.now() / 1000).toString()
generateStore.setSessionId(sessionId.value)
})
2025-10-14 16:42:09 +08:00
2025-10-30 15:00:44 +08:00
onActivated(() => {
sendPrefilledMessage()
2025-10-30 15:00:44 +08:00
noticeListRef.value?.scrollToBottom()
})
2025-10-14 16:42:09 +08:00
2025-10-24 17:37:15 +08:00
onUnmounted(() => {
abort.abort()
2025-10-21 11:21:15 +08:00
})
2025-10-14 16:42:09 +08:00
const handleSendMessage = (message: string): void => {
2025-10-24 17:37:15 +08:00
console.log('发送:', message)
2025-10-28 11:33:20 +08:00
// 添加用户消息到列表
const userMessage: ChatMessage = {
id: generateUUID(),
type: 'text',
content: message,
timestamp: new Date().toISOString(),
2025-11-03 15:52:20 +08:00
sessionId: sessionId.value,
2025-10-28 11:33:20 +08:00
self: true
}
messageList.value.push(userMessage)
// 开始流式接收AI回复
handleFetchMessage(message)
2025-10-24 17:37:15 +08:00
}
const abort = new AbortController()
2025-10-28 11:33:20 +08:00
const handleFetchMessage = (message: string) => {
const params = {
message: message,
2025-11-03 15:52:20 +08:00
sessionId: sessionId.value,
2025-10-28 11:33:20 +08:00
gender: 'male'
}
// 创建AI消息对象
const aiMessage: ChatMessage = {
2025-10-30 15:19:46 +08:00
id: generateUUID(),
2025-10-28 11:33:20 +08:00
type: 'text',
content: '',
timestamp: new Date().toISOString(),
2025-11-03 15:52:20 +08:00
sessionId: sessionId.value
2025-10-28 11:33:20 +08:00
}
// 添加到消息列表
isStreaming.value = true
messageList.value.push(aiMessage)
currentStreamingMessage.value = aiMessage
// 直接使用 fetch 进行流式请求
const token = userInfoStore.state.token
const baseURL = import.meta.env.MODE === 'development' ? '' : import.meta.env.VITE_APP_URL
// 构建查询参数
const queryParams = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
queryParams.append(key, String(value))
})
const url = `${baseURL}${streamChatAddress}?${queryParams.toString()}`
fetch(url, {
method: 'GET',
headers: {
Authorization: token,
'Content-Type': 'application/json'
2025-10-24 17:37:15 +08:00
},
2025-10-28 11:33:20 +08:00
credentials: 'include'
})
2025-10-28 11:33:20 +08:00
.then(async (response) => {
// 检查响应内容类型,判断是否为流式响应
const contentType = response.headers.get('content-type') || ''
2025-11-03 15:52:20 +08:00
const isStreamResponse =
contentType.includes('text/event-stream') || contentType.includes('stream')
2025-10-30 13:16:50 +08:00
if (!response.ok) {
// 非流式错误响应,使用 text() 读取错误信息
const errorText = await response.text()
console.error('请求错误:', errorText)
2025-10-30 13:16:50 +08:00
showToast({
message: `failed to fetch: ${response.status}`,
position: 'top',
icon: 'none'
})
throw new Error(`发起对话错误--- ${response.status}: ${errorText}`)
}
// 不是流式响应,使用 text()读取错误信息
if (!isStreamResponse) {
const text = await response.text()
2025-11-03 15:52:20 +08:00
try {
const errorData = JSON.parse(text)
if (errorData.message || errorData.error) {
showToast({
message: errorData.message || errorData.error || '请求失败',
position: 'top',
icon: 'none'
})
}
} catch (e) {
// 如果不是 JSON直接显示文本内容
showToast({
message: text || '请求失败',
position: 'top',
icon: 'none'
})
throw new Error(text || '请求失败')
}
2025-11-03 15:52:20 +08:00
isStreaming.value = false
currentStreamingMessage.value = null
return
2025-10-30 13:16:50 +08:00
}
2025-10-28 11:33:20 +08:00
// 流式响应处理
2025-10-28 11:33:20 +08:00
let contentBody = ''
let buffer = ''
const reader = response.body?.getReader()
if (!reader) throw new Error('无法获取流读取器')
const decoder = new TextDecoder()
try {
let flag = true
while (flag) {
const { done, value } = await reader.read()
if (done) {
console.log('传输结束 end---', contentBody)
isStreaming.value = false
currentStreamingMessage.value = null
flag = false
break
}
buffer += decoder.decode(value, { stream: true })
// 优先按空行拆分事件块SSE标准
let events = buffer.split(/\n\n/)
buffer = events.pop() // 保留不完整块
for (let event of events) {
if (!event.trim()) continue
// 过滤掉 id: 等字段,只取 data:
const dataLines = event
.split(/\n/)
.filter((line) => line.startsWith('data:'))
.map((line) => line.replace(/^data:\s*/, '').trim())
if (dataLines.length === 0) continue
const jsonText = dataLines.join('\n')
try {
const jsonData = JSON.parse(jsonText)
if (jsonData.content && jsonData.content.length > 0 && jsonData.type !== 'end') {
contentBody += jsonData.content
currentStreamingMessage.value.content = contentBody
}
} catch (e) {
// JSON 不完整:保留到下一次循环
if (!jsonText.trim().endsWith('}')) {
buffer = 'data: ' + jsonText // 重新放回缓存
continue
} else {
console.warn('⚠️ JSON 格式错误,跳过:', jsonText)
}
}
}
}
} catch (error) {
console.error('流式传输错误:', error)
isStreaming.value = false
currentStreamingMessage.value = null
} finally {
reader.releaseLock()
}
})
.catch((error) => {
console.error('fetch请求失败:', error)
isStreaming.value = false
currentStreamingMessage.value = null
})
}
const handleContinue = () => {
// router.push('/workshop/selectStyle')
// 模拟接口之后再跳转
isLoading.value = true
2025-10-30 14:22:24 +08:00
generateStore.clearProductData()
2025-10-14 16:42:09 +08:00
setTimeout(() => {
router.push('/workshop/selectStyle')
2025-10-21 11:21:15 +08:00
isLoading.value = false
}, 1000)
2025-10-14 16:42:09 +08:00
}
</script>
<style lang="less" scoped>
.asistant-container {
height: 100vh;
overflow: hidden;
}
.header {
flex-shrink: 0;
}
.content {
overflow: hidden;
display: flex;
flex-direction: column;
}
.footer {
flex-shrink: 0;
2025-10-15 15:46:01 +08:00
2025-10-14 16:42:09 +08:00
.continue {
font-family: 'satoshiRegular';
font-size: 3.6rem;
color: #fff;
text-align: right;
padding: 2.6rem 4.5rem;
.btn {
border-radius: 7px;
background-color: #000;
width: 24.6rem;
height: 5.9rem;
}
}
}
2025-10-15 15:46:01 +08:00
.loading-container {
2025-10-14 16:42:09 +08:00
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
}
</style>