|
|
<template> <!-- 顶部锚点 --> <div id="top-anchor" class="top-anchor"></div> <div class="ai-emotion-container" ref="userInputDisplayRef"> <!-- 金轮 --> <div class="golden-wheel"> <img src="@/assets/img/AiEmotion/金轮.png" class="golden-wheel-img" alt="金轮图标" :class="{ 'rotating-image': isRotating }" /> </div>
<!-- 消息显示区域 --> <div class="user-input-display"> <div v-for="(message, index) in messages" :key="index" class="message-container"> <!-- 用户输入内容 --> <div v-if="message.sender === 'user'" class="message-bubble user-message"> {{ message.text }} </div> <!-- AI返回结果 --> <div v-if="message.sender === 'ai'" class="message-bubble ai-message"> {{ message.text }} </div> </div> </div> </div> <!-- 加载提示 --> <div v-if="isLoading" class="loading-container"> <div class="loading-content"> <div class="loading-spinner"></div> <div class="loading-text">AI情绪大模型正在努力为您加载,请稍候...</div> </div> </div> <!-- 股票标签页 --> <StockTabs />
<!-- 渲染整个页面 --> <div v-if="isPageLoaded" class="class01"> <div class="class00"> <!-- 四维矩阵图 --> <div class="class02"> <div class="container"> <!-- <img class="item" :src="item" alt="思维矩阵图片" /> --> <div class="span01"> {{ stockName }}{{ stockName ? '量子四维矩阵图' : '' }} </div> </div> <span class="span02">{{ displayDate }}</span> </div> <div class="class0201" v-if="chartVisibility.marketTemperature"> <img src="@/assets/img/AiEmotion/L1.png" alt="情绪监控图标"> </div> <!-- 温度计图表 --> <div class="class03" v-if="chartVisibility.marketTemperature"> <div class="class003"> <div class="content1"> <img class="img01" src="@/assets/img/AiEmotion/温度计.png" alt="温度计图标"> <span class="title1">股票温度计</span> </div> <div class="div00"> <div class="div01">股票温度:{{ data2 ?? "NA" }}</div> <div class="div02">市场温度:{{ data1 }}</div> </div> </div> <marketTemperature ref="marketTemperatureRef" /> </div> </div> <div class="class0301" v-if="chartVisibility.emotionDecod"> <img src="@/assets/img/AiEmotion/L2.png" alt="情绪解码图标"> </div> <!-- 情绪解码器图表 --> <div class="class04" v-if="chartVisibility.emotionDecod"> <div class="class0401"> <img class="img02" src='@/assets/img/AiEmotion/emotionDecod.png' alt="情绪解码器图标"> <span class="title2">情绪解码器</span> </div> <div class="class0402"> <emotionDecod ref="emotionDecodRef"></emotionDecod> </div> </div> <div class="class0403" v-if="chartVisibility.emotionalBottomRadar"> <img src="@/assets/img/AiEmotion/L3.png" alt="情绪推演图标"> </div> <!-- 情绪探底雷达图表 --> <div class="class05" v-if="chartVisibility.emotionalBottomRadar"> <div class="class0502"> <img class="img03" src="@/assets/img/AiEmotion/探底雷达.png" alt="探底雷达图表"> <span class="title3">情绪探底雷达</span> </div> <div class="class0503"> <emotionalBottomRadar ref="emotionalBottomRadarRef"></emotionalBottomRadar> </div> </div> <div class="class0501" v-if="chartVisibility.emoEnergyConverter"> <img src="@/assets/img/AiEmotion/L4.png" alt="情绪套利"> </div> <!-- 情绪能量转化器图表 --> <div class="class06" v-if="chartVisibility.emoEnergyConverter"> <div class="class0601"> <img class="img04" src="@/assets/img/AiEmotion/能量转化器.png" alt="能量转化器图标"> <span class="title4">情绪能量转化器</span> </div> <div class="class0603"> <emoEnergyConverter ref="emoEnergyConverterRef"></emoEnergyConverter> </div> </div> <!-- 核心看点 --> <div class="class0702"> <img src="@/assets/img/AiEmotion/核心看点.png" alt="核心看点字样"> </div> <div class="bk-image"> <div class="text-container"> <p><span class="title">情绪监控-金融宇宙的【量子检测网络】</span> <span class="content">核心任务:构建全市场情绪引力场雷达,实时监测资金流向和情绪波动</span> </p> <p><span class="title">情绪解码-主力思维的【神经破译矩阵】</span> <span class="content">核心任务:解构资金行为的量子密码,破译主力操盘意图和策略布局</span> </p> <p><span class="title">情绪推演-未来战争的【时空推演舱】</span> <span class="content">核心任务:基于情绪数据推演未来走势,预测市场转折点和机会窗口</span> </p> <p><span class="title">情绪套利-财富裂变的【粒子对撞机】</span> <span class="content">核心任务:将情绪差转化为收益粒子流,实现情绪能量的价值转换</span> </p> </div> </div> <!-- 核心逻辑 --> <div class="class0700"> <img src="@/assets/img/AiEmotion/核心逻辑.png" alt="核心逻辑字样"> </div> <div class="class08"> <div class="lz-img"> <img src="@/assets/img/AiEmotion/量子神经决策树.png" alt="树标题"> </div> <div class="scaled-img"> <!-- <img src="@/assets/img/AiEmotion/tree02.jpg" alt="树图片"> --> </div> </div> <!-- 场景应用 --> <div class="class09" ref="scenarioApplicationRef"> <img src="@/assets/img/AiEmotion/场景应用.png" alt="场景应用标题"> <div class="bk-image"> <div class="conclusion-container" v-if="parsedConclusion"> <div class="conclusion-item" v-if="(parsedConclusion.one1 || parsedConclusion.one2) && moduleVisibility.one"> <h4 class="conclusion-title">{{ displayedTitles.one }}</h4> <p class="conclusion-text" v-if="parsedConclusion.one1">{{ displayedTexts.one1 }}</p> <p class="conclusion-text" v-if="parsedConclusion.one2">{{ displayedTexts.one2 }}</p> </div> <div class="conclusion-item" v-if="parsedConclusion.two && moduleVisibility.two"> <h4 class="conclusion-title">{{ displayedTitles.two }}</h4> <p class="conclusion-text">{{ displayedTexts.two }}</p> </div> <div class="conclusion-item" v-if="parsedConclusion.three && moduleVisibility.three"> <h4 class="conclusion-title">{{ displayedTitles.three }}</h4> <p class="conclusion-text">{{ displayedTexts.three }}</p> </div> <div class="conclusion-item" v-if="parsedConclusion.four && moduleVisibility.four"> <h4 class="conclusion-title">{{ displayedTitles.four }}</h4> <p class="conclusion-text">{{ displayedTexts.four }}</p> </div> <!-- AI生成内容免责声明 --> <div class="disclaimer-item" v-if="parsedConclusion && moduleVisibility.disclaimer"> <p class="disclaimer-text">{{ displayedTexts.disclaimer }}</p> </div> </div> <div class="conclusion-placeholder" v-else> <p>等待股票分析结论...</p> </div> </div> </div>
<!-- 返回顶部按钮 --> <div class="back-to-top" @click="scrollToTop" v-show="isPageLoaded"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M12 4L12 20M12 4L6 10M12 4L18 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> </svg> </div> </div> </template>
<script setup> import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'; import { getReplyAPI, getConclusionAPI } from '@/api/AiEmotionApi.js'; // 导入工作流接口方法
import axios from 'axios'; import item from '@/assets/img/AiEmotion/bk01.png'; // 导入思维矩阵图片
import emotionDecod from '@/views/components/emotionDecod.vue'; // 导入情绪解码组件
import emotionalBottomRadar from '@/views/components/emotionalBottomRadar.vue'; // 导入情绪探底雷达图组件
import emoEnergyConverter from '@/views/components/emoEnergyConverter.vue'; // 导入情绪能量转化器组件
import marketTemperature from '@/views/components/marketTemperature.vue'; import StockTabs from '@/views/components/StockTabs.vue'; // 导入股票标签页组件
import blueBorderImg from '@/assets/img/AiEmotion/blueBorder.png' //导入蓝色背景框图片
import { ElMessage } from 'element-plus'; // 接口失败提示已改为对话形式,保留用于输入验证
import { useEmotionStore } from '@/store/emotion'; // 导入Pinia store
import { useEmotionAudioStore } from '@/store/emotionAudio.js'; // 导入音频store
import { useChatStore } from '@/store/chat.js'; // 导入聊天store
import { Howl, Howler } from 'howler'; // 导入音频播放库
import { reactive } from 'vue'; import { marked } from 'marked'; // 引入marked库
import { useUserStore } from "../store/userPessionCode";
// 使用Pinia store
const emotionStore = useEmotionStore(); const emotionAudioStore = useEmotionAudioStore(); const chatStore = useChatStore(); // 获取权限
const userStore = useUserStore(); // 处理refuse数据的函数
function processRefuseMessage(refuseData) { if (!refuseData) return '未知错误';
// 如果refuse数据包含Markdown格式,进行转换
try { // 配置marked选项
marked.setOptions({ breaks: true, // 支持换行符转换为 <br>
gfm: true, // 启用 GitHub Flavored Markdown
sanitize: false, // 不清理 HTML
smartLists: true, // 智能列表
smartypants: true, // 智能标点符号
xhtml: false, // 不使用 XHTML 输出
});
// 将Markdown转换为HTML
const htmlContent = marked(refuseData);
// 移除HTML标签,只保留纯文本用于ElMessage显示
const tempDiv = document.createElement('div'); tempDiv.innerHTML = htmlContent; return tempDiv.textContent || tempDiv.innerText || refuseData; } catch (error) { console.error('处理refuse消息时出错:', error); return refuseData; } }
// 组件引用
const marketTemperatureRef = ref(null); // 引用市场温度计组件
const emoEnergyConverterRef = ref(null) const emotionDecodRef = ref(null) const emotionalBottomRadarRef = ref(null) const userInputDisplayRef = ref(null);//消息区域的引用
// 响应式数据
const messages = ref([]); const isPageLoaded = ref(false); // 控制页面是否显示
const isLoading = ref(false); // 控制加载状态
const isRotating = ref(false);//控制旋转
const version1 = ref(1); // 版本号
const conclusionData = ref(''); // 存储第二个工作流接口返回的结论数据
// 自动滚动相关数据
const isAutoScrolling = ref(false); const currentSection = ref(0); const sectionRefs = ref([]); const scenarioApplicationRef = ref(null); // 场景应用部分的引用
const hasTriggeredAudio = ref(false); // 是否已触发音频播放
const hasTriggeredTypewriter = ref(false); // 是否已触发打字机效果
const intersectionObserver = ref(null); // 存储observer实例
const isUserInitiated = ref(false); // 标记是否为用户主动搜索
// 显示的文本内容(用于打字机效果)
const displayedTexts = ref({ one1: '', one2: '', two: '', three: '', four: '', disclaimer: '' });
// 显示的标题内容(用于打字机效果)
const displayedTitles = ref({ one: '', two: '', three: '', four: '' });
// 模块显示状态
const moduleVisibility = ref({ one: false, two: false, three: false, four: false, disclaimer: false });
// 图表组件显示状态
const chartVisibility = ref({ marketTemperature: false, emotionDecod: false, emotionalBottomRadar: false, emoEnergyConverter: false }); const typewriterTimers = ref([]); // 记录每个股票是否已经显示过打字机效果
const stockTypewriterShown = ref(new Map()); // 记录每个股票是否已经播放过音频
const stockAudioPlayed = ref(new Map()); // 存储当前的完成回调函数
const currentOnCompleteCallback = ref(null);
// 音频播放相关数据
const audioUrl = ref(''); const isAudioPlaying = ref(false);
// 返回顶部按钮相关数据
const showBackToTop = ref(false);
// 计算属性 - 从store获取当前股票数据
const currentStock = computed(() => emotionStore.activeStock); const stockName = computed(() => currentStock.value?.stockInfo.name || ""); const displayDate = computed(() => { if (!currentStock.value?.apiData) return ""; const lastData = currentStock.value.apiData.GSWDJ?.at(-1); if (!lastData || !lastData[0]) return "";
const dateStr = lastData[0]; // 假设原格式为 YYYY-MM-DD 或 YYYY/MM/DD
const dateMatch = dateStr.match(/(\d{4})[\-\/](\d{1,2})[\-\/](\d{1,2})/); if (dateMatch) { const [, year, month, day] = dateMatch; // 转换为 DD/MM/YYYY 格式
return `更新时间:${day.padStart(2, '0')}/${month.padStart(2, '0')}/${year}`; }
// 如果不匹配预期格式,返回原始值
return dateStr; }); const data1 = computed(() => { if (!currentStock.value?.apiData) return null; const lastData = currentStock.value.apiData.GSWDJ?.at(-1); return lastData ? Math.round(lastData[1]) : null; }); const data2 = computed(() => { if (!currentStock.value?.apiData) return null; const lastData = currentStock.value.apiData.GSWDJ?.at(-1); return lastData ? Math.round(lastData[2]) : null; }); const currentConclusion = computed(() => { return currentStock.value?.conclusionData || ''; }); const parsedConclusion = computed(() => { if (!currentConclusion.value) return null; try { return JSON.parse(currentConclusion.value); } catch (error) { console.error('解析结论数据失败:', error); return null; } });
// 监听股票列表变化,当列表为空时隐藏页面数据
watch(() => emotionStore.stockList, (newStockList) => { if (newStockList.length === 0) { // 当股票列表为空时,隐藏页面数据
isPageLoaded.value = false; // 停止音频播放
stopAudio(); // 清理音频URL
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 }; console.log('股票列表已清空,页面数据已隐藏'); } }, { deep: true });
// 监听当前股票变化,重新渲染图表
watch(currentStock, (newStock) => { if (newStock && newStock.apiData) { // 页面加载状态现在由 handleSendMessage 统一控制
// 停止当前播放的音频
stopAudio(); // 清理音频URL,确保不会播放之前股票的音频
audioUrl.value = ''; // 清理store中的音频URL,确保不会播放之前股票的音频
emotionAudioStore.resetAudioState(); // 清理正在进行的打字机效果定时器
clearTypewriterTimers(); // 重置触发状态,让每个股票都能独立触发效果
hasTriggeredAudio.value = false; hasTriggeredTypewriter.value = false;
// 获取股票代码作为唯一标识
const stockCode = newStock.stockInfo?.code || newStock.stockInfo?.symbol;
// 检查该股票是否已经显示过打字机效果
if (stockCode && stockTypewriterShown.value.has(stockCode)) { // 如果已经显示过,直接显示完整文本和标题
if (newStock.conclusionData) { try { const conclusion = JSON.parse(newStock.conclusionData); 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 };
// 提取音频URL但不自动播放,等待用户手动点击
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); } } } else { // 如果没有显示过,清空显示文本,等待打字机效果
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 };
// 即使没有显示过,也需要设置音频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); } } }
// 只有在页面已加载的情况下才渲染图表
if (isPageLoaded.value) { nextTick(() => { renderCharts(newStock.apiData); console.log('图表数据已准备完成,开始渲染:', newStock.apiData) // 检查场景应用部分是否已经在视口中,如果是则立即触发效果
setTimeout(() => { if (scenarioApplicationRef.value && parsedConclusion.value) { const stockCode = newStock.stockInfo?.code || newStock.stockInfo?.symbol;
// 如果该股票已经显示过,不需要再处理
if (stockCode && stockTypewriterShown.value.has(stockCode)) { return; }
const rect = scenarioApplicationRef.value.getBoundingClientRect(); const isInViewport = rect.top < window.innerHeight && rect.bottom > 0;
if (isInViewport) { console.log('股票切换后检测到场景应用部分在视口中');
if (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); }
stockTypewriterShown.value.set(stockCode, true); } else { 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 }; } } } } }, 500); // 延迟500ms确保数据完全加载
}); } else { console.log('页面尚未加载完成,等待数据加载完成后再渲染图表'); } } else { console.log('股票数据不存在或API数据未加载'); // 隐藏所有图表组件
chartVisibility.value = { marketTemperature: false, emotionDecod: false, emotionalBottomRadar: false, emoEnergyConverter: false }; } }, { immediate: true });
// 监听parsedConclusion变化,准备数据但不立即触发打字机效果
watch(parsedConclusion, (newConclusion) => { if (newConclusion) { console.log('场景应用结论数据:', newConclusion); // 不再立即开始打字机效果,等待滚动到场景应用部分时触发
// 尝试多种可能的语音URL字段名
let voiceUrl = null; if (newConclusion.url) { // 清理URL字符串,去除空格、反引号等特殊字符
voiceUrl = newConclusion.url.toString().trim().replace(/[`\s]/g, ''); } else if (newConclusion.audioUrl) { voiceUrl = newConclusion.audioUrl.toString().trim().replace(/[`\s]/g, ''); } else if (newConclusion.voice_url) { voiceUrl = newConclusion.voice_url.toString().trim().replace(/[`\s]/g, ''); } else if (newConclusion.audio) { voiceUrl = newConclusion.audio.toString().trim().replace(/[`\s]/g, ''); } else if (newConclusion.tts_url) { voiceUrl = newConclusion.tts_url.toString().trim().replace(/[`\s]/g, ''); }
if (voiceUrl && voiceUrl.startsWith('http')) { console.log('找到并清理后的语音URL:', voiceUrl); audioUrl.value = voiceUrl; // 同时更新store中的音频URL
emotionAudioStore.setCurrentAudioUrl(voiceUrl); console.log('音频URL已准备,检查是否需要立即触发效果');
// 音频准备好后,只有在用户主动搜索时才自动触发效果
// 数据恢复时不自动播放音频和打字机效果
console.log('音频URL已准备完成,等待用户手动触发播放'); } else { console.log('未找到有效的语音URL,原始URL:', newConclusion.url); console.log('结论数据中的所有字段:', Object.keys(newConclusion)); } } }, { immediate: true });
// 打字机效果函数
function startTypewriterEffect(conclusion, onComplete) { console.log('开始打字机效果,结论数据:', conclusion);
// 保存当前的完成回调函数
currentOnCompleteCallback.value = onComplete;
// 详细调试各个字段
console.log('L1字段 - one1:', conclusion.one1); console.log('L1字段 - one2:', conclusion.one2); console.log('L2字段 - two:', conclusion.two); console.log('L3字段 - three:', conclusion.three); console.log('L4字段 - four:', conclusion.four);
// 清除之前的定时器
typewriterTimers.value.forEach(timer => clearTimeout(timer)); typewriterTimers.value = [];
// 重置显示文本和状态
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 };
// 定义打字速度(毫秒)
const typeSpeed = 200; let totalDelay = 0;
// 定义模块配置
const modules = [ { key: 'one', title: 'L1: 情绪监控', contents: [ { key: 'one1', text: conclusion.one1 }, { key: 'one2', text: conclusion.one2 } ] }, { key: 'two', title: 'L2: 情绪解码', contents: [ { key: 'two', text: conclusion.two } ] }, { key: 'three', title: 'L3: 情绪推演', contents: [ { key: 'three', text: conclusion.three } ] }, { key: 'four', title: 'L4: 情绪套利', contents: [ { key: 'four', text: conclusion.four } ] } ];
// 按模块顺序处理
modules.forEach((module) => { // 检查模块是否有内容
const hasContent = module.contents.some(content => content.text && content.text.trim()); console.log(`模块 ${module.key} 是否有内容:`, hasContent, '内容:', module.contents.map(c => c.text)); if (!hasContent) return;
console.log(`开始显示模块 ${module.key}`); // 显示模块
const showModuleTimer = setTimeout(() => { moduleVisibility.value[module.key] = true; console.log(`模块 ${module.key} 已设置为可见`); }, totalDelay); typewriterTimers.value.push(showModuleTimer); totalDelay += 100;
// 打字机效果显示标题
const title = module.title; for (let i = 0; i <= title.length; i++) { const timer = setTimeout(() => { displayedTitles.value[module.key] = title.substring(0, i); }, totalDelay + i * typeSpeed); typewriterTimers.value.push(timer); } totalDelay += title.length * typeSpeed + 300; // 标题完成后间隔
// 打字机效果显示内容
module.contents.forEach((content) => { if (content.text && content.text.trim()) { const text = content.text; for (let i = 0; i <= text.length; i++) { const timer = setTimeout(() => { displayedTexts.value[content.key] = text.substring(0, i); }, totalDelay + i * typeSpeed); typewriterTimers.value.push(timer); } totalDelay += text.length * typeSpeed + 500; // 内容完成后间隔
} });
totalDelay += 800; // 模块间间隔
});
// 添加免责声明的打字机效果(在所有模块显示完成后)
const disclaimerText = '该内容由AI生成,请注意甄别';
// 显示免责声明模块
const showDisclaimerTimer = setTimeout(() => { moduleVisibility.value.disclaimer = true; }, totalDelay); typewriterTimers.value.push(showDisclaimerTimer); totalDelay += 100;
// 打字机效果显示免责声明
for (let i = 0; i <= disclaimerText.length; i++) { const timer = setTimeout(() => { displayedTexts.value.disclaimer = disclaimerText.substring(0, i); }, totalDelay + i * typeSpeed); typewriterTimers.value.push(timer); } }
// 清理定时器的函数
function clearTypewriterTimers() { typewriterTimers.value.forEach(timer => clearTimeout(timer)); typewriterTimers.value = []; }
// 音频播放函数
function playAudio(url) { console.log('尝试播放音频:', url);
if (!url) { console.warn('音频URL为空,跳过播放'); isAudioPlaying.value = false; return; }
// 检查是否启用了语音功能
console.log('语音功能状态:', emotionAudioStore.isVoiceEnabled); if (!emotionAudioStore.isVoiceEnabled) { console.log('语音功能已关闭,跳过播放'); return; }
console.log('开始创建音频实例...');
try { // 设置当前音频URL
emotionAudioStore.setCurrentAudioUrl(url);
// 停止之前的音频
if (emotionAudioStore.nowSound && emotionAudioStore.nowSound.playing()) { emotionAudioStore.nowSound.stop(); }
// 创建新的音频实例
const newSound = new Howl({ src: [url], html5: true, format: ['mp3', 'wav'], onplay: () => { isAudioPlaying.value = true; emotionAudioStore.isPlaying = true; console.log('开始播放场景应用语音'); // 音频开始播放时的自动滚动已移除
}, onend: () => { isAudioPlaying.value = false; emotionAudioStore.isPlaying = false; emotionAudioStore.isPaused = false; emotionAudioStore.playbackPosition = 0; console.log('场景应用语音播放结束'); }, onstop: () => { isAudioPlaying.value = false; emotionAudioStore.isPlaying = false; console.log('场景应用语音播放停止'); }, onpause: () => { isAudioPlaying.value = false; emotionAudioStore.isPlaying = false; console.log('场景应用语音播放暂停'); }, onerror: (error) => { isAudioPlaying.value = false; emotionAudioStore.isPlaying = false; console.error('音频播放错误:', error); }, onload: () => { // 音频加载完成,获取时长
emotionAudioStore.duration = newSound.duration(); console.log('音频加载完成,时长:', emotionAudioStore.duration); } });
// 保存音频实例到store
emotionAudioStore.nowSound = newSound; emotionAudioStore.setAudioInstance(newSound);
// 播放音频
newSound.play();
} catch (error) { console.error('创建音频实例失败:', error); isAudioPlaying.value = false; } }
// 停止音频播放
function stopAudio() { if (emotionAudioStore.nowSound) { emotionAudioStore.nowSound.stop(); } isAudioPlaying.value = false; } // 触发图片旋转的方法
function startImageRotation() { isRotating.value = true; // 如果你想在一段时间后停止旋转,可以添加以下代码
setTimeout(() => { isRotating.value = false; }, 5000); // 5 秒后停止旋转
}
// 发送消息方法
async function handleSendMessage(input, onComplete) { console.log("发送内容:", input); // 标记为用户主动搜索
isUserInitiated.value = true;
// 检查用户输入内容是否为空
if (!input || !input.trim()) { ElMessage.warning("输入内容不能为空"); // 调用完成回调,重新启用输入框
if (onComplete && typeof onComplete === 'function') { onComplete(); // 清除保存的回调函数
currentOnCompleteCallback.value = null; } return; }
// 用户输入不为空,立即触发图片旋转逻辑,隐藏历史数据
isRotating.value = true; const previousMessages = [...messages.value]; // 保存历史消息
messages.value = []; // 清空历史数据
// 检查用户剩余次数
await chatStore.getUserCount(); // 获取最新的用户次数
if (chatStore.UserCount <= 0) { const userMessage = reactive({ sender: 'user', text: input }); messages.value.push(userMessage); const aiMessage = reactive({ sender: 'ai', text: '您的剩余次数为0,无法使用情绪大模型,请联系客服或购买服务包。' }); messages.value.push(aiMessage); // 停止图片旋转,恢复历史数据
isRotating.value = false; messages.value = [...previousMessages, ...messages.value]; // 调用完成回调,重新启用输入框
if (onComplete && typeof onComplete === 'function') { onComplete(); // 清除保存的回调函数
currentOnCompleteCallback.value = null; } return; }
// 检查用户是否有使用次数(检查是否有任何权限)
// const hasPermission = userStore.brainPerssion || userStore.swordPerssion ||
// userStore.pricePerssion || userStore.timePerssion ||
// userStore.aibullPerssion || userStore.aiGnbullPerssion ||
// userStore.airadarPerssion;
// if (!hasPermission) {
// const userMessage = reactive({ sender: 'user', text: input });
// messages.value.push(userMessage);
// const aiMessage = reactive({ sender: 'ai', text: '您当前没有可用权限,请联系客服或购买服务包。' });
// messages.value.push(aiMessage);
// // 停止图片旋转,恢复历史数据
// isRotating.value = false;
// messages.value = [...previousMessages, ...messages.value];
// // 调用完成回调,重新启用输入框
// if (onComplete && typeof onComplete === 'function') {
// onComplete();
// // 清除保存的回调函数
// currentOnCompleteCallback.value = null;
// }
// return;
// }
const userMessage = reactive({ sender: 'user', text: input }); messages.value.push(userMessage);
try { // 第一步:调用第一个接口验证用户输入内容是否合法
const params = { content: userMessage.text, userData: { token: localStorage.getItem('localToken'), language: "cn", // brainPrivilegeState: userStore.brainPerssion,
// swordPrivilegeState: userStore.swordPerssion,
// stockForecastPrivilegeState: userStore.pricePerssion,
// spaceForecastPrivilegeState: userStore.timePerssion,
// aibullPrivilegeState: userStore.aibullPerssion,
// aigoldBullPrivilegeState: userStore.aiGnbullPerssion,
// airadarPrivilegeState: userStore.airadarPerssion,
// marketList: userStore.aiGoldMarketList,
brainPrivilegeState: '1', swordPrivilegeState: '1', stockForecastPrivilegeState: '1', spaceForecastPrivilegeState: '1', aibullPrivilegeState: '1', aigoldBullPrivilegeState: '1', airadarPrivilegeState: '1', marketList: "hk,cn,usa,my,sg,vi,in,gb", }, };
const result = await getReplyAPI(params); const response = await result.json(); const parsedData = JSON.parse(response.data);
// 检查用户输入是否合法
if (!parsedData || !parsedData.market || !parsedData.code) { // 输入不合法,关闭加载状态和等待提示,返回refuse信息,停止图片旋转,恢复历史数据
isLoading.value = false; isPageLoaded.value = false; const aiMessage = reactive({ sender: 'ai', text: processRefuseMessage(parsedData.refuse) }); messages.value.push(aiMessage); isRotating.value = false; messages.value = [...previousMessages, ...messages.value]; // 调用完成回调,重新启用输入框
if (onComplete && typeof onComplete === 'function') { onComplete(); // 清除保存的回调函数
currentOnCompleteCallback.value = null; } return; }
// 输入合法,继续执行后续处理
// 设置加载状态,隐藏图表页面
isLoading.value = true;
isPageLoaded.value = false;
// 调用第二个工作流接口
const conclusionParams = { content: input.trim(), userData: { token: localStorage.getItem('localToken'), language: "cn", marketList: "hk,cn,usa,my,sg,vi,in,gb", }, code: parsedData.code, market: parsedData.market, }; // 同时调用第二个数据流接口和fetchData方法
const [conclusionResult, fetchDataResult] = await Promise.all([ getConclusionAPI(conclusionParams), fetchData(parsedData.code, parsedData.market, parsedData.name || "未知股票", input.trim()) ]);
// 处理结论接口返回的数据
const conclusionResponse = await conclusionResult.json();
// 检查所有数据是否都加载成功
if (conclusionResponse && conclusionResponse.data && fetchDataResult) { // 将结论数据存储到响应式变量和store中
conclusionData.value = conclusionResponse.data; // 将结论数据存储到store中的当前激活股票
emotionStore.updateActiveStockConclusion(conclusionResponse.data);
// 所有数据加载完成,关闭加载状态,显示页面
isLoading.value = false; isPageLoaded.value = true;
// 数据获取成功后,重新获取用户次数以实现实时更新
try { await chatStore.getUserCount(); console.log('数据获取成功后,用户次数已更新'); } catch (error) { console.error('更新用户次数失败:', error); }
// 确保页面状态更新后触发图表渲染和音频文本
nextTick(() => { if (currentStock.value && currentStock.value.apiData) { renderCharts(currentStock.value.apiData);
// 只有在用户主动搜索时才自动触发音频和文本
if (isUserInitiated.value && parsedConclusion.value && audioUrl.value) { const stockCode = currentStock.value.stockInfo?.code || currentStock.value.stockInfo?.symbol; if (stockCode && !stockTypewriterShown.value.has(stockCode)) { startTypewriterEffect(parsedConclusion.value, onComplete);
if (!stockAudioPlayed.value.has(stockCode)) { stockAudioPlayed.value.set(stockCode, true); playAudio(audioUrl.value); } stockTypewriterShown.value.set(stockCode, true); } else { // 如果不需要打字机效果,直接调用完成回调
if (onComplete && typeof onComplete === 'function') { onComplete(); // 清除保存的回调函数
currentOnCompleteCallback.value = null; } } }
// 重置用户主动搜索标志
isUserInitiated.value = false; } }); } else { // 数据加载失败,停止图片旋转,恢复历史数据
isLoading.value = false; // 如果 fetchDataResult 为 false,说明数据不完整的错误信息已经在 fetchData 中添加到 messages
// 只有在 conclusionResponse 有问题时才添加通用错误信息
if (!conclusionResponse || !conclusionResponse.data) { const aiMessage = reactive({ sender: 'ai', text: '数据加载失败,请重试' }); messages.value.push(aiMessage); } isRotating.value = false; messages.value = [...previousMessages, ...messages.value];
// 如果有之前的股票数据且页面已加载,重新渲染图表
if (isPageLoaded.value && emotionStore.activeStock && emotionStore.activeStock.apiData) { nextTick(() => { renderCharts(emotionStore.activeStock.apiData); console.log('搜索失败,恢复显示之前股票的图表:', emotionStore.activeStock.stockInfo.name); }); }
// 调用完成回调,重新启用输入框
if (onComplete && typeof onComplete === 'function') { onComplete(); // 清除保存的回调函数
currentOnCompleteCallback.value = null; } return; } } catch (error) { // 请求失败时关闭加载状态
isLoading.value = false;
// 如果有之前的股票数据,恢复显示状态;否则设置为false
if (emotionStore.stockList.length > 0 && emotionStore.activeStock) { isPageLoaded.value = true; console.log('请求工作流接口失败,但恢复显示之前的股票数据'); // 立即渲染之前股票的图表,提升用户体验
nextTick(() => { renderCharts(emotionStore.activeStock.apiData); console.log('立即恢复显示之前股票的图表:', emotionStore.activeStock.stockInfo.name); }); } else { isPageLoaded.value = false; }
const aiMessage = reactive({ sender: 'ai', text: '请求工作流接口失败,请检查网络连接' }); messages.value.push(aiMessage); // 请求失败时停止图片旋转,恢复历史数据
isRotating.value = false; messages.value = [...previousMessages, ...messages.value];
// 如果有之前的股票数据且页面已加载,重新渲染图表
if (isPageLoaded.value && emotionStore.activeStock && emotionStore.activeStock.apiData) { nextTick(() => { renderCharts(emotionStore.activeStock.apiData); console.log('请求失败,恢复显示之前股票的图表:', emotionStore.activeStock.stockInfo.name); }); }
// 调用完成回调,重新启用输入框
if (onComplete && typeof onComplete === 'function') { onComplete(); // 清除保存的回调函数
currentOnCompleteCallback.value = null; } return; } finally { // 停止图片旋转(只有在设置了旋转状态时才需要停止)
if (isRotating.value) { isRotating.value = false; } } }
// 请求数据接口
async function fetchData(code, market, stockName, queryText) { try { const stockDataParams = { // token: '+XgqsgdW0RLIbIG2pxnnbZi0+fEeMx8pywnIlrmTxtkSaPZ9xjSOWrxq+s0rL3RrfNhXPvGtz9srFfjwu8A',
token: localStorage.getItem('localToken'), market: market, code: code, language: 'cn', version: version1.value };
const stockDataResult = await axios.post( // "http://39.101.133.168:8828/link/api/aiEmotion/client/getAiEmotionData",
'https://api.homilychart.com/link/api/aiEmotion/client/getAiEmotionData', stockDataParams, { headers: { "Content-Type": "application/json", }, } );
const stockDataResponse = stockDataResult.data; // 获取返回所有的数据
if (stockDataResponse.code === 200 && stockDataResponse.data) { // 检查关键数据字段是否完整
const validation = validateRequiredFields(stockDataResponse.data);
// 如果有关键数据缺失,返回失败,不添加到StockTabs
if (!validation.isValid) { console.log('API返回数据不完整,缺失字段:', validation.missingFields); // 关闭加载状态
isLoading.value = false;
// 如果有之前的股票数据,恢复显示状态;否则设置为false
if (emotionStore.stockList.length > 0 && emotionStore.activeStock) { isPageLoaded.value = true; console.log('数据验证失败,但恢复显示之前的股票数据'); // 立即渲染之前股票的图表,提升用户体验
nextTick(() => { renderCharts(emotionStore.activeStock.apiData); console.log('立即恢复显示之前股票的图表:', emotionStore.activeStock.stockInfo.name); }); } else { isPageLoaded.value = false; }
const aiMessage = reactive({ sender: 'ai', text: `数据丢失了,请稍后重试。` }); messages.value.push(aiMessage); return false; // 返回失败标识,不添加股票到标签
}
// 只有数据完整时才创建股票数据对象并添加到store
const stockData = { queryText: queryText, stockInfo: { name: stockName, code: code, market: market }, apiData: stockDataResponse.data, conclusionData: conclusionData.value, // 包含结论数据
timestamp: new Date().toISOString() }; // 将股票数据添加到store中,显示在StockTabs中
emotionStore.addStock(stockData); return true; // 返回成功标识
} else { // 关闭加载状态
isLoading.value = false;
// 如果有之前的股票数据,恢复显示状态;否则设置为false
if (emotionStore.stockList.length > 0 && emotionStore.activeStock) { isPageLoaded.value = true; console.log('API请求失败,但恢复显示之前的股票数据'); // 立即渲染之前股票的图表,提升用户体验
nextTick(() => { renderCharts(emotionStore.activeStock.apiData); console.log('立即恢复显示之前股票的图表:', emotionStore.activeStock.stockInfo.name); }); } else { isPageLoaded.value = false; }
const aiMessage = reactive({ sender: 'ai', text: '图表数据请求失败,请检查网络连接' }); messages.value.push(aiMessage); return false; // 返回失败标识
} } catch (error) { // 关闭加载状态
isLoading.value = false;
// 如果有之前的股票数据,恢复显示状态;否则设置为false
if (emotionStore.stockList.length > 0 && emotionStore.activeStock) { isPageLoaded.value = true; console.log('网络异常,但恢复显示之前的股票数据'); // 立即渲染之前股票的图表,提升用户体验
nextTick(() => { renderCharts(emotionStore.activeStock.apiData); console.log('立即恢复显示之前股票的图表:', emotionStore.activeStock.stockInfo.name); }); } else { isPageLoaded.value = false; }
const aiMessage = reactive({ sender: 'ai', text: '图表数据请求失败,请检查网络连接' }); messages.value.push(aiMessage); return false; // 返回失败标识
} }
// 检查关键数据字段是否完整的函数
function validateRequiredFields(data) { const requiredFields = ['GSWDJ', 'KLine20', 'QXJMQ', 'QXTDLD', 'WDRL']; const missingFields = [];
for (const field of requiredFields) { if (!data[field] || (Array.isArray(data[field]) && data[field].length === 0) || (typeof data[field] === 'object' && !hasValidData(data[field]))) { missingFields.push(field); } }
return { isValid: missingFields.length === 0, missingFields: missingFields }; }
// 检查对象是否包含有效数据的辅助函数
function hasValidData(obj) { if (!obj || typeof obj !== 'object') { return false; }
// 定义可以为空的数组字段
const allowedEmptyArrays = ['lowxh', 'qixh', 'topxh'];
// 检查对象的所有属性值
for (const key in obj) { if (obj.hasOwnProperty(key)) { const value = obj[key];
// 如果是字符串字段
if (typeof value === 'string') { // 字符串字段必须有内容,为空则表示异常
if (value.trim() !== '') { return true; } } // 如果是数组字段
else if (Array.isArray(value)) { // 数组字段可以为空,但如果有内容则表示有效
if (value.length > 0) { return true; } } // 如果是数字且不为0
else if (typeof value === 'number' && value !== 0) { return true; } // 如果是布尔值且为true
else if (typeof value === 'boolean' && value === true) { return true; } // 如果是对象且包含有效数据(递归检查)
else if (typeof value === 'object' && value !== null) { if (hasValidData(value)) { return true; } } } }
return false; }
// 渲染组件图表的方法
function renderCharts(data) { console.log('开始渲染图表,数据:', data);
// 深拷贝数据避免污染原始数据
const clonedData = JSON.parse(JSON.stringify(data));
// 检查关键数据字段是否完整
const validation = validateRequiredFields(clonedData);
// 如果有任何关键数据缺失,不渲染页面并返回提示
if (!validation.isValid) { console.log('关键数据缺失:', validation.missingFields); const aiMessage = reactive({ sender: 'ai', text: `数据不完整,缺少以下关键数据:${validation.missingFields.join('、')}。请稍后重试或联系客服。` }); messages.value.push(aiMessage);
// 隐藏页面内容
isPageLoaded.value = false; isLoading.value = false;
return; // 直接返回,不进行后续渲染
}
// 先设置图表组件显示状态
chartVisibility.value = { marketTemperature: !!(clonedData.GSWDJ && clonedData.GSWDJ.length > 0), emotionDecod: !!(clonedData.QXJMQ && clonedData.QXJMQ.length > 0), emotionalBottomRadar: !!(clonedData.QXTDLD && clonedData.QXTDLD.length > 0), emoEnergyConverter: !!(clonedData.QXNLZHQ && (Array.isArray(clonedData.QXNLZHQ) ? clonedData.QXNLZHQ.length > 0 : hasValidData(clonedData.QXNLZHQ))) };
console.log('图表显示状态:', chartVisibility.value); console.log('数据检查:', { GSWDJ: !!(clonedData.GSWDJ && clonedData.GSWDJ.length > 0), QXJMQ: !!(clonedData.QXJMQ && clonedData.QXJMQ.length > 0), QXTDLD: !!(clonedData.QXTDLD && clonedData.QXTDLD.length > 0), QXNLZHQ: !!(clonedData.QXNLZHQ && (Array.isArray(clonedData.QXNLZHQ) ? clonedData.QXNLZHQ.length > 0 : hasValidData(clonedData.QXNLZHQ))) }); console.log('QXNLZHQ数据详情:', clonedData.QXNLZHQ);
nextTick(() => { // 增加延迟确保DOM完全更新和组件完全挂载
setTimeout(() => { try { console.log('图表组件ref状态:', { marketTemperatureRef: !!marketTemperatureRef.value, emotionDecodRef: !!emotionDecodRef.value, emotionalBottomRadarRef: !!emotionalBottomRadarRef.value, emoEnergyConverterRef: !!emoEnergyConverterRef.value });
// 检查DOM元素是否存在
console.log('DOM元素检查:', { marketTemperatureDOM: !!document.querySelector('.class03'), emotionDecodDOM: !!document.querySelector('.class04'), emotionalBottomRadarDOM: !!document.querySelector('.class05'), emoEnergyConverterDOM: !!document.querySelector('.class06') });
// 检查具体的组件元素
const emoEnergyElement = document.querySelector('emo-energy-converter'); console.log('emoEnergyConverter元素:', emoEnergyElement);
// 等待更长时间再次检查ref
setTimeout(() => { console.log('延迟检查emoEnergyConverterRef:', !!emoEnergyConverterRef.value); if (emoEnergyConverterRef.value) { console.log('emoEnergyConverter方法:', typeof emoEnergyConverterRef.value.initQXNLZHEcharts); } }, 1000);
// 渲染股市温度计图表
if (marketTemperatureRef.value && chartVisibility.value.marketTemperature) { console.log('开始渲染股市温度计图表'); console.log('marketTemperatureRef方法:', typeof marketTemperatureRef.value.initChart); if (typeof marketTemperatureRef.value.initChart === 'function') { try { marketTemperatureRef.value.initChart(clonedData.GSWDJ, clonedData.KLine20, clonedData.WDRL); console.log('股市温度计图表渲染成功'); } catch (error) { console.error('股市温度计图表渲染失败:', error); } } else { console.error('marketTemperatureRef.initChart 方法不存在'); } } else { console.log('股市温度计图表未渲染,ref存在:', !!marketTemperatureRef.value, '数据存在:', chartVisibility.value.marketTemperature); }
// 渲染情绪解码器图表
if (emotionDecodRef.value && chartVisibility.value.emotionDecod) { console.log('开始渲染情绪解码器图表'); console.log('emotionDecodRef方法:', typeof emotionDecodRef.value.initQXNLZHEcharts); if (typeof emotionDecodRef.value.initQXNLZHEcharts === 'function') { try { emotionDecodRef.value.initQXNLZHEcharts(clonedData.KLine20, clonedData.QXJMQ); console.log('情绪解码器图表渲染成功'); } catch (error) { console.error('情绪解码器图表渲染失败:', error); } } else { console.error('emotionDecodRef.initQXNLZHEcharts 方法不存在'); } } else { console.log('情绪解码器图表未渲染,ref存在:', !!emotionDecodRef.value, '数据存在:', chartVisibility.value.emotionDecod); }
// 渲染情绪探底雷达图表
if (emotionalBottomRadarRef.value && chartVisibility.value.emotionalBottomRadar) { console.log('开始渲染情绪探底雷达图表'); console.log('emotionalBottomRadarRef方法:', typeof emotionalBottomRadarRef.value.initEmotionalBottomRadar); if (typeof emotionalBottomRadarRef.value.initEmotionalBottomRadar === 'function') { try { emotionalBottomRadarRef.value.initEmotionalBottomRadar( clonedData.KLine20, clonedData.QXTDLD ); console.log('情绪探底雷达图表渲染成功'); } catch (error) { console.error('情绪探底雷达图表渲染失败:', error); } } else { console.error('emotionalBottomRadarRef.initEmotionalBottomRadar 方法不存在'); } } else { console.log('情绪探底雷达图表未渲染,ref存在:', !!emotionalBottomRadarRef.value, '数据存在:', chartVisibility.value.emotionalBottomRadar); }
// 渲染情绪能量转化器图表
if (emoEnergyConverterRef.value && chartVisibility.value.emoEnergyConverter) { console.log('开始渲染情绪能量转化器图表'); console.log('emoEnergyConverterRef方法:', typeof emoEnergyConverterRef.value.initQXNLZHEcharts); if (typeof emoEnergyConverterRef.value.initQXNLZHEcharts === 'function') { try { emoEnergyConverterRef.value.initQXNLZHEcharts(clonedData.KLine20, clonedData.QXNLZHQ); console.log('情绪能量转化器图表渲染成功'); } catch (error) { console.error('情绪能量转化器图表渲染失败:', error); } } else { console.error('emoEnergyConverterRef.initQXNLZHEcharts 方法不存在'); } } else { console.log('情绪能量转化器图表未渲染,ref存在:', !!emoEnergyConverterRef.value, '数据存在:', chartVisibility.value.emoEnergyConverter); }
console.log('图表渲染完成'); } catch (error) { console.error('图表渲染错误:', error); const aiMessage = reactive({ sender: 'ai', text: '图表渲染失败,请重试' }); messages.value.push(aiMessage); } }, 500); // 增加延迟到500ms确保DOM和组件完全稳定
}); }
// scrollToBottom函数已移除
// 处理用户滚动事件(用于其他滚动相关功能)
const handleUserScroll = () => { // 用户滚动事件处理逻辑已简化,因为自动滚动功能已移除
};
// 处理滚轮事件
const handleWheel = (event) => { handleUserScroll(); };
// 处理触摸滚动事件
const handleTouchMove = (event) => { handleUserScroll(); };
// 检查数据是否已加载完成
function isDataLoaded() { // 检查页面是否已加载
if (!isPageLoaded.value) { console.log('页面数据尚未加载完成'); return false; }
// 检查当前股票数据是否存在
if (!currentStock.value || !currentStock.value.apiData) { console.log('股票数据尚未加载完成'); return false; }
// 检查图表组件是否已渲染
const requiredRefs = [ marketTemperatureRef.value, emotionDecodRef.value, emotionalBottomRadarRef.value, emoEnergyConverterRef.value ];
const allRefsLoaded = requiredRefs.every(ref => ref !== null); if (!allRefsLoaded) { console.log('图表组件尚未完全加载'); return false; }
console.log('所有数据和组件已加载完成'); return true; }
// 自动滚动函数已移除
// 设置Intersection Observer监听场景应用部分
function setupIntersectionObserver() { if (!scenarioApplicationRef.value) return;
const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { console.log('场景应用部分进入视口');
// 获取当前股票代码
const stockCode = currentStock.value?.stockInfo?.code || currentStock.value?.stockInfo?.symbol;
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); // 标记为已播放,避免后续自动播放
} 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 }; } } } }); }, { threshold: 0.3, // 当30%的元素进入视口时触发
rootMargin: '0px 0px -100px 0px' // 提前100px触发
} );
observer.observe(scenarioApplicationRef.value); intersectionObserver.value = observer; }
// 手动触发自动滚动函数已移除
// 返回顶部功能
const scrollToTop = () => { const topAnchor = document.getElementById('top-anchor'); if (topAnchor) { topAnchor.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }); } else { window.scrollTo({ top: 0, behavior: 'smooth' }); }
// 备用方案:直接滚动到页面顶部
setTimeout(() => { const currentScrollTop = window.pageYOffset || document.documentElement.scrollTop; if (currentScrollTop > 50) { document.documentElement.scrollTop = 0; document.body.scrollTop = 0; } }, 1000); };
// 监听页面滚动,控制返回顶部按钮显示
const handlePageScroll = () => { const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; showBackToTop.value = scrollTop > 200; };
// 监听容器滚动(备用方案)
const handleContainerScroll = () => { const container = userInputDisplayRef.value; if (container) { const scrollTop = container.scrollTop; if (scrollTop > 200) { showBackToTop.value = true; } } };
// 页面挂载完成后触发图片旋转和设置滚动监听
onMounted(async () => { // 确保获取用户次数
try { await chatStore.getUserCount(); console.log('情绪大模型页面:用户次数获取成功'); } catch (error) { console.error('情绪大模型页面:获取用户次数失败', error); }
// 添加全局resize监听器,确保所有图表和容器响应页面宽度变化
const globalResizeHandler = debounce(() => { console.log('AiEmotion页面:窗口大小变化,触发容器和图表resize');
// 强制重新计算容器布局
const mainContainer = document.querySelector('.class01'); if (mainContainer) { // 触发重排,确保容器尺寸正确更新
mainContainer.style.display = 'none'; mainContainer.offsetHeight; // 强制重排
mainContainer.style.display = ''; }
// 触发所有图表组件的resize
const resizeHandlers = [ window.emoEnergyConverterResizeHandler, window.marketTempResizeHandler, window.emotionalBottomRadarResizeHandler, window.emotionDecodResizeHandler ];
resizeHandlers.forEach(handler => { if (typeof handler === 'function') { try { handler(); } catch (error) { console.error('AiEmotion页面:图表resize失败', error); } } });
// 延迟再次触发图表resize,确保容器尺寸稳定后图表能正确适配
setTimeout(() => { resizeHandlers.forEach(handler => { if (typeof handler === 'function') { try { handler(); } catch (error) { console.error('AiEmotion页面:延迟图表resize失败', error); } } }); }, 100); }, 150); // 150ms防抖延迟
// 移除之前的监听器(如果存在)
if (window.aiEmotionGlobalResizeHandler) { window.removeEventListener('resize', window.aiEmotionGlobalResizeHandler); }
// 添加新的监听器
window.addEventListener('resize', globalResizeHandler); window.aiEmotionGlobalResizeHandler = globalResizeHandler;
// 添加滚动事件监听器
const container = userInputDisplayRef.value; if (container) { container.addEventListener('wheel', handleWheel, { passive: true }); container.addEventListener('touchmove', handleTouchMove, { passive: true }); container.addEventListener('scroll', handleUserScroll, { passive: true }); // 添加容器滚动监听器用于返回顶部按钮
container.addEventListener('scroll', handleContainerScroll, { passive: true }); }
// 添加页面滚动监听器,控制返回顶部按钮显示
window.addEventListener('scroll', handlePageScroll, { passive: true });
// 添加document滚动监听器(备用方案)
document.addEventListener('scroll', handlePageScroll, { passive: true });
// 防抖函数定义
function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }
startImageRotation();
// 检查是否有已保存的股票数据需要恢复
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
onUnmounted(() => { clearTypewriterTimers();
// 如果有未完成的回调函数,调用它来重新启用输入框
if (currentOnCompleteCallback.value && typeof currentOnCompleteCallback.value === 'function') { currentOnCompleteCallback.value(); currentOnCompleteCallback.value = null; }
stopAudio();
// 重置触发状态
hasTriggeredAudio.value = false; hasTriggeredTypewriter.value = false;
// 清理Intersection Observer
if (intersectionObserver.value) { intersectionObserver.value.disconnect(); intersectionObserver.value = null; }
// 清理全局resize监听器
if (window.aiEmotionGlobalResizeHandler) { window.removeEventListener('resize', window.aiEmotionGlobalResizeHandler); window.aiEmotionGlobalResizeHandler = null; }
// 清理滚动事件监听器
const container = userInputDisplayRef.value; if (container) { container.removeEventListener('wheel', handleWheel); container.removeEventListener('touchmove', handleTouchMove); container.removeEventListener('scroll', handleUserScroll); container.removeEventListener('scroll', handleContainerScroll); }
// 清理页面滚动监听器
window.removeEventListener('scroll', handlePageScroll); document.removeEventListener('scroll', handlePageScroll);
// 滚动相关清理已简化
});
// 声明组件可以触发的事件
const emit = defineEmits(['updateMessage', 'sendMessage', 'ensureAIchat']);
// 导出方法供外部使用
defineExpose({ handleSendMessage }); </script>
<style scoped> .disclaimer-item p { color: #ffffff !important; font-size: 20px; font-weight: bold; }
.container { padding-top: 2%; }
.class003 { padding-top: 8%; /* padding-left: 10%; */ display: flex; align-items: center; justify-content: center; gap: 25%; /* flex-direction: column; */ /* gap: 1rem; */ }
.img01 { width: 11vw; /* height: auto; */ }
.div00 { display: flex; flex-direction: column; /* 竖向排列元素 */ /* margin-left: 15%; */ gap: 30px; /* margin-top: -12%; */ /* width: 100%; */ /* height: auto; */ }
.div00::after { content: ""; display: table; clear: both; }
.class003 .div02 { background-image: url('@/assets/img/AiEmotion/redBorder.png'); background-repeat: no-repeat; background-size: 100% 100%; /* width: 50%; */ width: 30vw; max-width: 400px; min-width: 200px; min-height: 40px; text-align: center; font-size: 24px; color: white; display: flex; justify-content: center; align-items: center; flex-shrink: 0; }
.class003 .div01 { background-image: url('@/assets/img/AiEmotion/blueBorder.png'); background-repeat: no-repeat; background-size: 100% 100%; /* width: 35%; */ width: 30vw; max-width: 400px; min-width: 200px; min-height: 40px; text-align: center; font-size: 24px; color: white; display: flex; justify-content: center; align-items: center; flex-shrink: 0; }
.golden-wheel { width: 100%; display: flex; justify-content: center; }
.golden-wheel-img { width: 60%; max-width: 500px; height: auto; }
/* 定义旋转动画 */ @keyframes rotate { from { transform: rotate(0deg); }
to { transform: rotate(360deg); } }
/* 应用动画到图片 */ .rotating-image { animation: rotate 5s linear; /* 5 秒完成一次旋转,线性速度*/ will-change: transform; /* 优化动画性能 */ }
.bk-image { background-image: url("@/assets/img/AiEmotion/bk00000.png"); background-size: 100% 100%; background-repeat: no-repeat; width: 100%; height: auto; margin: 0 auto; margin-top: 20px; }
.bk-image .conclusion-container { padding: 20px; border-radius: 15px; margin: 20px; /* background: linear-gradient(135deg, rgba(0, 212, 255, 0.15) 0%, rgba(0, 100, 200, 0.15) 100%); */ border: 2px solid rgba(0, 212, 255, 0.4); }
.bk-image .conclusion-container .conclusion-item { margin-bottom: 20px; padding: 20px; border-radius: 12px; border: 1px solid rgba(0, 212, 255, 0.5); transition: all 0.3s ease; position: relative; overflow: hidden; }
.bk-image .conclusion-container .conclusion-item::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, #00d4ff, #0099ff, #00d4ff); opacity: 0.8; }
.bk-image .conclusion-container .conclusion-item:last-child { margin-bottom: 0; }
.bk-image .conclusion-container .conclusion-item .conclusion-title { color: #FFD700; font-size: 22px; font-weight: bold; margin: 0 0 15px 0; text-align: center; letter-spacing: 2px; }
.bk-image .conclusion-container .conclusion-item .conclusion-title::after { content: ''; position: absolute; bottom: -5px; left: 50%; transform: translateX(-50%); width: 60px; height: 2px; background: linear-gradient(90deg, transparent, #00d4ff, transparent); }
.bk-image .conclusion-container .conclusion-item .conclusion-text { color: #ffffff; font-size: 20px; line-height: 1.8; margin: 0 0 12px 0; text-align: left; word-wrap: break-word; padding-left: 15px; position: relative; }
.bk-image .conclusion-container .conclusion-item .conclusion-text:last-child { margin-bottom: 0; }
.bk-image .conclusion-placeholder { padding: 30px; text-align: center; border-radius: 12px; background: rgba(255, 255, 255, 0.05); border: 1px dashed rgba(153, 153, 153, 0.3); }
.bk-image .conclusion-placeholder p { color: #999999; font-size: 16px; margin: 0; font-style: italic; }
/* 最后文字的颜色 */ .text-container { position: relative; color: white; text-align: left; padding: 20px; border-radius: 15px; margin: 20px; /* background: linear-gradient(135deg, rgba(0, 212, 255, 0.15) 0%, rgba(0, 100, 200, 0.15) 100%); */ border: 2px solid rgba(0, 212, 255, 0.4); }
.text-container p { margin: 0 auto; font-size: 40px; margin-left: 0%; padding: 20px; border-radius: 12px; transition: all 0.3s ease; position: relative; overflow: hidden; letter-spacing: 2px; border: 1px solid rgba(0, 212, 255, 0.5); }
.text-container p::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, #00d4ff, #0099ff, #00d4ff); opacity: 0.8; }
.text-container .title { display: block; color: #FFD700; font-weight: bold; margin-top: 0px; margin-bottom: 20px; font-size: 22px; }
.text-container .content { display: block; color: white; font-size: 20px; text-align: center; }
.class07 { background-image: url("@/assets/img/AiEmotion/bk00000.png"); /* 使用导入的背景图片 */ background-size: cover; /* 确保背景图片完整显示 */ background-repeat: no-repeat; /* 防止背景图片重复 */ width: 95%; /* 设置容器宽度 */ height: auto; /* 高度根据内容动态变化 */ min-height: 70rem; /* 设置最小高度,确保图片显示 */ margin: 0 auto; }
.class0700 { margin: 0 auto; width: fit-content; margin-top: 2%; margin-bottom: 1%; }
.class0702 { margin: 0 auto; width: fit-content; margin-top: 2%; margin-bottom: 1%; }
.class0402 { /* width: 80vw; */ margin: 0 auto; }
.class0603 { min-width: 100%; margin-top: 3%; }
.class0502 { padding-top: 7%; display: flex; justify-content: center; align-items: center; flex-direction: column; /* 竖向排列元素 */ gap: 1rem; /* margin-left: 2rem; */ }
.class0601 { padding-top: 5rem; display: flex; justify-content: center; align-items: center; flex-direction: column; /* 竖向排列元素 */ gap: 1rem; /* margin-left: 2rem; */ }
.img03 { width: 10%; height: auto; /* margin-left: 43%; */ }
.img04 { width: 10%; height: auto; /* margin-left: 43%; */ }
.class0701 { margin: 0 auto; width: fit-content; }
.class0501 { margin: 0 auto; width: fit-content; margin-top: 2%; margin-bottom: 1%; }
.class0403 { margin: 0 auto; width: fit-content; margin-top: 2%; margin-bottom: 1%; }
.class0301 { margin: 0 auto; width: fit-content; margin-top: 2%; margin-bottom: 1%; }
.class0201 { margin: 0 auto; width: fit-content; margin-bottom: 1%; }
.class0401 { padding-top: 5rem; display: flex; flex-direction: column; justify-content: center; align-items: center; /* 竖向排列元素 */ gap: 1rem; /* margin-left: 2rem; */ }
.img02 { width: 10%; height: auto; /* margin-left: 43%; */ }
.title2 { color: white; font-size: 20px; font-weight: bold; /* margin-left: 45%; */ }
.title3 { color: white; font-size: 20px; font-weight: bold; /* margin-left: 44.6%; */ }
.title4 { color: white; font-size: 20px; font-weight: bold; /* margin-left: 44%; */ }
.class09 { text-align: center; margin-top: 2%; margin-bottom: 1%; }
/* 为需要放大的图片添加样式 */ .scaled-img { background-image: url('@/assets/img/AiEmotion/tree00000.jpg'); background-size: 100% 100%; background-position: center; background-repeat: no-repeat; width: 70%; height: 400px; min-height: 35rem; text-align: center; margin: 0 auto; margin-top: 3%; }
.lz-img { text-align: center; padding-top: 70px; }
.class08 { background-image: url("@/assets/img/AiEmotion/bk00000.png"); background-size: 100% 100%; background-repeat: no-repeat; width: 100%; height: auto; min-height: 50rem; margin: 0 auto; }
.class06 { background-image: url('@/assets/img/AiEmotion/bk00000.png'); /* 使用导入的背景图片 */ background-size: 100% 100%; /* 确保背景图片完整显示 */ background-repeat: no-repeat; /* 防止背景图片重复 */ width: 100%; /* 响应式容器宽度 */ max-width: 100%; /* 确保不超出父容器 */ height: auto; /* 高度根据内容动态变化 */ min-height: 69rem; /* 设置最小高度,确保图片显示 */ margin: 0 auto; box-sizing: border-box; /* 包括内边距在宽度计算中 */ transition: all 0.3s ease; /* 添加平滑过渡效果 */ }
.class05 { background-image: url("@/assets/img/AiEmotion/bk00000.png"); /* 使用导入的背景图片 */ background-size: 100% 100%; /* 确保背景图片完整显示 */ background-repeat: no-repeat; /* 防止背景图片重复 */ width: 100%; /* 响应式容器宽度 */ max-width: 100%; /* 确保不超出父容器 */ height: auto; /* 高度根据内容动态变化 */ /* min-height: 90rem; */ /* 设置最小高度,确保图片显示 */ margin: 0 auto; box-sizing: border-box; /* 包括内边距在宽度计算中 */ transition: all 0.3s ease; /* 添加平滑过渡效果 */ }
.class04 { background-image: url('@/assets/img/AiEmotion/bk00000.png'); /* 使用导入的背景图片 */ background-size: 100% 100%; /* 确保背景图片完整显示 */ background-repeat: no-repeat; /* 防止背景图片重复 */ width: 100%; /* 响应式容器宽度 */ max-width: 100%; /* 确保不超出父容器 */ height: auto; /* 高度根据内容动态变化 */ /* min-height: 75rem; */ /* 设置最小高度,确保图片显示 */ margin: 0 auto; box-sizing: border-box; /* 包括内边距在宽度计算中 */ transition: all 0.3s ease; /* 添加平滑过渡效果 */ padding-bottom: 1rem; }
.class03 { background-image: url('@/assets/img/AiEmotion/bk00000.png'); /* 使用导入的背景图片 */ background-size: 100% 100%; /* 确保背景图片完整显示 */ background-repeat: no-repeat; /* 防止背景图片重复 */ width: 100%; /* 响应式容器宽度 */ max-width: 100%; /* 确保不超出父容器 */ height: auto; /* 高度根据内容动态变化 */ min-height: 70rem; /* 设置最小高度,确保图片显示 */ margin: 0 auto; box-sizing: border-box; /* 包括内边距在宽度计算中 */ transition: all 0.3s ease; /* 添加平滑过渡效果 */ }
.class00 { background-size: 100% 100%; /* 确保背景图片完整显示 */ background-repeat: no-repeat; /* 防止背景图片重复 */ width: 100%; /* 响应式容器宽度 */ max-width: 100%; /* 确保不超出父容器 */ height: auto; /* 高度根据内容动态变化 */ /* min-height: 55rem; */ /* 设置最小高度,确保图片显示 */ margin: 0 auto; box-sizing: border-box; /* 包括内边距在宽度计算中 */ transition: all 0.3s ease; /* 添加平滑过渡效果 */ }
.content1 { display: flex; flex-direction: column; align-items: center; /* 竖向排列元素 */ /* gap: 1rem; */ /* margin-left: 10%; */ }
.title1 { color: white; font-size: 30px; font-weight: bold; margin-left: 0%;
}
.span02 { font-size: 1.5rem; font-weight: bold; color: white; float: right; margin-top: -2%; }
.span01 { background-image: url('@/assets/img/AiEmotion/bk01.png'); /* 使用导入的背景图片 */ background-size: 100% 100%; /* 背景图片覆盖整个容器 */ background-repeat: no-repeat; /* 防止背景图片重复 */ /* display: inline-block; */ /* 确保容器是块级元素 */ padding: 10px; /* 添加内边距以显示内容 */ color: #fff; /* 设置文字颜色以确保可读性 */ font-size: 1.5rem; /* 增加字体大小以便更清晰显示股票名称 */ text-align: center; /* transform: translate(-50%, -50%); */ margin-left: 0; width: 30%; height: auto; }
.class01 { width: 90%; /* 响应式容器宽度 */ max-width: 1400px; /* 设置最大宽度,避免在大屏幕上过度拉伸 */ min-width: 320px; /* 设置最小宽度,确保在小屏幕上可用 */ min-height: 100px; /* 设置最小高度,确保初始显示 */ height: auto; /* 高度根据内容动态变化 */ padding: 1rem; /* 添加内边距,确保内容与边界有间距 */ box-sizing: border-box; /* 包括内边距在宽度和高度计算中 */ background-color: #2b378d; margin: 0 auto; /* 居中容器 */ transition: width 0.3s ease; /* 添加平滑过渡效果 */ margin-bottom: 10rem; }
.ai-emotion-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; position: relative; }
.user-input-display { margin-top: 20px; display: flex; flex-direction: column; width: 100%; }
.message-container { display: flex; margin-bottom: 10px; width: 100%; }
.user-message { color: #6d22f8; background: white; font-weight: bold; padding: 10px 15px; border-radius: 15px; max-width: 60%; text-align: left; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); margin-left: auto; /* 将用户消息推到右边 */ padding: 20px 20px; }
.ai-message { background-color: #f1f1f1; color: #333; font-weight: bold; padding: 10px 15px; border-radius: 15px; max-width: 60%; text-align: left; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); margin-right: auto; /* 将AI消息保持在左边 */ padding: 20px 20px; }
.input-container { display: flex; align-items: center; gap: 10px; }
.fixed-bottom { position: fixed; bottom: 100px; left: 0; width: 100%; background-color: #f8f9fa; padding: 10px 20px; box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1); }
.input-box { padding: 10px; font-size: 16px; border: 1px solid #ccc; border-radius: 5px; width: calc(100% - 120px); }
.send-button { padding: 10px 20px; font-size: 16px; color: #fff; background-color: #007bff; border: none; border-radius: 5px; cursor: pointer; }
.send-button:hover { background-color: #0056b3; }
/* 响应式布局媒体查询 */ @media only screen and (max-width: 1200px) { .class01 { width: 95%; padding: 0.8rem; }
.span01 { width: 40%; font-size: 1.3rem; }
.span02 { font-size: 1.3rem; }
/* 调整图表容器高度 */ /* .class00 { min-height: 45rem; } */
.class03 { min-height: 60rem; }
.class06 { min-height: 58rem; }
.class08 { min-height: 42rem; }
.scaled-img { height: 350px; min-height: 30rem; } }
@media only screen and (max-width: 992px) { .class01 { width: 98%; padding: 0.6rem; }
/* 调整图表容器高度 */ /* .class00 { min-height: 40rem; } */
.class03 { min-height: 55rem; }
.class06 { min-height: 50rem; }
.class08 { min-height: 35rem; }
.scaled-img { height: 300px; min-height: 25rem; background-size: contain; } }
/* 手机端适配样式 */ @media only screen and (max-width: 768px) { .class01 { width: 100%; padding: 0.5rem; margin-bottom: 5rem; }
.container { padding-top: 2%; }
.title4 { color: white; font-size: 20px; font-weight: bold; margin-left: 28%; }
.class0603 { min-width: 100%; /* margin-top: 25%; */ }
.scaled-img { background-image: url('@/assets/img/AiEmotion/tree00000.jpg'); background-size: 100% 100%; background-position: center; background-repeat: no-repeat; background-size: contain; text-align: center; width: 100%; margin-top: 4%; height: 200px; min-height: 200px; }
.title3 { color: white; font-size: 20px; font-weight: bold; margin-left: 30%; }
.title2 { color: white; font-size: 20px; font-weight: bold; margin-left: 30%; }
/* 图片样式 */ .golden-wheel img { width: 100%; }
.class0201 img { width: 100%; }
.class0301 img { width: 100%; margin: 10px 10px; }
.class0403 img { width: 100%; margin: 10px 10px; }
.class0501 img { width: 100%; margin: 10px 10px; }
.class0702 img { width: 100%; margin: 10px 10px; }
.class0700 img { width: 100%; margin: 10px 10px; }
.scaled-img img { width: 30%; height: auto; }
.class09 img { width: 100%; margin: 10px 10px; }
.img01 { height: auto; /* margin-left: 7%; */ width: 15vw; /* margin-top: 10px; */ }
.title1 { font-size: 20px; margin-left: 5%; }
.class02 .span02 { font-size: 14px; color: white; float: right; margin-top: -6%; }
.class03 { background-image: url('@/assets/img/AiEmotion/bk00000.png'); background-size: 100% 100%; background-repeat: no-repeat; width: 100%; /* margin-left: -45px; */ height: auto; }
.class01 { min-height: 100px; height: auto; box-sizing: border-box; background-color: #02107d; margin-bottom: 10rem; }
.class04 { width: 100%; height: auto; margin: 0 auto; /* min-height: 38rem; */ /* min-height: 51rem; */ /* margin-left: -39px; */ /* min-height: 38rem; */ }
/* 调整其他图表容器高度 */ /* .class00 { min-height: 35rem; } */
.class03 { min-height: 45rem; }
.class06 { min-height: 35rem; }
.class02 .container img { width: 68%; height: auto; /* margin-top: 5%; */ margin-left: 0%; }
.img02 { width: 25%; height: auto; /* margin-left: 32%; */ }
.class0401 { padding-top: 3rem; }
.img03, .img04 { width: 25%; height: auto; /* margin-left: 33%; */ }
.text-container p { font-size: 16px; }
.lz-img { margin-bottom: 0; padding-top: 0;
img { width: 30%; height: auto; margin-top: 5%; } }
.class08 { background-size: 100% 100%; background-repeat: no-repeat; width: 100%; height: auto; min-height: 20rem; margin: 0 auto; }
.bk-image { .conclusion-container { padding: 15px; border-radius: 8px; margin: 8px;
.conclusion-item { margin-bottom: 15px;
&:last-child { margin-bottom: 0; }
.conclusion-title { color: #FFD700; font-size: 16px; font-weight: bold; margin: 0 0 8px 0; text-align: center; }
.conclusion-text { color: #ffffff; font-size: 14px; line-height: 1.5; margin: 0 0 6px 0; text-align: left; word-wrap: break-word;
&:last-child { margin-bottom: 0; } } } }
.conclusion-placeholder { padding: 15px; text-align: center;
p { color: #999999; font-size: 12px; margin: 0; } } }
.bk-image { background-size: 100% 100%; background-repeat: no-repeat; width: 100%; height: auto; margin: 0 auto; margin-top: 0px; margin-left: 0;
.conclusion-container { padding: 20px; border-radius: 15px; margin: 20px; /* background: linear-gradient(135deg, rgba(0, 212, 255, 0.15) 0%, rgba(0, 100, 200, 0.15) 100%); */ border: 2px solid rgba(0, 212, 255, 0.4); box-shadow: 0 8px 25px rgba(0, 212, 255, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1);
.conclusion-item { margin-bottom: 20px; padding: 20px; border-radius: 12px; /* background: linear-gradient(135deg, rgba(0, 212, 255, 0.2) 0%, rgba(0, 150, 255, 0.1) 100%); */ /* border: 1px solid rgba(0, 212, 255, 0.5); */ /* border-left: 5px solid #00d4ff; */ /* box-shadow: 0 4px 15px rgba(0, 212, 255, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1); */ transition: all 0.3s ease; position: relative; overflow: hidden; }
.conclusion-item::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, #00d4ff, #0099ff, #00d4ff); opacity: 0.8; }
.conclusion-item:last-child { margin-bottom: 0; }
.conclusion-item .conclusion-title { color: #FFD700; font-size: 22px; font-weight: bold; margin: 0 0 15px 0; text-align: center; letter-spacing: 2px; margin-top: 10px; }
.conclusion-item .conclusion-title::after { content: ''; position: absolute; bottom: -5px; left: 50%; transform: translateX(-50%); width: 60px; height: 2px; background: linear-gradient(90deg, transparent, #00d4ff, transparent); }
.conclusion-item .conclusion-text { color: #ffffff; font-size: 20px; line-height: 1.8; margin: 0 0 12px 0; text-align: left; word-wrap: break-word; padding-left: 15px; position: relative; }
.conclusion-item .conclusion-text::before { content: '▶'; position: absolute; left: 0; top: 0; color: #00d4ff; font-size: 12px; opacity: 0.7; }
.conclusion-item .conclusion-text:last-child { margin-bottom: 0; } }
.conclusion-placeholder { padding: 30px; text-align: center; border-radius: 12px; background: rgba(255, 255, 255, 0.05); border: 1px dashed rgba(153, 153, 153, 0.3); }
.conclusion-placeholder p { color: #999999; font-size: 16px; margin: 0; font-style: italic; }
.disclaimer-item { /* margin-top: 30px; */ padding-bottom: 15%; border-top: 1px solid rgba(153, 153, 153, 0.2); text-align: center; }
.disclaimer-item p { color: #ffffff !important; font-size: 16px; margin: 0; letter-spacing: 1px; } }
.class05 { background-size: 100% 100%; background-repeat: no-repeat; width: 100%; height: auto; /* min-height: 48rem; */ }
.class06 { background-size: 100% 100%; background-repeat: no-repeat; width: 100%; height: auto; min-height: 35rem; margin: 0 auto; }
.text-container { position: relative; top: 0; left: 0; color: white; text-align: left; padding: 5%; }
.class003 { padding-top: 12%; gap: 20vw; }
.div00 { display: flex; flex-direction: column; /* margin-left: 5rem; */ gap: 0; /* margin-top: -6rem; */ /* width: 100%; */ height: auto; }
.class003 .div01, .class003 .div02 { /* width: 80%; */ min-width: 250px; font-size: 16px; min-height: 35px; }
.span01 { /* 使用导入的背景图片 */ background-image: url('@/assets/img/AiEmotion/bk01.png'); background-size: 100% 100%; /* 背景图片覆盖整个容器 */ background-repeat: no-repeat; /* 防止背景图片重复 */ display: inline-block; /* 确保容器是块级元素 */ padding: 10px; /* 添加内边距以显示内容 */ color: #fff; /* 设置文字颜色以确保可读性 */ font-size: 14px; /* 增加字体大小以便更清晰显示股票名称 */ text-align: center; width: 50%; }
.class0502, .class0601 { padding-top: 10%; }
.div00 { flex-direction: column; gap: 1rem; align-items: center; }
.span01 { width: 60%; font-size: 1.2rem; padding: 8px; }
.span02 { font-size: 1.2rem; margin-top: -3%; }
.title1, .title2, .title3, .title4 { font-size: 18px; margin-left: 0; } }
/* 超小屏幕设备 */ @media only screen and (max-width: 480px) { .class01 { width: 100%; padding: 0.3rem; margin-bottom: 3rem; }
.div00 { flex-direction: column; gap: 0.8rem; align-items: center; }
.class003 { padding-top: 14%; gap: 30px; }
.class003 .div01, .class003 .div02 { /* width: 90%; */ width: 10vw; min-width: 200px; font-size: 14px; min-height: 30px; }
.span01 { width: 50%; font-size: 1rem; padding: 6px; }
.span02 { font-size: 1rem; }
.golden-wheel-img { width: 80%; }
/* 调整图表容器高度适配超小屏幕 */ /* .class00 { min-height: 25rem; } */
.class03 { min-height: 35rem; }
.class06 { min-height: 32rem; }
.class08 { min-height: 15rem; }
.scaled-img { height: 150px; min-height: 150px; } }
/* 加载提示样式 */ .loading-container { display: flex; justify-content: center; align-items: center; min-height: 20vh; padding: 20px 10px; }
.loading-content { text-align: center; background: linear-gradient(135deg, rgba(0, 212, 255, 0.15) 0%, rgba(0, 100, 200, 0.15) 100%); border: 2px solid rgba(0, 212, 255, 0.4); border-radius: 20px; padding: 40px; box-shadow: 0 8px 25px rgba(0, 212, 255, 0.3); }
.loading-spinner { width: 60px; height: 60px; border: 4px solid rgba(0, 212, 255, 0.3); border-top: 4px solid #00d4ff; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 20px; }
@keyframes spin { 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } }
.loading-text { color: #00d4ff; font-size: 18px; font-weight: bold; text-shadow: 0 2px 8px rgba(0, 212, 255, 0.5); letter-spacing: 1px; }
/* 顶部锚点样式 */ .top-anchor { position: relative; top: 0; left: 0; width: 100%; height: 1px; display: block; visibility: visible; opacity: 0; pointer-events: none; }
/* 返回顶部按钮样式 */ .back-to-top { position: sticky !important; bottom: 20px !important; left: calc(100% - 70px) !important; width: 50px !important; height: 50px !important; background: linear-gradient(135deg, #00d4ff 0%, #0066cc 100%) !important; border-radius: 50% !important; display: flex !important; align-items: center !important; justify-content: center !important; cursor: pointer !important; box-shadow: 0 4px 15px rgba(0, 212, 255, 0.3) !important; transition: all 0.3s ease !important; z-index: 100 !important; color: white !important; opacity: 1 !important; visibility: visible !important; margin-top: 20px !important; margin-bottom: 20px !important; }
.back-to-top:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(0, 212, 255, 0.5); background: linear-gradient(135deg, #00e6ff 0%, #0077dd 100%); }
.back-to-top:active { transform: translateY(-1px); }
/* class01容器样式 */ .class01 { position: relative; }
/* 移动端适配 */ @media only screen and (max-width: 768px) { .back-to-top { left: calc(100% - 65px) !important; width: 45px !important; height: 45px !important; } }
@media only screen and (max-width: 480px) { .back-to-top { left: calc(100% - 60px) !important; width: 40px !important; height: 40px !important; }
.back-to-top svg { width: 20px; height: 20px; } } </style>
|