feat: dressfor页面&生成outfit逻辑修改
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped

This commit is contained in:
2026-02-27 13:44:19 +08:00
parent 10ee247b8d
commit 8100459c4e
4 changed files with 258 additions and 76 deletions

112
src/hooks/useStreamChat.ts Normal file
View File

@@ -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<void> => {
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
}
}

View File

@@ -9,6 +9,7 @@ import { FlowType, IsHistoryFlow } from '@/types/enum'
import GenerateLoading from '@/views/asistant/components/GenerateLoading.vue' import GenerateLoading from '@/views/asistant/components/GenerateLoading.vue'
import gradientButton from '@/components/gradientButton.vue' import gradientButton from '@/components/gradientButton.vue'
import StyleListDom from '@/views/Workshop/selectStyle/styleList.vue' import StyleListDom from '@/views/Workshop/selectStyle/styleList.vue'
import { useStreamChat } from '@/hooks/useStreamChat'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
//const props = defineProps({ //const props = defineProps({
@@ -19,10 +20,10 @@ const hGenerateStore = useHGenerateStore()
const query = computed(() => route.query) const query = computed(() => route.query)
const isHistoryFlow = computed(() => IsHistoryFlow(query.value.flowType)) const isHistoryFlow = computed(() => IsHistoryFlow(query.value.flowType))
const isLoading = ref(false) const isLoading = ref(true)
// const loadingTitle= ref('Analyzing the Outfit...') // const loadingTitle= ref('Analyzing the Outfit...')
const loadingTitle = computed(()=>{ const loadingTitle = computed(()=>{
let str = '' let str = 'Analyzing the Outfit...'
if(!select.value.status)str = 'Analyzing the Outfit...' if(!select.value.status)str = 'Analyzing the Outfit...'
if(select.value.status == 'RUNNING')str = 'Generating Results...' if(select.value.status == 'RUNNING')str = 'Generating Results...'
if(select.value.status == 'PENDING' || select.value.status == 'ALMOST_DONE')str = 'Almost there...' if(select.value.status == 'PENDING' || select.value.status == 'ALMOST_DONE')str = 'Almost there...'
@@ -151,10 +152,35 @@ const styleListInit = ()=>{
dataDom.styleListVue.init(data.select) dataDom.styleListVue.init(data.select)
} }
// 使用 useStreamChat在流式请求成功后执行原本的逻辑
const { fetchMessage, isGenerating } = useStreamChat(() => {
// 流式请求成功后,执行原本的请求逻辑
requestOutfit({ num: 4 })
})
onMounted(()=>{ onMounted(()=>{
// generateStore.clearProductData() // generateStore.clearProductData()
// if(!data.styleList[0]?.id)getRequestOutfitList(0) // if(!data.styleList[0]?.id)getRequestOutfitList(0)
if(getGenerateTime)clearTimeout(getGenerateTime) 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 const taskIdList = data.styleList
.filter(item => item?.taskId && item?.status !== 'SUCCEEDED') .filter(item => item?.taskId && item?.status !== 'SUCCEEDED')
.map(item => item.taskId); .map(item => item.taskId);
@@ -225,7 +251,7 @@ const { styleListVue } = toRefs(dataDom);
</div> </div>
</div> </div>
<!-- <div class="footer placeholder"></div> --> <!-- <div class="footer placeholder"></div> -->
<div class="loading-container" v-if="isLoading"> <div class="loading-container" v-if="isGenerating || isLoading">
<GenerateLoading :title="loadingTitle"/> <GenerateLoading :title="loadingTitle"/>
</div> </div>
<StyleListDom ref="styleListVue"></StyleListDom> <StyleListDom ref="styleListVue"></StyleListDom>

View File

@@ -1,9 +1,10 @@
<template> <template>
<div class="dressfor-container flex"> <div class="dressfor-container flex">
<div class="content flex-1 flex flex-column"> <div class="content flex-1 flex flex-column">
<div class="loading-container flex flex-center"> <!-- 移除始终显示的 loading改为按需显示 -->
<!-- <div class="loading-container flex flex-center">
<Icon class="icon-element" title="" /> <Icon class="icon-element" title="" />
</div> </div> -->
<!-- <div class="text"> <!-- <div class="text">
What are you <br /> What are you <br />
dressing for? dressing for?
@@ -12,7 +13,7 @@
<img class="text" src="@/assets/images/dressfor.png" alt="" /> <img class="text" src="@/assets/images/dressfor.png" alt="" />
</div> </div>
<!-- <div class="start-btn" @click="handleStart">Start</div> --> <!-- <div class="start-btn" @click="handleStart">Start</div> -->
<div class="chatbox flex flex-center"> <!-- <div class="chatbox flex flex-center">
<div class="input-box flex"> <div class="input-box flex">
<div class="input-wrapper flex-1 flex"> <div class="input-wrapper flex-1 flex">
<input <input
@@ -37,9 +38,9 @@
<div class="send flex flex-center" @click="handleSendMessage"> <div class="send flex flex-center" @click="handleSendMessage">
<SvgIcon class="send-icon" name="send_bold" size="26" color="#6d6868" /> <SvgIcon class="send-icon" name="send_bold" size="26" color="#6d6868" />
</div> </div>
</div> </div> -->
<div class="tag-container flex flex-column flex-center"> <div class="tag-container flex flex-column flex-center">
<div class="tag-list short flex flex-justify-center"> <div class="tag-list short flex">
<div <div
class="tag-item" class="tag-item"
:class="{ active: item === inputValue }" :class="{ active: item === inputValue }"
@@ -50,7 +51,7 @@
{{ item }} {{ item }}
</div> </div>
</div> </div>
<div class="tag-list long flex flex-justify-center"> <!-- <div class="tag-list long flex flex-justify-center">
<div <div
class="tag-item" class="tag-item"
v-for="item in tagListLong" v-for="item in tagListLong"
@@ -60,26 +61,46 @@
> >
{{ item }} {{ item }}
</div> </div>
</div> </div> -->
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onUnmounted, nextTick, watch } from 'vue' import { ref, onUnmounted, nextTick, watch } from 'vue'
import { useUserInfoStore, useGenerateStore } from '@/stores'
import { showToast, closeToast } from 'vant' import { showToast, closeToast } from 'vant'
import { useRouter } from 'vue-router'
import HeaderTitle from '@/components/HeaderTitle.vue' import HeaderTitle from '@/components/HeaderTitle.vue'
import FooterNavigation from '@/components/FooterNavigation.vue' import FooterNavigation from '@/components/FooterNavigation.vue'
import { useRouter } from 'vue-router'
import AudioVisualizer from '@/views/asistant/components/AudioVisualizer.vue' import AudioVisualizer from '@/views/asistant/components/AudioVisualizer.vue'
import Icon from '../asistant/components/GenerateLoading.vue' import Icon from '../asistant/components/GenerateLoading.vue'
const router = useRouter() const router = useRouter()
const userInfoStore = useUserInfoStore()
const generateStore = useGenerateStore()
const tagListShort = [
'Casual',
'Formal',
'Activewear',
'Resort',
'Business casual',
'Evening',
'Outdoor',
'Business',
'Cocktail',
const tagListShort = ['Silk Slip Dress', 'Business Casual', 'Suggest Shoe Styles'] 'Bridal',
const tagListLong = ['Linen Suit For Summer Gaka', 'Recomment Evening Bags'] 'Festival',
'Travel',
'Athleisure',
'Beach',
'Ski'
]
// const tagListLong = ['Linen Suit For Summer Gaka', 'Recomment Evening Bags']
const inputValue = ref('') const inputValue = ref('')
const sessionId = ref('')
const isRecording = ref(false) const isRecording = ref(false)
const audioVisualizerRef = ref<InstanceType<typeof AudioVisualizer> | null>(null) const audioVisualizerRef = ref<InstanceType<typeof AudioVisualizer> | null>(null)
let speechRecognition: any = null let speechRecognition: any = null
@@ -106,11 +127,6 @@ const handleSendMessage = () => {
showToast('Please enter a message') showToast('Please enter a message')
return return
} }
router.push({
path: '/asistant',
query: message ? { message } : undefined
})
} }
const handleClickAudio = () => { const handleClickAudio = () => {
@@ -200,6 +216,16 @@ const stopRecording = () => {
const handleClickTag = (tag: string) => { const handleClickTag = (tag: string) => {
inputValue.value = tag inputValue.value = tag
sessionId.value = Math.floor(Date.now() / 1000).toString()
generateStore.setSessionId(sessionId.value)
// 直接跳转到 selectStyle 页面,传递消息和 sessionId
router.push({
path: '/workshop/selectStyle',
query: {
message: tag,
sessionId: sessionId.value
}
})
} }
onUnmounted(() => { onUnmounted(() => {
@@ -224,6 +250,20 @@ onUnmounted(() => {
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
padding: 15.9rem 0 0 0; padding: 15.9rem 0 0 0;
.loading-container {
width: 100%;
height: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
color: #000;
}
.content { .content {
.loading-container { .loading-container {
:deep(.loading-image) { :deep(.loading-image) {
@@ -246,70 +286,74 @@ onUnmounted(() => {
padding-bottom: 7.7rem; padding-bottom: 7.7rem;
width: 60rem; width: 60rem;
} }
.chatbox { // .chatbox {
height: 9.3rem; // height: 9.3rem;
// background-color: #fff; // // background-color: #fff;
column-gap: 2.29rem; // column-gap: 2.29rem;
.input-box { // .input-box {
width: 59.8rem; // width: 59.8rem;
height: 100%; // height: 100%;
background-color: #efefef; // background-color: #efefef;
// border: 2px solid #5f5f5f; // // border: 2px solid #5f5f5f;
border-radius: 1rem; // border-radius: 1rem;
color: #222222; // color: #222222;
font-size: 3.2rem; // font-size: 3.2rem;
font-family: 'satoshiRegular'; // font-family: 'satoshiRegular';
padding: 0 2.6rem; // padding: 0 2.6rem;
column-gap: 2.6rem; // column-gap: 2.6rem;
overflow: hidden; // overflow: hidden;
.input-wrapper { // .input-wrapper {
overflow: hidden; // overflow: hidden;
} // }
.recording-visualizer { // .recording-visualizer {
display: flex; // display: flex;
align-items: center; // align-items: center;
height: 100%; // height: 100%;
:deep(.audio-visualizer) { // :deep(.audio-visualizer) {
width: 100%; // width: 100%;
padding: 0; // padding: 0;
} // }
:deep(.visualizer-container) { // :deep(.visualizer-container) {
height: 100%; // height: 100%;
} // }
} // }
.input-item { // .input-item {
// width: 100%; // // width: 100%;
height: 100%; // height: 100%;
outline: none; // outline: none;
border: none; // border: none;
background-color: #efefef; // background-color: #efefef;
} // }
.audio-icon { // .audio-icon {
width: initial; // width: initial;
} // }
} // }
.send { // .send {
width: 7.6rem; // width: 7.6rem;
height: 7.6rem; // height: 7.6rem;
background-color: #efefef; // background-color: #efefef;
border-radius: 1rem; // border-radius: 1rem;
} // }
} // }
.tag-container { .tag-container {
row-gap: 3.1rem; // padding: 5.7rem 0;
padding-top: 5.7rem; margin: 0 auto;
width: 65.8rem;
.tag-list { .tag-list {
color: #000; color: #000;
flex-wrap: wrap; flex-wrap: wrap;
&.short { justify-content: space-between;
column-gap: 1.91rem; align-content: flex-start;
} gap: 3rem;
&.long { &::after {
column-gap: 3.1rem; content: '';
padding-left: 2.1rem; flex-grow: 1;
height: 0;
} }
.tag-item { .tag-item {
height: 6.8rem; height: 6.8rem;
min-width: 12rem;
line-height: 6.8rem; line-height: 6.8rem;
box-sizing: border-box; box-sizing: border-box;
font-family: 'satoshiRegular'; font-family: 'satoshiRegular';

View File

@@ -56,7 +56,7 @@ export default defineConfig(({ mode }) => {
}, },
server: { server: {
host: '0.0.0.0', // 允许局域网内的IP访问 host: '0.0.0.0', // 允许局域网内的IP访问
port: 8060, // 根据环境设置端口 port: 8066, // 根据环境设置端口
open: false, // 自动打开浏览器 open: false, // 自动打开浏览器
strictPort: true, // 如果端口已被占用,则尝试下一个可用端口 strictPort: true, // 如果端口已被占用,则尝试下一个可用端口
hmr: { hmr: {