Files
lanecarford_front/src/views/asistant/index.vue
2025-10-28 11:33:20 +08:00

257 lines
6.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="asistant-container flex flex-column">
<div class="header">
<HeaderTitle hasSetting styleType="2" />
</div>
<div class="loading-container" v-if="isLoading">
<GenerateLoading />
</div>
<template v-else>
<div class="content flex-1" v-if="!isLoading">
<NoticeList
ref="noticeListRef"
:list="messageList"
:is-streaming="isStreaming"
:streaming-message="currentStreamingMessage"
/>
</div>
<div class="footer" v-if="!isLoading">
<InputArea @send-message="handleSendMessage" />
<div class="continue">
<button class="btn" @click="handleContinue">Continue</button>
</div>
</div>
</template>
</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'
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useUserInfoStore } from '@/stores'
import { streamChatAddress } from '@/api/workshop'
import { generateUUID } from '@/utils/tools'
const router = useRouter()
const userInfoStore = useUserInfoStore()
defineOptions({
name: 'asistant'
})
// 定义NoticeList组件引用类型
interface NoticeListRef {
simulateSendMessage: () => void
}
// 定义消息类型
interface ChatMessage {
sessionId: string | number
type: string
content: string
timestamp: string
id?: string
self?: boolean
}
const isLoading = ref<boolean>(false)
const noticeListRef = ref<NoticeListRef | null>(null)
const messageList = ref<ChatMessage[]>([])
// 流式消息相关状态
const isStreaming = ref<boolean>(false)
const currentStreamingMessage = ref<ChatMessage | null>(null)
onMounted(() => {})
onUnmounted(() => {
abort.abort()
})
const handleSendMessage = (message: string): void => {
console.log('发送:', message)
// 添加用户消息到列表
const userMessage: ChatMessage = {
id: generateUUID(),
type: 'text',
content: message,
timestamp: new Date().toISOString(),
sessionId: (userInfoStore.state.userInfo as any).id,
self: true
}
messageList.value.push(userMessage)
// 开始流式接收AI回复
handleFetchMessage(message)
}
const abort = new AbortController()
const handleFetchMessage = (message: string) => {
const params = {
message: message,
sessionId: (userInfoStore.state.userInfo as any).id,
gender: 'male'
}
// 创建AI消息对象
const aiMessage: ChatMessage = {
id: '',
type: 'text',
content: '',
timestamp: new Date().toISOString(),
sessionId: (userInfoStore.state.userInfo as any).id
}
// 添加到消息列表
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'
},
credentials: 'include'
})
.then(async (response) => {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
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
setTimeout(() => {
router.push('/workshop/selectStyle')
isLoading.value = false
}, 1000)
}
</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;
.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;
}
}
}
.loading-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
}
</style>