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 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>
|
||||||
|
|||||||
@@ -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%;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
:deep(.visualizer-container) {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.input-item {
|
|
||||||
// width: 100%;
|
// width: 100%;
|
||||||
height: 100%;
|
// padding: 0;
|
||||||
outline: none;
|
// }
|
||||||
border: none;
|
// :deep(.visualizer-container) {
|
||||||
background-color: #efefef;
|
// height: 100%;
|
||||||
}
|
// }
|
||||||
.audio-icon {
|
// }
|
||||||
width: initial;
|
// .input-item {
|
||||||
}
|
// // width: 100%;
|
||||||
}
|
// height: 100%;
|
||||||
.send {
|
// outline: none;
|
||||||
width: 7.6rem;
|
// border: none;
|
||||||
height: 7.6rem;
|
// background-color: #efefef;
|
||||||
background-color: #efefef;
|
// }
|
||||||
border-radius: 1rem;
|
// .audio-icon {
|
||||||
}
|
// width: initial;
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
// .send {
|
||||||
|
// width: 7.6rem;
|
||||||
|
// height: 7.6rem;
|
||||||
|
// background-color: #efefef;
|
||||||
|
// 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';
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user