|
|
<template> <!-- 顶部锚点 --> <div id="top-anchor" class="top-anchor"></div>
<!-- 主容器:包含对话框和main容器 --> <div class="page-container"> <!-- 对话框区域 --> <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="conversation-area" v-if="messages.length > 0 && !isHistoryMode" > <div class="message-list"> <div v-for="(message, index) in messages" :key="index" class="message-item" :class="{ 'user-message-item': message.sender === 'user', 'ai-message-item': message.sender === 'ai', }" > <!-- 用户消息 --> <div v-if="message.sender === 'user'" class="user-message-wrapper"> <div class="message-bubble user-message"> {{ message.text }} </div> </div>
<!-- AI消息(包括思考过程) --> <div v-else class="ai-message-wrapper"> <div class="ai-message-container"> <img v-if="message.gif" :src="message.gif" alt="思考过程" class="thinking-gif" /> <div class="message-bubble ai-message"> <div v-if="message.flag"> <span>{{ message.text }}</span> <span class="loading-dots"> <span class="dot">.</span> <span class="dot">.</span> <span class="dot">.</span> <span class="dot">.</span> <span class="dot">.</span> <span class="dot">.</span> </span> </div> <div v-else>{{ message.text }}</div> </div> </div> </div> </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>
<!-- main容器区域 --> <!-- 移除股票标签页,改为对话形式展示 -->
<!-- 渲染整个页面 - 遍历过滤后的股票列表显示所有股票 --> <div class="master" v-for="(stock, stockIndex) in filteredStockList" :key="`stock-${stockIndex}-${stock.timestamp}`" v-if="isPageLoaded" > <!-- 对应股票的消息显示区域 --> <div class="user-input-display"> <div class="message-container"> <!-- 显示该股票对应的用户输入内容 --> <div class="user-message-container"> <div class="user-content"> <img :src=" isVoice && getStockAudioState(stock).isPlaying ? voice : voiceNoActive " class="user-message-speaker" :class="{ 'speaker-active': isVoice && getStockAudioState(stock).isPlaying, }" @click="() => toggleVoiceForUser(stock)" alt="喇叭" /> <div class="message-bubble user-message"> {{ stock.queryText }} </div> </div> <div class="user-sendTime"> {{ moment(stock.timestamp).format("YYYY-MM-DD HH:mm:ss") }} </div> </div> </div> </div>
<div class="main"> <div class="main-content-wrapper"> <!-- 四维矩阵图 --> <div class="matrix-header"> <!-- <img class="item" :src="item" alt="思维矩阵图片" /> --> <div class="market-temperature-label"> {{ stock.stockInfo.name }}{{ stock.stockInfo.name ? "量子四维矩阵图" : "" }} </div> <div class="market-temperature-value"> {{ getDisplayDate(stock) }} </div> </div> <div class="market-temperature-icon" v-if="chartVisibility.marketTemperature" > <img src="@/assets/img/AiEmotion/L1.png" alt="情绪监控图标" /> </div> <!-- 温度计图表 --> <div class="market-temperature-section" v-if="chartVisibility.marketTemperature" > <div class="temperature-content"> <div class="content1"> <img src="@/assets/img/AiEmotion/温度计.png" alt="温度计图标" /> <span class="matrix-main-title">股市温度计</span> </div> <div class="temperature-display"> <div class="temperature-cold"> 股票温度:{{ getStockData2(stock) ?? "NA" }} </div> <div class="temperature-hot"> 市场温度:{{ getStockData1(stock) }} </div> </div> </div> <marketTemperature :ref="(el) => (marketTemperatureRef[stockIndex] = el)" :companyName="stock.stockInfo.name" :stockCode="stock.stockInfo.code" /> </div> </div> <div class="emotion-decoder-icon" v-if="chartVisibility.emotionDecod"> <img src="@/assets/img/AiEmotion/L2.png" alt="情绪解码图标" /> </div> <!-- 情绪解码器图表 --> <div class="emotion-decoder-section" v-if="chartVisibility.emotionDecod" > <div class="emotion-decoder-header"> <img src="@/assets/img/AiEmotion/emotionDecod.png" alt="情绪解码器图标" /> <span class="emotion-decoder-text">情绪解码器</span> </div> <div class="emotion-decoder-content"> <emotionDecod :ref="(el) => (emotionDecodRef[stockIndex] = el)" ></emotionDecod> </div> </div> <div class="bottom-radar-icon" v-if="chartVisibility.emotionalBottomRadar" > <img src="@/assets/img/AiEmotion/L3.png" alt="情绪推演图标" /> </div> <!-- 情绪探底雷达图表 --> <div class="bottom-radar-section" v-if="chartVisibility.emotionalBottomRadar" > <div class="bottom-radar-header"> <img src="@/assets/img/AiEmotion/探底雷达.png" alt="探底雷达图表" /> <span class="bottom-radar-text">情绪探底雷达</span> </div> <div class="bottom-radar-content"> <emotionalBottomRadar :ref="(el) => (emotionalBottomRadarRef[stockIndex] = el)" ></emotionalBottomRadar> </div> </div> <div class="energy-converter-icon" v-if="chartVisibility.emoEnergyConverter" > <img src="@/assets/img/AiEmotion/L4.png" alt="情绪套利" /> </div> <!-- 情绪能量转化器图表 --> <div class="energy-converter-section" v-if="chartVisibility.emoEnergyConverter" > <div class="energy-converter-header"> <img src="@/assets/img/AiEmotion/能量转化器.png" alt="能量转化器图标" /> <span class="energy-converter-text">情绪能量转化器</span> </div> <div class="energy-converter-content"> <emoEnergyConverter :ref="(el) => (emoEnergyConverterRef[stockIndex] = el)" ></emoEnergyConverter> </div> </div> <!-- 核心看点 --> <div class="core-highlights-header"> <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="core-logic-header"> <img src="@/assets/img/AiEmotion/核心逻辑.png" alt="核心逻辑字样" /> </div> <div class="decision-tree-section"> <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="scenario-application-section" ref="scenarioApplicationRef"> <img src="@/assets/img/AiEmotion/场景应用.png" alt="场景应用标题" /> <div class="bk-image"> <div class="conclusion-container"> <!-- 使用当前股票项的结论数据显示内容,支持打字机效果 --> <!-- L1: 情绪监控 --> <div class="conclusion-item" v-if="(getStockTypewriterVisibility(stock) && getStockTypewriterVisibility(stock).one) || (getStockConclusion(stock) && !getStockTypewriterTexts(stock) && (getStockConclusion(stock).one1 || getStockConclusion(stock).one2))"> <h4 class="conclusion-title">L1: 情绪监控</h4> <!-- 打字机效果文本 --> <p class="conclusion-text" v-if="getStockTypewriterTexts(stock) && getStockTypewriterTexts(stock).one1"> {{ getStockTypewriterTexts(stock).one1 }} </p> <p class="conclusion-text" v-if="getStockTypewriterTexts(stock) && getStockTypewriterTexts(stock).one2"> {{ getStockTypewriterTexts(stock).one2 }} </p> <!-- 完整文本 --> <p class="conclusion-text" v-if="!getStockTypewriterTexts(stock) && getStockConclusion(stock) && getStockConclusion(stock).one1"> {{ getStockConclusion(stock).one1 }} </p> <p class="conclusion-text" v-if="!getStockTypewriterTexts(stock) && getStockConclusion(stock) && getStockConclusion(stock).one2"> {{ getStockConclusion(stock).one2 }} </p> </div> <!-- L2: 情绪解码 --> <div class="conclusion-item" v-if="(getStockTypewriterVisibility(stock) && getStockTypewriterVisibility(stock).two) || (getStockConclusion(stock) && !getStockTypewriterTexts(stock) && getStockConclusion(stock).two)"> <h4 class="conclusion-title">L2: 情绪解码</h4> <!-- 打字机效果文本 --> <p class="conclusion-text" v-if="getStockTypewriterTexts(stock) && getStockTypewriterTexts(stock).two"> {{ getStockTypewriterTexts(stock).two }} </p> <!-- 完整文本 --> <p class="conclusion-text" v-if="!getStockTypewriterTexts(stock) && getStockConclusion(stock) && getStockConclusion(stock).two"> {{ getStockConclusion(stock).two }} </p> </div> <!-- L3: 情绪推演 --> <div class="conclusion-item" v-if="(getStockTypewriterVisibility(stock) && getStockTypewriterVisibility(stock).three) || (getStockConclusion(stock) && !getStockTypewriterTexts(stock) && getStockConclusion(stock).three)"> <h4 class="conclusion-title">L3: 情绪推演</h4> <!-- 打字机效果文本 --> <p class="conclusion-text" v-if="getStockTypewriterTexts(stock) && getStockTypewriterTexts(stock).three"> {{ getStockTypewriterTexts(stock).three }} </p> <!-- 完整文本 --> <p class="conclusion-text" v-if="!getStockTypewriterTexts(stock) && getStockConclusion(stock) && getStockConclusion(stock).three"> {{ getStockConclusion(stock).three }} </p> </div> <!-- L4: 情绪套利 --> <div class="conclusion-item" v-if="(getStockTypewriterVisibility(stock) && getStockTypewriterVisibility(stock).four) || (getStockConclusion(stock) && !getStockTypewriterTexts(stock) && getStockConclusion(stock).four)"> <h4 class="conclusion-title">L4: 情绪套利</h4> <!-- 打字机效果文本 --> <p class="conclusion-text" v-if="getStockTypewriterTexts(stock) && getStockTypewriterTexts(stock).four"> {{ getStockTypewriterTexts(stock).four }} </p> <!-- 完整文本 --> <p class="conclusion-text" v-if="!getStockTypewriterTexts(stock) && getStockConclusion(stock) && getStockConclusion(stock).four"> {{ getStockConclusion(stock).four }} </p> </div> <!-- AI生成内容免责声明 --> <div class="disclaimer-item" v-if="(getStockTypewriterVisibility(stock) && getStockTypewriterVisibility(stock).disclaimer) || (getStockConclusion(stock) && !getStockTypewriterTexts(stock))"> <!-- 打字机效果文本 --> <p class="disclaimer-text" v-if="getStockTypewriterTexts(stock) && getStockTypewriterTexts(stock).disclaimer"> {{ getStockTypewriterTexts(stock).disclaimer }} </p> <!-- 完整文本 --> <!-- <p class="disclaimer-text" v-if="!getStockTypewriterTexts(stock)"> 该内容由AI生成,请注意甄别 </p> --> </div> </div> <div class="conclusion-placeholder" v-if="!getStockConclusion(stock) || (!getStockConclusion(stock).one1 && !getStockConclusion(stock).one2 && !getStockConclusion(stock).two && !getStockConclusion(stock).three && !getStockConclusion(stock).four)" > <p>等待股票分析结论...</p> </div> </div> <p class="disclaimer-text" v-if="!getStockTypewriterTexts(stock)"> 该内容由AI生成,请注意甄别 </p> </div>
</div> </div> <!-- 全局返回顶部按钮 --> <div class="back-to-top" @click="scrollToTop" v-if="(filteredStockList.length > 0 || messages.length > 0)"> <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 moment from "moment"; 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
// 导入思考过程GIF
import thinkingGif from "@/assets/img/gif/思考.gif"; import analyzeGif from "@/assets/img/gif/解析.gif"; import generateGif from "@/assets/img/gif/生成.gif"; import getCountAll from "../assets/img/homePage/get-count-all.png"; import voice from "../assets/img/homePage/tail/voice.png"; import voiceNoActive from "../assets/img/homePage/tail/voice-no-active.png"; import { Howl, Howler } from "howler"; // 导入音频播放库
import { reactive } from "vue"; import { marked } from "marked"; // 引入marked库
import { useUserStore } from "../store/userPessionCode"; const APIurl = import.meta.env.VITE_APP_API_BASE_URL;
// 使用Pinia store
const emotionStore = useEmotionStore(); const emotionAudioStore = useEmotionAudioStore();
// 语音播放控制函数
const toggleVoiceForUser = (stock) => { if (!emotionAudioStore.isVoiceEnabled) { // 如果语音功能关闭,先开启语音功能
emotionAudioStore.toggleVoice(); } else { // 获取该股票的结论数据和当前状态
const stockConclusion = getStockConclusion(stock); const currentState = getStockAudioState(stock);
// 检查是否有任何音频正在播放(包括全局播放状态和当前股票状态)
const isAnyAudioPlaying = emotionAudioStore.isPlaying || currentState.isPlaying;
// 如果当前点击的股票正在播放,则暂停
if (currentState.isPlaying) { console.log("暂停当前股票音频:", stock.stockInfo?.name); // 暂停音频而不是停止
if (emotionAudioStore.nowSound && emotionAudioStore.nowSound.playing()) { emotionAudioStore.nowSound.pause(); emotionAudioStore.isPaused = true; emotionAudioStore.isPlaying = false; } setStockAudioState(stock, { isPlaying: false, isPaused: true }); } else if (currentState.isPaused) { // 如果当前股票处于暂停状态,则继续播放
console.log("继续播放当前股票音频:", stock.stockInfo?.name); if (emotionAudioStore.nowSound) { emotionAudioStore.nowSound.play(); emotionAudioStore.isPaused = false; emotionAudioStore.isPlaying = true; } setStockAudioState(stock, { isPlaying: true, isPaused: false }); } else { // 如果有其他音频正在播放,先停止
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 { console.log("该股票没有可播放的音频数据"); } } } };
// 计算属性:判断语音是否启用
const isVoice = computed(() => { return emotionAudioStore.isVoiceEnabled; }); 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([]); // 引用市场温度计组件数组
const emoEnergyConverterRef = ref([]); const emotionDecodRef = ref([]); const emotionalBottomRadarRef = ref([]); const userInputDisplayRef = ref(null); //消息区域的引用
// 响应式数据
const messages = ref([]);
// 从emotion store中恢复对话记录
const loadConversationsFromStore = () => { const storedConversations = emotionStore.getConversations(); messages.value = storedConversations.map((conv) => ({ sender: conv.sender, text: conv.text, })); };
// 记录已经添加到对话中的股票,避免重复添加
const addedStocks = ref(new Set());
// 从stockList生成对话历史
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) => msg.sender === "user" && msg.text === stock.queryText );
// 只有当messages中不存在相同消息时才添加
if (!existingMessage) { // 只添加用户输入消息,不添加AI回复
const userMessage = { sender: "user", text: stock.queryText, }; messages.value.push(userMessage);
// 只将用户消息添加到emotion store中(如果store中也不存在)
const storeConversations = emotionStore.getConversations(); const existingInStore = storeConversations.find( (conv) => conv.sender === "user" && conv.text === stock.queryText ); if (!existingInStore) { emotionStore.addConversation(userMessage); } }
// 将这个股票标记为已添加
addedStocks.value.add(stockKey); } }); };
// 清空对话记录
const clearConversations = () => { messages.value = []; emotionStore.clearConversations(); // 清空已添加股票的记录
addedStocks.value.clear(); };
// 添加股票数据的包装方法
const addStock = (stockData) => { console.log("AiEmotion组件接收到股票数据:", stockData);
// 设置为历史记录模式,并重置用户主动搜索标志
isHistoryMode.value = true; isUserInitiated.value = false;
// 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();
// 清理状态变量(保留用于其他功能的变量)
// 隐藏所有图表组件
chartVisibility.value = { marketTemperature: false, emotionDecod: false, emotionalBottomRadar: false, emoEnergyConverter: false, };
// 2. 将新的数据存储到stockList中
emotionStore.addStock(stockData); // 3. 设置页面为已加载状态,重新渲染页面
isPageLoaded.value = true;
// 4. 标记历史记录股票已显示过,避免重复触发
if (stockData.conclusionData) { const stockCode = stockData.stockInfo?.code || stockData.stockInfo?.symbol; if (stockCode) { stockTypewriterShown.value.set(stockCode, true); stockAudioPlayed.value.set(stockCode, true); } console.log("历史记录股票已标记为已显示"); }
// 5. 使用nextTick确保DOM更新后启动高度监听器并滚动到底部
nextTick(() => { // 启动页面高度监听器,实时监听内容变化并自动滚动
startHeightObserver(); // 立即滚动到底部
scrollToBottom(); // 6. 历史记录加载完成后,通知父组件重新启用输入框
emit('enableInput'); }); };
// 暴露方法给父组件
defineExpose({ handleSendMessage, clearConversations, addStock, }); const isPageLoaded = ref(false); // 控制页面是否显示
const isHistoryMode = 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); // 标记是否为用户主动搜索
// 打字机效果相关的变量已移除,现在直接使用parsedConclusion显示内容
// 图表组件显示状态
const chartVisibility = ref({ marketTemperature: true, emotionDecod: true, emotionalBottomRadar: true, emoEnergyConverter: true, }); const typewriterTimers = ref([]); // 记录每个股票是否已经显示过打字机效果
const stockTypewriterShown = ref(new Map()); // 记录每个股票的打字机显示状态
const stockTypewriterTexts = ref(new Map()); // 记录每个股票的打字机模块可见性
const stockTypewriterVisibility = ref(new Map()); // 记录每个股票是否已经播放过音频
const stockAudioPlayed = ref(new Map()); // 跟踪每个股票的音频播放状态
const stockAudioStates = ref(new Map()); // 存储当前的完成回调函数
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);
// 计算属性 - 从store获取当前股票数据
const currentStock = computed(() => emotionStore.activeStock); const stockName = computed(() => currentStock.value?.stockInfo.name || ""); const stockCode = computed( () => currentStock.value?.stockInfo.code || currentStock.value?.stockInfo.symbol || "" ); 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;
// 如果conclusionData已经是对象,直接返回
if (typeof currentConclusion.value === "object") { return currentConclusion.value; }
// 如果是字符串,尝试解析JSON
try { return JSON.parse(currentConclusion.value); } catch (error) { console.error("解析结论数据失败:", error); return null; } });
// 多股票数据显示的计算属性
// 过滤和排序后的股票列表
const filteredStockList = computed(() => { return emotionStore.stockList .filter((stock) => { // 过滤掉数据不完整的股票
return stock.stockInfo?.name && stock.apiData && stock.queryText; }) .sort((a, b) => { // 按时间戳降序排序,最新的在前
return new Date(a.timestamp) - new Date(b.timestamp); }); });
// 辅助函数:获取股票的显示日期
const getDisplayDate = (stock) => { if (!stock?.apiData) return ""; const lastData = stock.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 getStockData1 = (stock) => { if (!stock?.apiData) return null; const lastData = stock.apiData.GSWDJ?.at(-1); return lastData ? Math.round(lastData[1]) : null; };
// 辅助函数:获取股票的股票温度数据
const getStockData2 = (stock) => { if (!stock?.apiData) return null; const lastData = stock.apiData.GSWDJ?.at(-1); return lastData ? Math.round(lastData[2]) : null; };
// 辅助函数:获取股票的结论数据
const getStockConclusion = (stock) => { if (!stock?.conclusionData) return null;
// 如果conclusionData已经是对象,直接返回
if (typeof stock.conclusionData === "object") { return stock.conclusionData; }
// 如果是字符串,尝试解析JSON
try { return JSON.parse(stock.conclusionData); } catch (error) { console.error("解析股票结论数据失败:", error); return null; } };
// 辅助函数:获取股票的打字机文本状态
const getStockTypewriterTexts = (stock) => { const stockCode = stock.stockInfo?.code || stock.stockInfo?.symbol; if (!stockCode) return null; return stockTypewriterTexts.value.get(stockCode) || null; };
// 辅助函数:获取股票的打字机可见性状态
const getStockTypewriterVisibility = (stock) => { const stockCode = stock.stockInfo?.code || stock.stockInfo?.symbol; if (!stockCode) return null; return stockTypewriterVisibility.value.get(stockCode) || null; };
// 辅助函数:检查股票是否正在进行打字机效果
const isStockTypewriting = (stock) => { const stockCode = stock.stockInfo?.code || stock.stockInfo?.symbol; if (!stockCode) return false; return stockTypewriterShown.value.has(stockCode) && !stockTypewriterTexts.value.has(stockCode); };
// 监听股票列表变化,当列表为空时隐藏页面数据
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(); // 清理已添加股票的记录
addedStocks.value.clear(); // 隐藏所有图表组件
chartVisibility.value = { marketTemperature: false, emotionDecod: false, emotionalBottomRadar: false, emoEnergyConverter: false, }; console.log("股票列表已清空,页面数据已隐藏"); } else { // 当stockList有数据时,更新对话记录
loadConversationsFromStockList(); } }, { 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;
// 处理当前股票的音频URL
if (newStock.conclusionData) { try { // 如果conclusionData已经是对象,直接使用;否则解析JSON
const conclusion = typeof newStock.conclusionData === "object" ? newStock.conclusionData : JSON.parse(newStock.conclusionData);
// 检查该股票是否已经显示过打字机效果
if (stockCode && stockTypewriterShown.value.has(stockCode)) { // 如果已经显示过,直接显示完整内容,不需要打字机效果
// 提取音频URL但不自动播放,等待用户手动点击
let voiceUrl = null; // 优先使用one1_url,如果没有则尝试其他音频URL
if (conclusion.one1_url) { voiceUrl = conclusion.one1_url .toString() .trim() .replace(/[`\s]/g, ""); } else if (conclusion.one2_url) { voiceUrl = conclusion.one2_url .toString() .trim() .replace(/[`\s]/g, ""); } else if (conclusion.two_url) { voiceUrl = conclusion.two_url .toString() .trim() .replace(/[`\s]/g, ""); } else if (conclusion.three_url) { voiceUrl = conclusion.three_url .toString() .trim() .replace(/[`\s]/g, ""); } else if (conclusion.four_url) { voiceUrl = conclusion.four_url .toString() .trim() .replace(/[`\s]/g, ""); } else 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 { // 如果没有结论数据,清空音频URL
audioUrl.value = ""; emotionAudioStore.resetAudioState(); console.log("当前股票没有结论数据,已清空音频"); }
// 只有在页面已加载的情况下才渲染图表
if (isPageLoaded.value) { nextTick(() => { renderCharts(newStock.apiData); console.log("图表数据已准备完成,开始渲染:", newStock.apiData); // 检查场景应用部分是否已经在视口中,如果是则立即触发效果
setTimeout(() => { // 检查 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;
// 如果该股票已经显示过,不需要再处理
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 (isUserInitiated.value && audioUrl.value) { console.log( "用户主动搜索,该股票第一次进入场景应用,开始打字机效果和音频播放" ); hasTriggeredTypewriter.value = true; hasTriggeredAudio.value = true;
if (!stockAudioPlayed.value.has(stockCode)) { console.log("开始音频播放和打字机效果"); stockAudioPlayed.value.set(stockCode, true); playAudioQueue(parsedConclusion.value, true); } else { // 如果音频已播放过,只启动打字机效果
startTypewriterEffect(parsedConclusion.value, stockCode); }
stockTypewriterShown.value.set(stockCode, true); } else if (isUserInitiated.value && !audioUrl.value) { console.log( "音频尚未准备好,等待音频加载完成后再触发效果(股票切换后)" ); return; } else { // 非用户主动搜索(如历史记录恢复),直接显示完整内容
console.log( "非用户主动搜索,该股票第一次进入场景应用,直接显示完整内容" );
const conclusion = parsedConclusion.value; // 结论内容现在直接通过parsedConclusion计算属性显示
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, }; } } } } }, 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.one1_url) { voiceUrl = newConclusion.one1_url .toString() .trim() .replace(/[`\s]/g, ""); } else if (newConclusion.one2_url) { voiceUrl = newConclusion.one2_url .toString() .trim() .replace(/[`\s]/g, ""); } else if (newConclusion.two_url) { voiceUrl = newConclusion.two_url .toString() .trim() .replace(/[`\s]/g, ""); } else if (newConclusion.three_url) { voiceUrl = newConclusion.three_url .toString() .trim() .replace(/[`\s]/g, ""); } else if (newConclusion.four_url) { voiceUrl = newConclusion.four_url .toString() .trim() .replace(/[`\s]/g, ""); } else 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, stockId, onComplete) { // 如果没有传入stockId,使用当前活跃股票
if (!stockId && emotionStore.activeStock) { const stock = emotionStore.activeStock; stockId = stock.stockInfo?.code || stock.stockInfo?.symbol; } if (!stockId) { console.warn("无法确定股票ID,跳过打字机效果"); return; }
console.log("开始打字机效果,结论数据:", conclusion, "股票ID:", stockId);
// 保存当前的完成回调函数
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 = [];
// 初始化该股票的打字机状态
if (!stockTypewriterTexts.value.has(stockId)) { stockTypewriterTexts.value.set(stockId, { one1: "", one2: "", two: "", three: "", four: "", disclaimer: "", }); } if (!stockTypewriterVisibility.value.has(stockId)) { stockTypewriterVisibility.value.set(stockId, { one: false, two: false, three: false, four: false, disclaimer: false, }); }
// 重置该股票的显示文本和状态
stockTypewriterTexts.value.set(stockId, { one1: "", one2: "", two: "", three: "", four: "", disclaimer: "", });
stockTypewriterVisibility.value.set(stockId, { 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(() => { const visibility = stockTypewriterVisibility.value.get(stockId); if (visibility) { visibility[module.key] = true; stockTypewriterVisibility.value.set(stockId, { ...visibility }); } }, totalDelay); typewriterTimers.value.push(showModuleTimer); totalDelay += 100;
// 为每个内容项添加打字机效果
module.contents.forEach((content) => { if (content.text && content.text.trim()) { for (let i = 0; i <= content.text.length; i++) { const timer = setTimeout(() => { const texts = stockTypewriterTexts.value.get(stockId); if (texts) { texts[content.key] = content.text.substring(0, i); stockTypewriterTexts.value.set(stockId, { ...texts }); } }, totalDelay + i * typeSpeed); typewriterTimers.value.push(timer); } totalDelay += content.text.length * typeSpeed + 200; // 内容间间隔
} });
totalDelay += 800; // 模块间间隔
});
// 添加免责声明的打字机效果(在所有模块显示完成后)
const disclaimerText = "该内容由AI生成,请注意甄别";
// 显示免责声明模块
const showDisclaimerTimer = setTimeout(() => { const visibility = stockTypewriterVisibility.value.get(stockId); if (visibility) { visibility.disclaimer = true; stockTypewriterVisibility.value.set(stockId, { ...visibility }); } }, totalDelay); typewriterTimers.value.push(showDisclaimerTimer); totalDelay += 100;
// 打字机效果显示免责声明
for (let i = 0; i <= disclaimerText.length; i++) { const timer = setTimeout(() => { const texts = stockTypewriterTexts.value.get(stockId); if (texts) { texts.disclaimer = disclaimerText.substring(0, i); stockTypewriterTexts.value.set(stockId, { ...texts }); } // 在免责声明打字机效果完成后调用回调函数
if (i === disclaimerText.length) { console.log("打字机效果完成,调用onComplete回调"); if (onComplete && typeof onComplete === "function") { onComplete(); // 清除保存的回调函数
currentOnCompleteCallback.value = null; } } }, totalDelay + i * typeSpeed); typewriterTimers.value.push(timer); } }
// 清理定时器的函数
function clearTypewriterTimers() { typewriterTimers.value.forEach((timer) => clearTimeout(timer)); typewriterTimers.value = []; }
// 音频队列管理
const audioQueue = ref([]); const isPlayingQueueAudio = ref(false); let currentPlayIndex = 0; let isCallingPlayNext = false;
// 音频队列顺序管理
const audioQueueOrder = { one1_url: 1, one2_url: 2, two_url: 3, three_url: 4, four_url: 5, url: 6, audioUrl: 7, voice_url: 8, audio: 9, tts_url: 10, };
// 播放音频队列中的下一个音频
const playNextAudio = () => { console.log("=== playNextAudio 被调用 ==="); console.log("当前队列状态:", { queueLength: audioQueue.value.length, queueItems: audioQueue.value.map((item) => item.name), currentPlayIndex: currentPlayIndex, isPlayingQueueAudio: isPlayingQueueAudio.value, isCallingPlayNext: isCallingPlayNext, audioStoreIsPlaying: emotionAudioStore.isPlaying, });
if ( audioQueue.value.length === 0 || isPlayingQueueAudio.value || isCallingPlayNext ) { console.log( "❌ 播放条件不满足 - 队列长度:", audioQueue.value.length, "正在播放:", isPlayingQueueAudio.value, "正在调用:", isCallingPlayNext ); return; }
// 检查是否已播放完所有音频
if ( currentPlayIndex >= audioQueue.value.length && audioQueue.value.length > 0 ) { console.log("🔄 所有音频播放完成,重置索引从第一个开始"); currentPlayIndex = 0; isCallingPlayNext = false; // 重置调用标志
}
isCallingPlayNext = true; isPlayingQueueAudio.value = true; const audioInfo = audioQueue.value[currentPlayIndex];
console.log( `✅ 开始播放${audioInfo.name}音频 (索引:${currentPlayIndex}),队列总长度:`, audioQueue.value.length );
// 停止之前的音频
if (emotionAudioStore.nowSound && emotionAudioStore.nowSound.playing()) { emotionAudioStore.nowSound.stop(); }
// 创建新的音频实例
const audio = new Howl({ src: [audioInfo.url], html5: true, format: ["mp3", "wav"], onplay: () => { isAudioPlaying.value = true; isPlayingQueueAudio.value = true; emotionAudioStore.isPlaying = true; console.log(`开始播放${audioInfo.name}音频`);
// 设置当前股票的音频播放状态为播放中
const currentStock = emotionStore.activeStock; if (currentStock) { setStockAudioState(currentStock, { isPlaying: true, isPaused: false }); console.log('设置当前股票音频状态为播放中:', currentStock.stockInfo?.name); }
// 如果是第一个音频且需要启动打字机效果,则启动
if ( currentPlayIndex === 0 && audioInfo.shouldStartTypewriter && parsedConclusion.value ) { console.log("🎬 第一个音频开始播放,同时启动打字机效果"); const stockId = currentStock?.stockInfo?.code || currentStock?.stockInfo?.symbol; startTypewriterEffect(parsedConclusion.value, stockId, audioInfo.onComplete); } }, onpause: () => { emotionAudioStore.isPlaying = false; emotionAudioStore.isPaused = true; // 保存当前播放位置
if (audio && audio.seek) { emotionAudioStore.playbackPosition = audio.seek() || 0; } console.log(`${audioInfo.name}音频暂停播放,位置:`, emotionAudioStore.playbackPosition); // 设置当前股票的音频播放状态为暂停
const currentStock = emotionStore.activeStock; if (currentStock) { setStockAudioState(currentStock, { isPlaying: false, isPaused: true }); console.log('设置当前股票音频状态为暂停:', currentStock.stockInfo?.name); } }, onresume: () => { emotionAudioStore.isPlaying = true; emotionAudioStore.isPaused = false; // 如果有保存的播放位置,从该位置继续播放
if (emotionAudioStore.playbackPosition > 0 && audio && audio.seek) { audio.seek(emotionAudioStore.playbackPosition); } console.log(`${audioInfo.name}音频继续播放,位置:`, emotionAudioStore.playbackPosition); // 设置当前股票的音频播放状态为播放中
const currentStock = emotionStore.activeStock; if (currentStock) { setStockAudioState(currentStock, { isPlaying: true, isPaused: false }); console.log('设置当前股票音频状态为播放中:', currentStock.stockInfo?.name); } }, onend: () => { console.log(`${audioInfo.name}音频播放完成,准备播放下一个`); emotionAudioStore.isPaused = false; emotionAudioStore.playbackPosition = 0;
// 移动到下一个音频索引
currentPlayIndex++;
// 确保只有在音频真正播放完成时才播放下一个
if (currentPlayIndex < audioQueue.value.length) { console.log( `队列中还有音频,500ms后播放下一个 (索引:${currentPlayIndex})` ); // 重置调用标志,但保持播放状态,直到所有音频播放完成
setTimeout(() => { isCallingPlayNext = false; isPlayingQueueAudio.value = false; // 重置队列播放状态,允许播放下一个音频
// 保持emotionAudioStore.isPlaying为true,确保喇叭图片保持voice状态
emotionAudioStore.isPlaying = true; // 设置下一个音频的URL,确保togglePlayPause能正确工作
const nextAudioInfo = audioQueue.value[currentPlayIndex]; if (nextAudioInfo && nextAudioInfo.url) { emotionAudioStore.setCurrentAudioUrl(nextAudioInfo.url); } playNextAudio(); }, 500); } else { console.log("🎉 所有音频播放完成"); emotionAudioStore.nowSound = null; isCallingPlayNext = false; // 重置音频store的播放状态
emotionAudioStore.isPlaying = false; emotionAudioStore.isPaused = false; emotionAudioStore.playbackPosition = 0; isAudioPlaying.value = false; isPlayingQueueAudio.value = false;
// 重置当前股票的音频播放状态(所有音频播放完成后按钮应该变暗)
const currentStock = emotionStore.activeStock; if (currentStock) { console.log('所有音频播放完成,重置当前股票音频状态:', currentStock.stockInfo?.name); setStockAudioState(currentStock, { isPlaying: false, isPaused: false }); }
// 调用完成回调(如果有的话)
if ( audioInfo.onComplete && typeof audioInfo.onComplete === "function" ) { console.log("调用音频播放完成回调"); audioInfo.onComplete(); } } }, onstop: () => { console.log(`${audioInfo.name}音频被停止`); emotionAudioStore.isPlaying = false; emotionAudioStore.isPaused = false; emotionAudioStore.playbackPosition = 0; isAudioPlaying.value = false; isPlayingQueueAudio.value = false; // 重置当前股票的音频播放状态(音频被停止时按钮应该变暗)
const currentStock = emotionStore.activeStock; if (currentStock) { console.log('音频被停止,重置当前股票音频状态:', currentStock.stockInfo?.name); setStockAudioState(currentStock, { isPlaying: false, isPaused: false }); } }, onerror: (error) => { console.error(`${audioInfo.name}音频播放失败:`, error); isAudioPlaying.value = false; isPlayingQueueAudio.value = false; isCallingPlayNext = false; // 播放下一个音频
setTimeout(() => { playNextAudio(); }, 100); }, onload: () => { emotionAudioStore.duration = audio.duration(); console.log( `${audioInfo.name}音频加载完成,时长:`, emotionAudioStore.duration ); }, });
// 设置当前音频URL到store
emotionAudioStore.setCurrentAudioUrl(audioInfo.url); emotionAudioStore.nowSound = audio; emotionAudioStore.setAudioInstance(audio);
console.log(`尝试播放${audioInfo.name}音频`); audio.play(); };
// 添加音频到播放队列
const addToAudioQueue = ( url, name, shouldStartTypewriter = false, onComplete = null ) => { console.log(`=== 添加音频到队列 ===`); console.log("URL:", url); console.log("Name:", name); console.log("是否启动打字机效果:", shouldStartTypewriter); console.log("音频启用状态:", emotionAudioStore.isVoiceEnabled);
if (url && emotionAudioStore.isVoiceEnabled) { const audioItem = { url, name, order: audioQueueOrder[name] || 999, shouldStartTypewriter, // 添加打字机效果标志
onComplete, // 添加完成回调
}; audioQueue.value.push(audioItem);
// 按顺序排序队列
audioQueue.value.sort((a, b) => a.order - b.order);
console.log(`音频${name}已添加到播放队列,顺序:${audioItem.order}`); console.log( "当前队列顺序:", audioQueue.value.map((item) => `${item.name}(${item.order})`) );
// 只有在确实没有音频在播放且这是第一个音频时才开始播放
if ( !isPlayingQueueAudio.value && !emotionAudioStore.isPlaying && audioQueue.value.length === 1 ) { console.log("✅ 条件满足:没有音频在播放且这是第一个音频,立即开始播放"); playNextAudio(); } else { console.log("⏳ 等待条件:", { isPlayingQueueAudio: isPlayingQueueAudio.value, audioStoreIsPlaying: emotionAudioStore.isPlaying, queueLength: audioQueue.value.length, reason: audioQueue.value.length > 1 ? "队列中已有其他音频" : "有音频正在播放", }); } } else { console.log("❌ 跳过添加音频:", { hasUrl: !!url, voiceEnabled: emotionAudioStore.isVoiceEnabled, }); } };
// 修改后的音频播放函数 - 支持多音频播放和同步打字机效果
function playAudioQueue( conclusionData, shouldStartTypewriter = false, onComplete = null ) { if (!conclusionData) { console.log("没有结论数据,跳过播放"); return; }
// 检查是否启用了语音功能
console.log("语音功能状态:", emotionAudioStore.isVoiceEnabled); if (!emotionAudioStore.isVoiceEnabled) { console.log("语音功能已关闭,跳过播放"); return; }
console.log( "开始处理多音频播放...", shouldStartTypewriter ? "同时启动打字机效果" : "" );
try { // 解析结论数据
const conclusion = typeof conclusionData === "object" ? conclusionData : JSON.parse(conclusionData);
// 清空之前的音频队列
audioQueue.value = []; currentPlayIndex = 0; isCallingPlayNext = false; isPlayingQueueAudio.value = false;
// 按优先级顺序检查并添加所有可用的音频URL
const audioSources = [ { key: "one1_url", name: "one1_url" }, { key: "one2_url", name: "one2_url" }, { key: "two_url", name: "two_url" }, { key: "three_url", name: "three_url" }, { key: "four_url", name: "four_url" }, { key: "url", name: "url" }, { key: "audioUrl", name: "audioUrl" }, { key: "voice_url", name: "voice_url" }, { key: "audio", name: "audio" }, { key: "tts_url", name: "tts_url" }, ];
audioSources.forEach((source) => { if (conclusion[source.key]) { const voiceUrl = conclusion[source.key] .toString() .trim() .replace(/[`\s]/g, ""); if (voiceUrl && voiceUrl.startsWith("http")) { console.log(`找到音频URL: ${source.name} = ${voiceUrl}`); addToAudioQueue( voiceUrl, source.name, shouldStartTypewriter && audioQueue.value.length === 0, onComplete ); } } });
if (audioQueue.value.length === 0) { console.log("未找到有效的音频URL"); // 如果没有音频但需要启动打字机效果,直接启动
if (shouldStartTypewriter) { console.log("没有音频但需要启动打字机效果"); const currentStock = emotionStore.activeStock; const stockId = currentStock?.stockInfo?.code || currentStock?.stockInfo?.symbol; startTypewriterEffect(conclusion, stockId, onComplete); } } else { console.log(`总共找到 ${audioQueue.value.length} 个音频,准备播放`); // 设置当前股票的音频播放状态为播放中(自动播放时按钮应该是亮的)
const currentStock = emotionStore.activeStock; if (currentStock) { console.log('设置当前股票音频状态为播放中:', currentStock.stockInfo?.name); setStockAudioState(currentStock, { isPlaying: true, isPaused: false }); } } } catch (error) { console.error("处理音频播放失败:", error); } }
// 原有的单音频播放函数(保持兼容性)
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("开始播放场景应用语音"); // 设置当前股票的音频播放状态为播放中
const currentStock = emotionStore.activeStock; if (currentStock) { setStockAudioState(currentStock, { isPlaying: true, isPaused: false }); console.log('设置当前股票音频状态为播放中:', currentStock.stockInfo?.name); } // 音频开始播放时的自动滚动已移除
}, onend: () => { isAudioPlaying.value = false; emotionAudioStore.isPlaying = false; emotionAudioStore.isPaused = false; emotionAudioStore.playbackPosition = 0; console.log("场景应用语音播放结束"); // 重置当前股票的音频播放状态
const currentStock = emotionStore.activeStock; if (currentStock) { setStockAudioState(currentStock, { isPlaying: false, isPaused: false }); console.log('重置当前股票音频状态:', currentStock.stockInfo?.name); } }, onstop: () => { isAudioPlaying.value = false; emotionAudioStore.isPlaying = false; console.log("场景应用语音播放停止"); // 重置当前股票的音频播放状态
const currentStock = emotionStore.activeStock; if (currentStock) { setStockAudioState(currentStock, { isPlaying: false, isPaused: false }); console.log('重置当前股票音频状态:', currentStock.stockInfo?.name); } }, onpause: () => { isAudioPlaying.value = false; emotionAudioStore.isPlaying = false; console.log("场景应用语音播放暂停"); // 设置当前股票的音频播放状态为暂停
const currentStock = emotionStore.activeStock; if (currentStock) { setStockAudioState(currentStock, { isPlaying: false, isPaused: true }); console.log('设置当前股票音频状态为暂停:', currentStock.stockInfo?.name); } }, 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 showThinkingProcess(stockName = null) { // 第一步:正在思考
const thinkingMessage1 = reactive({ sender: "ai", text: "AI情绪大模型正在思考", gif: thinkingGif, flag: true, // 添加flag属性以显示动态加载点
}); messages.value.push(thinkingMessage1); await new Promise((resolve) => setTimeout(resolve, 1500)); messages.value.pop();
// 第二步:正在解析关键数据(持续显示直到获取到股票名称)
const thinkingMessage2 = reactive({ sender: "ai", text: "正在解析关键数据", gif: analyzeGif, flag: true, // 添加flag属性以显示动态加载点
}); messages.value.push(thinkingMessage2);
// 如果没有股票名称,保持第二步显示
if (!stockName) { return thinkingMessage2; // 返回消息引用,以便后续更新
}
// 有股票名称后,继续后续步骤
await new Promise((resolve) => setTimeout(resolve, 1500)); messages.value.pop();
// 第三步:生成具体股票的量子四维矩阵图
const thinkingMessage3 = reactive({ sender: "ai", text: `正在生成${stockName}量子四维矩阵图`, gif: generateGif, flag: true, // 添加flag属性以显示动态加载点
}); 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", text: `正在生成${stockName}量子四维矩阵图`, gif: generateGif, flag: true, // 添加flag属性以显示动态加载点
}); messages.value.push(thinkingMessage3);
// 返回第三步消息的引用,以便后续处理
return thinkingMessage3; }
// 完成思考过程(当第二个工作流接口成功后调用)
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); await new Promise((resolve) => setTimeout(resolve, 1500)); messages.value.pop(); }
// 发送消息方法
async function handleSendMessage(input, onComplete) { console.log("发送内容:", input); // 标记为用户主动搜索
isUserInitiated.value = true; // 重置历史记录模式状态,确保正常对话时显示conversation-area
isHistoryMode.value = false;
// 检查用户输入内容是否为空
if (!input || !input.trim()) { ElMessage.warning("输入内容不能为空"); // 调用完成回调,重新启用输入框
if (onComplete && typeof onComplete === "function") { onComplete(); // 清除保存的回调函数
currentOnCompleteCallback.value = null; } return; }
// 用户输入不为空,立即清空页面内容并触发图片旋转逻辑
isPageLoaded.value = false; // 立即隐藏页面内容
isRotating.value = true; const previousMessages = [...messages.value]; // 保存历史消息
messages.value = []; // 清空历史数据
// 添加用户消息(只添加一次)
const userMessage = reactive({ sender: "user", text: input }); messages.value.push(userMessage);
// 将用户消息添加到emotion store中
emotionStore.addConversation({ sender: "user", text: input, timestamp: new Date().toISOString(), });
// 检查用户剩余次数
await chatStore.getUserCount(); // 获取最新的用户次数
if (chatStore.UserCount <= 0) { const aiMessage = reactive({ sender: "ai", text: "您的剩余次数为0,无法使用情绪大模型,请联系客服或购买服务包。", }); messages.value.push(aiMessage);
// 将AI消息添加到emotion store中
emotionStore.addConversation({ sender: "ai", text: "您的剩余次数为0,无法使用情绪大模型,请联系客服或购买服务包。", timestamp: new Date().toISOString(), }); // 停止图片旋转,恢复历史数据
isRotating.value = false; messages.value = [...previousMessages, ...messages.value]; // 调用完成回调,重新启用输入框
if (onComplete && typeof onComplete === "function") { onComplete(); // 清除保存的回调函数
currentOnCompleteCallback.value = null; } return; }
// 开始思考过程(不带股票名称)
const thinkingMessageRef = await showThinkingProcess(); let thinkingMessage3Ref = null;
try { // 第一步:调用第一个接口验证用户输入内容是否合法
// const params = {
// content: userMessage.text,
// userData: {
// token: localStorage.getItem('localToken'),
// language: "cn",
// 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({ token: localStorage.getItem("localToken"), language: "cn", marketList: "hk,cn,usa,my,sg,vi,in,gb", content: userMessage.text, }); const response = result; const parsedData = response.data; console.log("第一个接口返回的完整数据:", parsedData); // 检查用户输入是否合法
if (!parsedData || !parsedData.market || !parsedData.code) { // 输入不合法,先清理思考过程消息
if (thinkingMessageRef) { const index = messages.value.indexOf(thinkingMessageRef); if (index > -1) { messages.value.splice(index, 1); } }
// 关闭加载状态和等待提示,返回refuse信息,停止图片旋转,恢复历史数据
// isLoading.value = false;
isPageLoaded.value = false; const refuseMessage = response && response.msg ? response.msg : "接口返回数据异常,请重试"; const aiMessage = reactive({ sender: "ai", text: processRefuseMessage(refuseMessage), }); messages.value.push(aiMessage);
// 将AI消息添加到emotion store中
emotionStore.addConversation({ sender: "ai", text: processRefuseMessage(refuseMessage), timestamp: new Date().toISOString(), }); isRotating.value = false; messages.value = [...previousMessages, ...messages.value]; // 调用完成回调,重新启用输入框
if (onComplete && typeof onComplete === "function") { onComplete(); // 清除保存的回调函数
currentOnCompleteCallback.value = null; } return; }
// 输入合法,继续执行后续处理
// 获取到股票名称后,继续思考过程
if (thinkingMessageRef && parsedData.name) { thinkingMessage3Ref = await continueThinkingProcess( thinkingMessageRef, parsedData.name ); } // 设置加载状态,隐藏图表页面
// isLoading.value = true;
isPageLoaded.value = false;
// 调用第二个工作流接口
const conclusionParams = { recordId: parsedData.recordId, parentId: parsedData.parentId, stockId: parsedData.stockId, token: localStorage.getItem("localToken"), language: "cn", };
console.log("第二个接口参数:", conclusionParams); // 同时调用第二个数据流接口和fetchData方法
const [conclusionResult, fetchDataResult] = await Promise.all([ getConclusionAPI(conclusionParams), fetchData( parsedData.code, parsedData.market, parsedData.name || "未知股票", input.trim(), parsedData.stockId ), ]);
// 处理结论接口返回的数据
const conclusionResponse = conclusionResult; // 检查所有数据是否都加载成功
if (conclusionResponse && conclusionResponse.data && fetchDataResult) { // 第二个工作流接口成功,完成思考过程
if (thinkingMessage3Ref) { await finishThinkingProcess(thinkingMessage3Ref); }
// 将结论数据存储到响应式变量和store中
conclusionData.value = conclusionResponse.data; console.log("第二个接口返回的完整数据结构:", conclusionResponse.data); // 将结论数据存储到store中的当前激活股票
emotionStore.updateActiveStockConclusion(conclusionResponse.data);
// 所有数据加载完成,关闭加载状态,显示页面
// isLoading.value = false;
isPageLoaded.value = true;
// 使用nextTick确保DOM更新后清空对话显示并启动高度监听器
nextTick(() => { messages.value = []; // 启动页面高度监听器,实时监听内容变化并自动滚动
startHeightObserver(); // 立即滚动到底部
scrollToBottom(); });
// 数据获取成功后,重新获取用户次数以实现实时更新
try { await chatStore.getUserCount(); console.log("数据获取成功后,用户次数已更新"); } catch (error) { console.error("更新用户次数失败:", error); }
// 重新调用获取历史记录的接口
try { chatStore.searchRecord = true; console.log("getConclusionAPI成功后,已触发历史记录更新"); } 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)) { if (!stockAudioPlayed.value.has(stockCode)) { stockAudioPlayed.value.set(stockCode, true); playAudioQueue(parsedConclusion.value, true, onComplete); } else { // 如果音频已播放过,只启动打字机效果
const stockCode = currentStock.value?.stockInfo?.code || currentStock.value?.stockInfo?.symbol; startTypewriterEffect(parsedConclusion.value, stockCode, onComplete); } stockTypewriterShown.value.set(stockCode, true); } else { // 如果不需要打字机效果,直接调用完成回调
if (onComplete && typeof onComplete === "function") { onComplete(); // 清除保存的回调函数
currentOnCompleteCallback.value = null; } } }
// 重置用户主动搜索标志
isUserInitiated.value = false; } }); } else { // 数据加载失败,清理第三步思考过程消息
if (thinkingMessage3Ref) { const index = messages.value.indexOf(thinkingMessage3Ref); if (index > -1) { messages.value.splice(index, 1); } }
// 数据加载失败,停止图片旋转,恢复历史数据
// 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) { // 请求失败,清理第三步思考过程消息
if (thinkingMessage3Ref) { const index = messages.value.indexOf(thinkingMessage3Ref); if (index > -1) { 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(() => { renderCharts(emotionStore.activeStock.apiData); console.log( "立即恢复显示之前股票的图表:", emotionStore.activeStock.stockInfo.name ); }); } else { isPageLoaded.value = false; }
// 请求失败时停止图片旋转,恢复历史数据
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, stockId) { try { const stockDataParams = { stockId: stockId, }; const stockDataResult = await axios.post( // "http://39.101.133.168:8828/link/api/aiEmotion/client/getAiEmotionData",
`${APIurl}/api/workflow/getStockData`, 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; // 使用nextTick确保DOM更新后清空对话显示并启动高度监听器
nextTick(() => { messages.value = []; // 启动页面高度监听器,实时监听内容变化并自动滚动
startHeightObserver(); // 立即滚动到底部
scrollToBottom(); }); 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);
// 将AI消息添加到emotion store中
emotionStore.addConversation({ sender: "ai", text: "数据丢失了,请稍后重试。", timestamp: new Date().toISOString(), }); 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; // 使用nextTick确保DOM更新后清空对话显示并启动高度监听器
nextTick(() => { messages.value = []; // 启动页面高度监听器,实时监听内容变化并自动滚动
startHeightObserver(); // 立即滚动到底部
scrollToBottom(); }); 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);
// 将AI消息添加到emotion store中
emotionStore.addConversation({ sender: "ai", text: "图表数据请求失败,请检查网络连接", timestamp: new Date().toISOString(), }); return false; // 返回失败标识
} } catch (error) { // 关闭加载状态
// isLoading.value = false;
// 如果有之前的股票数据,恢复显示状态;否则设置为false
if (emotionStore.stockList.length > 0 && emotionStore.activeStock) { isPageLoaded.value = true; // 使用nextTick确保DOM更新后清空对话显示并启动高度监听器
nextTick(() => { messages.value = []; // 启动页面高度监听器,实时监听内容变化并自动滚动
startHeightObserver(); // 立即滚动到底部
scrollToBottom(); }); 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);
// 将AI消息添加到emotion store中
emotionStore.addConversation({ sender: "ai", text: "图表数据请求失败,请检查网络连接", timestamp: new Date().toISOString(), }); 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; }
// 依次渲染图表的方法 - 支持多个股票
async function renderChartsSequentially(clonedData, stockIndex = 0) { console.log(`开始渲染第${stockIndex}个股票的图表`);
// 定义图表渲染顺序和配置
const chartConfigs = [ { name: "股市温度计", ref: marketTemperatureRef.value[stockIndex], visibility: chartVisibility.value.marketTemperature, method: "initChart", params: [clonedData.GSWDJ, clonedData.KLine20, clonedData.WDRL], }, { name: "情绪解码器", ref: emotionDecodRef.value[stockIndex], visibility: chartVisibility.value.emotionDecod, method: "initQXNLZHEcharts", params: [clonedData.KLine20, clonedData.QXJMQ], }, { name: "情绪探底雷达", ref: emotionalBottomRadarRef.value[stockIndex], visibility: chartVisibility.value.emotionalBottomRadar, method: "initEmotionalBottomRadar", params: [clonedData.KLine20, clonedData.QXTDLD], }, { name: "情绪能量转化器", ref: emoEnergyConverterRef.value[stockIndex], visibility: chartVisibility.value.emoEnergyConverter, method: "initQXNLZHEcharts", params: [clonedData.KLine20, clonedData.QXNLZHQ], }, ];
// 依次渲染每个图表
for (const config of chartConfigs) { 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) { console.error( `第${stockIndex}个股票的${config.name}图表渲染失败:`, error ); } } else { console.error( `第${stockIndex}个股票的${config.name}Ref.${config.method} 方法不存在` ); } } else { console.log( `第${stockIndex}个股票的${config.name}图表未渲染,ref存在:`, !!config.ref, "数据存在:", config.visibility ); } }
console.log(`第${stockIndex}个股票的所有图表依次渲染完成`); }
// 渲染组件图表的方法
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);
// 将AI消息添加到emotion store中
emotionStore.addConversation({ sender: "ai", text: `数据不完整,缺少以下关键数据:${validation.missingFields.join( "、" )}。请稍后重试或联系客服。`,
timestamp: new Date().toISOString(), });
// 隐藏页面内容
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( ".market-temperature-section" ), emotionDecodDOM: !!document.querySelector(".emotion-decoder-section"), emotionalBottomRadarDOM: !!document.querySelector( ".bottom-radar-section" ), emoEnergyConverterDOM: !!document.querySelector( ".energy-converter-section" ), });
// 检查具体的组件元素
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);
// 开始依次渲染图表 - 为每个股票渲染
emotionStore.stockList.forEach((stock, index) => { if (stock.apiData) { renderChartsSequentially(stock.apiData, index); } });
console.log("图表渲染完成"); } catch (error) { console.error("图表渲染错误:", error); const aiMessage = reactive({ sender: "ai", text: "图表渲染失败,请重试", }); messages.value.push(aiMessage);
// 将AI消息添加到emotion store中
emotionStore.addConversation({ sender: "ai", text: "图表渲染失败,请重试", timestamp: new Date().toISOString(), }); } }, 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 stockCount = emotionStore.stockList.length; if (stockCount === 0) { console.log("没有股票数据"); return false; }
// 检查每个股票的图表组件是否都已加载
for (let i = 0; i < stockCount; i++) { const requiredRefs = [ marketTemperatureRef.value[i], emotionDecodRef.value[i], emotionalBottomRadarRef.value[i], emoEnergyConverterRef.value[i], ];
const allRefsLoaded = requiredRefs.every((ref) => ref !== null); if (!allRefsLoaded) { console.log(`第${i}个股票的图表组件尚未完全加载`); return false; } }
console.log("所有数据和组件已加载完成"); return true; }
// 自动滚动函数已移除
// 设置Intersection Observer监听场景应用部分
function setupIntersectionObserver() { // 检查 scenarioApplicationRef.value 是否存在且是有效的 DOM 元素
if ( !scenarioApplicationRef.value || !(scenarioApplicationRef.value instanceof Element) ) { console.warn( "scenarioApplicationRef.value 不是有效的 DOM 元素,跳过 IntersectionObserver 设置" ); 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)) { // 如果是用户主动搜索,启动打字机效果和音频播放
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, stockCode); }
stockTypewriterShown.value.set(stockCode, true); } else { // 非用户主动搜索(如历史记录恢复),直接显示完整内容
console.log( "非用户主动搜索,该股票第一次进入场景应用,直接显示完整内容" );
// 结论内容现在直接通过parsedConclusion计算属性显示
// 记录该股票已显示过
stockTypewriterShown.value.set(stockCode, true); stockAudioPlayed.value.set(stockCode, true); } } else { // 非第一次或已经触发过:直接显示完整内容,不播放音频和打字机效果
console.log("非第一次进入场景应用或已触发过,直接显示完整内容");
// 结论内容现在直接通过parsedConclusion计算属性显示
} } } }); }, { 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 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; showBackToTop.value = scrollTop > 200; };
// 监听容器滚动(备用方案)
const handleContainerScroll = () => { const container = userInputDisplayRef.value; if (container) { const scrollTop = container.scrollTop; if (scrollTop > 200) { showBackToTop.value = true; } } };
// 页面挂载完成后触发图片旋转和设置滚动监听
onMounted(async () => { // 恢复对话记录
loadConversationsFromStore(); // 从stockList加载对话记录
loadConversationsFromStockList();
// 确保获取用户次数
// try {
// await chatStore.getUserCount();
// console.log('情绪大模型页面:用户次数获取成功');
// } catch (error) {
// console.error('情绪大模型页面:获取用户次数失败', error);
// }
// 添加全局resize监听器,确保所有图表和容器响应页面宽度变化
const globalResizeHandler = debounce(() => { console.log("AiEmotion页面:窗口大小变化,触发容器和图表resize");
// 强制重新计算容器布局
const mainContainer = document.querySelector(".main"); 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; // 使用nextTick确保DOM更新后清空对话显示并启动高度监听器
nextTick(() => { messages.value = []; // 启动页面高度监听器,实时监听内容变化并自动滚动
startHeightObserver(); // 立即滚动到底部
scrollToBottom(); });
// 等待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;
// 结论内容现在直接通过parsedConclusion计算属性显示
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;
// 清理页面高度监听器
stopHeightObserver();
// 清理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", "enableInput"]);
// 导出方法供外部使用(已在上方定义)
</script>
<style scoped> .disclaimer-text { font-size: 24px; color: #EEEEEE; font-weight: bold; text-align: center; } /* 股票统计概览样式 */ .stock-statistics-overview { background: rgba(255, 255, 255, 0.95); border-radius: 15px; padding: 20px; margin-bottom: 20px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.2); }
.statistics-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid #f0f0f0; }
.statistics-header h3 { margin: 0; color: #333; font-size: 18px; font-weight: 600; }
.stock-count { background: linear-gradient(45deg, #667eea, #764ba2); color: white; padding: 5px 12px; border-radius: 20px; font-size: 12px; font-weight: 500; }
.statistics-content { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px; }
.stat-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; background: rgba(102, 126, 234, 0.1); border-radius: 8px; border-left: 4px solid #667eea; }
.stat-label { color: #666; font-size: 14px; font-weight: 500; }
.stat-value { color: #333; font-size: 16px; font-weight: 600; }
.stat-value.hot-temp { color: #ff4757; }
.stat-value.cold-temp { color: #3742fa; }
.temperature-distribution { display: flex; flex-wrap: wrap; gap: 10px; }
.distribution-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: rgba(255, 255, 255, 0.8); border-radius: 20px; border: 1px solid #e0e0e0; }
.temp-level { font-size: 12px; font-weight: 500; padding: 2px 8px; border-radius: 10px; color: white; }
.temp-level.hot { background: #ff4757; }
.temp-level.warm { background: #ff6b35; }
.temp-level.normal { background: #26de81; }
.temp-level.cool { background: #45aaf2; }
.temp-level.cold { background: #3742fa; }
.temp-count { color: #666; font-size: 12px; font-weight: 500; }
.matrix-header { width: 100%; display: flex; align-items: center; }
.disclaimer-item p { color: #ffffff !important; font-size: 24px; font-weight: bold; }
.temperature-content { padding-top: 8%; display: flex; align-items: center; justify-content: center; gap: 10rem; }
.temperature-content .content1 { display: flex; flex-direction: column; align-items: center; }
.temperature-content .content1 img { scale: 0.5; }
.temperature-display { display: flex; flex-direction: column; /* 竖向排列元素 */ /* margin-left: 15%; */ gap: 30px; /* margin-top: -12%; */ /* width: 100%; */ /* height: auto; */ }
.temperature-display::after { content: ""; display: table; clear: both; }
.temperature-content .temperature-hot { background-image: url("@/assets/img/AiEmotion/redBorder.png"); background-repeat: no-repeat; background-size: 100% 100%; /* width: 50%; */ width: 22vw; max-width: 400px; min-width: 200px; text-align: center; font-size: 24px; color: white; display: flex; justify-content: center; align-items: center; flex-shrink: 0; }
.temperature-content .temperature-cold { background-image: url("@/assets/img/AiEmotion/blueBorder.png"); background-repeat: no-repeat; background-size: 100% 100%; /* width: 35%; */ width: 22vw; max-width: 400px; min-width: 200px; 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: 50vw; height: auto; margin: 0 auto; margin-top: 20px; }
.bk-image .conclusion-container { padding: 20px; border-radius: 15px; margin: 20px; }
.bk-image .conclusion-container .conclusion-item { border-radius: 12px; transition: all 0.3s ease; overflow: hidden; }
.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; margin-top: 22px; }
.bk-image .conclusion-container .conclusion-item .conclusion-text { color: #ffffff; font-size: 20px; line-height: 1.8; margin: 0 0 12px 0; text-align: center; word-wrap: break-word; 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; }
.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; }
/* .text-container p { margin: 0 auto; font-size: 40px; margin-left: 0%; border-radius: 12px; transition: all 0.3s ease; overflow: hidden; letter-spacing: 2px; } */
.text-container .title { display: block; color: #ffd700; font-weight: bold; margin-top: 0px; margin-bottom: 20px; font-size: 22px; text-align: center; }
.text-container .content { display: block; color: white; text-align: center; font-size: 22px; }
.core-logic-section { 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; }
.core-logic-header { margin: 0 auto; width: fit-content; margin-top: 2%; margin-bottom: 1%; }
.core-highlights-header { margin: 0 auto; /* width: fit-content; */ margin-top: 2%; margin-bottom: 1%; display: flex; align-items: center; justify-content: center; }
.emotion-decoder-content { /* width: 80vw; */ margin: 0 auto; }
.energy-converter-content { min-width: 100%; margin-top: 3%; }
.bottom-radar-header { display: flex; justify-content: center; align-items: center; flex-direction: column; }
.bottom-radar-header img { scale: 0.5; }
.energy-converter-header { display: flex; justify-content: center; align-items: center; flex-direction: column; }
.energy-converter-header img { scale: 0.5; }
.bottom-radar-icon { width: fit-content; height: auto; margin: 0 auto; margin-top: 2%; margin-bottom: 1%; display: flex; align-items: center; justify-content: center; }
.energy-converter-icon { width: fit-content; height: auto; margin: 0 auto; margin-top: 2%; margin-bottom: 1%; display: flex; align-items: center; justify-content: center; }
.core-logic-content { margin: 0 auto; width: fit-content; }
.market-temperature-icon { width: fit-content; height: auto; margin: 0 auto; margin-top: 2%; margin-bottom: 1%; display: flex; align-items: center; justify-content: center; }
.bottom-radar-title { margin: 0 auto; width: fit-content; margin-top: 2%; margin-bottom: 1%; }
.emotion-decoder-title { margin: 0 auto; width: fit-content; margin-top: 2%; margin-bottom: 1%; }
.temperature-title { margin: 0 auto; width: fit-content; margin-top: 2%; margin-bottom: 1%; }
.matrix-title { margin: 0 auto; width: fit-content; margin-bottom: 1%; }
.emotion-decoder-header { display: flex; flex-direction: column; justify-content: center; align-items: center; }
.emotion-decoder-header img { scale: 0.5; }
.emotion-decoder-icon { width: fit-content; height: auto; margin: 0 auto; margin-top: 2%; margin-bottom: 1%; display: flex; align-items: center; justify-content: center; }
.emotion-decoder-text { color: white; font-size: 20px; font-weight: bold; /* margin-left: 45%; */ }
.bottom-radar-text { color: white; font-size: 20px; font-weight: bold; /* margin-left: 44.6%; */ }
.energy-converter-text { color: white; font-size: 20px; font-weight: bold; /* margin-left: 44%; */ }
.scenario-application-section { 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: 80%; height: 350px; margin: 0 auto; margin-top: 2%; margin-bottom: 3%; }
.lz-img { text-align: center; padding-top: 30px; }
.decision-tree-section { background-image: url("@/assets/img/AiEmotion/bk00000.png"); background-size: 100% 100%; background-repeat: no-repeat; width: 50vw; height: auto; min-height: 30rem; margin: 0 auto; }
.energy-converter-section { background-image: url("@/assets/img/AiEmotion/bk00000.png"); background-size: 100% 100%; background-repeat: no-repeat; width: 50vw; height: auto; margin: 0 auto; box-sizing: border-box; transition: all 0.3s ease; min-height: 85vh; }
.bottom-radar-section { background-image: url("@/assets/img/AiEmotion/bk00000.png"); background-size: 100% 100%; background-repeat: no-repeat; width: 50vw; max-width: 100%; height: auto; margin: 0 auto; box-sizing: border-box; transition: all 0.3s ease; }
.emotion-decoder-section { background-image: url("@/assets/img/AiEmotion/bk00000.png"); background-size: 100% 100%; background-repeat: no-repeat; width: 50vw; max-width: 100%; height: auto; margin: 0 auto; box-sizing: border-box; transition: all 0.3s ease; padding-bottom: 1rem; }
.market-temperature-section { background-image: url("@/assets/img/AiEmotion/bk00000.png"); background-size: 100% 100%; background-repeat: no-repeat; width: 50vw; max-width: 100%; height: auto; min-height: 70rem; margin: 0 auto; box-sizing: border-box; transition: all 0.3s ease; }
.main-content-wrapper { background-size: 100% 100%; background-repeat: no-repeat; width: 77vw; max-width: 100%; height: auto; margin: 0 auto; box-sizing: border-box; transition: all 0.3s ease; }
.matrix-content { display: flex; flex-direction: column; align-items: center; /* 竖向排列元素 */ /* gap: 1rem; */ /* margin-left: 10%; */ }
.matrix-main-title { color: white; font-size: 20px; font-weight: bold; margin-left: 0%; }
.market-temperature-value { font-size: 1.5rem; font-weight: bold; color: white; margin-left: auto; }
.market-temperature-label { background-image: url("@/assets/img/AiEmotion/bk01.png"); background-size: 100% 100%; background-repeat: no-repeat; padding: 10px; color: #fff; font-size: 20px; font-weight: bold; text-align: center; margin-left: 0; width: 30%; min-width: 200px; max-width: 50%; height: auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; box-sizing: border-box; }
.main { width: 83%; 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: 30px; width: 100%; }
/* 用户消息容器样式 */ .user-message-container { align-items: center; margin-left: auto; gap: 10px; margin-right: 5px; }
.user-content { display: flex; height: 100%; align-items: center; margin-right: 5px; }
.user-message-speaker { width: 32px; height: 32px; object-fit: contain; margin-right: 5px; cursor: pointer; transition: all 0.3s ease; }
.user-sendTime { width: 100%; text-align: center; color: rgba(255, 255, 255, 0.6); font-size: 0.8rem; }
.user-message-speaker:hover { transform: scale(1.1); }
.user-message-speaker.speaker-active { animation: pulse 1.5s infinite; }
@keyframes pulse { 0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); } }
.user-message { color: #6d22f8; background: white; font-weight: bold; padding: 15px 20px; border-radius: 15px; max-width: 60%; text-align: left; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); margin: 0; /* 将用户消息推到右边 */ }
/* AI消息容器样式 */ .ai-message-container { background-color: #f1f1f1; border-radius: 15px; align-items: flex-start; gap: 10px; margin-right: auto; /* max-width: 80%; */ white-space: normal; width: fit-content; overflow: visible; align-items: center; display: flex; }
/* 思考过程动图样式 */ .thinking-gif { width: 40px; height: 40px; object-fit: contain; margin-top: 5px; border-radius: 8px; animation: float 2s ease-in-out infinite; }
@keyframes float { 0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-5px); } }
/* 动态加载点样式 */ .loading-dots { display: inline-block; }
.dot { opacity: 0.4; animation: loading 1.4s infinite; }
.dot:nth-child(1) { animation-delay: 0s; }
.dot:nth-child(2) { animation-delay: 0.2s; }
.dot:nth-child(3) { animation-delay: 0.4s; }
.dot:nth-child(4) { animation-delay: 0.6s; }
.dot:nth-child(5) { animation-delay: 0.8s; }
.dot:nth-child(6) { animation-delay: 1s; }
@keyframes loading { 0%, 60%, 100% { opacity: 0.4; }
30% { opacity: 1; } }
.ai-message { /* background-color: #f1f1f1; */ color: #000000; font-weight: bold; padding: 20px 30px; border-radius: 15px; text-align: left; margin-right: auto; /* 将AI消息保持在左边 */ /* white-space: nowrap; */ width: fit-content; overflow: visible; align-items: center; display: flex; }
.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) { .main { width: 95%; padding: 0.8rem; }
.market-temperature-label { width: 40%; min-width: 170px; max-width: 60%; font-size: 1.3rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.market-temperature-value { font-size: 1.3rem; }
/* 调整图表容器高度 */ /* .class00 { min-height: 45rem; } */
.market-temperature-section { min-height: 60rem; }
.decision-tree-section { min-height: 42rem; }
.scaled-img { height: 350px; min-height: 30rem; } }
@media only screen and (max-width: 992px) { .main { width: 98%; padding: 0.6rem; }
/* 调整图表容器高度 */ /* .class00 { min-height: 40rem; } */
.market-temperature-section { min-height: 55rem; }
.decision-tree-section { min-height: 35rem; }
.scaled-img { height: 300px; min-height: 25rem; background-size: contain; } }
/* 手机端适配样式 */ @media only screen and (max-width: 768px) { .disclaimer-text { font-size: 18px; color: #EEEEEE; font-weight: bold; text-align: center; }
.text-container .content { display: block; color: white; text-align: center; font-size: 20px; }
.market-temperature-icon { width: auto; height: auto; display: flex; align-items: center; justify-content: center; scale: 0.5; }
.matrix-header { display: flex; align-items: center; }
.main { width: 90%; padding: 0.5rem; margin-bottom: 5rem; background-color: #2b378d; border-radius: 0px 0px 10px 10px; }
.energy-converter-text { color: white; font-size: 20px; font-weight: bold; margin-left: 28%; }
.energy-converter-content { 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: 90%; margin-top: 4%; height: 200px; min-height: 200px; }
.bottom-radar-text { color: white; font-size: 20px; font-weight: bold; margin-left: 30%; }
.emotion-decoder-text { color: white; font-size: 20px; font-weight: bold; margin-left: 30%; }
/* 图片样式 */ .golden-wheel img { width: 50%; }
.matrix-title img { width: 100%; }
.temperature-title img { width: 100%; margin: 10px 10px; }
.emotion-decoder-title img { width: 100%; margin: 10px 10px; }
.bottom-radar-title img { width: 100%; margin: 10px 10px; }
.core-highlights-header img { scale: 0.5; }
.core-logic-header img { width: 100%; margin: 10px 10px; }
.scaled-img img { width: 30%; height: auto; }
.scenario-application-section img { width: 100%; margin: 10px 10px; }
.matrix-main-title { font-size: 20px; margin-left: 5%; }
/* .span01 { width: 50%; min-width: 150px; max-width: 70%; font-size: 16px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 8px; } */
.matrix-header .market-temperature-value { font-size: 14px; color: white; float: right; }
.market-temperature-section { background-image: url("@/assets/img/AiEmotion/bk00000.png"); background-size: 100% 100%; background-repeat: no-repeat; width: 100%; /* margin-left: -45px; */ height: auto; }
/* .main { min-height: 100px; height: auto; box-sizing: border-box; background-color: #02107d; margin-bottom: 10rem; } */
.emotion-decoder-section { width: 80vw; height: auto; margin: 0 auto; /* min-height: 38rem; */ /* min-height: 51rem; */ /* margin-left: -39px; */ /* min-height: 38rem; */ }
/* 调整其他图表容器高度 */ /* .class00 { min-height: 35rem; } */
.market-temperature-section { min-height: 45rem; }
.matrix-header img { width: 68%; height: auto; /* margin-top: 5%; */ margin-left: 0%; }
.emotion-decoder-icon { width: 25%; height: auto; scale: 0.5; }
.bottom-radar-icon, .energy-converter-icon { width: 25%; height: auto; scale: 0.5; }
.lz-img { margin-bottom: 0; padding-top: 0;
img { width: 30%; height: auto; margin-top: 5%; } }
.decision-tree-section { background-size: 100% 100%; background-repeat: no-repeat; width: 80vw; height: auto; min-height: 20rem; margin: 0 auto; }
.bk-image { .conclusion-container { padding: 15px; border-radius: 8px; margin: 8px;
.conclusion-item { &: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: 80vw; height: auto; margin: 0 auto;
.conclusion-container { padding-top: 20px; border-radius: 15px; margin: 20px;
.conclusion-item { border-radius: 12px; transition: all 0.3s ease; overflow: hidden; }
.conclusion-item:last-child { margin-bottom: 0; }
.conclusion-item .conclusion-title { color: #ffd700; font-size: 22px; font-weight: bold; text-align: center; letter-spacing: 2px; margin-top: 22px; }
.conclusion-item .conclusion-text { color: #ffffff; font-size: 20px; line-height: 1.8; margin: 0 0 12px 0; text-align: center; word-wrap: break-word; position: relative; }
.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%; text-align: center; }
.disclaimer-item p { color: #ffffff !important; font-size: 18px; margin: 0; letter-spacing: 1px; } }
.bottom-radar-section { background-size: 100% 100%; background-repeat: no-repeat; width: 80vw; height: auto; /* min-height: 48rem; */ }
.energy-converter-section { background-size: 100% 100%; background-repeat: no-repeat; width: 80vw; height: auto; margin: 0 auto; min-height: 55vh; }
.temperature-display { display: flex; flex-direction: column; /* margin-left: 5rem; */ gap: 0; /* margin-top: -6rem; */ /* width: 100%; */ height: auto; }
.temperature-content .temperature-cold, .temperature-content .temperature-hot { min-width: 200px; font-size: 16px; }
.temperature-content { padding-top: 8%; display: flex; align-items: center; justify-content: center; gap: 0rem; }
.market-temperature-label { 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%; }
.temperature-display { flex-direction: column; gap: 1rem; align-items: center; }
/* .span01 { width: 60%; font-size: 1.2rem; padding: 8px; } */
.market-temperature-value { font-size: 1.2rem; }
.matrix-main-title, .emotion-decoder-text, .bottom-radar-text, .energy-converter-text { font-size: 18px; margin-left: 0; } }
/* 超小屏幕设备 */ @media only screen and (max-width: 480px) { .main { width: 90%; padding: 0.3rem; margin-bottom: 3rem; }
.temperature-display { flex-direction: column; gap: 0.8rem; align-items: center; margin-right: 30px; }
.temperature-content .temperature-cold, .temperature-content .temperature-hot { /* width: 90%; */ width: 10vw; min-width: 100px; font-size: 14px; }
/* .span01 { width: 50%; font-size: 1rem; padding: 6px; } */
.market-temperature-value { font-size: 1rem; }
.golden-wheel-img { width: 80%; }
/* 调整图表容器高度适配超小屏幕 */ /* .class00 { min-height: 25rem; } */
.market-temperature-section { min-height: 35rem; }
.decision-tree-section { 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; } */
/* 对话区域样式 */ .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; 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; 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); }
/* 页面主容器样式 */ .page-container { position: relative; width: 100%; }
.master:last-child { border-bottom: none; margin-bottom: 0; }
/* class01容器样式 */ .main { 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>
|