diff --git a/src/store/emotionAudio.js b/src/store/emotionAudio.js index 9c707a5..9cbd440 100644 --- a/src/store/emotionAudio.js +++ b/src/store/emotionAudio.js @@ -1,4 +1,5 @@ import { defineStore } from 'pinia' +import { Howl } from 'howler' export const useEmotionAudioStore = defineStore('emotionAudio', { state: () => ({ @@ -21,6 +22,11 @@ export const useEmotionAudioStore = defineStore('emotionAudio', { }, // 播放控制 play() { + // 如果没有音频实例但有音频URL,先创建实例 + if (!this.soundInstance && this.currentAudioUrl) { + this.createAudioInstance(this.currentAudioUrl) + } + if (this.soundInstance) { if (this.isPaused && this.playbackPosition > 0) { // 从暂停位置继续播放 @@ -83,6 +89,50 @@ export const useEmotionAudioStore = defineStore('emotionAudio', { this.ttsUrl = '' this.soundInstance = null this.nowSound = '' + }, + // 创建音频实例 + createAudioInstance(url) { + // 停止之前的音频 + if (this.soundInstance) { + this.soundInstance.stop() + } + + // 创建新的音频实例 + const newSound = new Howl({ + src: [url], + html5: true, + format: ['mp3', 'wav'], + onplay: () => { + this.isPlaying = true + console.log('音频开始播放') + }, + onend: () => { + this.isPlaying = false + this.isPaused = false + this.playbackPosition = 0 + console.log('音频播放结束') + }, + onstop: () => { + this.isPlaying = false + console.log('音频播放停止') + }, + onpause: () => { + this.isPlaying = false + console.log('音频播放暂停') + }, + onerror: (error) => { + this.isPlaying = false + console.error('音频播放错误:', error) + }, + onload: () => { + this.duration = newSound.duration() + console.log('音频加载完成,时长:', this.duration) + } + }) + + // 保存音频实例 + this.soundInstance = newSound + this.nowSound = newSound } } }) diff --git a/src/views/AiEmotion.vue b/src/views/AiEmotion.vue index b79b692..9ad3bad 100644 --- a/src/views/AiEmotion.vue +++ b/src/views/AiEmotion.vue @@ -247,6 +247,7 @@ const scenarioApplicationRef = ref(null); // 场景应用部分的引用 const hasTriggeredAudio = ref(false); // 是否已触发音频播放 const hasTriggeredTypewriter = ref(false); // 是否已触发打字机效果 const intersectionObserver = ref(null); // 存储observer实例 +const isUserInitiated = ref(false); // 标记是否为用户主动搜索 // 显示的文本内容(用于打字机效果) const displayedTexts = ref({ @@ -325,8 +326,8 @@ watch(currentStock, (newStock) => { stopAudio(); // 清理音频URL,确保不会播放之前股票的音频 audioUrl.value = ''; - // 只停止音频播放,不清理currentAudioUrl,让新音频能正确设置 - emotionAudioStore.stop(); + // 清理store中的音频URL,确保不会播放之前股票的音频 + emotionAudioStore.resetAudioState(); // 清理正在进行的打字机效果定时器 clearTypewriterTimers(); // 重置触发状态,让每个股票都能独立触发效果 @@ -365,8 +366,7 @@ watch(currentStock, (newStock) => { disclaimer: true }; - // 重要:即使已经显示过,也要播放当前股票的音频 - // 提取音频URL并播放 + // 提取音频URL但不自动播放,等待用户手动点击 let voiceUrl = null; if (conclusion.url) { voiceUrl = conclusion.url.toString().trim().replace(/[`\s]/g, ''); @@ -381,10 +381,11 @@ watch(currentStock, (newStock) => { } if (voiceUrl && voiceUrl.startsWith('http')) { - console.log('切换到已显示股票,播放对应音频:', voiceUrl); + console.log('切换到已显示股票,准备音频URL但不自动播放:', voiceUrl); audioUrl.value = voiceUrl; - // 立即播放当前股票的音频 - playAudio(voiceUrl); + // 同时更新store中的音频URL + emotionAudioStore.setCurrentAudioUrl(voiceUrl); + // 不自动播放,等待用户手动点击 } } catch (error) { console.error('解析结论数据失败:', error); @@ -413,6 +414,34 @@ watch(currentStock, (newStock) => { four: false, disclaimer: false }; + + // 即使没有显示过,也需要设置音频URL以便用户手动播放 + if (newStock.conclusionData) { + try { + const conclusion = JSON.parse(newStock.conclusionData); + let voiceUrl = null; + if (conclusion.url) { + voiceUrl = conclusion.url.toString().trim().replace(/[`\s]/g, ''); + } else if (conclusion.audioUrl) { + voiceUrl = conclusion.audioUrl.toString().trim().replace(/[`\s]/g, ''); + } else if (conclusion.voice_url) { + voiceUrl = conclusion.voice_url.toString().trim().replace(/[`\s]/g, ''); + } else if (conclusion.audio) { + voiceUrl = conclusion.audio.toString().trim().replace(/[`\s]/g, ''); + } else if (conclusion.tts_url) { + voiceUrl = conclusion.tts_url.toString().trim().replace(/[`\s]/g, ''); + } + + if (voiceUrl && voiceUrl.startsWith('http')) { + console.log('切换到未显示股票,准备音频URL:', voiceUrl); + audioUrl.value = voiceUrl; + // 同时更新store中的音频URL + emotionAudioStore.setCurrentAudioUrl(voiceUrl); + } + } catch (error) { + console.error('解析结论数据失败:', error); + } + } } // 只有在页面已加载的情况下才渲染图表 @@ -523,37 +552,13 @@ watch(parsedConclusion, (newConclusion) => { if (voiceUrl && voiceUrl.startsWith('http')) { console.log('找到并清理后的语音URL:', voiceUrl); audioUrl.value = voiceUrl; + // 同时更新store中的音频URL + emotionAudioStore.setCurrentAudioUrl(voiceUrl); console.log('音频URL已准备,检查是否需要立即触发效果'); - // 音频准备好后,如果页面已加载则立即触发效果 - nextTick(() => { - setTimeout(() => { - if (currentStock.value?.stockInfo && isPageLoaded.value) { - const stockCode = currentStock.value.stockInfo.code || currentStock.value.stockInfo.symbol; - - if (parsedConclusion.value && stockCode) { - // 如果该股票已经显示过,不需要再处理 - if (stockTypewriterShown.value.has(stockCode)) { - return; - } - - // 该股票第一次:播放音频和打字机效果 - console.log('音频准备完成且页面已加载,立即触发效果'); - hasTriggeredTypewriter.value = true; - hasTriggeredAudio.value = true; - - startTypewriterEffect(parsedConclusion.value); - - if (!stockAudioPlayed.value.has(stockCode)) { - console.log('立即开始音频播放'); - stockAudioPlayed.value.set(stockCode, true); - playAudio(audioUrl.value); - } - stockTypewriterShown.value.set(stockCode, true); - } - } - }, 100); // 短暂延迟确保DOM更新完成 - }); + // 音频准备好后,只有在用户主动搜索时才自动触发效果 + // 数据恢复时不自动播放音频和打字机效果 + console.log('音频URL已准备完成,等待用户手动触发播放'); } else { console.log('未找到有效的语音URL,原始URL:', newConclusion.url); console.log('结论数据中的所有字段:', Object.keys(newConclusion)); @@ -806,6 +811,8 @@ function startImageRotation() { // 发送消息方法 async function handleSendMessage(input) { console.log("发送内容:", input); + // 标记为用户主动搜索 + isUserInitiated.value = true; // 检查用户输入内容是否为空 if (!input || !input.trim()) { @@ -931,11 +938,11 @@ async function handleSendMessage(input) { renderCharts(currentStock.value.apiData); console.log('数据加载完成后开始渲染图表'); - // 数据加载完成后立即触发音频和文本,不等待滚动到视口 - if (parsedConclusion.value && audioUrl.value) { + // 只有在用户主动搜索时才自动触发音频和文本 + if (isUserInitiated.value && parsedConclusion.value && audioUrl.value) { const stockCode = currentStock.value.stockInfo?.code || currentStock.value.stockInfo?.symbol; if (stockCode && !stockTypewriterShown.value.has(stockCode)) { - console.log('数据加载完成,立即触发音频和打字机效果'); + console.log('用户主动搜索,立即触发音频和打字机效果'); startTypewriterEffect(parsedConclusion.value); if (!stockAudioPlayed.value.has(stockCode)) { @@ -945,6 +952,9 @@ async function handleSendMessage(input) { stockTypewriterShown.value.set(stockCode, true); } } + + // 重置用户主动搜索标志 + isUserInitiated.value = false; } }); } else { @@ -1189,28 +1199,37 @@ function setupIntersectionObserver() { if (parsedConclusion.value && stockCode) { // 检查该股票是否是第一次触发 if (!stockTypewriterShown.value.has(stockCode)) { - // 该股票第一次:播放音频和打字机效果 - if (audioUrl.value) { - console.log('该股票第一次进入场景应用,开始打字机效果和音频播放'); - hasTriggeredTypewriter.value = true; - hasTriggeredAudio.value = true; - - // 开始打字机效果 - startTypewriterEffect(parsedConclusion.value); - - // 播放音频 - if (!stockAudioPlayed.value.has(stockCode)) { - console.log('开始音频播放'); - stockAudioPlayed.value.set(stockCode, true); - playAudio(audioUrl.value); - } + // 该股票第一次进入视口:只显示文本,不自动播放音频和打字机效果 + 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); - } else { - console.log('音频尚未准备好,等待音频加载完成后再触发效果'); - return; - } + // 记录该股票已显示过,但不播放音频 + stockTypewriterShown.value.set(stockCode, true); + stockAudioPlayed.value.set(stockCode, true); // 标记为已播放,避免后续自动播放 } else { // 非第一次或已经触发过:直接显示完整内容,不播放音频和打字机效果 console.log('非第一次进入场景应用或已触发过,直接显示完整内容'); @@ -1285,11 +1304,40 @@ onMounted(async () => { startImageRotation(); - // 等待DOM完全渲染后设置监听器 - nextTick(() => { - setupIntersectionObserver(); - // 移除自动滚动触发,改为在第二个工作流接口调用时触发 - }); + // 检查是否有已保存的股票数据需要恢复 + if (emotionStore.stockList.length > 0 && emotionStore.activeStock) { + console.log('检测到已保存的股票数据,开始恢复页面状态(不自动播放音频)'); + + // 恢复页面加载状态 + isPageLoaded.value = true; + + // 等待DOM渲染后恢复图表和数据 + nextTick(() => { + const currentStockData = emotionStore.activeStock; + if (currentStockData && currentStockData.apiData) { + console.log('恢复图表数据:', currentStockData.stockInfo.name); + renderCharts(currentStockData.apiData); + + // 恢复结论数据但不触发音频和打字机效果 + if (currentStockData.conclusionData) { + conclusionData.value = currentStockData.conclusionData; + // 标记该股票的打字机效果和音频已经显示过,避免后续自动触发 + const stockCode = currentStockData.stockInfo?.code || currentStockData.stockInfo?.symbol; + if (stockCode) { + stockTypewriterShown.value.set(stockCode, true); + stockAudioPlayed.value.set(stockCode, true); + } + } + } + + setupIntersectionObserver(); + }); + } else { + // 没有保存的数据,正常设置监听器 + nextTick(() => { + setupIntersectionObserver(); + }); + } }); // 组件卸载时清理定时器、音频和observer