feat: AI对话

This commit is contained in:
zhangyh
2025-10-28 11:33:20 +08:00
parent 2a1023aea0
commit 0ce0c41dac
12 changed files with 358 additions and 111 deletions

121
package-lock.json generated
View File

@@ -9,9 +9,9 @@
"version": "0.0.0",
"hasInstallScript": true,
"dependencies": {
"@microsoft/fetch-event-source": "^2.0.1",
"axios": "^1.3.6",
"gsap": "^3.13.0",
"markdown-it": "^14.1.0",
"normalize.css": "^8.0.1",
"pinia": "^2.0.32",
"pinia-persistedstate-plugin": "^0.1.0",
@@ -582,12 +582,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@microsoft/fetch-event-source": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==",
"license": "MIT"
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1481,8 +1475,7 @@
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/arr-diff": {
"version": "4.0.0",
@@ -4491,6 +4484,15 @@
"node": ">=10"
}
},
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
}
},
"node_modules/lint-staged": {
"version": "13.2.1",
"resolved": "https://registry.npmmirror.com/lint-staged/-/lint-staged-13.2.1.tgz",
@@ -4857,6 +4859,35 @@
"node": ">=0.10.0"
}
},
"node_modules/markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
"linkify-it": "^5.0.0",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/markdown-it/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -4872,6 +4903,12 @@
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"dev": true
},
"node_modules/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"license": "MIT"
},
"node_modules/memorystream": {
"version": "0.3.1",
"resolved": "https://registry.npmmirror.com/memorystream/-/memorystream-0.3.1.tgz",
@@ -6024,6 +6061,15 @@
"node": ">=6"
}
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/quansync": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
@@ -7707,6 +7753,12 @@
"node": ">=4.2.0"
}
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"license": "MIT"
},
"node_modules/ufo": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
@@ -8834,11 +8886,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"@microsoft/fetch-event-source": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -9518,8 +9565,7 @@
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"arr-diff": {
"version": "4.0.0",
@@ -11762,6 +11808,14 @@
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
"dev": true
},
"linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"requires": {
"uc.micro": "^2.0.0"
}
},
"lint-staged": {
"version": "13.2.1",
"resolved": "https://registry.npmmirror.com/lint-staged/-/lint-staged-13.2.1.tgz",
@@ -12047,6 +12101,26 @@
"object-visit": "^1.0.0"
}
},
"markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"requires": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
"linkify-it": "^5.0.0",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
},
"dependencies": {
"entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
}
}
},
"math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -12059,6 +12133,11 @@
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"dev": true
},
"mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="
},
"memorystream": {
"version": "0.3.1",
"resolved": "https://registry.npmmirror.com/memorystream/-/memorystream-0.3.1.tgz",
@@ -12918,6 +12997,11 @@
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
"dev": true
},
"punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="
},
"quansync": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
@@ -14218,6 +14302,11 @@
"integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
"devOptional": true
},
"uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
},
"ufo": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",

View File

