feat: dressfor页面&生成outfit逻辑修改
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
This commit is contained in:
112
src/hooks/useStreamChat.ts
Normal file
112
src/hooks/useStreamChat.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="footer placeholder"></div> -->
|
||||
<div class="loading-container" v-if="isLoading">
|
||||
<div class="loading-container" v-if="isGenerating || isLoading">
|
||||
<GenerateLoading :title="loadingTitle"/>
|
||||
</div>
|
||||
<StyleListDom ref="styleListVue"></StyleListDom>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<div class="dressfor-container flex">
|
||||
<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="" />
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- <div class="text">
|
||||
What are you <br />
|
||||
dressing for?
|
||||
@@ -12,7 +13,7 @@
|
||||
<img class="text" src="@/assets/images/dressfor.png" alt="" />
|
||||
</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-wrapper flex-1 flex">
|
||||
<input
|
||||
@@ -37,9 +38,9 @@
|
||||
<div class="send flex flex-center" @click="handleSendMessage">
|
||||
<SvgIcon class="send-icon" name="send_bold" size="26" color="#6d6868" />
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<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
|
||||
class="tag-item"
|
||||
:class="{ active: item === inputValue }"
|
||||
@@ -50,7 +51,7 @@
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag-list long flex flex-justify-center">
|
||||
<!-- <div class="tag-list long flex flex-justify-center">
|
||||
<div
|
||||
class="tag-item"
|
||||
v-for="item in tagListLong"
|
||||
@@ -60,26 +61,46 @@
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onUnmounted, nextTick, watch } from 'vue'
|
||||
import { useUserInfoStore, useGenerateStore } from '@/stores'
|
||||
import { showToast, closeToast } from 'vant'
|
||||
import { useRouter } from 'vue-router'
|
||||
import HeaderTitle from '@/components/HeaderTitle.vue'
|
||||
import FooterNavigation from '@/components/FooterNavigation.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import AudioVisualizer from '@/views/asistant/components/AudioVisualizer.vue'
|
||||
import Icon from '../asistant/components/GenerateLoading.vue'
|
||||
|
||||
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']
|
||||
const tagListLong = ['Linen Suit For Summer Gaka', 'Recomment Evening Bags']
|
||||
'Bridal',
|
||||
'Festival',
|
||||
'Travel',
|
||||
'Athleisure',
|
||||
'Beach',
|
||||
'Ski'
|
||||
]
|
||||
// const tagListLong = ['Linen Suit For Summer Gaka', 'Recomment Evening Bags']
|
||||
|
||||
const inputValue = ref('')
|
||||
const sessionId = ref('')
|
||||
const isRecording = ref(false)
|
||||
const audioVisualizerRef = ref<InstanceType<typeof AudioVisualizer> | null>(null)
|
||||
let speechRecognition: any = null
|
||||
@@ -106,11 +127,6 @@ const handleSendMessage = () => {
|
||||
showToast('Please enter a message')
|
||||
return
|
||||
}
|
||||
|
||||
router.push({
|
||||
path: '/asistant',
|
||||
query: message ? { message } : undefined
|
||||
})
|
||||
}
|
||||
|
||||
const handleClickAudio = () => {
|
||||
@@ -200,6 +216,16 @@ const stopRecording = () => {
|
||||
|
||||
const handleClickTag = (tag: string) => {
|
||||
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(() => {
|
||||
@@ -224,6 +250,20 @@ onUnmounted(() => {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
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 {
|
||||
.loading-container {
|
||||
:deep(.loading-image) {
|
||||
@@ -246,70 +286,74 @@ onUnmounted(() => {
|
||||
padding-bottom: 7.7rem;
|
||||
width: 60rem;
|
||||
}
|
||||
.chatbox {
|
||||
height: 9.3rem;
|
||||
// background-color: #fff;
|
||||
column-gap: 2.29rem;
|
||||
.input-box {
|
||||
width: 59.8rem;
|
||||
height: 100%;
|
||||
background-color: #efefef;
|
||||
// border: 2px solid #5f5f5f;
|
||||
border-radius: 1rem;
|
||||
color: #222222;
|
||||
font-size: 3.2rem;
|
||||
font-family: 'satoshiRegular';
|
||||
padding: 0 2.6rem;
|
||||
column-gap: 2.6rem;
|
||||
overflow: hidden;
|
||||
.input-wrapper {
|
||||
overflow: hidden;
|
||||
}
|
||||
.recording-visualizer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
:deep(.audio-visualizer) {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
:deep(.visualizer-container) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.input-item {
|
||||
// width: 100%;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: #efefef;
|
||||
}
|
||||
.audio-icon {
|
||||
width: initial;
|
||||
}
|
||||
}
|
||||
.send {
|
||||
width: 7.6rem;
|
||||
height: 7.6rem;
|
||||
background-color: #efefef;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
}
|
||||
// .chatbox {
|
||||
// height: 9.3rem;
|
||||
// // background-color: #fff;
|
||||
// column-gap: 2.29rem;
|
||||
// .input-box {
|
||||
// width: 59.8rem;
|
||||
// height: 100%;
|
||||
// background-color: #efefef;
|
||||
// // border: 2px solid #5f5f5f;
|
||||
// border-radius: 1rem;
|
||||
// color: #222222;
|
||||
// font-size: 3.2rem;
|
||||
// font-family: 'satoshiRegular';
|
||||
// padding: 0 2.6rem;
|
||||
// column-gap: 2.6rem;
|
||||
// overflow: hidden;
|
||||
// .input-wrapper {
|
||||
// overflow: hidden;
|
||||
// }
|
||||
// .recording-visualizer {
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// height: 100%;
|
||||
// :deep(.audio-visualizer) {
|
||||
// width: 100%;
|
||||
// padding: 0;
|
||||
// }
|
||||
// :deep(.visualizer-container) {
|
||||
// height: 100%;
|
||||
// }
|
||||
// }
|
||||
// .input-item {
|
||||
// // width: 100%;
|
||||
// height: 100%;
|
||||
// outline: none;
|
||||
// border: none;
|
||||
// background-color: #efefef;
|
||||
// }
|
||||
// .audio-icon {
|
||||
// width: initial;
|
||||
// }
|
||||
// }
|
||||
// .send {
|
||||
// width: 7.6rem;
|
||||
// height: 7.6rem;
|
||||
// background-color: #efefef;
|
||||
// border-radius: 1rem;
|
||||
// }
|
||||
// }
|
||||
.tag-container {
|
||||
row-gap: 3.1rem;
|
||||
padding-top: 5.7rem;
|
||||
// padding: 5.7rem 0;
|
||||
margin: 0 auto;
|
||||
width: 65.8rem;
|
||||
.tag-list {
|
||||
color: #000;
|
||||
flex-wrap: wrap;
|
||||
&.short {
|
||||
column-gap: 1.91rem;
|
||||
}
|
||||
&.long {
|
||||
column-gap: 3.1rem;
|
||||
padding-left: 2.1rem;
|
||||
justify-content: space-between;
|
||||
align-content: flex-start;
|
||||
gap: 3rem;
|
||||
&::after {
|
||||
content: '';
|
||||
flex-grow: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
height: 6.8rem;
|
||||
min-width: 12rem;
|
||||
line-height: 6.8rem;
|
||||
box-sizing: border-box;
|
||||
font-family: 'satoshiRegular';
|
||||
|
||||
@@ -56,7 +56,7 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0', // 允许局域网内的IP访问
|
||||
port: 8060, // 根据环境设置端口
|
||||
port: 8066, // 根据环境设置端口
|
||||
open: false, // 自动打开浏览器
|
||||
strictPort: true, // 如果端口已被占用,则尝试下一个可用端口
|
||||
hmr: {
|
||||
|
||||
Reference in New Issue
Block a user