From 8100459c4e7a70f410d4e098c0e2a9a3089c3a87 Mon Sep 17 00:00:00 2001 From: zhangyahui Date: Fri, 27 Feb 2026 13:44:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20dressfor=E9=A1=B5=E9=9D=A2&=E7=94=9F?= =?UTF-8?q?=E6=88=90outfit=E9=80=BB=E8=BE=91=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useStreamChat.ts | 112 ++++++++++++++ src/views/Workshop/selectStyle/index.vue | 32 +++- src/views/stylist/dressfor.vue | 188 ++++++++++++++--------- vite.config.ts | 2 +- 4 files changed, 258 insertions(+), 76 deletions(-) create mode 100644 src/hooks/useStreamChat.ts diff --git a/src/hooks/useStreamChat.ts b/src/hooks/useStreamChat.ts new file mode 100644 index 0000000..e59b061 --- /dev/null +++ b/src/hooks/useStreamChat.ts @@ -0,0 +1,112 @@ +import { ref } from 'vue' +import { showToast } from 'vant' +import { streamChatAddress } from '@/api/workshop' +import { useUserInfoStore } from '@/stores' + +/** + * 流式对话 Hook + * @param onSuccess - 成功时的回调(流式响应时调用) + * @returns { fetchMessage, isGenerating } + */ +export function useStreamChat(onSuccess?: () => void) { + const userInfoStore = useUserInfoStore() + const isGenerating = ref(false) + + const fetchMessage = (message: string, sessionId: string): Promise => { + isGenerating.value = true + + const params = { + message, + sessionId, + gender: userInfoStore.state.generateParams.sex + } + + // 直接使用 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()}` + + return fetch(url, { + method: 'GET', + headers: { + Authorization: token, + 'Content-Type': 'application/json' + }, + credentials: 'include' + }) + .then(async (response) => { + // 检查响应内容类型,判断是否为流式响应 + const contentType = response.headers.get('content-type') || '' + const isStreamResponse = + contentType.includes('text/event-stream') || contentType.includes('stream') + + if (!response.ok) { + // 非流式错误响应,使用 text() 读取错误信息 + const errorText = await response.text() + console.error('请求错误:', errorText) + 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() + + try { + const errorData = JSON.parse(text) + if (errorData.message || errorData.error) { + showToast({ + message: errorData.message || errorData.error || 'network error', + position: 'top', + icon: 'none' + }) + } + } catch (e) { + // 如果不是 JSON,直接显示文本内容 + showToast({ + message: text || 'network error', + position: 'top', + icon: 'none' + }) + throw new Error(text || 'network error') + } + + return + } + + // 流式响应处理 + const reader = response.body?.getReader() + if (!reader) throw new Error('无法获取流读取器') + + const decoder = new TextDecoder() + // 流式响应时调用成功回调 + onSuccess?.() + }) + .catch((error) => { + console.error('fetch请求失败:', error) + showToast({ + message: error.message || 'network error' + }) + throw error + }) + .finally(() => { + isGenerating.value = false + }) + } + + return { + fetchMessage, + isGenerating + } +} diff --git a/src/views/Workshop/selectStyle/index.vue b/src/views/Workshop/selectStyle/index.vue index 235d569..c8a9fe6 100644 --- a/src/views/Workshop/selectStyle/index.vue +++ b/src/views/Workshop/selectStyle/index.vue @@ -9,6 +9,7 @@ import { FlowType, IsHistoryFlow } from '@/types/enum' import GenerateLoading from '@/views/asistant/components/GenerateLoading.vue' import gradientButton from '@/components/gradientButton.vue' import StyleListDom from '@/views/Workshop/selectStyle/styleList.vue' +import { useStreamChat } from '@/hooks/useStreamChat' const router = useRouter() const route = useRoute() //const props = defineProps({ @@ -19,10 +20,10 @@ const hGenerateStore = useHGenerateStore() const query = computed(() => route.query) const isHistoryFlow = computed(() => IsHistoryFlow(query.value.flowType)) -const isLoading = ref(false) +const isLoading = ref(true) // const loadingTitle= ref('Analyzing the Outfit...') const loadingTitle = computed(()=>{ - let str = '' + let str = 'Analyzing the Outfit...' if(!select.value.status)str = 'Analyzing the Outfit...' if(select.value.status == 'RUNNING')str = 'Generating Results...' if(select.value.status == 'PENDING' || select.value.status == 'ALMOST_DONE')str = 'Almost there...' @@ -151,10 +152,35 @@ const styleListInit = ()=>{ dataDom.styleListVue.init(data.select) } +// 使用 useStreamChat,在流式请求成功后执行原本的逻辑 +const { fetchMessage, isGenerating } = useStreamChat(() => { + // 流式请求成功后,执行原本的请求逻辑 + requestOutfit({ num: 4 }) +}) + onMounted(()=>{ // generateStore.clearProductData() // if(!data.styleList[0]?.id)getRequestOutfitList(0) if(getGenerateTime)clearTimeout(getGenerateTime) + + // 检查是否有从 dressfor 传递过来的消息 + const message = query.value.message as string + const sessionId = query.value.sessionId as string + + if (message && sessionId) { + // 有消息,说明是从 dressfor 跳转过来的,先发起流式请求 + generateStore.setSessionId(sessionId) + fetchMessage(message, sessionId) + .then(() => { + // 流式请求完成后(失败或成功)继续执行 + }) + .catch(() => { + // 错误处理 + }) + return + } + + // 原本的逻辑 const taskIdList = data.styleList .filter(item => item?.taskId && item?.status !== 'SUCCEEDED') .map(item => item.taskId); @@ -225,7 +251,7 @@ const { styleListVue } = toRefs(dataDom); -
+
diff --git a/src/views/stylist/dressfor.vue b/src/views/stylist/dressfor.vue index a92c566..1016536 100644 --- a/src/views/stylist/dressfor.vue +++ b/src/views/stylist/dressfor.vue @@ -1,9 +1,10 @@