@@ -13,9 +13,9 @@
"postinstall": "husky install"
},
"dependencies": {
"@microsoft/fetch-event-source": "^2.0.1",
"axios": "^1.3.6",
"gsap": "^3.13.0",
"markdown-it": "^14.1.0",
"normalize.css": "^8.0.1",
"pinia": "^2.0.32",
"pinia-persistedstate-plugin": "^0.1.0",

View File

@@ -100,6 +100,7 @@ export function getTryOnEffectStyleList(styleId: string | number) {
}
// 选择顾客
interface CustomerInfo {
name: string
email: string
@@ -111,3 +112,11 @@ export const customerCheckin = (data: CustomerInfo) => {
params: data,
})
}
// AI对话
interface AIConversation {
message: string
sessionId: string | number //用户ID
gender: 'male' | 'female' //性别
}
export const streamChatAddress = '/api/llm/streamChat'

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -6,10 +6,9 @@ import { removeLocal, setLocal } from '@/utils/local'
export const useUserInfoStore = defineStore('userInfo', () => {
const state = ref({
userInfo: {},
customerId: '',
token: '',
generateParams: {
stylistId: '',
stylist: '',
sex: ''
}
})
@@ -22,10 +21,6 @@ export const useUserInfoStore = defineStore('userInfo', () => {
state.value.userInfo = data
}
const setCustomerId = (data: string) => {
state.value.customerId = data
}
const setToken = (data: string) => {
state.value.token = data
setLocal(data, 'token')
@@ -40,9 +35,8 @@ export const useUserInfoStore = defineStore('userInfo', () => {
}
const resetGenerateParams = () => {
state.value.customerId = ''
state.value.generateParams = {
stylistId: '',
stylist: '',
sex: ''
}
}
@@ -52,7 +46,6 @@ export const useUserInfoStore = defineStore('userInfo', () => {
return new Promise((resolve) => {
state.value.token = ''
state.value.userInfo = {}
state.value.customerId = ''
removeLocal('token')
resetGenerateParams()
resolve('')
@@ -64,7 +57,6 @@ export const useUserInfoStore = defineStore('userInfo', () => {
getUserInfo,
setToken,
setUserInfo,
setCustomerId,
setGenerateParams,
getGenerateParams,
resetGenerateParams,

View File

@@ -67,6 +67,11 @@ service.interceptors.response.use(
* 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
*/
(response: any) => {
// 如果是llm/streamChat这样的流式接口,不走这样的处理
if (response.config.url.includes('llm/streamChat')) {
return response
}
// 已完成请求的删除请求中数组
removePending(response.config)
// 关闭loading
@@ -81,8 +86,8 @@ service.interceptors.response.use(
message: res.errMsg || res.message,
// type: 'fail',
duration: 5000,
position:'top',
icon:'none'
position: 'top',
icon: 'none'
})
return Promise.reject(new Error('error'))

View File

@@ -42,6 +42,24 @@ const getMousePosition = (e: any, bor: any) => {
}
return event
}
/**
* 生成UUID v4
* @returns 返回一个标准的UUID v4字符串格式xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
*/
export function generateUUID(): string {
// 优先使用现代浏览器的crypto.randomUUID()方法
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID()
}
// 备用方案手动生成UUID v4
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0
const v = c === 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
}
export {
getUniversalZoomLevel,
getMousePosition,

View File

@@ -1,17 +1,15 @@
<template>
<div
class="chat-message"
:class="{ 'user-message': isMyself, 'ai-message': !isMyself }"
>
<div class="chat-message" :class="{ 'user-message': isMyself, 'ai-message': !isMyself }">
<!-- AI消息显示头像 -->
<div v-if="!isMyself" class="chat-avatar">
<img :src="value.thumb" alt="AI Avatar" />
<img src="@/assets/images/asistant.png" alt="AI Avatar" />
</div>
<!-- 消息内容 -->
<div class="message-content">
<div class="message-text">
{{ value.content }}
<div class="message-text" :class="{ streaming: isStreaming }">
<div v-html="content"></div>
<span v-if="isStreaming" class="streaming-cursor">|</span>
</div>
<!-- <div v-if="!isMyself" class="message-actions flex">
<SvgIcon
@@ -28,13 +26,18 @@
<script setup lang="ts">
import { computed } from 'vue'
import MarkdownIt from 'markdown-it'
const md = new MarkdownIt()
// 定义消息类型
interface ChatMessage {
id: string
sessionId: string | number
type: string
content: string
userId: string
time: string
thumb: string
timestamp: string
id?: string
self?: boolean
}
// 定义操作项类型
@@ -46,12 +49,17 @@ interface ActionItem {
// 定义组件props类型
interface NoticeItemProps {
value: ChatMessage
isStreaming?: boolean
}
const props = defineProps<NoticeItemProps>()
const isMyself = computed(()=>{
return props.value.userId === '1'
const isMyself = computed(() => {
return props.value.self || false
})
const content = computed(() => {
return md.render(props.value.content)
})
const handleClickAction = (value: string): void => {
@@ -117,6 +125,7 @@ const actionList: ActionItem[] = [
.message-text {
color: #000;
border-radius: 0 2rem 2rem 2rem;
word-break: break-word;
}
}
}
@@ -127,12 +136,14 @@ const actionList: ActionItem[] = [
height: 7.4rem;
border-radius: 50%;
margin-right: 1.88rem;
border: 2px solid #000;
padding: 1.4rem;
img {
width: 100%;
height: 100%;
width: 4.9rem;
height: 4.9rem;
border-radius: 50%;
object-fit: cover;
// object-fit: cover;
}
}
@@ -150,4 +161,21 @@ const actionList: ActionItem[] = [
cursor: pointer;
}
}
.streaming-cursor {
animation: blink 1s infinite;
font-weight: bold;
color: #000;
}
@keyframes blink {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: 0;
}
}
</style>

View File

@@ -1,7 +1,10 @@
<template>
<div class="chat-list" ref="chatListRef">
<div class="chat-message-item" v-for="message in list" :key="message.id">
<NoticeItem :value="message" />
<NoticeItem
:value="message"
:is-streaming="isStreaming && streamingMessage?.id === message.id"
/>
</div>
</div>
</template>
@@ -21,6 +24,8 @@ interface ChatMessage {
}
const props = defineProps<{
list: ChatMessage[]
isStreaming?: boolean
streamingMessage?: ChatMessage | null
}>()
const emits = defineEmits(['send-message'])
@@ -47,6 +52,17 @@ watch(
{ deep: true }
)
// 监听流式消息内容变化,实时滚动
watch(
() => props.streamingMessage?.content,
async () => {
if (props.isStreaming) {
await nextTick()
scrollToBottom()
}
}
)
const handleSendMessage = (message: string): void => {
console.log('list发送消息:', message)
emits('send-message', message)

View File

@@ -8,7 +8,12 @@
</div>
<template v-else>
<div class="content flex-1" v-if="!isLoading">
<NoticeList ref="noticeListRef" :list="messageList" />
<NoticeList
ref="noticeListRef"
:list="messageList"
:is-streaming="isStreaming"
:streaming-message="currentStreamingMessage"
/>
</div>
<div class="footer" v-if="!isLoading">
<InputArea @send-message="handleSendMessage" />
@@ -26,9 +31,12 @@ import InputArea from './components/InputArea.vue'
import GenerateLoading from './components/GenerateLoading.vue'
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { useUserInfoStore } from '@/stores'
import { streamChatAddress } from '@/api/workshop'
import { generateUUID } from '@/utils/tools'
const router = useRouter()
const userInfoStore = useUserInfoStore()
defineOptions({
name: 'asistant'
@@ -40,47 +48,21 @@ interface NoticeListRef {
}
// 定义消息类型
interface ChatMessage {
id: string
sessionId: string | number
type: string
content: string
userId: string
time: string
thumb: string
timestamp: string
id?: string
self?: boolean
}
const isLoading = ref<boolean>(false)
const noticeListRef = ref<NoticeListRef | null>(null)
const messageList = ref<ChatMessage[]>([
{
id: '1',
content: 'I want a chic outfit for dinner.',
userId: '1',
time: '14:30',
thumb: ''
},
{
id: '2',
content:
"Hey there! A chic dinner outfit, I love that! To give you the perfect recommendations, tell me: is this a romantic date, business dinner, or celebration with friends? And what's your go-to style vibe: classic elegance or something with more edge?",
userId: '2',
time: '14:31',
thumb: 'https://files-dev.deercal.com/uploads/platforms/logo_code/669933e1b873e798.jpg'
},
{
id: '3',
content: "It's a romantic date, and I prefer classic elegance.",
userId: '1',
time: '14:32',
thumb: ''
},
{
id: '4',
content:
"Perfect! For a romantic dinner with classic elegance, I'd suggest a little black dress with delicate jewelry and elegant heels. Would you like me to show you some specific options?",
userId: '2',
time: '14:33',
thumb: 'https://files-dev.deercal.com/uploads/platforms/logo_code/669933e1b873e798.jpg'
}
])
const messageList = ref<ChatMessage[]>([])
// 流式消息相关状态
const isStreaming = ref<boolean>(false)
const currentStreamingMessage = ref<ChatMessage | null>(null)
onMounted(() => {})
@@ -90,30 +72,134 @@ onUnmounted(() => {
const handleSendMessage = (message: string): void => {
console.log('发送:', message)
handleFetchMessage()
// 添加用户消息到列表
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 = () => {
fetchEventSource('/api/sse', {
method: 'POST',
openWhenHidden: true,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
signal: abort.signal,
onopen: async (res) => {
console.log('连接建立', res)
},
onmessage: (event) => {
console.log('收到消息', event)
},
onerror: (error) => {
console.log('错误', error)
},
onclose: () => {
console.log('连接关闭')
}
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 = () => {

View File

@@ -72,37 +72,40 @@ const userInfoStore = useUserInfoStore()
const stylists = ref<any[]>([
{
id: 1,
value: 'mini',
name: 'Vera Lo',
description: 'Contemporary, Classic, Simple Silhouettes, Statement Pieces',
image: '/src/assets/images/female.png'
},
{
id: 2,
value: 'crystal',
name: 'Sarah Chen',
description: 'Modern, Edgy, Bold Colors, Street Style',
image: '/src/assets/images/male.png'
},
{
id: 3,
value: 'mini',
name: 'Emma Wilson',
description: 'Elegant, Feminine, Vintage Inspired, Soft Tones',
image: '/src/assets/images/female.png'
},
{
id: 4,
value: 'crystal',
name: 'Alex Johnson',
description: 'Minimalist, Professional, Neutral Palette, Clean Lines',
image: '/src/assets/images/male.png'
}
])
const currentChoosed=ref('')
const currentChoosed = ref(1)
const swiperRef = ref<any>(null)
const showVideo = ref<boolean>(false)
const videoRef = ref<any>(null)
const handleChangeCurrent=(index:number)=>{
const handleChangeCurrent = (index: number) => {
currentChoosed.value = stylists.value[index].id
}
@@ -121,7 +124,8 @@ const handleClickStylist = (item: any) => {
const handleContinue = () => {
const generateParams = userInfoStore.getGenerateParams()
generateParams.stylistId = currentChoosed.value
generateParams.stylist =
stylists.value.find((item) => item.id === currentChoosed.value)?.value || ''
userInfoStore.setGenerateParams(generateParams)
router.push('/stylist/sex')

View File

@@ -23,8 +23,8 @@ const router = useRouter()
const userInfoStore = useUserInfoStore()
const options = ref<any[]>([
{ label: 'Female', value: '1' },
{ label: 'Male', value: '0' }
{ label: 'Female', value: 'female' },
{ label: 'Male', value: 'male' }
])
const handleSelect = (value: string) => {