You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

3910 lines
115 KiB

<template>
<!-- 顶部锚点 -->
<div id="top-anchor" class="top-anchor"></div>
<div class="ai-emotion-container" ref="userInputDisplayRef">
<!-- 金轮 -->
<div class="golden-wheel">
<img src="@/assets/img/AiEmotion/金轮.png" class="golden-wheel-img" alt="金轮图标"
:class="{ 'rotating-image': isRotating }" />
</div>
<!-- 消息显示区域 -->
<div class="user-input-display">
<div v-for="(message, index) in messages" :key="index" class="message-container">
<!-- 用户输入内容 -->
<div v-if="message.sender === 'user'" class="user-message-container">
<img
:src="isVoice && emotionAudioStore.isPlaying ? voiceNoActive : voice"
class="user-message-speaker"
:class="{
'speaker-active': isVoice && emotionAudioStore.isPlaying
}"
@click="toggleVoiceForUser"
alt="喇叭"
/>
<div class="message-bubble user-message">
{{ message.text }}
</div>
</div>
<!-- AI返回结果 -->
<div v-if="message.sender === 'ai'" class="ai-message-container">
<!-- 思考过程动图 -->
<!-- <img v-if="message.gif" :src="message.gif" class="thinking-gif" alt="思考动图" /> -->
<div class="message-bubble ai-message">
<!-- 思考过程动图 -->
<img v-if="message.gif" :src="message.gif" class="thinking-gif" alt="思考动图" />
{{ message.text }}
</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>
<!-- 移除股票标签页,改为对话形式展示 -->
<!-- 渲染整个页面 - 遍历stockList显示所有股票 -->
<div v-for="(stock, stockIndex) in emotionStore.stockList" :key="`stock-${stockIndex}-${stock.timestamp}`" v-if="isPageLoaded" 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" v-if="getStockConclusion(stock)">
<!-- 打字机效果显示的内容 -->
<div class="conclusion-item" v-if="moduleVisibility.one">
<h4 class="conclusion-title">{{ displayedTitles.one }}</h4>
<p class="conclusion-text" v-if="displayedTexts.one1">{{ displayedTexts.one1 }}</p>
<p class="conclusion-text" v-if="displayedTexts.one2">{{ displayedTexts.one2 }}</p>
</div>
<div class="conclusion-item" v-if="moduleVisibility.two">
<h4 class="conclusion-title">{{ displayedTitles.two }}</h4>
<p class="conclusion-text">{{ displayedTexts.two }}</p>
</div>
<div class="conclusion-item" v-if="moduleVisibility.three">
<h4 class="conclusion-title">{{ displayedTitles.three }}</h4>
<p class="conclusion-text">{{ displayedTexts.three }}</p>
</div>
<div class="conclusion-item" v-if="moduleVisibility.four">
<h4 class="conclusion-title">{{ displayedTitles.four }}</h4>
<p class="conclusion-text">{{ displayedTexts.four }}</p>
</div>
<!-- AI生成内容免责声明 -->
<div class="disclaimer-item" v-if="moduleVisibility.disclaimer">
<p class="disclaimer-text">{{ displayedTexts.disclaimer }}</p>
</div>
</div>
<div class="conclusion-placeholder" v-else>
<p>等待股票分析结论...</p>
</div>
</div>
</div>
<!-- 返回顶部按钮 -->
<div class="back-to-top" @click="scrollToTop" v-show="isPageLoaded">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 4L12 20M12 4L6 10M12 4L18 10" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue';
import { getReplyAPI, getConclusionAPI } from '@/api/AiEmotionApi.js'; // 导入工作流接口方法
import axios from 'axios';
import item from '@/assets/img/AiEmotion/bk01.png'; // 导入思维矩阵图片
import emotionDecod from '@/views/components/emotionDecod.vue'; // 导入情绪解码组件
import emotionalBottomRadar from '@/views/components/emotionalBottomRadar.vue'; // 导入情绪探底雷达图组件
import emoEnergyConverter from '@/views/components/emoEnergyConverter.vue'; // 导入情绪能量转化器组件
import marketTemperature from '@/views/components/marketTemperature.vue';
import StockTabs from '@/views/components/StockTabs.vue'; // 导入股票标签页组件
import blueBorderImg from '@/assets/img/AiEmotion/blueBorder.png' //导入蓝色背景框图片
import { ElMessage } from 'element-plus'; // 接口失败提示已改为对话形式,保留用于输入验证
import { useEmotionStore } from '@/store/emotion'; // 导入Pinia store
import { useEmotionAudioStore } from '@/store/emotionAudio.js'; // 导入音频store
import { useChatStore } from '@/store/chat.js'; // 导入聊天store
import 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 = () => {
if (!emotionAudioStore.isVoiceEnabled) {
// 如果语音功能关闭,先开启语音功能
emotionAudioStore.toggleVoice();
} else {
// 如果语音功能开启,则切换播放/暂停状态
if (emotionAudioStore.isPlaying) {
// 如果正在播放,则暂停
emotionAudioStore.togglePlayPause();
} else {
// 如果没有在播放,先检查是否处于暂停状态
if (emotionAudioStore.isPaused && (emotionAudioStore.currentAudioUrl || emotionAudioStore.ttsUrl)) {
// 如果处于暂停状态且有音频,则继续播放
console.log('从暂停状态继续播放');
emotionAudioStore.togglePlayPause();
} else if (parsedConclusion.value && (parsedConclusion.value.one1_url || parsedConclusion.value.two_url || parsedConclusion.value.three_url || parsedConclusion.value.four_url)) {
// 有结论数据时,重新播放整个音频队列
console.log('用户点击播放,重新播放音频队列');
playAudioQueue(parsedConclusion.value, false); // 不启动打字机效果,因为内容已经显示
} else if (emotionAudioStore.currentAudioUrl || emotionAudioStore.ttsUrl) {
// 有单个音频URL时切换播放/暂停
emotionAudioStore.togglePlayPause();
} else {
// 没有音频时关闭语音功能
emotionAudioStore.toggleVoice();
}
}
}
};
// 计算属性:判断语音是否启用
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();
};
// 暴露清空对话记录的方法给父组件
defineExpose({
handleSendMessage,
clearConversations
});
const isPageLoaded = ref(false); // 控制页面是否显示
// const isLoading = ref(false); // 控制加载状态
const isRotating = ref(false);//控制旋转
const version1 = ref(1); // 版本号
const conclusionData = ref(''); // 存储第二个工作流接口返回的结论数据
// 自动滚动相关数据
const isAutoScrolling = ref(false);
const currentSection = ref(0);
const sectionRefs = ref([]);
const scenarioApplicationRef = ref(null); // 场景应用部分的引用
const hasTriggeredAudio = ref(false); // 是否已触发音频播放
const hasTriggeredTypewriter = ref(false); // 是否已触发打字机效果
const intersectionObserver = ref(null); // 存储observer实例
const isUserInitiated = ref(false); // 标记是否为用户主动搜索
// 显示的文本内容(用于打字机效果)
const displayedTexts = ref({
one1: '',
one2: '',
two: '',
three: '',
four: '',
disclaimer: ''
});
// 显示的标题内容(用于打字机效果)
const displayedTitles = ref({
one: '',
two: '',
three: '',
four: ''
});
// 模块显示状态
const moduleVisibility = ref({
one: false,
two: false,
three: false,
four: false,
disclaimer: false
});
// 图表组件显示状态
const chartVisibility = ref({
marketTemperature: true,
emotionDecod: true,
emotionalBottomRadar: true,
emoEnergyConverter: true
});
const typewriterTimers = ref([]);
// 记录每个股票是否已经显示过打字机效果
const stockTypewriterShown = ref(new Map());
// 记录每个股票是否已经播放过音频
const stockAudioPlayed = ref(new Map());
// 存储当前的完成回调函数
const currentOnCompleteCallback = ref(null);
// 音频播放相关数据
const audioUrl = ref('');
const isAudioPlaying = ref(false);
// 返回顶部按钮相关数据
const showBackToTop = ref(false);
// 计算属性 - 从store获取当前股票数据
const currentStock = computed(() => emotionStore.activeStock);
const stockName = computed(() => currentStock.value?.stockInfo.name || "");
const 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 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;
}
};
// 监听股票列表变化,当列表为空时隐藏页面数据
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();
// 清理显示的文本和标题
displayedTexts.value = {
one1: '',
one2: '',
two: '',
three: '',
four: '',
disclaimer: ''
};
displayedTitles.value = {
one: '',
two: '',
three: '',
four: ''
};
// 隐藏所有模块
moduleVisibility.value = {
one: false,
two: false,
three: false,
four: false,
disclaimer: false
};
// 隐藏所有图表组件
chartVisibility.value = {
marketTemperature: false,
emotionDecod: false,
emotionalBottomRadar: false,
emoEnergyConverter: false
};
console.log('股票列表已清空,页面数据已隐藏');
} 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;
// 检查该股票是否已经显示过打字机效果
if (stockCode && stockTypewriterShown.value.has(stockCode)) {
// 如果已经显示过,直接显示完整文本和标题
if (newStock.conclusionData) {
try {
// 如果conclusionData已经是对象,直接使用;否则解析JSON
const conclusion = typeof newStock.conclusionData === 'object'
? newStock.conclusionData
: JSON.parse(newStock.conclusionData);
displayedTexts.value = {
one1: conclusion.one1 || '',
one2: conclusion.one2 || '',
two: conclusion.two || '',
three: conclusion.three || '',
four: conclusion.four || '',
disclaimer: '该内容由AI生成,请注意甄别'
};
displayedTitles.value = {
one: 'L1: 情绪监控',
two: 'L2: 情绪解码',
three: 'L3: 情绪推演',
four: 'L4: 情绪套利'
};
// 显示所有有内容的模块
moduleVisibility.value = {
one: !!(conclusion.one1 || conclusion.one2),
two: !!conclusion.two,
three: !!conclusion.three,
four: !!conclusion.four,
disclaimer: true
};
// 提取音频URL但不自动播放,等待用户手动点击
let voiceUrl = null;
// 优先使用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 {
// 如果没有显示过,清空显示文本,等待打字机效果
displayedTexts.value = {
one1: '',
one2: '',
two: '',
three: '',
four: '',
disclaimer: ''
};
displayedTitles.value = {
one: '',
two: '',
three: '',
four: ''
};
moduleVisibility.value = {
one: false,
two: false,
three: false,
four: false,
disclaimer: false
};
// 即使没有显示过,也需要设置音频URL以便用户手动播放
if (newStock.conclusionData) {
try {
// 如果conclusionData已经是对象,直接使用;否则解析JSON
const conclusion = typeof newStock.conclusionData === 'object'
? newStock.conclusionData
: JSON.parse(newStock.conclusionData);
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);
}
}
}
// 只有在页面已加载的情况下才渲染图表
if (isPageLoaded.value) {
nextTick(() => {
renderCharts(newStock.apiData);
console.log('图表数据已准备完成,开始渲染:', newStock.apiData)
// 检查场景应用部分是否已经在视口中,如果是则立即触发效果
setTimeout(() => {
if (scenarioApplicationRef.value && parsedConclusion.value) {
const stockCode = newStock.stockInfo?.code || newStock.stockInfo?.symbol;
// 如果该股票已经显示过,不需要再处理
if (stockCode && stockTypewriterShown.value.has(stockCode)) {
return;
}
const rect = scenarioApplicationRef.value.getBoundingClientRect();
const isInViewport = rect.top < window.innerHeight && rect.bottom > 0;
if (isInViewport) {
console.log('股票切换后检测到场景应用部分在视口中');
if (stockCode) {
// 检查该股票是否是第一次触发
if (!stockTypewriterShown.value.has(stockCode)) {
// 该股票第一次:播放音频和打字机效果
if (audioUrl.value) {
console.log('该股票第一次进入场景应用,开始打字机效果和音频播放');
hasTriggeredTypewriter.value = true;
hasTriggeredAudio.value = true;
if (!stockAudioPlayed.value.has(stockCode)) {
console.log('开始音频播放和打字机效果');
stockAudioPlayed.value.set(stockCode, true);
playAudioQueue(parsedConclusion.value, true);
} else {
// 如果音频已播放过,只启动打字机效果
startTypewriterEffect(parsedConclusion.value);
}
stockTypewriterShown.value.set(stockCode, true);
} else {
console.log('音频尚未准备好,等待音频加载完成后再触发效果(股票切换后)');
return;
}
} else {
// 非第一次或已经触发过:直接显示完整内容,不播放音频和打字机效果
console.log('非第一次股票切换或已触发过,直接显示完整内容');
// 直接显示完整内容
const conclusion = parsedConclusion.value;
displayedTexts.value = {
one1: conclusion.one1 || '',
one2: conclusion.one2 || '',
two: conclusion.two || '',
three: conclusion.three || '',
four: conclusion.four || '',
disclaimer: '该内容由AI生成,请注意甄别'
};
displayedTitles.value = {
one: 'L1: 情绪监控',
two: 'L2: 情绪解码',
three: 'L3: 情绪推演',
four: 'L4: 情绪套利'
};
moduleVisibility.value = {
one: !!(conclusion.one1 || conclusion.one2),
two: !!conclusion.two,
three: !!conclusion.three,
four: !!conclusion.four,
disclaimer: true
};
}
}
}
}
}, 500); // 延迟500ms确保数据完全加载
});
} else {
console.log('页面尚未加载完成,等待数据加载完成后再渲染图表');
}
} else {
console.log('股票数据不存在或API数据未加载');
// 隐藏所有图表组件
chartVisibility.value = {
marketTemperature: false,
emotionDecod: false,
emotionalBottomRadar: false,
emoEnergyConverter: false
};
}
}, { immediate: true });
// 监听parsedConclusion变化,准备数据但不立即触发打字机效果
watch(parsedConclusion, (newConclusion) => {
if (newConclusion) {
console.log('场景应用结论数据:', newConclusion);
// 不再立即开始打字机效果,等待滚动到场景应用部分时触发
// 尝试多种可能的语音URL字段名,优先使用新的数据结构
let voiceUrl = null;
if (newConclusion.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, onComplete) {
console.log('开始打字机效果,结论数据:', conclusion);
// 保存当前的完成回调函数
currentOnCompleteCallback.value = onComplete;
// 详细调试各个字段
console.log('L1字段 - one1:', conclusion.one1);
console.log('L1字段 - one2:', conclusion.one2);
console.log('L2字段 - two:', conclusion.two);
console.log('L3字段 - three:', conclusion.three);
console.log('L4字段 - four:', conclusion.four);
// 清除之前的定时器
typewriterTimers.value.forEach(timer => clearTimeout(timer));
typewriterTimers.value = [];
// 重置显示文本和状态
displayedTexts.value = {
one1: '',
one2: '',
two: '',
three: '',
four: '',
disclaimer: ''
};
displayedTitles.value = {
one: '',
two: '',
three: '',
four: ''
};
moduleVisibility.value = {
one: false,
two: false,
three: false,
four: false,
disclaimer: false
};
// 定义打字速度(毫秒)
const typeSpeed = 200;
let totalDelay = 0;
// 定义模块配置
const modules = [
{
key: 'one',
title: 'L1: 情绪监控',
contents: [
{ key: 'one1', text: conclusion.one1 },
{ key: 'one2', text: conclusion.one2 }
]
},
{
key: 'two',
title: 'L2: 情绪解码',
contents: [
{ key: 'two', text: conclusion.two }
]
},
{
key: 'three',
title: 'L3: 情绪推演',
contents: [
{ key: 'three', text: conclusion.three }
]
},
{
key: 'four',
title: 'L4: 情绪套利',
contents: [
{ key: 'four', text: conclusion.four }
]
}
];
// 按模块顺序处理
modules.forEach((module) => {
// 检查模块是否有内容
const hasContent = module.contents.some(content => content.text && content.text.trim());
console.log(`模块 ${module.key} 是否有内容:`, hasContent, '内容:', module.contents.map(c => c.text));
if (!hasContent) return;
console.log(`开始显示模块 ${module.key}`);
// 显示模块
const showModuleTimer = setTimeout(() => {
moduleVisibility.value[module.key] = true;
console.log(`模块 ${module.key} 已设置为可见`);
}, totalDelay);
typewriterTimers.value.push(showModuleTimer);
totalDelay += 100;
// 打字机效果显示标题
const title = module.title;
for (let i = 0; i <= title.length; i++) {
const timer = setTimeout(() => {
displayedTitles.value[module.key] = title.substring(0, i);
}, totalDelay + i * typeSpeed);
typewriterTimers.value.push(timer);
}
totalDelay += title.length * typeSpeed + 300; // 标题完成后间隔
// 打字机效果显示内容
module.contents.forEach((content) => {
if (content.text && content.text.trim()) {
const text = content.text;
for (let i = 0; i <= text.length; i++) {
const timer = setTimeout(() => {
displayedTexts.value[content.key] = text.substring(0, i);
}, totalDelay + i * typeSpeed);
typewriterTimers.value.push(timer);
}
totalDelay += text.length * typeSpeed + 500; // 内容完成后间隔
}
});
totalDelay += 800; // 模块间间隔
});
// 添加免责声明的打字机效果(在所有模块显示完成后)
const disclaimerText = '该内容由AI生成,请注意甄别';
// 显示免责声明模块
const showDisclaimerTimer = setTimeout(() => {
moduleVisibility.value.disclaimer = true;
}, totalDelay);
typewriterTimers.value.push(showDisclaimerTimer);
totalDelay += 100;
// 打字机效果显示免责声明
for (let i = 0; i <= disclaimerText.length; i++) {
const timer = setTimeout(() => {
displayedTexts.value.disclaimer = disclaimerText.substring(0, i);
// 在免责声明打字机效果完成后调用回调函数
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}音频`);
// 如果是第一个音频且需要启动打字机效果,则启动
if (currentPlayIndex === 0 && audioInfo.shouldStartTypewriter && parsedConclusion.value) {
console.log('🎬 第一个音频开始播放,同时启动打字机效果');
startTypewriterEffect(parsedConclusion.value, audioInfo.onComplete);
}
},
onpause: () => {
emotionAudioStore.isPaused = true;
console.log(`${audioInfo.name}音频暂停播放`);
},
onresume: () => {
emotionAudioStore.isPaused = false;
console.log(`${audioInfo.name}音频继续播放`);
},
onend: () => {
console.log(`${audioInfo.name}音频播放完成,准备播放下一个`);
emotionAudioStore.isPlaying = false;
emotionAudioStore.isPaused = false;
emotionAudioStore.playbackPosition = 0;
isAudioPlaying.value = false;
isPlayingQueueAudio.value = false;
// 移动到下一个音频索引
currentPlayIndex++;
// 确保只有在音频真正播放完成时才播放下一个
if (currentPlayIndex < audioQueue.value.length) {
console.log(`队列中还有音频,500ms后播放下一个 (索引:${currentPlayIndex})`);
setTimeout(() => {
isCallingPlayNext = false;
playNextAudio();
}, 500);
} else {
console.log("🎉 所有音频播放完成");
emotionAudioStore.nowSound = null;
isCallingPlayNext = false;
}
},
onstop: () => {
console.log(`${audioInfo.name}音频被停止`);
emotionAudioStore.isPlaying = false;
emotionAudioStore.isPaused = false;
emotionAudioStore.playbackPosition = 0;
isAudioPlaying.value = false;
isPlayingQueueAudio.value = 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('没有音频但需要启动打字机效果');
startTypewriterEffect(conclusion, onComplete);
}
} else {
console.log(`总共找到 ${audioQueue.value.length} 个音频,准备播放`);
}
} 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('开始播放场景应用语音');
// 音频开始播放时的自动滚动已移除
},
onend: () => {
isAudioPlaying.value = false;
emotionAudioStore.isPlaying = false;
emotionAudioStore.isPaused = false;
emotionAudioStore.playbackPosition = 0;
console.log('场景应用语音播放结束');
},
onstop: () => {
isAudioPlaying.value = false;
emotionAudioStore.isPlaying = false;
console.log('场景应用语音播放停止');
},
onpause: () => {
isAudioPlaying.value = false;
emotionAudioStore.isPlaying = false;
console.log('场景应用语音播放暂停');
},
onerror: (error) => {
isAudioPlaying.value = false;
emotionAudioStore.isPlaying = false;
console.error('音频播放错误:', error);
},
onload: () => {
// 音频加载完成,获取时长
emotionAudioStore.duration = newSound.duration();
console.log('音频加载完成,时长:', emotionAudioStore.duration);
}
});
// 保存音频实例到store
emotionAudioStore.nowSound = newSound;
emotionAudioStore.setAudioInstance(newSound);
// 播放音频
newSound.play();
} catch (error) {
console.error('创建音频实例失败:', error);
isAudioPlaying.value = false;
}
}
// 停止音频播放
function stopAudio() {
if (emotionAudioStore.nowSound) {
emotionAudioStore.nowSound.stop();
}
isAudioPlaying.value = false;
}
// 触发图片旋转的方法
function startImageRotation() {
isRotating.value = true;
// 如果你想在一段时间后停止旋转,可以添加以下代码
setTimeout(() => {
isRotating.value = false;
}, 5000); // 5 秒后停止旋转
}
// 显示思考过程
async function showThinkingProcess(stockName = null) {
// 第一步:正在思考
const thinkingMessage1 = reactive({
sender: 'ai',
text: 'AI情绪大模型正在思考......',
gif: '/src/assets/img/gif/思考.gif'
});
messages.value.push(thinkingMessage1);
await new Promise(resolve => setTimeout(resolve, 1500));
messages.value.pop();
// 第二步:正在解析关键数据(持续显示直到获取到股票名称)
const thinkingMessage2 = reactive({
sender: 'ai',
text: 'AI情绪大模型正在解析关键数据......',
gif: '/src/assets/img/gif/解析.gif'
});
messages.value.push(thinkingMessage2);
// 如果没有股票名称,保持第二步显示
if (!stockName) {
return thinkingMessage2; // 返回消息引用,以便后续更新
}
// 有股票名称后,继续后续步骤
await new Promise(resolve => setTimeout(resolve, 1500));
messages.value.pop();
// 第三步:生成具体股票的量子四维矩阵图
const thinkingMessage3 = reactive({
sender: 'ai',
text: `AI情绪大模型正在生成${stockName}量子四维矩阵图......`,
gif: '/src/assets/img/gif/生成.gif'
});
messages.value.push(thinkingMessage3);
await new Promise(resolve => setTimeout(resolve, 1500));
messages.value.pop();
// 第四步:报告已生成
const thinkingMessage4 = reactive({ sender: 'ai', text: '报告已生成!' });
messages.value.push(thinkingMessage4);
await new Promise(resolve => setTimeout(resolve, 1500));
messages.value.pop();
return null;
}
// 继续思考过程(当获取到股票名称后调用)
async function continueThinkingProcess(thinkingMessageRef, stockName) {
if (!thinkingMessageRef || !stockName) return;
// 等待一段时间后继续
await new Promise(resolve => setTimeout(resolve, 1500));
// 移除第二步消息
const index = messages.value.indexOf(thinkingMessageRef);
if (index > -1) {
messages.value.splice(index, 1);
}
// 第三步:生成具体股票的量子四维矩阵图
const thinkingMessage3 = reactive({
sender: 'ai',
text: `正在生成${stockName}量子四维矩阵图......`,
gif: '/src/assets/img/gif/生成.gif'
});
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;
// 检查用户输入内容是否为空
if (!input || !input.trim()) {
ElMessage.warning("输入内容不能为空");
// 调用完成回调,重新启用输入框
if (onComplete && typeof onComplete === 'function') {
onComplete();
// 清除保存的回调函数
currentOnCompleteCallback.value = null;
}
return;
}
// 用户输入不为空,立即触发图片旋转逻辑,隐藏历史数据
isRotating.value = true;
const previousMessages = [...messages.value]; // 保存历史消息
messages.value = []; // 清空历史数据
// 添加用户消息(只添加一次)
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 aiMessage = reactive({ sender: 'ai', text: processRefuseMessage(parsedData.refuse) });
messages.value.push(aiMessage);
// 将AI消息添加到emotion store中
emotionStore.addConversation({
sender: 'ai',
text: processRefuseMessage(parsedData.refuse),
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;
// 数据获取成功后,重新获取用户次数以实现实时更新
try {
await chatStore.getUserCount();
console.log('数据获取成功后,用户次数已更新');
} catch (error) {
console.error('更新用户次数失败:', error);
}
// 确保页面状态更新后触发图表渲染和音频文本
nextTick(() => {
if (currentStock.value && currentStock.value.apiData) {
renderCharts(currentStock.value.apiData);
// 只有在用户主动搜索时才自动触发音频和文本
if (isUserInitiated.value && parsedConclusion.value && audioUrl.value) {
const stockCode = currentStock.value.stockInfo?.code || currentStock.value.stockInfo?.symbol;
if (stockCode && !stockTypewriterShown.value.has(stockCode)) {
if (!stockAudioPlayed.value.has(stockCode)) {
stockAudioPlayed.value.set(stockCode, true);
playAudioQueue(parsedConclusion.value, true, onComplete);
} else {
// 如果音频已播放过,只启动打字机效果
startTypewriterEffect(parsedConclusion.value, 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;
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()
});
// 请求失败时停止图片旋转,恢复历史数据
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;
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;
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;
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 {
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)) {
// 该股票第一次进入视口:只显示文本,不自动播放音频和打字机效果
console.log('该股票第一次进入场景应用,直接显示完整内容,不自动播放');
// 直接显示完整内容,不使用打字机效果
const conclusion = parsedConclusion.value;
displayedTexts.value = {
one1: conclusion.one1 || '',
one2: conclusion.one2 || '',
two: conclusion.two || '',
three: conclusion.three || '',
four: conclusion.four || '',
disclaimer: '该内容由AI生成,请注意甄别'
};
displayedTitles.value = {
one: 'L1: 情绪监控',
two: 'L2: 情绪解码',
three: 'L3: 情绪推演',
four: 'L4: 情绪套利'
};
// 显示所有有内容的模块
moduleVisibility.value = {
one: !!(conclusion.one1 || conclusion.one2),
two: !!conclusion.two,
three: !!conclusion.three,
four: !!conclusion.four,
disclaimer: true
};
// 记录该股票已显示过,但不播放音频
stockTypewriterShown.value.set(stockCode, true);
stockAudioPlayed.value.set(stockCode, true); // 标记为已播放,避免后续自动播放
} else {
// 非第一次或已经触发过:直接显示完整内容,不播放音频和打字机效果
console.log('非第一次进入场景应用或已触发过,直接显示完整内容');
// 直接显示完整内容
const conclusion = parsedConclusion.value;
displayedTexts.value = {
one1: conclusion.one1 || '',
one2: conclusion.one2 || '',
two: conclusion.two || '',
three: conclusion.three || '',
four: conclusion.four || '',
disclaimer: '该内容由AI生成,请注意甄别'
};
displayedTitles.value = {
one: 'L1: 情绪监控',
two: 'L2: 情绪解码',
three: 'L3: 情绪推演',
four: 'L4: 情绪套利'
};
// 显示所有有内容的模块
moduleVisibility.value = {
one: !!(conclusion.one1 || conclusion.one2),
two: !!conclusion.two,
three: !!conclusion.three,
four: !!conclusion.four,
disclaimer: true
};
}
}
}
});
},
{
threshold: 0.3, // 当30%的元素进入视口时触发
rootMargin: '0px 0px -100px 0px' // 提前100px触发
}
);
observer.observe(scenarioApplicationRef.value);
intersectionObserver.value = observer;
}
// 手动触发自动滚动函数已移除
// 返回顶部功能
const scrollToTop = () => {
const topAnchor = document.getElementById('top-anchor');
if (topAnchor) {
topAnchor.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'nearest'
});
} else {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
// 备用方案:直接滚动到页面顶部
setTimeout(() => {
const currentScrollTop = window.pageYOffset || document.documentElement.scrollTop;
if (currentScrollTop > 50) {
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
}
}, 1000);
};
// 监听页面滚动,控制返回顶部按钮显示
const handlePageScroll = () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
showBackToTop.value = scrollTop > 200;
};
// 监听容器滚动(备用方案)
const handleContainerScroll = () => {
const container = userInputDisplayRef.value;
if (container) {
const scrollTop = container.scrollTop;
if (scrollTop > 200) {
showBackToTop.value = true;
}
}
};
// 页面挂载完成后触发图片旋转和设置滚动监听
onMounted(async () => {
// 恢复对话记录
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;
// 等待DOM渲染后恢复图表和数据
nextTick(() => {
const currentStockData = emotionStore.activeStock;
if (currentStockData && currentStockData.apiData) {
console.log('恢复图表数据:', currentStockData.stockInfo.name);
renderCharts(currentStockData.apiData);
// 恢复结论数据并显示内容
if (currentStockData.conclusionData) {
conclusionData.value = currentStockData.conclusionData;
// 直接显示所有内容,不使用打字机效果
const conclusion = currentStockData.conclusionData;
displayedTexts.value = {
one1: conclusion.one1 || '',
one2: conclusion.one2 || '',
two: conclusion.two || '',
three: conclusion.three || '',
four: conclusion.four || '',
disclaimer: '该内容由AI生成,请注意甄别'
};
displayedTitles.value = {
one: conclusion.one1 || conclusion.one2 ? 'L1: 情绪监控' : '',
two: conclusion.two ? 'L2: 情绪解码' : '',
three: conclusion.three ? 'L3: 情绪推演' : '',
four: conclusion.four ? 'L4: 情绪套利' : ''
};
moduleVisibility.value = {
one: !!(conclusion.one1 || conclusion.one2),
two: !!conclusion.two,
three: !!conclusion.three,
four: !!conclusion.four,
disclaimer: true
};
// 标记该股票的打字机效果和音频已经显示过,避免后续自动触发
const stockCode = currentStockData.stockInfo?.code || currentStockData.stockInfo?.symbol;
if (stockCode) {
stockTypewriterShown.value.set(stockCode, true);
stockAudioPlayed.value.set(stockCode, true);
}
}
}
setupIntersectionObserver();
});
} else {
// 没有保存的数据,正常设置监听器
nextTick(() => {
setupIntersectionObserver();
});
}
});
// 组件卸载时清理定时器、音频和observer
onUnmounted(() => {
clearTypewriterTimers();
// 如果有未完成的回调函数,调用它来重新启用输入框
if (currentOnCompleteCallback.value && typeof currentOnCompleteCallback.value === 'function') {
currentOnCompleteCallback.value();
currentOnCompleteCallback.value = null;
}
stopAudio();
// 重置触发状态
hasTriggeredAudio.value = false;
hasTriggeredTypewriter.value = false;
// 清理Intersection Observer
if (intersectionObserver.value) {
intersectionObserver.value.disconnect();
intersectionObserver.value = null;
}
// 清理全局resize监听器
if (window.aiEmotionGlobalResizeHandler) {
window.removeEventListener('resize', window.aiEmotionGlobalResizeHandler);
window.aiEmotionGlobalResizeHandler = null;
}
// 清理滚动事件监听器
const container = userInputDisplayRef.value;
if (container) {
container.removeEventListener('wheel', handleWheel);
container.removeEventListener('touchmove', handleTouchMove);
container.removeEventListener('scroll', handleUserScroll);
container.removeEventListener('scroll', handleContainerScroll);
}
// 清理页面滚动监听器
window.removeEventListener('scroll', handlePageScroll);
document.removeEventListener('scroll', handlePageScroll);
// 滚动相关清理已简化
});
// 声明组件可以触发的事件
const emit = defineEmits(['updateMessage', 'sendMessage', 'ensureAIchat']);
// 导出方法供外部使用(已在上方定义)
</script>
<style scoped>
.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;
background: rgba(255, 255, 255, 0.05);
border: 1px dashed rgba(153, 153, 153, 0.3);
}
.bk-image .conclusion-placeholder p {
color: #999999;
font-size: 16px;
margin: 0;
font-style: italic;
}
/* 最后文字的颜色 */
.text-container {
position: relative;
color: white;
text-align: left;
padding: 20px;
border-radius: 15px;
}
/* .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: 10px;
width: 100%;
}
/* 用户消息容器样式 */
.user-message-container {
display: flex;
align-items: center;
margin-left: auto;
gap: 10px;
}
.user-message-speaker {
width: 32px;
height: 32px;
object-fit: contain;
margin-top: 5px;
cursor: pointer;
transition: all 0.3s ease;
}
.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 {
display: flex;
align-items: flex-start;
gap: 10px;
margin-right: auto;
max-width: 80%;
}
/* 思考过程动图样式 */
.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);
}
}
.ai-message {
background-color: #f1f1f1;
color: #333;
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) {
.text-container .content {
display: block;
color: white;
text-align: center;
font-size: 16px;
}
.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: 100%;
padding: 0.5rem;
margin-bottom: 5rem;
}
.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: 80%;
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 {
margin-bottom: 15px;
&:last-child {
margin-bottom: 0;
}
.conclusion-title {
color: #FFD700;
font-size: 16px;
font-weight: bold;
margin: 0 0 8px 0;
text-align: center;
}
.conclusion-text {
color: #ffffff;
font-size: 14px;
line-height: 1.5;
margin: 0 0 6px 0;
text-align: left;
word-wrap: break-word;
&:last-child {
margin-bottom: 0;
}
}
}
}
.conclusion-placeholder {
padding: 15px;
text-align: center;
p {
color: #999999;
font-size: 12px;
margin: 0;
}
}
}
.bk-image {
background-size: 100% 100%;
background-repeat: no-repeat;
width: 80vw;
height: auto;
margin: 0 auto;
.conclusion-container {
padding: 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: 16px;
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: 16px;
margin: 0;
letter-spacing: 1px;
}
}
.bottom-radar-section {
background-size: 100% 100%;
background-repeat: no-repeat;
width: 80%;
height: auto;
/* min-height: 48rem; */
}
.energy-converter-section {
background-size: 100% 100%;
background-repeat: no-repeat;
width: 80%;
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: 100%;
padding: 0.3rem;
margin-bottom: 3rem;
}
.temperature-display {
flex-direction: column;
gap: 0.8rem;
align-items: center;
}
.temperature-content .temperature-cold,
.temperature-content .temperature-hot {
/* width: 90%; */
width: 10vw;
min-width: 120px;
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;
} */
/* 顶部锚点样式 */
.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);
}
/* 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>