diff --git a/src/store/emotion.ts b/src/store/emotion.ts index 0b7e9b0..6ab6192 100644 --- a/src/store/emotion.ts +++ b/src/store/emotion.ts @@ -68,13 +68,13 @@ export const useEmotionStore = defineStore('emotion', { } // 同时添加到历史记录 - this.addHistory({ - queryText: stockData.queryText, - stockInfo: stockData.stockInfo, - apiData: stockData.apiData, - conclusionData: stockData.conclusionData, - timestamp: stockData.timestamp - }); + // this.addHistory({ + // queryText: stockData.queryText, + // stockInfo: stockData.stockInfo, + // apiData: stockData.apiData, + // conclusionData: stockData.conclusionData, + // timestamp: stockData.timestamp + // }); }, // 切换股票 switchStock(index: number) { diff --git a/src/views/AiEmotion.vue b/src/views/AiEmotion.vue index 367574c..f2e1a6e 100644 --- a/src/views/AiEmotion.vue +++ b/src/views/AiEmotion.vue @@ -1,193 +1,214 @@ @@ -221,31 +242,47 @@ const emotionStore = useEmotionStore(); const emotionAudioStore = useEmotionAudioStore(); // 语音播放控制函数 -const toggleVoiceForUser = () => { +const toggleVoiceForUser = (stock) => { if (!emotionAudioStore.isVoiceEnabled) { // 如果语音功能关闭,先开启语音功能 emotionAudioStore.toggleVoice(); } else { - // 如果语音功能开启,则切换播放/暂停状态 - if (emotionAudioStore.isPlaying) { - // 如果正在播放,则暂停 - emotionAudioStore.togglePlayPause(); + // 获取该股票的结论数据和当前状态 + const stockConclusion = getStockConclusion(stock); + const currentState = getStockAudioState(stock); + + // 检查是否有任何音频正在播放(包括全局播放状态和当前股票状态) + const isAnyAudioPlaying = emotionAudioStore.isPlaying || currentState.isPlaying; + + // 如果当前点击的股票正在播放,则暂停 + if (currentState.isPlaying) { + console.log('暂停当前股票音频:', stock.stockInfo?.name); + stopAudio(); + emotionAudioStore.resetAudioState(); + setStockAudioState(stock, { isPlaying: false, isPaused: true }); } else { - // 如果没有在播放,先检查是否处于暂停状态 - if (emotionAudioStore.isPaused && (emotionAudioStore.currentAudioUrl || emotionAudioStore.ttsUrl)) { - // 如果处于暂停状态且有音频,则继续播放 - console.log('从暂停状态继续播放'); - emotionAudioStore.togglePlayPause(); - } else if (parsedConclusion.value && (parsedConclusion.value.one1_url || parsedConclusion.value.two_url || parsedConclusion.value.three_url || parsedConclusion.value.four_url)) { - // 有结论数据时,重新播放整个音频队列 - console.log('用户点击播放,重新播放音频队列'); - playAudioQueue(parsedConclusion.value, false); // 不启动打字机效果,因为内容已经显示 - } else if (emotionAudioStore.currentAudioUrl || emotionAudioStore.ttsUrl) { - // 有单个音频URL时切换播放/暂停 - emotionAudioStore.togglePlayPause(); + // 如果有其他音频正在播放,先停止 + if (isAnyAudioPlaying) { + console.log('停止其他正在播放的音频,准备播放新音频:', stock.stockInfo?.name); + stopAudio(); + emotionAudioStore.resetAudioState(); + } + + // 清除所有股票的播放状态 + clearAllStockAudioStates(); + + // 如果有音频数据,开始播放 + if (stockConclusion && (stockConclusion.one1_url || stockConclusion.two_url || stockConclusion.three_url || stockConclusion.four_url)) { + console.log('开始播放股票音频:', stock.stockInfo?.name); + setStockAudioState(stock, { isPlaying: true, isPaused: false }); + + // 播放音频队列 + playAudioQueue(stockConclusion, false, () => { + // 音频播放完成后重置状态 + setStockAudioState(stock, { isPlaying: false, isPaused: false }); + }); } else { - // 没有音频时关闭语音功能 - emotionAudioStore.toggleVoice(); + console.log('该股票没有可播放的音频数据'); } } } @@ -314,14 +351,14 @@ const loadConversationsFromStockList = () => { // 检查是否有新的股票需要添加到对话中 emotionStore.stockList.forEach(stock => { const stockKey = `${stock.stockInfo.code}_${stock.timestamp}`; - + // 如果这个股票还没有添加到对话中 if (!addedStocks.value.has(stockKey)) { // 检查messages中是否已经存在相同的用户消息 - const existingMessage = messages.value.find(msg => + const existingMessage = messages.value.find(msg => msg.sender === 'user' && msg.text === stock.queryText ); - + // 只有当messages中不存在相同消息时才添加 if (!existingMessage) { // 只添加用户输入消息,不添加AI回复 @@ -330,17 +367,17 @@ const loadConversationsFromStockList = () => { text: stock.queryText }; messages.value.push(userMessage); - + // 只将用户消息添加到emotion store中(如果store中也不存在) const storeConversations = emotionStore.getConversations(); - const existingInStore = storeConversations.find(conv => + const existingInStore = storeConversations.find(conv => conv.sender === 'user' && conv.text === stock.queryText ); if (!existingInStore) { emotionStore.addConversation(userMessage); } } - + // 将这个股票标记为已添加 addedStocks.value.add(stockKey); } @@ -355,12 +392,87 @@ const clearConversations = () => { addedStocks.value.clear(); }; -// 暴露清空对话记录的方法给父组件 +// 添加股票数据的包装方法 +const addStock = (stockData) => { + console.log('AiEmotion组件接收到股票数据:', stockData); + + // 设置为历史记录模式 + isHistoryMode.value = true; + + // 1. 先清空页面显示节点和stockList中的数据 + isPageLoaded.value = false; // 隐藏页面显示节点 + emotionStore.clearAllStocks(); // 清空stockList中的数据 + emotionStore.clearConversations(); // 清空对话记录 + messages.value = []; // 清空页面对话显示 + + // 清理已添加股票的记录 + addedStocks.value.clear(); + + // 停止音频播放和清理状态 + stopAudio(); + audioUrl.value = ''; + emotionAudioStore.resetAudioState(); + clearTypewriterTimers(); + hasTriggeredAudio.value = false; + hasTriggeredTypewriter.value = false; + stockTypewriterShown.value.clear(); + stockAudioPlayed.value.clear(); + + // 清理显示的文本和标题 + displayedTexts.value = { + one1: '', + one2: '', + two: '', + three: '', + four: '', + disclaimer: '' + }; + displayedTitles.value = { + one: '', + two: '', + three: '', + four: '' + }; + + // 隐藏所有模块 + moduleVisibility.value = { + one: false, + two: false, + three: false, + four: false, + disclaimer: false + }; + + // 隐藏所有图表组件 + chartVisibility.value = { + marketTemperature: false, + emotionDecod: false, + emotionalBottomRadar: false, + emoEnergyConverter: false + }; + + // 2. 将新的数据存储到stockList中 + emotionStore.addStock(stockData); + // 3. 设置页面为已加载状态,重新渲染页面 + isPageLoaded.value = true; + + // 4. 使用nextTick确保DOM更新后启动高度监听器并滚动到底部 + nextTick(() => { + // 启动页面高度监听器,实时监听内容变化并自动滚动 + startHeightObserver(); + // 立即滚动到底部 + scrollToBottom(); + }); +}; + +// 暴露方法给父组件 defineExpose({ handleSendMessage, - clearConversations + clearConversations, + addStock }); const isPageLoaded = ref(false); // 控制页面是否显示 +const isHistoryMode = ref(false); // 控制是否为历史记录模式 // const isLoading = ref(false); // 控制加载状态 const isRotating = ref(false);//控制旋转 const version1 = ref(1); // 版本号 @@ -415,6 +527,8 @@ const typewriterTimers = ref([]); const stockTypewriterShown = ref(new Map()); // 记录每个股票是否已经播放过音频 const stockAudioPlayed = ref(new Map()); +// 跟踪每个股票的音频播放状态 +const stockAudioStates = ref(new Map()); // 存储当前的完成回调函数 const currentOnCompleteCallback = ref(null); @@ -422,6 +536,29 @@ const currentOnCompleteCallback = ref(null); const audioUrl = ref(''); const isAudioPlaying = ref(false); +// 获取股票的音频播放状态 +const getStockAudioState = (stock) => { + const stockCode = stock.stockInfo?.code || stock.stockInfo?.symbol; + if (!stockCode) return { isPlaying: false, isPaused: false }; + + return stockAudioStates.value.get(stockCode) || { isPlaying: false, isPaused: false }; +}; + +// 设置股票的音频播放状态 +const setStockAudioState = (stock, state) => { + const stockCode = stock.stockInfo?.code || stock.stockInfo?.symbol; + if (!stockCode) return; + + stockAudioStates.value.set(stockCode, { ...state }); +}; + +// 清除所有股票的播放状态(当开始播放新音频时) +const clearAllStockAudioStates = () => { + for (const [key, value] of stockAudioStates.value.entries()) { + stockAudioStates.value.set(key, { isPlaying: false, isPaused: false }); + } +}; + // 返回顶部按钮相关数据 const showBackToTop = ref(false); @@ -461,12 +598,12 @@ const currentConclusion = computed(() => { }); const parsedConclusion = computed(() => { if (!currentConclusion.value) return null; - + // 如果conclusionData已经是对象,直接返回 if (typeof currentConclusion.value === 'object') { return currentConclusion.value; } - + // 如果是字符串,尝试解析JSON try { return JSON.parse(currentConclusion.value); @@ -512,12 +649,12 @@ const getStockData2 = (stock) => { // 辅助函数:获取股票的结论数据 const getStockConclusion = (stock) => { if (!stock?.conclusionData) return null; - + // 如果conclusionData已经是对象,直接返回 if (typeof stock.conclusionData === 'object') { return stock.conclusionData; } - + // 如果是字符串,尝试解析JSON try { return JSON.parse(stock.conclusionData); @@ -610,8 +747,8 @@ watch(currentStock, (newStock) => { if (newStock.conclusionData) { try { // 如果conclusionData已经是对象,直接使用;否则解析JSON - const conclusion = typeof newStock.conclusionData === 'object' - ? newStock.conclusionData + const conclusion = typeof newStock.conclusionData === 'object' + ? newStock.conclusionData : JSON.parse(newStock.conclusionData); displayedTexts.value = { one1: conclusion.one1 || '', @@ -700,8 +837,8 @@ watch(currentStock, (newStock) => { if (newStock.conclusionData) { try { // 如果conclusionData已经是对象,直接使用;否则解析JSON - const conclusion = typeof newStock.conclusionData === 'object' - ? newStock.conclusionData + const conclusion = typeof newStock.conclusionData === 'object' + ? newStock.conclusionData : JSON.parse(newStock.conclusionData); let voiceUrl = null; // 优先使用one1_url,如果没有则尝试其他音频URL @@ -746,7 +883,13 @@ watch(currentStock, (newStock) => { console.log('图表数据已准备完成,开始渲染:', newStock.apiData) // 检查场景应用部分是否已经在视口中,如果是则立即触发效果 setTimeout(() => { - if (scenarioApplicationRef.value && parsedConclusion.value) { + // 检查 scenarioApplicationRef.value 是否存在且是有效的 DOM 元素 + if (!scenarioApplicationRef.value || !(scenarioApplicationRef.value instanceof Element)) { + console.warn('scenarioApplicationRef.value 不是有效的 DOM 元素,跳过处理'); + return; + } + + if (parsedConclusion.value) { const stockCode = newStock.stockInfo?.code || newStock.stockInfo?.symbol; // 如果该股票已经显示过,不需要再处理 @@ -763,9 +906,9 @@ watch(currentStock, (newStock) => { if (stockCode) { // 检查该股票是否是第一次触发 if (!stockTypewriterShown.value.has(stockCode)) { - // 该股票第一次:播放音频和打字机效果 - if (audioUrl.value) { - console.log('该股票第一次进入场景应用,开始打字机效果和音频播放'); + // 如果是用户主动搜索,启动打字机效果和音频播放 + if (isUserInitiated.value && audioUrl.value) { + console.log('用户主动搜索,该股票第一次进入场景应用,开始打字机效果和音频播放'); hasTriggeredTypewriter.value = true; hasTriggeredAudio.value = true; @@ -779,9 +922,38 @@ watch(currentStock, (newStock) => { } stockTypewriterShown.value.set(stockCode, true); - } else { + } else if (isUserInitiated.value && !audioUrl.value) { console.log('音频尚未准备好,等待音频加载完成后再触发效果(股票切换后)'); return; + } else { + // 非用户主动搜索(如历史记录恢复),直接显示完整内容 + console.log('非用户主动搜索,该股票第一次进入场景应用,直接显示完整内容'); + + const conclusion = parsedConclusion.value; + displayedTexts.value = { + one1: conclusion.one1 || '', + one2: conclusion.one2 || '', + two: conclusion.two || '', + three: conclusion.three || '', + four: conclusion.four || '', + disclaimer: '该内容由AI生成,请注意甄别' + }; + displayedTitles.value = { + one: 'L1: 情绪监控', + two: 'L2: 情绪解码', + three: 'L3: 情绪推演', + four: 'L4: 情绪套利' + }; + moduleVisibility.value = { + one: !!(conclusion.one1 || conclusion.one2), + two: !!conclusion.two, + three: !!conclusion.three, + four: !!conclusion.four, + disclaimer: true + }; + + stockTypewriterShown.value.set(stockCode, true); + stockAudioPlayed.value.set(stockCode, true); } } else { // 非第一次或已经触发过:直接显示完整内容,不播放音频和打字机效果 @@ -1105,7 +1277,7 @@ const playNextAudio = () => { isPlayingQueueAudio.value = true; emotionAudioStore.isPlaying = true; console.log(`开始播放${audioInfo.name}音频`); - + // 如果是第一个音频且需要启动打字机效果,则启动 if (currentPlayIndex === 0 && audioInfo.shouldStartTypewriter && parsedConclusion.value) { console.log('🎬 第一个音频开始播放,同时启动打字机效果'); @@ -1142,6 +1314,12 @@ const playNextAudio = () => { console.log("🎉 所有音频播放完成"); emotionAudioStore.nowSound = null; isCallingPlayNext = false; + + // 调用完成回调(如果有的话) + if (audioInfo.onComplete && typeof audioInfo.onComplete === 'function') { + console.log('调用音频播放完成回调'); + audioInfo.onComplete(); + } } }, onstop: () => { @@ -1240,7 +1418,7 @@ function playAudioQueue(conclusionData, shouldStartTypewriter = false, onComplet try { // 解析结论数据 const conclusion = typeof conclusionData === 'object' ? conclusionData : JSON.parse(conclusionData); - + // 清空之前的音频队列 audioQueue.value = []; currentPlayIndex = 0; @@ -1388,72 +1566,72 @@ function startImageRotation() { // 显示思考过程 async function showThinkingProcess(stockName = null) { // 第一步:正在思考 - const thinkingMessage1 = reactive({ - sender: 'ai', + const thinkingMessage1 = reactive({ + sender: 'ai', text: 'AI情绪大模型正在思考......', gif: '/src/assets/img/gif/思考.gif' }); messages.value.push(thinkingMessage1); await new Promise(resolve => setTimeout(resolve, 1500)); messages.value.pop(); - + // 第二步:正在解析关键数据(持续显示直到获取到股票名称) - const thinkingMessage2 = reactive({ - sender: 'ai', + const thinkingMessage2 = reactive({ + sender: 'ai', text: 'AI情绪大模型正在解析关键数据......', gif: '/src/assets/img/gif/解析.gif' }); messages.value.push(thinkingMessage2); - + // 如果没有股票名称,保持第二步显示 if (!stockName) { return thinkingMessage2; // 返回消息引用,以便后续更新 } - + // 有股票名称后,继续后续步骤 await new Promise(resolve => setTimeout(resolve, 1500)); messages.value.pop(); - + // 第三步:生成具体股票的量子四维矩阵图 - const thinkingMessage3 = reactive({ - sender: 'ai', + const thinkingMessage3 = reactive({ + sender: 'ai', text: `AI情绪大模型正在生成${stockName}量子四维矩阵图......`, gif: '/src/assets/img/gif/生成.gif' }); messages.value.push(thinkingMessage3); await new Promise(resolve => setTimeout(resolve, 1500)); messages.value.pop(); - + // 第四步:报告已生成 const thinkingMessage4 = reactive({ sender: 'ai', text: '报告已生成!' }); messages.value.push(thinkingMessage4); await new Promise(resolve => setTimeout(resolve, 1500)); messages.value.pop(); - + return null; } // 继续思考过程(当获取到股票名称后调用) async function continueThinkingProcess(thinkingMessageRef, stockName) { if (!thinkingMessageRef || !stockName) return; - + // 等待一段时间后继续 await new Promise(resolve => setTimeout(resolve, 1500)); - + // 移除第二步消息 const index = messages.value.indexOf(thinkingMessageRef); if (index > -1) { messages.value.splice(index, 1); } - + // 第三步:生成具体股票的量子四维矩阵图 - const thinkingMessage3 = reactive({ - sender: 'ai', + const thinkingMessage3 = reactive({ + sender: 'ai', text: `正在生成${stockName}量子四维矩阵图......`, gif: '/src/assets/img/gif/生成.gif' }); messages.value.push(thinkingMessage3); - + // 返回第三步消息的引用,以便后续处理 return thinkingMessage3; } @@ -1461,16 +1639,16 @@ async function continueThinkingProcess(thinkingMessageRef, stockName) { // 完成思考过程(当第二个工作流接口成功后调用) async function finishThinkingProcess(thinkingMessage3Ref) { if (!thinkingMessage3Ref) return; - + // 等待一段时间 await new Promise(resolve => setTimeout(resolve, 1500)); - + // 移除第三步消息 const index = messages.value.indexOf(thinkingMessage3Ref); if (index > -1) { messages.value.splice(index, 1); } - + // 第四步:报告已生成 const thinkingMessage4 = reactive({ sender: 'ai', text: '报告已生成!' }); messages.value.push(thinkingMessage4); @@ -1483,6 +1661,8 @@ async function handleSendMessage(input, onComplete) { console.log("发送内容:", input); // 标记为用户主动搜索 isUserInitiated.value = true; + // 重置历史记录模式状态,确保正常对话时显示conversation-area + isHistoryMode.value = false; // 检查用户输入内容是否为空 if (!input || !input.trim()) { @@ -1496,7 +1676,8 @@ async function handleSendMessage(input, onComplete) { return; } - // 用户输入不为空,立即触发图片旋转逻辑,隐藏历史数据 + // 用户输入不为空,立即清空页面内容并触发图片旋转逻辑 + isPageLoaded.value = false; // 立即隐藏页面内容 isRotating.value = true; const previousMessages = [...messages.value]; // 保存历史消息 messages.value = []; // 清空历史数据 @@ -1504,7 +1685,7 @@ async function handleSendMessage(input, onComplete) { // 添加用户消息(只添加一次) const userMessage = reactive({ sender: 'user', text: input }); messages.value.push(userMessage); - + // 将用户消息添加到emotion store中 emotionStore.addConversation({ sender: 'user', @@ -1517,7 +1698,7 @@ async function handleSendMessage(input, onComplete) { if (chatStore.UserCount <= 0) { const aiMessage = reactive({ sender: 'ai', text: '您的剩余次数为0,无法使用情绪大模型,请联系客服或购买服务包。' }); messages.value.push(aiMessage); - + // 将AI消息添加到emotion store中 emotionStore.addConversation({ sender: 'ai', @@ -1576,13 +1757,13 @@ async function handleSendMessage(input, onComplete) { messages.value.splice(index, 1); } } - + // 关闭加载状态和等待提示,返回refuse信息,停止图片旋转,恢复历史数据 // isLoading.value = false; isPageLoaded.value = false; const aiMessage = reactive({ sender: 'ai', text: processRefuseMessage(parsedData.refuse) }); messages.value.push(aiMessage); - + // 将AI消息添加到emotion store中 emotionStore.addConversation({ sender: 'ai', @@ -1618,7 +1799,7 @@ async function handleSendMessage(input, onComplete) { token: localStorage.getItem('localToken'), language: "cn", }; - + console.log('第二个接口参数:', conclusionParams); // 同时调用第二个数据流接口和fetchData方法 const [conclusionResult, fetchDataResult] = await Promise.all([ @@ -1634,7 +1815,7 @@ async function handleSendMessage(input, onComplete) { if (thinkingMessage3Ref) { await finishThinkingProcess(thinkingMessage3Ref); } - + // 将结论数据存储到响应式变量和store中 conclusionData.value = conclusionResponse.data; console.log('第二个接口返回的完整数据结构:', conclusionResponse.data); @@ -1644,6 +1825,15 @@ async function handleSendMessage(input, onComplete) { // 所有数据加载完成,关闭加载状态,显示页面 // isLoading.value = false; isPageLoaded.value = true; + + // 使用nextTick确保DOM更新后清空对话显示并启动高度监听器 + nextTick(() => { + messages.value = []; + // 启动页面高度监听器,实时监听内容变化并自动滚动 + startHeightObserver(); + // 立即滚动到底部 + scrollToBottom(); + }); // 数据获取成功后,重新获取用户次数以实现实时更新 try { @@ -1693,7 +1883,7 @@ async function handleSendMessage(input, onComplete) { messages.value.splice(index, 1); } } - + // 数据加载失败,停止图片旋转,恢复历史数据 // isLoading.value = false; // 如果 fetchDataResult 为 false,说明数据不完整的错误信息已经在 fetchData 中添加到 messages @@ -1729,13 +1919,21 @@ async function handleSendMessage(input, onComplete) { messages.value.splice(index, 1); } } - + // 请求失败时关闭加载状态 // isLoading.value = false; // 如果有之前的股票数据,恢复显示状态;否则设置为false if (emotionStore.stockList.length > 0 && emotionStore.activeStock) { isPageLoaded.value = true; + // 使用nextTick确保DOM更新后清空对话显示并启动高度监听器 + nextTick(() => { + messages.value = []; + // 启动页面高度监听器,实时监听内容变化并自动滚动 + startHeightObserver(); + // 立即滚动到底部 + scrollToBottom(); + }); console.log('请求工作流接口失败,但恢复显示之前的股票数据'); // 立即渲染之前股票的图表,提升用户体验 nextTick(() => { @@ -1748,7 +1946,7 @@ async function handleSendMessage(input, onComplete) { const aiMessage = reactive({ sender: 'ai', text: '请求工作流接口失败,请检查网络连接' }); messages.value.push(aiMessage); - + // 将AI消息添加到emotion store中 emotionStore.addConversation({ sender: 'ai', @@ -1794,7 +1992,7 @@ async function fetchData(code, market, stockName, queryText, stockId) { `${APIurl}/api/workflow/getStockData`, stockDataParams, { - headers: { + headers: { "Content-Type": "application/json", }, } @@ -1815,6 +2013,14 @@ async function fetchData(code, market, stockName, queryText, stockId) { // 如果有之前的股票数据,恢复显示状态;否则设置为false if (emotionStore.stockList.length > 0 && emotionStore.activeStock) { isPageLoaded.value = true; + // 使用nextTick确保DOM更新后清空对话显示并启动高度监听器 + nextTick(() => { + messages.value = []; + // 启动页面高度监听器,实时监听内容变化并自动滚动 + startHeightObserver(); + // 立即滚动到底部 + scrollToBottom(); + }); console.log('数据验证失败,但恢复显示之前的股票数据'); // 立即渲染之前股票的图表,提升用户体验 nextTick(() => { @@ -1830,7 +2036,7 @@ async function fetchData(code, market, stockName, queryText, stockId) { text: `数据丢失了,请稍后重试。` }); messages.value.push(aiMessage); - + // 将AI消息添加到emotion store中 emotionStore.addConversation({ sender: 'ai', @@ -1862,6 +2068,14 @@ async function fetchData(code, market, stockName, queryText, stockId) { // 如果有之前的股票数据,恢复显示状态;否则设置为false if (emotionStore.stockList.length > 0 && emotionStore.activeStock) { isPageLoaded.value = true; + // 使用nextTick确保DOM更新后清空对话显示并启动高度监听器 + nextTick(() => { + messages.value = []; + // 启动页面高度监听器,实时监听内容变化并自动滚动 + startHeightObserver(); + // 立即滚动到底部 + scrollToBottom(); + }); console.log('API请求失败,但恢复显示之前的股票数据'); // 立即渲染之前股票的图表,提升用户体验 nextTick(() => { @@ -1873,15 +2087,15 @@ async function fetchData(code, market, stockName, queryText, stockId) { } const aiMessage = reactive({ sender: 'ai', text: '图表数据请求失败,请检查网络连接' }); - messages.value.push(aiMessage); - - // 将AI消息添加到emotion store中 - emotionStore.addConversation({ - sender: 'ai', - text: '图表数据请求失败,请检查网络连接', - timestamp: new Date().toISOString() - }); - return false; // 返回失败标识 + messages.value.push(aiMessage); + + // 将AI消息添加到emotion store中 + emotionStore.addConversation({ + sender: 'ai', + text: '图表数据请求失败,请检查网络连接', + timestamp: new Date().toISOString() + }); + return false; // 返回失败标识 } } catch (error) { // 关闭加载状态 @@ -1890,6 +2104,14 @@ async function fetchData(code, market, stockName, queryText, stockId) { // 如果有之前的股票数据,恢复显示状态;否则设置为false if (emotionStore.stockList.length > 0 && emotionStore.activeStock) { isPageLoaded.value = true; + // 使用nextTick确保DOM更新后清空对话显示并启动高度监听器 + nextTick(() => { + messages.value = []; + // 启动页面高度监听器,实时监听内容变化并自动滚动 + startHeightObserver(); + // 立即滚动到底部 + scrollToBottom(); + }); console.log('网络异常,但恢复显示之前的股票数据'); // 立即渲染之前股票的图表,提升用户体验 nextTick(() => { @@ -1902,7 +2124,7 @@ async function fetchData(code, market, stockName, queryText, stockId) { const aiMessage = reactive({ sender: 'ai', text: '图表数据请求失败,请检查网络连接' }); messages.value.push(aiMessage); - + // 将AI消息添加到emotion store中 emotionStore.addConversation({ sender: 'ai', @@ -1983,7 +2205,7 @@ function hasValidData(obj) { // 依次渲染图表的方法 - 支持多个股票 async function renderChartsSequentially(clonedData, stockIndex = 0) { console.log(`开始渲染第${stockIndex}个股票的图表`); - + // 定义图表渲染顺序和配置 const chartConfigs = [ { @@ -2021,12 +2243,15 @@ async function renderChartsSequentially(clonedData, stockIndex = 0) { if (config.ref && config.visibility) { console.log(`开始渲染第${stockIndex}个股票的${config.name}图表`); console.log(`${config.name}Ref方法:`, typeof config.ref[config.method]); - + if (typeof config.ref[config.method] === 'function') { try { + // 等待DOM元素完全渲染 + await new Promise(resolve => setTimeout(resolve, 100)); + config.ref[config.method](...config.params); console.log(`第${stockIndex}个股票的${config.name}图表渲染成功`); - + // 每个图表渲染完成后等待一段时间再渲染下一个 await new Promise(resolve => setTimeout(resolve, 800)); } catch (error) { @@ -2039,7 +2264,7 @@ async function renderChartsSequentially(clonedData, stockIndex = 0) { console.log(`第${stockIndex}个股票的${config.name}图表未渲染,ref存在:`, !!config.ref, '数据存在:', config.visibility); } } - + console.log(`第${stockIndex}个股票的所有图表依次渲染完成`); } @@ -2061,7 +2286,7 @@ function renderCharts(data) { text: `数据不完整,缺少以下关键数据:${validation.missingFields.join('、')}。请稍后重试或联系客服。` }); messages.value.push(aiMessage); - + // 将AI消息添加到emotion store中 emotionStore.addConversation({ sender: 'ai', @@ -2136,7 +2361,7 @@ function renderCharts(data) { console.error('图表渲染错误:', error); const aiMessage = reactive({ sender: 'ai', text: '图表渲染失败,请重试' }); messages.value.push(aiMessage); - + // 将AI消息添加到emotion store中 emotionStore.addConversation({ sender: 'ai', @@ -2229,37 +2454,52 @@ function setupIntersectionObserver() { if (parsedConclusion.value && stockCode) { // 检查该股票是否是第一次触发 if (!stockTypewriterShown.value.has(stockCode)) { - // 该股票第一次进入视口:只显示文本,不自动播放音频和打字机效果 - console.log('该股票第一次进入场景应用,直接显示完整内容,不自动播放'); - - // 直接显示完整内容,不使用打字机效果 - const conclusion = parsedConclusion.value; - displayedTexts.value = { - one1: conclusion.one1 || '', - one2: conclusion.one2 || '', - two: conclusion.two || '', - three: conclusion.three || '', - four: conclusion.four || '', - disclaimer: '该内容由AI生成,请注意甄别' - }; - displayedTitles.value = { - one: 'L1: 情绪监控', - two: 'L2: 情绪解码', - three: 'L3: 情绪推演', - four: 'L4: 情绪套利' - }; - // 显示所有有内容的模块 - moduleVisibility.value = { - one: !!(conclusion.one1 || conclusion.one2), - two: !!conclusion.two, - three: !!conclusion.three, - four: !!conclusion.four, - disclaimer: true - }; - - // 记录该股票已显示过,但不播放音频 - stockTypewriterShown.value.set(stockCode, true); - stockAudioPlayed.value.set(stockCode, true); // 标记为已播放,避免后续自动播放 + // 如果是用户主动搜索,启动打字机效果和音频播放 + if (isUserInitiated.value && audioUrl.value) { + console.log('用户主动搜索,该股票第一次进入场景应用,开始打字机效果和音频播放'); + + if (!stockAudioPlayed.value.has(stockCode)) { + console.log('开始音频播放和打字机效果'); + stockAudioPlayed.value.set(stockCode, true); + playAudioQueue(parsedConclusion.value, true); + } else { + // 如果音频已播放过,只启动打字机效果 + startTypewriterEffect(parsedConclusion.value); + } + + stockTypewriterShown.value.set(stockCode, true); + } else { + // 非用户主动搜索(如历史记录恢复),直接显示完整内容 + console.log('非用户主动搜索,该股票第一次进入场景应用,直接显示完整内容'); + + const conclusion = parsedConclusion.value; + displayedTexts.value = { + one1: conclusion.one1 || '', + one2: conclusion.one2 || '', + two: conclusion.two || '', + three: conclusion.three || '', + four: conclusion.four || '', + disclaimer: '该内容由AI生成,请注意甄别' + }; + displayedTitles.value = { + one: 'L1: 情绪监控', + two: 'L2: 情绪解码', + three: 'L3: 情绪推演', + four: 'L4: 情绪套利' + }; + // 显示所有有内容的模块 + moduleVisibility.value = { + one: !!(conclusion.one1 || conclusion.one2), + two: !!conclusion.two, + three: !!conclusion.three, + four: !!conclusion.four, + disclaimer: true + }; + + // 记录该股票已显示过 + stockTypewriterShown.value.set(stockCode, true); + stockAudioPlayed.value.set(stockCode, true); + } } else { // 非第一次或已经触发过:直接显示完整内容,不播放音频和打字机效果 console.log('非第一次进入场景应用或已触发过,直接显示完整内容'); @@ -2328,6 +2568,131 @@ const scrollToTop = () => { }, 1000); }; +// 页面高度监听器 +const heightObserver = ref(null); +const isAutoScrollEnabled = ref(false); + +// 滚动到底部功能 +const scrollToBottom = () => { + // 使用nextTick确保DOM已更新 + nextTick(() => { + // 获取页面的总高度 + const documentHeight = Math.max( + document.body.scrollHeight, + document.body.offsetHeight, + document.documentElement.clientHeight, + document.documentElement.scrollHeight, + document.documentElement.offsetHeight + ); + + // 平滑滚动到页面底部 + window.scrollTo({ + top: documentHeight, + behavior: 'smooth' + }); + + // 备用方案:直接设置滚动位置 + setTimeout(() => { + document.documentElement.scrollTop = documentHeight; + document.body.scrollTop = documentHeight; + }, 1000); + }); +}; + +// 防抖滚动函数 +const debouncedScrollToBottom = (() => { + let timeoutId = null; + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(() => { + if (isAutoScrollEnabled.value && isPageLoaded.value) { + scrollToBottom(); + } + }, 150); + }; +})(); + +// 启动页面高度监听器 +const startHeightObserver = () => { + // 先停止之前的监听器 + stopHeightObserver(); + + isAutoScrollEnabled.value = true; + + // 创建ResizeObserver监听页面内容变化 + heightObserver.value = new ResizeObserver((entries) => { + if (isAutoScrollEnabled.value && isPageLoaded.value) { + debouncedScrollToBottom(); + } + }); + + // 监听document.body的尺寸变化 + if (document.body) { + heightObserver.value.observe(document.body); + } + + // 创建MutationObserver监听DOM结构变化 + const mutationObserver = new MutationObserver((mutations) => { + let shouldScroll = false; + mutations.forEach((mutation) => { + if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { + // 检查新增的节点是否包含实际内容 + const hasContent = Array.from(mutation.addedNodes).some(node => { + if (node.nodeType === Node.ELEMENT_NODE) { + return node.offsetHeight > 0 || node.scrollHeight > 0; + } + return node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0; + }); + if (hasContent) { + shouldScroll = true; + } + } + }); + + if (shouldScroll && isAutoScrollEnabled.value && isPageLoaded.value) { + debouncedScrollToBottom(); + } + }); + + // 监听主要内容区域的DOM变化 + const mainContainer = document.querySelector('.main') || document.body; + if (mainContainer) { + mutationObserver.observe(mainContainer, { + childList: true, + subtree: true, + attributes: false, + characterData: true + }); + } + + // 保存mutationObserver引用以便清理 + heightObserver.value.mutationObserver = mutationObserver; + + console.log('页面高度监听器已启动'); +}; + +// 停止页面高度监听器 +const stopHeightObserver = () => { + isAutoScrollEnabled.value = false; + + if (heightObserver.value) { + // 清理ResizeObserver + heightObserver.value.disconnect(); + + // 清理MutationObserver + if (heightObserver.value.mutationObserver) { + heightObserver.value.mutationObserver.disconnect(); + heightObserver.value.mutationObserver = null; + } + + heightObserver.value = null; + } + + console.log('页面高度监听器已停止'); +}; + // 监听页面滚动,控制返回顶部按钮显示 const handlePageScroll = () => { const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; @@ -2351,7 +2716,7 @@ onMounted(async () => { loadConversationsFromStore(); // 从stockList加载对话记录 loadConversationsFromStockList(); - + // 确保获取用户次数 // try { // await chatStore.getUserCount(); @@ -2453,6 +2818,14 @@ onMounted(async () => { // 恢复页面加载状态 isPageLoaded.value = true; + // 使用nextTick确保DOM更新后清空对话显示并启动高度监听器 + nextTick(() => { + messages.value = []; + // 启动页面高度监听器,实时监听内容变化并自动滚动 + startHeightObserver(); + // 立即滚动到底部 + scrollToBottom(); + }); // 等待DOM渲染后恢复图表和数据 nextTick(() => { @@ -2464,7 +2837,7 @@ onMounted(async () => { // 恢复结论数据并显示内容 if (currentStockData.conclusionData) { conclusionData.value = currentStockData.conclusionData; - + // 直接显示所有内容,不使用打字机效果 const conclusion = currentStockData.conclusionData; displayedTexts.value = { @@ -2475,14 +2848,14 @@ onMounted(async () => { four: conclusion.four || '', disclaimer: '该内容由AI生成,请注意甄别' }; - + displayedTitles.value = { one: conclusion.one1 || conclusion.one2 ? 'L1: 情绪监控' : '', two: conclusion.two ? 'L2: 情绪解码' : '', three: conclusion.three ? 'L3: 情绪推演' : '', four: conclusion.four ? 'L4: 情绪套利' : '' }; - + moduleVisibility.value = { one: !!(conclusion.one1 || conclusion.one2), two: !!conclusion.two, @@ -2490,7 +2863,7 @@ onMounted(async () => { four: !!conclusion.four, disclaimer: true }; - + // 标记该股票的打字机效果和音频已经显示过,避免后续自动触发 const stockCode = currentStockData.stockInfo?.code || currentStockData.stockInfo?.symbol; if (stockCode) { @@ -2526,6 +2899,9 @@ onUnmounted(() => { hasTriggeredAudio.value = false; hasTriggeredTypewriter.value = false; + // 清理页面高度监听器 + stopHeightObserver(); + // 清理Intersection Observer if (intersectionObserver.value) { intersectionObserver.value.disconnect(); @@ -3150,9 +3526,11 @@ const emit = defineEmits(['updateMessage', 'sendMessage', 'ensureAIchat']); 0% { transform: scale(1); } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); } @@ -3191,9 +3569,12 @@ const emit = defineEmits(['updateMessage', 'sendMessage', 'ensureAIchat']); } @keyframes float { - 0%, 100% { + + 0%, + 100% { transform: translateY(0px); } + 50% { transform: translateY(-5px); } @@ -3836,6 +4217,43 @@ const emit = defineEmits(['updateMessage', 'sendMessage', 'ensureAIchat']); letter-spacing: 1px; } */ +/* 对话区域样式 */ +.conversation-area { + width: 100%; + padding: 0 20px; +} + +.message-list { + display: flex; + flex-direction: column; + gap: 15px; +} + +.message-item { + width: 100%; + display: flex; +} + +.user-message-item { + justify-content: flex-end; +} + +.ai-message-item { + justify-content: flex-start; +} + +.user-message-wrapper { + display: flex; + justify-content: flex-end; + max-width: 70%; +} + +.ai-message-wrapper { + display: flex; + justify-content: flex-start; + max-width: 80%; +} + /* 顶部锚点样式 */ .top-anchor { position: relative; @@ -3881,6 +4299,18 @@ const emit = defineEmits(['updateMessage', 'sendMessage', 'ensureAIchat']); transform: translateY(-1px); } +/* 页面主容器样式 */ +.page-container { + position: relative; + width: 100%; + min-height: 100vh; +} + +.master:last-child { + border-bottom: none; + margin-bottom: 0; +} + /* class01容器样式 */ .main { position: relative; diff --git a/src/views/components/HistoryRecord.vue b/src/views/components/HistoryRecord.vue index c9a5e15..bf5bced 100644 --- a/src/views/components/HistoryRecord.vue +++ b/src/views/components/HistoryRecord.vue @@ -541,12 +541,35 @@ const openDetail = (record) => { const historyData = ref({}); const selectRecord = async (record) => { try { + selectedRecordId.value = record.id; const result = await clickRecordAPI({ model: props.currentType == "AIchat" ? 1 : 2, parentId: record.parentId, recordId: record.id, }); - historyData.value; + + if (result && result.data) { + historyData.value = result.data; + + // 构造股票数据对象,保持与现有结构一致 + const stockData = { + queryText: record.stockCode || record.stockName || '', // 使用记录中的股票代码或名称作为查询文本 + stockInfo: { + name: result.data.stockData?.stockName || record.stockName || '', + code: record.stockCode || '', + market: record.stockMarket || 'cn' + }, + apiData: result.data.stockData || {}, // 图表数据 + conclusionData: result.data.wokeFlowData?.One || {}, // 场景应用的结论和音频 + timestamp: new Date().toISOString() + }; + + // 通过emit将数据传递给父组件 + emit('selectRecord', stockData); + console.log('历史记录数据已发送给父组件:', stockData); + } else { + console.error('历史记录数据格式不正确:', result); + } } catch (e) { console.error("获取历史记录数据失败", e); } diff --git a/src/views/components/emoEnergyConverter.vue b/src/views/components/emoEnergyConverter.vue index 3ea78ff..9019815 100644 --- a/src/views/components/emoEnergyConverter.vue +++ b/src/views/components/emoEnergyConverter.vue @@ -303,6 +303,12 @@ function initQXNLZHEcharts(kline, qxnlzhqData) { } }); + // 检查DOM元素是否存在 + if (!qxnlzhqEchartsRef.value) { + console.error('emoEnergyConverter: DOM元素未找到,无法初始化图表'); + return; + } + // 初始化图表 qxnlzhqEchartsInstance = echarts.init(qxnlzhqEchartsRef.value); let option; diff --git a/src/views/homePage.vue b/src/views/homePage.vue index c939dac..b0e51bc 100644 --- a/src/views/homePage.vue +++ b/src/views/homePage.vue @@ -240,15 +240,22 @@ const enableInput = () => { }; // 处理历史记录选择 -const handleHistorySelect = (record) => { - // 设置输入框内容 - message.value = record.question; - - // 如果记录类型与当前激活的tab不同,切换到对应的tab - if (record.type !== activeTab.value) { - const tabIndex = record.type === "AIchat" ? 0 : 1; - setActiveTab(record.type, tabIndex); +const handleHistorySelect = (stockData) => { + console.log('接收到历史记录数据:', stockData); + + // 如果当前不在AiEmotion页面,切换到AiEmotion页面 + if (activeTab.value !== 'AiEmotion') { + setActiveTab('AiEmotion', 1); } + + // 等待组件渲染完成后调用addStock方法 + nextTick(() => { + if (aiEmotionRef.value && aiEmotionRef.value.addStock) { + aiEmotionRef.value.addStock(stockData); + } else { + console.error('AiEmotion组件或addStock方法不可用'); + } + }); }; // 公告 @@ -917,9 +924,9 @@ onUnmounted(() => { /* 添加平滑滚动效果 */ } -.pcTabContent { +/* .pcTabContent { margin: 0 6%; -} +} */ @media (max-width: 768px) { .tab-container {