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.

4394 lines
131 KiB

2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
2 months ago
1 month ago
  1. <template>
  2. <!-- 顶部锚点 -->
  3. <div id="top-anchor" class="top-anchor"></div>
  4. <!-- 主容器包含对话框和main容器 -->
  5. <div class="page-container">
  6. <!-- 对话框区域 -->
  7. <div class="ai-emotion-container" ref="userInputDisplayRef">
  8. <!-- 金轮 -->
  9. <div class="golden-wheel">
  10. <img src="@/assets/img/AiEmotion/金轮.png" class="golden-wheel-img" alt="金轮图标"
  11. :class="{ 'rotating-image': isRotating }" />
  12. </div>
  13. <!-- 对话消息显示区域 -->
  14. <div class="conversation-area" v-if="messages.length > 0 && !isHistoryMode">
  15. <div class="message-list">
  16. <div v-for="(message, index) in messages" :key="index" class="message-item"
  17. :class="{ 'user-message-item': message.sender === 'user', 'ai-message-item': message.sender === 'ai' }">
  18. <!-- 用户消息 -->
  19. <div v-if="message.sender === 'user'" class="user-message-wrapper">
  20. <div class="message-bubble user-message">
  21. {{ message.text }}
  22. </div>
  23. </div>
  24. <!-- AI消息包括思考过程 -->
  25. <div v-else class="ai-message-wrapper">
  26. <div class="ai-message-container">
  27. <img v-if="message.gif" :src="message.gif" alt="思考过程" class="thinking-gif" />
  28. <div class="message-bubble ai-message">
  29. {{ message.text }}
  30. </div>
  31. </div>
  32. </div>
  33. </div>
  34. </div>
  35. </div>
  36. </div>
  37. <!-- 加载提示 -->
  38. <div v-if="isLoading" class="loading-container">
  39. <div class="loading-content">
  40. <div class="loading-spinner"></div>
  41. <div class="loading-text">AI情绪大模型正在努力为您加载请稍候...</div>
  42. </div>
  43. </div>
  44. <!-- main容器区域 -->
  45. <!-- 移除股票标签页改为对话形式展示 -->
  46. <!-- 渲染整个页面 - 遍历stockList显示所有股票 -->
  47. <div class="master" v-for="(stock, stockIndex) in emotionStore.stockList" :key="`stock-${stockIndex}-${stock.timestamp}`" v-if="isPageLoaded">
  48. <!-- 对应股票的消息显示区域 -->
  49. <div class="user-input-display">
  50. <div class="message-container">
  51. <!-- 显示该股票对应的用户输入内容 -->
  52. <div class="user-message-container">
  53. <img :src="isVoice && getStockAudioState(stock).isPlaying ? voice : voiceNoActive" class="user-message-speaker"
  54. :class="{
  55. 'speaker-active': isVoice && getStockAudioState(stock).isPlaying
  56. }" @click="() => toggleVoiceForUser(stock)" alt="喇叭" />
  57. <div class="message-bubble user-message">
  58. {{ stock.queryText }}
  59. </div>
  60. </div>
  61. </div>
  62. </div>
  63. <div class="main">
  64. <div class="main-content-wrapper">
  65. <!-- 四维矩阵图 -->
  66. <div class="matrix-header">
  67. <!-- <img class="item" :src="item" alt="思维矩阵图片" /> -->
  68. <div class="market-temperature-label">
  69. {{ stock.stockInfo.name }}{{ stock.stockInfo.name ? '量子四维矩阵图' : '' }}
  70. </div>
  71. <div class="market-temperature-value">{{ getDisplayDate(stock) }}</div>
  72. </div>
  73. <div class="market-temperature-icon" v-if="chartVisibility.marketTemperature">
  74. <img src="@/assets/img/AiEmotion/L1.png" alt="情绪监控图标">
  75. </div>
  76. <!-- 温度计图表 -->
  77. <div class="market-temperature-section" v-if="chartVisibility.marketTemperature">
  78. <div class="temperature-content">
  79. <div class="content1">
  80. <img src="@/assets/img/AiEmotion/温度计.png" alt="温度计图标">
  81. <span class="matrix-main-title">股市温度计</span>
  82. </div>
  83. <div class="temperature-display">
  84. <div class="temperature-cold">股票温度{{ getStockData2(stock) ?? "NA" }}</div>
  85. <div class="temperature-hot">市场温度{{ getStockData1(stock) }}</div>
  86. </div>
  87. </div>
  88. <marketTemperature :ref="el => marketTemperatureRef[stockIndex] = el" :companyName="stock.stockInfo.name"
  89. :stockCode="stock.stockInfo.code" />
  90. </div>
  91. </div>
  92. <div class="emotion-decoder-icon" v-if="chartVisibility.emotionDecod">
  93. <img src="@/assets/img/AiEmotion/L2.png" alt="情绪解码图标">
  94. </div>
  95. <!-- 情绪解码器图表 -->
  96. <div class="emotion-decoder-section" v-if="chartVisibility.emotionDecod">
  97. <div class="emotion-decoder-header">
  98. <img src='@/assets/img/AiEmotion/emotionDecod.png' alt="情绪解码器图标">
  99. <span class="emotion-decoder-text">情绪解码器</span>
  100. </div>
  101. <div class="emotion-decoder-content">
  102. <emotionDecod :ref="el => emotionDecodRef[stockIndex] = el"></emotionDecod>
  103. </div>
  104. </div>
  105. <div class="bottom-radar-icon" v-if="chartVisibility.emotionalBottomRadar">
  106. <img src="@/assets/img/AiEmotion/L3.png" alt="情绪推演图标">
  107. </div>
  108. <!-- 情绪探底雷达图表 -->
  109. <div class="bottom-radar-section" v-if="chartVisibility.emotionalBottomRadar">
  110. <div class="bottom-radar-header">
  111. <img src="@/assets/img/AiEmotion/探底雷达.png" alt="探底雷达图表">
  112. <span class="bottom-radar-text">情绪探底雷达</span>
  113. </div>
  114. <div class="bottom-radar-content">
  115. <emotionalBottomRadar :ref="el => emotionalBottomRadarRef[stockIndex] = el"></emotionalBottomRadar>
  116. </div>
  117. </div>
  118. <div class="energy-converter-icon" v-if="chartVisibility.emoEnergyConverter">
  119. <img src="@/assets/img/AiEmotion/L4.png" alt="情绪套利">
  120. </div>
  121. <!-- 情绪能量转化器图表 -->
  122. <div class="energy-converter-section" v-if="chartVisibility.emoEnergyConverter">
  123. <div class="energy-converter-header">
  124. <img src="@/assets/img/AiEmotion/能量转化器.png" alt="能量转化器图标">
  125. <span class="energy-converter-text">情绪能量转化器</span>
  126. </div>
  127. <div class="energy-converter-content">
  128. <emoEnergyConverter :ref="el => emoEnergyConverterRef[stockIndex] = el"></emoEnergyConverter>
  129. </div>
  130. </div>
  131. <!-- 核心看点 -->
  132. <div class="core-highlights-header">
  133. <img src="@/assets/img/AiEmotion/核心看点.png" alt="核心看点字样">
  134. </div>
  135. <div class="bk-image">
  136. <div class="text-container">
  137. <p><span class="title">情绪监控-金融宇宙的量子检测网络</span>
  138. <span class="content">核心任务:构建全市场情绪引力场雷达实时监测资金流向和情绪波动</span>
  139. </p>
  140. <p><span class="title">情绪解码-主力思维的神经破译矩阵</span>
  141. <span class="content">核心任务:解构资金行为的量子密码破译主力操盘意图和策略布局</span>
  142. </p>
  143. <p><span class="title">情绪推演-未来战争的时空推演舱</span>
  144. <span class="content">核心任务:基于情绪数据推演未来走势预测市场转折点和机会窗口</span>
  145. </p>
  146. <p><span class="title">情绪套利-财富裂变的粒子对撞机</span>
  147. <span class="content">核心任务:将情绪差转化为收益粒子流实现情绪能量的价值转换</span>
  148. </p>
  149. </div>
  150. </div>
  151. <!-- 核心逻辑 -->
  152. <div class="core-logic-header">
  153. <img src="@/assets/img/AiEmotion/核心逻辑.png" alt="核心逻辑字样">
  154. </div>
  155. <div class="decision-tree-section">
  156. <div class="lz-img">
  157. <img src="@/assets/img/AiEmotion/量子神经决策树.png" alt="树标题">
  158. </div>
  159. <div class="scaled-img">
  160. <!-- <img src="@/assets/img/AiEmotion/tree02.jpg" alt="树图片"> -->
  161. </div>
  162. </div>
  163. <!-- 场景应用 -->
  164. <div class="scenario-application-section" ref="scenarioApplicationRef">
  165. <img src="@/assets/img/AiEmotion/场景应用.png" alt="场景应用标题">
  166. <div class="bk-image">
  167. <div class="conclusion-container">
  168. <!-- 使用打字机效果显示结论内容 -->
  169. <div class="conclusion-item" v-if="moduleVisibility.one">
  170. <h4 class="conclusion-title">{{ displayedTitles.one }}</h4>
  171. <p class="conclusion-text" v-if="displayedTexts.one1">{{ displayedTexts.one1 }}</p>
  172. <p class="conclusion-text" v-if="displayedTexts.one2">{{ displayedTexts.one2 }}</p>
  173. </div>
  174. <div class="conclusion-item" v-if="moduleVisibility.two">
  175. <h4 class="conclusion-title">{{ displayedTitles.two }}</h4>
  176. <p class="conclusion-text">{{ displayedTexts.two }}</p>
  177. </div>
  178. <div class="conclusion-item" v-if="moduleVisibility.three">
  179. <h4 class="conclusion-title">{{ displayedTitles.three }}</h4>
  180. <p class="conclusion-text">{{ displayedTexts.three }}</p>
  181. </div>
  182. <div class="conclusion-item" v-if="moduleVisibility.four">
  183. <h4 class="conclusion-title">{{ displayedTitles.four }}</h4>
  184. <p class="conclusion-text">{{ displayedTexts.four }}</p>
  185. </div>
  186. <!-- AI生成内容免责声明 -->
  187. <div class="disclaimer-item" v-if="moduleVisibility.disclaimer">
  188. <p class="disclaimer-text">{{ displayedTexts.disclaimer }}</p>
  189. </div>
  190. </div>
  191. <div class="conclusion-placeholder" v-if="!moduleVisibility.one && !moduleVisibility.two && !moduleVisibility.three && !moduleVisibility.four">
  192. <p>等待股票分析结论...</p>
  193. </div>
  194. </div>
  195. </div>
  196. <!-- 返回顶部按钮 -->
  197. <div class="back-to-top" @click="scrollToTop" v-show="isPageLoaded">
  198. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  199. <path d="M12 4L12 20M12 4L6 10M12 4L18 10" stroke="currentColor" stroke-width="2" stroke-linecap="round"
  200. stroke-linejoin="round" />
  201. </svg>
  202. </div>
  203. </div>
  204. </div>
  205. </div>
  206. </template>
  207. <script setup>
  208. import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue';
  209. import { getReplyAPI, getConclusionAPI } from '@/api/AiEmotionApi.js'; // 导入工作流接口方法
  210. import axios from 'axios';
  211. import item from '@/assets/img/AiEmotion/bk01.png'; // 导入思维矩阵图片
  212. import emotionDecod from '@/views/components/emotionDecod.vue'; // 导入情绪解码组件
  213. import emotionalBottomRadar from '@/views/components/emotionalBottomRadar.vue'; // 导入情绪探底雷达图组件
  214. import emoEnergyConverter from '@/views/components/emoEnergyConverter.vue'; // 导入情绪能量转化器组件
  215. import marketTemperature from '@/views/components/marketTemperature.vue';
  216. import StockTabs from '@/views/components/StockTabs.vue'; // 导入股票标签页组件
  217. import blueBorderImg from '@/assets/img/AiEmotion/blueBorder.png' //导入蓝色背景框图片
  218. import { ElMessage } from 'element-plus'; // 接口失败提示已改为对话形式,保留用于输入验证
  219. import { useEmotionStore } from '@/store/emotion'; // 导入Pinia store
  220. import { useEmotionAudioStore } from '@/store/emotionAudio.js'; // 导入音频store
  221. import { useChatStore } from '@/store/chat.js'; // 导入聊天store
  222. import getCountAll from "../assets/img/homePage/get-count-all.png";
  223. import voice from "../assets/img/homePage/tail/voice.png";
  224. import voiceNoActive from "../assets/img/homePage/tail/voice-no-active.png";
  225. import { Howl, Howler } from 'howler'; // 导入音频播放库
  226. import { reactive } from 'vue';
  227. import { marked } from 'marked'; // 引入marked库
  228. import { useUserStore } from "../store/userPessionCode";
  229. const APIurl = import.meta.env.VITE_APP_API_BASE_URL;
  230. // 使用Pinia store
  231. const emotionStore = useEmotionStore();
  232. const emotionAudioStore = useEmotionAudioStore();
  233. // 语音播放控制函数
  234. const toggleVoiceForUser = (stock) => {
  235. if (!emotionAudioStore.isVoiceEnabled) {
  236. // 如果语音功能关闭,先开启语音功能
  237. emotionAudioStore.toggleVoice();
  238. } else {
  239. // 获取该股票的结论数据和当前状态
  240. const stockConclusion = getStockConclusion(stock);
  241. const currentState = getStockAudioState(stock);
  242. // 检查是否有任何音频正在播放(包括全局播放状态和当前股票状态)
  243. const isAnyAudioPlaying = emotionAudioStore.isPlaying || currentState.isPlaying;
  244. // 如果当前点击的股票正在播放,则暂停
  245. if (currentState.isPlaying) {
  246. console.log('暂停当前股票音频:', stock.stockInfo?.name);
  247. // 暂停音频而不是停止
  248. if (emotionAudioStore.nowSound && emotionAudioStore.nowSound.playing()) {
  249. emotionAudioStore.nowSound.pause();
  250. emotionAudioStore.isPaused = true;
  251. emotionAudioStore.isPlaying = false;
  252. }
  253. setStockAudioState(stock, { isPlaying: false, isPaused: true });
  254. } else if (currentState.isPaused) {
  255. // 如果当前股票处于暂停状态,则继续播放
  256. console.log('继续播放当前股票音频:', stock.stockInfo?.name);
  257. if (emotionAudioStore.nowSound) {
  258. emotionAudioStore.nowSound.play();
  259. emotionAudioStore.isPaused = false;
  260. emotionAudioStore.isPlaying = true;
  261. }
  262. setStockAudioState(stock, { isPlaying: true, isPaused: false });
  263. } else {
  264. // 如果有其他音频正在播放,先停止
  265. if (isAnyAudioPlaying) {
  266. console.log('停止其他正在播放的音频,准备播放新音频:', stock.stockInfo?.name);
  267. stopAudio();
  268. emotionAudioStore.resetAudioState();
  269. }
  270. // 清除所有股票的播放状态
  271. clearAllStockAudioStates();
  272. // 如果有音频数据,开始播放
  273. if (stockConclusion && (stockConclusion.one1_url || stockConclusion.two_url || stockConclusion.three_url || stockConclusion.four_url)) {
  274. console.log('开始播放股票音频:', stock.stockInfo?.name);
  275. setStockAudioState(stock, { isPlaying: true, isPaused: false });
  276. // 播放音频队列
  277. playAudioQueue(stockConclusion, false, () => {
  278. // 音频播放完成后重置状态
  279. setStockAudioState(stock, { isPlaying: false, isPaused: false });
  280. });
  281. } else {
  282. console.log('该股票没有可播放的音频数据');
  283. }
  284. }
  285. }
  286. };
  287. // 计算属性:判断语音是否启用
  288. const isVoice = computed(() => {
  289. return emotionAudioStore.isVoiceEnabled;
  290. });
  291. const chatStore = useChatStore();
  292. // 获取权限
  293. const userStore = useUserStore();
  294. // 处理refuse数据的函数
  295. function processRefuseMessage(refuseData) {
  296. if (!refuseData) return '未知错误';
  297. // 如果refuse数据包含Markdown格式,进行转换
  298. try {
  299. // 配置marked选项
  300. marked.setOptions({
  301. breaks: true, // 支持换行符转换为 <br>
  302. gfm: true, // 启用 GitHub Flavored Markdown
  303. sanitize: false, // 不清理 HTML
  304. smartLists: true, // 智能列表
  305. smartypants: true, // 智能标点符号
  306. xhtml: false, // 不使用 XHTML 输出
  307. });
  308. // 将Markdown转换为HTML
  309. const htmlContent = marked(refuseData);
  310. // 移除HTML标签,只保留纯文本用于ElMessage显示
  311. const tempDiv = document.createElement('div');
  312. tempDiv.innerHTML = htmlContent;
  313. return tempDiv.textContent || tempDiv.innerText || refuseData;
  314. } catch (error) {
  315. console.error('处理refuse消息时出错:', error);
  316. return refuseData;
  317. }
  318. }
  319. // 组件引用 - 修改为数组形式支持多个股票
  320. const marketTemperatureRef = ref([]); // 引用市场温度计组件数组
  321. const emoEnergyConverterRef = ref([])
  322. const emotionDecodRef = ref([])
  323. const emotionalBottomRadarRef = ref([])
  324. const userInputDisplayRef = ref(null);//消息区域的引用
  325. // 响应式数据
  326. const messages = ref([]);
  327. // 从emotion store中恢复对话记录
  328. const loadConversationsFromStore = () => {
  329. const storedConversations = emotionStore.getConversations();
  330. messages.value = storedConversations.map(conv => ({
  331. sender: conv.sender,
  332. text: conv.text
  333. }));
  334. };
  335. // 记录已经添加到对话中的股票,避免重复添加
  336. const addedStocks = ref(new Set());
  337. // 从stockList生成对话历史
  338. const loadConversationsFromStockList = () => {
  339. // 检查是否有新的股票需要添加到对话中
  340. emotionStore.stockList.forEach(stock => {
  341. const stockKey = `${stock.stockInfo.code}_${stock.timestamp}`;
  342. // 如果这个股票还没有添加到对话中
  343. if (!addedStocks.value.has(stockKey)) {
  344. // 检查messages中是否已经存在相同的用户消息
  345. const existingMessage = messages.value.find(msg =>
  346. msg.sender === 'user' && msg.text === stock.queryText
  347. );
  348. // 只有当messages中不存在相同消息时才添加
  349. if (!existingMessage) {
  350. // 只添加用户输入消息,不添加AI回复
  351. const userMessage = {
  352. sender: 'user',
  353. text: stock.queryText
  354. };
  355. messages.value.push(userMessage);
  356. // 只将用户消息添加到emotion store中(如果store中也不存在)
  357. const storeConversations = emotionStore.getConversations();
  358. const existingInStore = storeConversations.find(conv =>
  359. conv.sender === 'user' && conv.text === stock.queryText
  360. );
  361. if (!existingInStore) {
  362. emotionStore.addConversation(userMessage);
  363. }
  364. }
  365. // 将这个股票标记为已添加
  366. addedStocks.value.add(stockKey);
  367. }
  368. });
  369. };
  370. // 清空对话记录
  371. const clearConversations = () => {
  372. messages.value = [];
  373. emotionStore.clearConversations();
  374. // 清空已添加股票的记录
  375. addedStocks.value.clear();
  376. };
  377. // 添加股票数据的包装方法
  378. const addStock = (stockData) => {
  379. console.log('AiEmotion组件接收到股票数据:', stockData);
  380. // 设置为历史记录模式,并重置用户主动搜索标志
  381. isHistoryMode.value = true;
  382. isUserInitiated.value = false;
  383. // 1. 先清空页面显示节点和stockList中的数据
  384. isPageLoaded.value = false; // 隐藏页面显示节点
  385. emotionStore.clearAllStocks(); // 清空stockList中的数据
  386. emotionStore.clearConversations(); // 清空对话记录
  387. messages.value = []; // 清空页面对话显示
  388. // 清理已添加股票的记录
  389. addedStocks.value.clear();
  390. // 停止音频播放和清理状态
  391. stopAudio();
  392. audioUrl.value = '';
  393. emotionAudioStore.resetAudioState();
  394. clearTypewriterTimers();
  395. hasTriggeredAudio.value = false;
  396. hasTriggeredTypewriter.value = false;
  397. stockTypewriterShown.value.clear();
  398. stockAudioPlayed.value.clear();
  399. // 清理显示的文本和标题
  400. displayedTexts.value = {
  401. one1: '',
  402. one2: '',
  403. two: '',
  404. three: '',
  405. four: '',
  406. disclaimer: ''
  407. };
  408. displayedTitles.value = {
  409. one: '',
  410. two: '',
  411. three: '',
  412. four: ''
  413. };
  414. // 隐藏所有模块
  415. moduleVisibility.value = {
  416. one: false,
  417. two: false,
  418. three: false,
  419. four: false,
  420. disclaimer: false
  421. };
  422. // 隐藏所有图表组件
  423. chartVisibility.value = {
  424. marketTemperature: false,
  425. emotionDecod: false,
  426. emotionalBottomRadar: false,
  427. emoEnergyConverter: false
  428. };
  429. // 2. 将新的数据存储到stockList中
  430. emotionStore.addStock(stockData);
  431. // 3. 设置页面为已加载状态,重新渲染页面
  432. isPageLoaded.value = true;
  433. // 4. 立即显示历史记录的结论文本
  434. if (stockData.conclusionData) {
  435. try {
  436. const conclusion = typeof stockData.conclusionData === 'object'
  437. ? stockData.conclusionData
  438. : JSON.parse(stockData.conclusionData);
  439. displayedTexts.value = {
  440. one1: conclusion.one1 || '',
  441. one2: conclusion.one2 || '',
  442. two: conclusion.two || '',
  443. three: conclusion.three || '',
  444. four: conclusion.four || '',
  445. disclaimer: '该内容由AI生成,请注意甄别'
  446. };
  447. displayedTitles.value = {
  448. one: 'L1: 情绪监控',
  449. two: 'L2: 情绪解码',
  450. three: 'L3: 情绪推演',
  451. four: 'L4: 情绪套利'
  452. };
  453. moduleVisibility.value = {
  454. one: !!(conclusion.one1 || conclusion.one2),
  455. two: !!conclusion.two,
  456. three: !!conclusion.three,
  457. four: !!conclusion.four,
  458. disclaimer: true
  459. };
  460. // 标记该股票已显示过,避免重复触发
  461. const stockCode = stockData.stockInfo?.code || stockData.stockInfo?.symbol;
  462. if (stockCode) {
  463. stockTypewriterShown.value.set(stockCode, true);
  464. stockAudioPlayed.value.set(stockCode, true);
  465. }
  466. console.log('历史记录结论文本已立即显示:', conclusion);
  467. } catch (error) {
  468. console.error('解析历史记录结论数据失败:', error);
  469. }
  470. }
  471. // 5. 使用nextTick确保DOM更新后启动高度监听器并滚动到底部
  472. nextTick(() => {
  473. // 启动页面高度监听器,实时监听内容变化并自动滚动
  474. startHeightObserver();
  475. // 立即滚动到底部
  476. scrollToBottom();
  477. });
  478. };
  479. // 暴露方法给父组件
  480. defineExpose({
  481. handleSendMessage,
  482. clearConversations,
  483. addStock
  484. });
  485. const isPageLoaded = ref(false); // 控制页面是否显示
  486. const isHistoryMode = ref(false); // 控制是否为历史记录模式
  487. // const isLoading = ref(false); // 控制加载状态
  488. const isRotating = ref(false);//控制旋转
  489. const version1 = ref(1); // 版本号
  490. const conclusionData = ref(''); // 存储第二个工作流接口返回的结论数据
  491. // 自动滚动相关数据
  492. const isAutoScrolling = ref(false);
  493. const currentSection = ref(0);
  494. const sectionRefs = ref([]);
  495. const scenarioApplicationRef = ref(null); // 场景应用部分的引用
  496. const hasTriggeredAudio = ref(false); // 是否已触发音频播放
  497. const hasTriggeredTypewriter = ref(false); // 是否已触发打字机效果
  498. const intersectionObserver = ref(null); // 存储observer实例
  499. const isUserInitiated = ref(false); // 标记是否为用户主动搜索
  500. // 显示的文本内容(用于打字机效果)
  501. const displayedTexts = ref({
  502. one1: '',
  503. one2: '',
  504. two: '',
  505. three: '',
  506. four: '',
  507. disclaimer: ''
  508. });
  509. // 显示的标题内容(用于打字机效果)
  510. const displayedTitles = ref({
  511. one: '',
  512. two: '',
  513. three: '',
  514. four: ''
  515. });
  516. // 模块显示状态
  517. const moduleVisibility = ref({
  518. one: false,
  519. two: false,
  520. three: false,
  521. four: false,
  522. disclaimer: false
  523. });
  524. // 图表组件显示状态
  525. const chartVisibility = ref({
  526. marketTemperature: true,
  527. emotionDecod: true,
  528. emotionalBottomRadar: true,
  529. emoEnergyConverter: true
  530. });
  531. const typewriterTimers = ref([]);
  532. // 记录每个股票是否已经显示过打字机效果
  533. const stockTypewriterShown = ref(new Map());
  534. // 记录每个股票是否已经播放过音频
  535. const stockAudioPlayed = ref(new Map());
  536. // 跟踪每个股票的音频播放状态
  537. const stockAudioStates = ref(new Map());
  538. // 存储当前的完成回调函数
  539. const currentOnCompleteCallback = ref(null);
  540. // 音频播放相关数据
  541. const audioUrl = ref('');
  542. const isAudioPlaying = ref(false);
  543. // 获取股票的音频播放状态
  544. const getStockAudioState = (stock) => {
  545. const stockCode = stock.stockInfo?.code || stock.stockInfo?.symbol;
  546. if (!stockCode) return { isPlaying: false, isPaused: false };
  547. return stockAudioStates.value.get(stockCode) || { isPlaying: false, isPaused: false };
  548. };
  549. // 设置股票的音频播放状态
  550. const setStockAudioState = (stock, state) => {
  551. const stockCode = stock.stockInfo?.code || stock.stockInfo?.symbol;
  552. if (!stockCode) return;
  553. stockAudioStates.value.set(stockCode, { ...state });
  554. };
  555. // 清除所有股票的播放状态(当开始播放新音频时)
  556. const clearAllStockAudioStates = () => {
  557. for (const [key, value] of stockAudioStates.value.entries()) {
  558. stockAudioStates.value.set(key, { isPlaying: false, isPaused: false });
  559. }
  560. };
  561. // 返回顶部按钮相关数据
  562. const showBackToTop = ref(false);
  563. // 计算属性 - 从store获取当前股票数据
  564. const currentStock = computed(() => emotionStore.activeStock);
  565. const stockName = computed(() => currentStock.value?.stockInfo.name || "");
  566. const stockCode = computed(() => currentStock.value?.stockInfo.code || currentStock.value?.stockInfo.symbol || "");
  567. const displayDate = computed(() => {
  568. if (!currentStock.value?.apiData) return "";
  569. const lastData = currentStock.value.apiData.GSWDJ?.at(-1);
  570. if (!lastData || !lastData[0]) return "";
  571. const dateStr = lastData[0];
  572. // 假设原格式为 YYYY-MM-DD 或 YYYY/MM/DD
  573. const dateMatch = dateStr.match(/(\d{4})[\-\/](\d{1,2})[\-\/](\d{1,2})/);
  574. if (dateMatch) {
  575. const [, year, month, day] = dateMatch;
  576. // 转换为 DD/MM/YYYY 格式
  577. return `更新时间:${day.padStart(2, '0')}/${month.padStart(2, '0')}/${year}`;
  578. }
  579. // 如果不匹配预期格式,返回原始值
  580. return dateStr;
  581. });
  582. const data1 = computed(() => {
  583. if (!currentStock.value?.apiData) return null;
  584. const lastData = currentStock.value.apiData.GSWDJ?.at(-1);
  585. return lastData ? Math.round(lastData[1]) : null;
  586. });
  587. const data2 = computed(() => {
  588. if (!currentStock.value?.apiData) return null;
  589. const lastData = currentStock.value.apiData.GSWDJ?.at(-1);
  590. return lastData ? Math.round(lastData[2]) : null;
  591. });
  592. const currentConclusion = computed(() => {
  593. return currentStock.value?.conclusionData || '';
  594. });
  595. const parsedConclusion = computed(() => {
  596. if (!currentConclusion.value) return null;
  597. // 如果conclusionData已经是对象,直接返回
  598. if (typeof currentConclusion.value === 'object') {
  599. return currentConclusion.value;
  600. }
  601. // 如果是字符串,尝试解析JSON
  602. try {
  603. return JSON.parse(currentConclusion.value);
  604. } catch (error) {
  605. console.error('解析结论数据失败:', error);
  606. return null;
  607. }
  608. });
  609. // 辅助函数:获取股票的显示日期
  610. const getDisplayDate = (stock) => {
  611. if (!stock?.apiData) return "";
  612. const lastData = stock.apiData.GSWDJ?.at(-1);
  613. if (!lastData || !lastData[0]) return "";
  614. const dateStr = lastData[0];
  615. // 假设原格式为 YYYY-MM-DD 或 YYYY/MM/DD
  616. const dateMatch = dateStr.match(/(\d{4})[\-\/](\d{1,2})[\-\/](\d{1,2})/);
  617. if (dateMatch) {
  618. const [, year, month, day] = dateMatch;
  619. // 转换为 DD/MM/YYYY 格式
  620. return `更新时间:${day.padStart(2, '0')}/${month.padStart(2, '0')}/${year}`;
  621. }
  622. // 如果不匹配预期格式,返回原始值
  623. return dateStr;
  624. };
  625. // 辅助函数:获取股票的市场温度数据
  626. const getStockData1 = (stock) => {
  627. if (!stock?.apiData) return null;
  628. const lastData = stock.apiData.GSWDJ?.at(-1);
  629. return lastData ? Math.round(lastData[1]) : null;
  630. };
  631. // 辅助函数:获取股票的股票温度数据
  632. const getStockData2 = (stock) => {
  633. if (!stock?.apiData) return null;
  634. const lastData = stock.apiData.GSWDJ?.at(-1);
  635. return lastData ? Math.round(lastData[2]) : null;
  636. };
  637. // 辅助函数:获取股票的结论数据
  638. const getStockConclusion = (stock) => {
  639. if (!stock?.conclusionData) return null;
  640. // 如果conclusionData已经是对象,直接返回
  641. if (typeof stock.conclusionData === 'object') {
  642. return stock.conclusionData;
  643. }
  644. // 如果是字符串,尝试解析JSON
  645. try {
  646. return JSON.parse(stock.conclusionData);
  647. } catch (error) {
  648. console.error('解析股票结论数据失败:', error);
  649. return null;
  650. }
  651. };
  652. // 监听股票列表变化,当列表为空时隐藏页面数据
  653. watch(() => emotionStore.stockList, (newStockList) => {
  654. if (newStockList.length === 0) {
  655. // 当股票列表为空时,隐藏页面数据
  656. isPageLoaded.value = false;
  657. // 停止音频播放
  658. stopAudio();
  659. // 清理音频URL
  660. audioUrl.value = '';
  661. emotionAudioStore.resetAudioState();
  662. // 清理打字机效果
  663. clearTypewriterTimers();
  664. // 重置所有状态
  665. hasTriggeredAudio.value = false;
  666. hasTriggeredTypewriter.value = false;
  667. stockTypewriterShown.value.clear();
  668. stockAudioPlayed.value.clear();
  669. // 清理已添加股票的记录
  670. addedStocks.value.clear();
  671. // 清理显示的文本和标题
  672. displayedTexts.value = {
  673. one1: '',
  674. one2: '',
  675. two: '',
  676. three: '',
  677. four: '',
  678. disclaimer: ''
  679. };
  680. displayedTitles.value = {
  681. one: '',
  682. two: '',
  683. three: '',
  684. four: ''
  685. };
  686. // 隐藏所有模块
  687. moduleVisibility.value = {
  688. one: false,
  689. two: false,
  690. three: false,
  691. four: false,
  692. disclaimer: false
  693. };
  694. // 隐藏所有图表组件
  695. chartVisibility.value = {
  696. marketTemperature: false,
  697. emotionDecod: false,
  698. emotionalBottomRadar: false,
  699. emoEnergyConverter: false
  700. };
  701. console.log('股票列表已清空,页面数据已隐藏');
  702. } else {
  703. // 当stockList有数据时,更新对话记录
  704. loadConversationsFromStockList();
  705. }
  706. }, { deep: true });
  707. // 监听当前股票变化,重新渲染图表
  708. watch(currentStock, (newStock) => {
  709. if (newStock && newStock.apiData) {
  710. // 页面加载状态现在由 handleSendMessage 统一控制
  711. // 停止当前播放的音频
  712. stopAudio();
  713. // 清理音频URL,确保不会播放之前股票的音频
  714. audioUrl.value = '';
  715. // 清理store中的音频URL,确保不会播放之前股票的音频
  716. emotionAudioStore.resetAudioState();
  717. // 清理正在进行的打字机效果定时器
  718. clearTypewriterTimers();
  719. // 重置触发状态,让每个股票都能独立触发效果
  720. hasTriggeredAudio.value = false;
  721. hasTriggeredTypewriter.value = false;
  722. // 获取股票代码作为唯一标识
  723. const stockCode = newStock.stockInfo?.code || newStock.stockInfo?.symbol;
  724. // 检查该股票是否已经显示过打字机效果
  725. if (stockCode && stockTypewriterShown.value.has(stockCode)) {
  726. // 如果已经显示过,直接显示完整文本和标题
  727. if (newStock.conclusionData) {
  728. try {
  729. // 如果conclusionData已经是对象,直接使用;否则解析JSON
  730. const conclusion = typeof newStock.conclusionData === 'object'
  731. ? newStock.conclusionData
  732. : JSON.parse(newStock.conclusionData);
  733. displayedTexts.value = {
  734. one1: conclusion.one1 || '',
  735. one2: conclusion.one2 || '',
  736. two: conclusion.two || '',
  737. three: conclusion.three || '',
  738. four: conclusion.four || '',
  739. disclaimer: '该内容由AI生成,请注意甄别'
  740. };
  741. displayedTitles.value = {
  742. one: 'L1: 情绪监控',
  743. two: 'L2: 情绪解码',
  744. three: 'L3: 情绪推演',
  745. four: 'L4: 情绪套利'
  746. };
  747. // 显示所有有内容的模块
  748. moduleVisibility.value = {
  749. one: !!(conclusion.one1 || conclusion.one2),
  750. two: !!conclusion.two,
  751. three: !!conclusion.three,
  752. four: !!conclusion.four,
  753. disclaimer: true
  754. };
  755. // 提取音频URL但不自动播放,等待用户手动点击
  756. let voiceUrl = null;
  757. // 优先使用one1_url,如果没有则尝试其他音频URL
  758. if (conclusion.one1_url) {
  759. voiceUrl = conclusion.one1_url.toString().trim().replace(/[`\s]/g, '');
  760. } else if (conclusion.one2_url) {
  761. voiceUrl = conclusion.one2_url.toString().trim().replace(/[`\s]/g, '');
  762. } else if (conclusion.two_url) {
  763. voiceUrl = conclusion.two_url.toString().trim().replace(/[`\s]/g, '');
  764. } else if (conclusion.three_url) {
  765. voiceUrl = conclusion.three_url.toString().trim().replace(/[`\s]/g, '');
  766. } else if (conclusion.four_url) {
  767. voiceUrl = conclusion.four_url.toString().trim().replace(/[`\s]/g, '');
  768. } else if (conclusion.url) {
  769. voiceUrl = conclusion.url.toString().trim().replace(/[`\s]/g, '');
  770. } else if (conclusion.audioUrl) {
  771. voiceUrl = conclusion.audioUrl.toString().trim().replace(/[`\s]/g, '');
  772. } else if (conclusion.voice_url) {
  773. voiceUrl = conclusion.voice_url.toString().trim().replace(/[`\s]/g, '');
  774. } else if (conclusion.audio) {
  775. voiceUrl = conclusion.audio.toString().trim().replace(/[`\s]/g, '');
  776. } else if (conclusion.tts_url) {
  777. voiceUrl = conclusion.tts_url.toString().trim().replace(/[`\s]/g, '');
  778. }
  779. if (voiceUrl && voiceUrl.startsWith('http')) {
  780. console.log('切换到已显示股票,准备音频URL但不自动播放:', voiceUrl);
  781. audioUrl.value = voiceUrl;
  782. // 同时更新store中的音频URL
  783. emotionAudioStore.setCurrentAudioUrl(voiceUrl);
  784. // 不自动播放,等待用户手动点击
  785. }
  786. } catch (error) {
  787. console.error('解析股票结论数据失败:', error);
  788. }
  789. }
  790. } else {
  791. // 如果没有显示过,清空显示文本,等待打字机效果
  792. displayedTexts.value = {
  793. one1: '',
  794. one2: '',
  795. two: '',
  796. three: '',
  797. four: '',
  798. disclaimer: ''
  799. };
  800. displayedTitles.value = {
  801. one: '',
  802. two: '',
  803. three: '',
  804. four: ''
  805. };
  806. moduleVisibility.value = {
  807. one: false,
  808. two: false,
  809. three: false,
  810. four: false,
  811. disclaimer: false
  812. };
  813. // 即使没有显示过,也需要设置音频URL以便用户手动播放
  814. if (newStock.conclusionData) {
  815. try {
  816. // 如果conclusionData已经是对象,直接使用;否则解析JSON
  817. const conclusion = typeof newStock.conclusionData === 'object'
  818. ? newStock.conclusionData
  819. : JSON.parse(newStock.conclusionData);
  820. let voiceUrl = null;
  821. // 优先使用one1_url,如果没有则尝试其他音频URL
  822. if (conclusion.one1_url) {
  823. voiceUrl = conclusion.one1_url.toString().trim().replace(/[`\s]/g, '');
  824. } else if (conclusion.one2_url) {
  825. voiceUrl = conclusion.one2_url.toString().trim().replace(/[`\s]/g, '');
  826. } else if (conclusion.two_url) {
  827. voiceUrl = conclusion.two_url.toString().trim().replace(/[`\s]/g, '');
  828. } else if (conclusion.three_url) {
  829. voiceUrl = conclusion.three_url.toString().trim().replace(/[`\s]/g, '');
  830. } else if (conclusion.four_url) {
  831. voiceUrl = conclusion.four_url.toString().trim().replace(/[`\s]/g, '');
  832. } else if (conclusion.url) {
  833. voiceUrl = conclusion.url.toString().trim().replace(/[`\s]/g, '');
  834. } else if (conclusion.audioUrl) {
  835. voiceUrl = conclusion.audioUrl.toString().trim().replace(/[`\s]/g, '');
  836. } else if (conclusion.voice_url) {
  837. voiceUrl = conclusion.voice_url.toString().trim().replace(/[`\s]/g, '');
  838. } else if (conclusion.audio) {
  839. voiceUrl = conclusion.audio.toString().trim().replace(/[`\s]/g, '');
  840. } else if (conclusion.tts_url) {
  841. voiceUrl = conclusion.tts_url.toString().trim().replace(/[`\s]/g, '');
  842. }
  843. if (voiceUrl && voiceUrl.startsWith('http')) {
  844. console.log('切换到未显示股票,准备音频URL:', voiceUrl);
  845. audioUrl.value = voiceUrl;
  846. // 同时更新store中的音频URL
  847. emotionAudioStore.setCurrentAudioUrl(voiceUrl);
  848. }
  849. } catch (error) {
  850. console.error('解析股票结论数据失败:', error);
  851. }
  852. }
  853. }
  854. // 只有在页面已加载的情况下才渲染图表
  855. if (isPageLoaded.value) {
  856. nextTick(() => {
  857. renderCharts(newStock.apiData);
  858. console.log('图表数据已准备完成,开始渲染:', newStock.apiData)
  859. // 检查场景应用部分是否已经在视口中,如果是则立即触发效果
  860. setTimeout(() => {
  861. // 检查 scenarioApplicationRef.value 是否存在且是有效的 DOM 元素
  862. if (!scenarioApplicationRef.value || !(scenarioApplicationRef.value instanceof Element)) {
  863. console.warn('scenarioApplicationRef.value 不是有效的 DOM 元素,跳过处理');
  864. return;
  865. }
  866. if (parsedConclusion.value) {
  867. const stockCode = newStock.stockInfo?.code || newStock.stockInfo?.symbol;
  868. // 如果该股票已经显示过,不需要再处理
  869. if (stockCode && stockTypewriterShown.value.has(stockCode)) {
  870. return;
  871. }
  872. const rect = scenarioApplicationRef.value.getBoundingClientRect();
  873. const isInViewport = rect.top < window.innerHeight && rect.bottom > 0;
  874. if (isInViewport) {
  875. console.log('股票切换后检测到场景应用部分在视口中');
  876. if (stockCode) {
  877. // 检查该股票是否是第一次触发
  878. if (!stockTypewriterShown.value.has(stockCode)) {
  879. // 如果是用户主动搜索,启动打字机效果和音频播放
  880. if (isUserInitiated.value && audioUrl.value) {
  881. console.log('用户主动搜索,该股票第一次进入场景应用,开始打字机效果和音频播放');
  882. hasTriggeredTypewriter.value = true;
  883. hasTriggeredAudio.value = true;
  884. if (!stockAudioPlayed.value.has(stockCode)) {
  885. console.log('开始音频播放和打字机效果');
  886. stockAudioPlayed.value.set(stockCode, true);
  887. playAudioQueue(parsedConclusion.value, true);
  888. } else {
  889. // 如果音频已播放过,只启动打字机效果
  890. startTypewriterEffect(parsedConclusion.value);
  891. }
  892. stockTypewriterShown.value.set(stockCode, true);
  893. } else if (isUserInitiated.value && !audioUrl.value) {
  894. console.log('音频尚未准备好,等待音频加载完成后再触发效果(股票切换后)');
  895. return;
  896. } else {
  897. // 非用户主动搜索(如历史记录恢复),直接显示完整内容
  898. console.log('非用户主动搜索,该股票第一次进入场景应用,直接显示完整内容');
  899. const conclusion = parsedConclusion.value;
  900. displayedTexts.value = {
  901. one1: conclusion.one1 || '',
  902. one2: conclusion.one2 || '',
  903. two: conclusion.two || '',
  904. three: conclusion.three || '',
  905. four: conclusion.four || '',
  906. disclaimer: '该内容由AI生成,请注意甄别'
  907. };
  908. displayedTitles.value = {
  909. one: 'L1: 情绪监控',
  910. two: 'L2: 情绪解码',
  911. three: 'L3: 情绪推演',
  912. four: 'L4: 情绪套利'
  913. };
  914. moduleVisibility.value = {
  915. one: !!(conclusion.one1 || conclusion.one2),
  916. two: !!conclusion.two,
  917. three: !!conclusion.three,
  918. four: !!conclusion.four,
  919. disclaimer: true
  920. };
  921. stockTypewriterShown.value.set(stockCode, true);
  922. stockAudioPlayed.value.set(stockCode, true);
  923. }
  924. } else {
  925. // 非第一次或已经触发过:直接显示完整内容,不播放音频和打字机效果
  926. console.log('非第一次股票切换或已触发过,直接显示完整内容');
  927. // 直接显示完整内容
  928. const conclusion = parsedConclusion.value;
  929. displayedTexts.value = {
  930. one1: conclusion.one1 || '',
  931. one2: conclusion.one2 || '',
  932. two: conclusion.two || '',
  933. three: conclusion.three || '',
  934. four: conclusion.four || '',
  935. disclaimer: '该内容由AI生成,请注意甄别'
  936. };
  937. displayedTitles.value = {
  938. one: 'L1: 情绪监控',
  939. two: 'L2: 情绪解码',
  940. three: 'L3: 情绪推演',
  941. four: 'L4: 情绪套利'
  942. };
  943. moduleVisibility.value = {
  944. one: !!(conclusion.one1 || conclusion.one2),
  945. two: !!conclusion.two,
  946. three: !!conclusion.three,
  947. four: !!conclusion.four,
  948. disclaimer: true
  949. };
  950. }
  951. }
  952. }
  953. }
  954. }, 500); // 延迟500ms确保数据完全加载
  955. });
  956. } else {
  957. console.log('页面尚未加载完成,等待数据加载完成后再渲染图表');
  958. }
  959. } else {
  960. console.log('股票数据不存在或API数据未加载');
  961. // 隐藏所有图表组件
  962. chartVisibility.value = {
  963. marketTemperature: false,
  964. emotionDecod: false,
  965. emotionalBottomRadar: false,
  966. emoEnergyConverter: false
  967. };
  968. }
  969. }, { immediate: true });
  970. // 监听parsedConclusion变化,准备数据但不立即触发打字机效果
  971. watch(parsedConclusion, (newConclusion) => {
  972. if (newConclusion) {
  973. console.log('场景应用结论数据:', newConclusion);
  974. // 不再立即开始打字机效果,等待滚动到场景应用部分时触发
  975. // 尝试多种可能的语音URL字段名,优先使用新的数据结构
  976. let voiceUrl = null;
  977. if (newConclusion.one1_url) {
  978. voiceUrl = newConclusion.one1_url.toString().trim().replace(/[`\s]/g, '');
  979. } else if (newConclusion.one2_url) {
  980. voiceUrl = newConclusion.one2_url.toString().trim().replace(/[`\s]/g, '');
  981. } else if (newConclusion.two_url) {
  982. voiceUrl = newConclusion.two_url.toString().trim().replace(/[`\s]/g, '');
  983. } else if (newConclusion.three_url) {
  984. voiceUrl = newConclusion.three_url.toString().trim().replace(/[`\s]/g, '');
  985. } else if (newConclusion.four_url) {
  986. voiceUrl = newConclusion.four_url.toString().trim().replace(/[`\s]/g, '');
  987. } else if (newConclusion.url) {
  988. // 清理URL字符串,去除空格、反引号等特殊字符
  989. voiceUrl = newConclusion.url.toString().trim().replace(/[`\s]/g, '');
  990. } else if (newConclusion.audioUrl) {
  991. voiceUrl = newConclusion.audioUrl.toString().trim().replace(/[`\s]/g, '');
  992. } else if (newConclusion.voice_url) {
  993. voiceUrl = newConclusion.voice_url.toString().trim().replace(/[`\s]/g, '');
  994. } else if (newConclusion.audio) {
  995. voiceUrl = newConclusion.audio.toString().trim().replace(/[`\s]/g, '');
  996. } else if (newConclusion.tts_url) {
  997. voiceUrl = newConclusion.tts_url.toString().trim().replace(/[`\s]/g, '');
  998. }
  999. if (voiceUrl && voiceUrl.startsWith('http')) {
  1000. console.log('找到并清理后的语音URL:', voiceUrl);
  1001. audioUrl.value = voiceUrl;
  1002. // 同时更新store中的音频URL
  1003. emotionAudioStore.setCurrentAudioUrl(voiceUrl);
  1004. console.log('音频URL已准备,检查是否需要立即触发效果');
  1005. // 音频准备好后,只有在用户主动搜索时才自动触发效果
  1006. // 数据恢复时不自动播放音频和打字机效果
  1007. console.log('音频URL已准备完成,等待用户手动触发播放');
  1008. } else {
  1009. console.log('未找到有效的语音URL,原始URL:', newConclusion.url);
  1010. console.log('结论数据中的所有字段:', Object.keys(newConclusion));
  1011. }
  1012. }
  1013. }, { immediate: true });
  1014. // 打字机效果函数
  1015. function startTypewriterEffect(conclusion, onComplete) {
  1016. console.log('开始打字机效果,结论数据:', conclusion);
  1017. // 保存当前的完成回调函数
  1018. currentOnCompleteCallback.value = onComplete;
  1019. // 详细调试各个字段
  1020. console.log('L1字段 - one1:', conclusion.one1);
  1021. console.log('L1字段 - one2:', conclusion.one2);
  1022. console.log('L2字段 - two:', conclusion.two);
  1023. console.log('L3字段 - three:', conclusion.three);
  1024. console.log('L4字段 - four:', conclusion.four);
  1025. // 清除之前的定时器
  1026. typewriterTimers.value.forEach(timer => clearTimeout(timer));
  1027. typewriterTimers.value = [];
  1028. // 重置显示文本和状态
  1029. displayedTexts.value = {
  1030. one1: '',
  1031. one2: '',
  1032. two: '',
  1033. three: '',
  1034. four: '',
  1035. disclaimer: ''
  1036. };
  1037. displayedTitles.value = {
  1038. one: '',
  1039. two: '',
  1040. three: '',
  1041. four: ''
  1042. };
  1043. moduleVisibility.value = {
  1044. one: false,
  1045. two: false,
  1046. three: false,
  1047. four: false,
  1048. disclaimer: false
  1049. };
  1050. // 定义打字速度(毫秒)
  1051. const typeSpeed = 200;
  1052. let totalDelay = 0;
  1053. // 定义模块配置
  1054. const modules = [
  1055. {
  1056. key: 'one',
  1057. title: 'L1: 情绪监控',
  1058. contents: [
  1059. { key: 'one1', text: conclusion.one1 },
  1060. { key: 'one2', text: conclusion.one2 }
  1061. ]
  1062. },
  1063. {
  1064. key: 'two',
  1065. title: 'L2: 情绪解码',
  1066. contents: [
  1067. { key: 'two', text: conclusion.two }
  1068. ]
  1069. },
  1070. {
  1071. key: 'three',
  1072. title: 'L3: 情绪推演',
  1073. contents: [
  1074. { key: 'three', text: conclusion.three }
  1075. ]
  1076. },
  1077. {
  1078. key: 'four',
  1079. title: 'L4: 情绪套利',
  1080. contents: [
  1081. { key: 'four', text: conclusion.four }
  1082. ]
  1083. }
  1084. ];
  1085. // 按模块顺序处理
  1086. modules.forEach((module) => {
  1087. // 检查模块是否有内容
  1088. const hasContent = module.contents.some(content => content.text && content.text.trim());
  1089. console.log(`模块 ${module.key} 是否有内容:`, hasContent, '内容:', module.contents.map(c => c.text));
  1090. if (!hasContent) return;
  1091. console.log(`开始显示模块 ${module.key}`);
  1092. // 显示模块
  1093. const showModuleTimer = setTimeout(() => {
  1094. moduleVisibility.value[module.key] = true;
  1095. console.log(`模块 ${module.key} 已设置为可见`);
  1096. }, totalDelay);
  1097. typewriterTimers.value.push(showModuleTimer);
  1098. totalDelay += 100;
  1099. // 打字机效果显示标题
  1100. const title = module.title;
  1101. for (let i = 0; i <= title.length; i++) {
  1102. const timer = setTimeout(() => {
  1103. displayedTitles.value[module.key] = title.substring(0, i);
  1104. }, totalDelay + i * typeSpeed);
  1105. typewriterTimers.value.push(timer);
  1106. }
  1107. totalDelay += title.length * typeSpeed + 300; // 标题完成后间隔
  1108. // 打字机效果显示内容
  1109. module.contents.forEach((content) => {
  1110. if (content.text && content.text.trim()) {
  1111. const text = content.text;
  1112. for (let i = 0; i <= text.length; i++) {
  1113. const timer = setTimeout(() => {
  1114. displayedTexts.value[content.key] = text.substring(0, i);
  1115. }, totalDelay + i * typeSpeed);
  1116. typewriterTimers.value.push(timer);
  1117. }
  1118. totalDelay += text.length * typeSpeed + 500; // 内容完成后间隔
  1119. }
  1120. });
  1121. totalDelay += 800; // 模块间间隔
  1122. });
  1123. // 添加免责声明的打字机效果(在所有模块显示完成后)
  1124. const disclaimerText = '该内容由AI生成,请注意甄别';
  1125. // 显示免责声明模块
  1126. const showDisclaimerTimer = setTimeout(() => {
  1127. moduleVisibility.value.disclaimer = true;
  1128. }, totalDelay);
  1129. typewriterTimers.value.push(showDisclaimerTimer);
  1130. totalDelay += 100;
  1131. // 打字机效果显示免责声明
  1132. for (let i = 0; i <= disclaimerText.length; i++) {
  1133. const timer = setTimeout(() => {
  1134. displayedTexts.value.disclaimer = disclaimerText.substring(0, i);
  1135. // 在免责声明打字机效果完成后调用回调函数
  1136. if (i === disclaimerText.length) {
  1137. console.log('打字机效果完成,调用onComplete回调');
  1138. if (onComplete && typeof onComplete === 'function') {
  1139. onComplete();
  1140. // 清除保存的回调函数
  1141. currentOnCompleteCallback.value = null;
  1142. }
  1143. }
  1144. }, totalDelay + i * typeSpeed);
  1145. typewriterTimers.value.push(timer);
  1146. }
  1147. }
  1148. // 清理定时器的函数
  1149. function clearTypewriterTimers() {
  1150. typewriterTimers.value.forEach(timer => clearTimeout(timer));
  1151. typewriterTimers.value = [];
  1152. }
  1153. // 音频队列管理
  1154. const audioQueue = ref([]);
  1155. const isPlayingQueueAudio = ref(false);
  1156. let currentPlayIndex = 0;
  1157. let isCallingPlayNext = false;
  1158. // 音频队列顺序管理
  1159. const audioQueueOrder = {
  1160. "one1_url": 1,
  1161. "one2_url": 2,
  1162. "two_url": 3,
  1163. "three_url": 4,
  1164. "four_url": 5,
  1165. "url": 6,
  1166. "audioUrl": 7,
  1167. "voice_url": 8,
  1168. "audio": 9,
  1169. "tts_url": 10
  1170. };
  1171. // 播放音频队列中的下一个音频
  1172. const playNextAudio = () => {
  1173. console.log("=== playNextAudio 被调用 ===");
  1174. console.log("当前队列状态:", {
  1175. queueLength: audioQueue.value.length,
  1176. queueItems: audioQueue.value.map((item) => item.name),
  1177. currentPlayIndex: currentPlayIndex,
  1178. isPlayingQueueAudio: isPlayingQueueAudio.value,
  1179. isCallingPlayNext: isCallingPlayNext,
  1180. audioStoreIsPlaying: emotionAudioStore.isPlaying,
  1181. });
  1182. if (
  1183. audioQueue.value.length === 0 ||
  1184. isPlayingQueueAudio.value ||
  1185. isCallingPlayNext
  1186. ) {
  1187. console.log("❌ 播放条件不满足 - 队列长度:", audioQueue.value.length, "正在播放:", isPlayingQueueAudio.value, "正在调用:", isCallingPlayNext);
  1188. return;
  1189. }
  1190. // 检查是否已播放完所有音频
  1191. if (currentPlayIndex >= audioQueue.value.length && audioQueue.value.length > 0) {
  1192. console.log("🔄 所有音频播放完成,重置索引从第一个开始");
  1193. currentPlayIndex = 0;
  1194. isCallingPlayNext = false; // 重置调用标志
  1195. }
  1196. isCallingPlayNext = true;
  1197. isPlayingQueueAudio.value = true;
  1198. const audioInfo = audioQueue.value[currentPlayIndex];
  1199. console.log(`✅ 开始播放${audioInfo.name}音频 (索引:${currentPlayIndex}),队列总长度:`, audioQueue.value.length);
  1200. // 停止之前的音频
  1201. if (emotionAudioStore.nowSound && emotionAudioStore.nowSound.playing()) {
  1202. emotionAudioStore.nowSound.stop();
  1203. }
  1204. // 创建新的音频实例
  1205. const audio = new Howl({
  1206. src: [audioInfo.url],
  1207. html5: true,
  1208. format: ['mp3', 'wav'],
  1209. onplay: () => {
  1210. isAudioPlaying.value = true;
  1211. isPlayingQueueAudio.value = true;
  1212. emotionAudioStore.isPlaying = true;
  1213. console.log(`开始播放${audioInfo.name}音频`);
  1214. // 如果是第一个音频且需要启动打字机效果,则启动
  1215. if (currentPlayIndex === 0 && audioInfo.shouldStartTypewriter && parsedConclusion.value) {
  1216. console.log('🎬 第一个音频开始播放,同时启动打字机效果');
  1217. startTypewriterEffect(parsedConclusion.value, audioInfo.onComplete);
  1218. }
  1219. },
  1220. onpause: () => {
  1221. emotionAudioStore.isPaused = true;
  1222. console.log(`${audioInfo.name}音频暂停播放`);
  1223. },
  1224. onresume: () => {
  1225. emotionAudioStore.isPaused = false;
  1226. console.log(`${audioInfo.name}音频继续播放`);
  1227. },
  1228. onend: () => {
  1229. console.log(`${audioInfo.name}音频播放完成,准备播放下一个`);
  1230. emotionAudioStore.isPlaying = false;
  1231. emotionAudioStore.isPaused = false;
  1232. emotionAudioStore.playbackPosition = 0;
  1233. isAudioPlaying.value = false;
  1234. isPlayingQueueAudio.value = false;
  1235. // 移动到下一个音频索引
  1236. currentPlayIndex++;
  1237. // 确保只有在音频真正播放完成时才播放下一个
  1238. if (currentPlayIndex < audioQueue.value.length) {
  1239. console.log(`队列中还有音频,500ms后播放下一个 (索引:${currentPlayIndex})`);
  1240. setTimeout(() => {
  1241. isCallingPlayNext = false;
  1242. playNextAudio();
  1243. }, 500);
  1244. } else {
  1245. console.log("🎉 所有音频播放完成");
  1246. emotionAudioStore.nowSound = null;
  1247. isCallingPlayNext = false;
  1248. // 调用完成回调(如果有的话)
  1249. if (audioInfo.onComplete && typeof audioInfo.onComplete === 'function') {
  1250. console.log('调用音频播放完成回调');
  1251. audioInfo.onComplete();
  1252. }
  1253. }
  1254. },
  1255. onstop: () => {
  1256. console.log(`${audioInfo.name}音频被停止`);
  1257. emotionAudioStore.isPlaying = false;
  1258. emotionAudioStore.isPaused = false;
  1259. emotionAudioStore.playbackPosition = 0;
  1260. isAudioPlaying.value = false;
  1261. isPlayingQueueAudio.value = false;
  1262. },
  1263. onerror: (error) => {
  1264. console.error(`${audioInfo.name}音频播放失败:`, error);
  1265. isAudioPlaying.value = false;
  1266. isPlayingQueueAudio.value = false;
  1267. isCallingPlayNext = false;
  1268. // 播放下一个音频
  1269. setTimeout(() => {
  1270. playNextAudio();
  1271. }, 100);
  1272. },
  1273. onload: () => {
  1274. emotionAudioStore.duration = audio.duration();
  1275. console.log(`${audioInfo.name}音频加载完成,时长:`, emotionAudioStore.duration);
  1276. }
  1277. });
  1278. // 设置当前音频URL到store
  1279. emotionAudioStore.setCurrentAudioUrl(audioInfo.url);
  1280. emotionAudioStore.nowSound = audio;
  1281. emotionAudioStore.setAudioInstance(audio);
  1282. console.log(`尝试播放${audioInfo.name}音频`);
  1283. audio.play();
  1284. };
  1285. // 添加音频到播放队列
  1286. const addToAudioQueue = (url, name, shouldStartTypewriter = false, onComplete = null) => {
  1287. console.log(`=== 添加音频到队列 ===`);
  1288. console.log("URL:", url);
  1289. console.log("Name:", name);
  1290. console.log("是否启动打字机效果:", shouldStartTypewriter);
  1291. console.log("音频启用状态:", emotionAudioStore.isVoiceEnabled);
  1292. if (url && emotionAudioStore.isVoiceEnabled) {
  1293. const audioItem = {
  1294. url,
  1295. name,
  1296. order: audioQueueOrder[name] || 999,
  1297. shouldStartTypewriter, // 添加打字机效果标志
  1298. onComplete, // 添加完成回调
  1299. };
  1300. audioQueue.value.push(audioItem);
  1301. // 按顺序排序队列
  1302. audioQueue.value.sort((a, b) => a.order - b.order);
  1303. console.log(`音频${name}已添加到播放队列,顺序:${audioItem.order}`);
  1304. console.log("当前队列顺序:", audioQueue.value.map((item) => `${item.name}(${item.order})`));
  1305. // 只有在确实没有音频在播放且这是第一个音频时才开始播放
  1306. if (!isPlayingQueueAudio.value && !emotionAudioStore.isPlaying && audioQueue.value.length === 1) {
  1307. console.log("✅ 条件满足:没有音频在播放且这是第一个音频,立即开始播放");
  1308. playNextAudio();
  1309. } else {
  1310. console.log("⏳ 等待条件:", {
  1311. isPlayingQueueAudio: isPlayingQueueAudio.value,
  1312. audioStoreIsPlaying: emotionAudioStore.isPlaying,
  1313. queueLength: audioQueue.value.length,
  1314. reason: audioQueue.value.length > 1 ? "队列中已有其他音频" : "有音频正在播放",
  1315. });
  1316. }
  1317. } else {
  1318. console.log("❌ 跳过添加音频:", {
  1319. hasUrl: !!url,
  1320. voiceEnabled: emotionAudioStore.isVoiceEnabled,
  1321. });
  1322. }
  1323. };
  1324. // 修改后的音频播放函数 - 支持多音频播放和同步打字机效果
  1325. function playAudioQueue(conclusionData, shouldStartTypewriter = false, onComplete = null) {
  1326. if (!conclusionData) {
  1327. console.log('没有结论数据,跳过播放');
  1328. return;
  1329. }
  1330. // 检查是否启用了语音功能
  1331. console.log('语音功能状态:', emotionAudioStore.isVoiceEnabled);
  1332. if (!emotionAudioStore.isVoiceEnabled) {
  1333. console.log('语音功能已关闭,跳过播放');
  1334. return;
  1335. }
  1336. console.log('开始处理多音频播放...', shouldStartTypewriter ? '同时启动打字机效果' : '');
  1337. try {
  1338. // 解析结论数据
  1339. const conclusion = typeof conclusionData === 'object' ? conclusionData : JSON.parse(conclusionData);
  1340. // 清空之前的音频队列
  1341. audioQueue.value = [];
  1342. currentPlayIndex = 0;
  1343. isCallingPlayNext = false;
  1344. isPlayingQueueAudio.value = false;
  1345. // 按优先级顺序检查并添加所有可用的音频URL
  1346. const audioSources = [
  1347. { key: 'one1_url', name: 'one1_url' },
  1348. { key: 'one2_url', name: 'one2_url' },
  1349. { key: 'two_url', name: 'two_url' },
  1350. { key: 'three_url', name: 'three_url' },
  1351. { key: 'four_url', name: 'four_url' },
  1352. { key: 'url', name: 'url' },
  1353. { key: 'audioUrl', name: 'audioUrl' },
  1354. { key: 'voice_url', name: 'voice_url' },
  1355. { key: 'audio', name: 'audio' },
  1356. { key: 'tts_url', name: 'tts_url' }
  1357. ];
  1358. audioSources.forEach(source => {
  1359. if (conclusion[source.key]) {
  1360. const voiceUrl = conclusion[source.key].toString().trim().replace(/[`\s]/g, '');
  1361. if (voiceUrl && voiceUrl.startsWith('http')) {
  1362. console.log(`找到音频URL: ${source.name} = ${voiceUrl}`);
  1363. addToAudioQueue(voiceUrl, source.name, shouldStartTypewriter && audioQueue.value.length === 0, onComplete);
  1364. }
  1365. }
  1366. });
  1367. if (audioQueue.value.length === 0) {
  1368. console.log('未找到有效的音频URL');
  1369. // 如果没有音频但需要启动打字机效果,直接启动
  1370. if (shouldStartTypewriter) {
  1371. console.log('没有音频但需要启动打字机效果');
  1372. startTypewriterEffect(conclusion, onComplete);
  1373. }
  1374. } else {
  1375. console.log(`总共找到 ${audioQueue.value.length} 个音频,准备播放`);
  1376. }
  1377. } catch (error) {
  1378. console.error('处理音频播放失败:', error);
  1379. }
  1380. }
  1381. // 原有的单音频播放函数(保持兼容性)
  1382. function playAudio(url) {
  1383. console.log('尝试播放音频:', url);
  1384. if (!url) {
  1385. console.warn('音频URL为空,跳过播放');
  1386. isAudioPlaying.value = false;
  1387. return;
  1388. }
  1389. // 检查是否启用了语音功能
  1390. console.log('语音功能状态:', emotionAudioStore.isVoiceEnabled);
  1391. if (!emotionAudioStore.isVoiceEnabled) {
  1392. console.log('语音功能已关闭,跳过播放');
  1393. return;
  1394. }
  1395. console.log('开始创建音频实例...');
  1396. try {
  1397. // 设置当前音频URL
  1398. emotionAudioStore.setCurrentAudioUrl(url);
  1399. // 停止之前的音频
  1400. if (emotionAudioStore.nowSound && emotionAudioStore.nowSound.playing()) {
  1401. emotionAudioStore.nowSound.stop();
  1402. }
  1403. // 创建新的音频实例
  1404. const newSound = new Howl({
  1405. src: [url],
  1406. html5: true,
  1407. format: ['mp3', 'wav'],
  1408. onplay: () => {
  1409. isAudioPlaying.value = true;
  1410. emotionAudioStore.isPlaying = true;
  1411. console.log('开始播放场景应用语音');
  1412. // 音频开始播放时的自动滚动已移除
  1413. },
  1414. onend: () => {
  1415. isAudioPlaying.value = false;
  1416. emotionAudioStore.isPlaying = false;
  1417. emotionAudioStore.isPaused = false;
  1418. emotionAudioStore.playbackPosition = 0;
  1419. console.log('场景应用语音播放结束');
  1420. },
  1421. onstop: () => {
  1422. isAudioPlaying.value = false;
  1423. emotionAudioStore.isPlaying = false;
  1424. console.log('场景应用语音播放停止');
  1425. },
  1426. onpause: () => {
  1427. isAudioPlaying.value = false;
  1428. emotionAudioStore.isPlaying = false;
  1429. console.log('场景应用语音播放暂停');
  1430. },
  1431. onerror: (error) => {
  1432. isAudioPlaying.value = false;
  1433. emotionAudioStore.isPlaying = false;
  1434. console.error('音频播放错误:', error);
  1435. },
  1436. onload: () => {
  1437. // 音频加载完成,获取时长
  1438. emotionAudioStore.duration = newSound.duration();
  1439. console.log('音频加载完成,时长:', emotionAudioStore.duration);
  1440. }
  1441. });
  1442. // 保存音频实例到store
  1443. emotionAudioStore.nowSound = newSound;
  1444. emotionAudioStore.setAudioInstance(newSound);
  1445. // 播放音频
  1446. newSound.play();
  1447. } catch (error) {
  1448. console.error('创建音频实例失败:', error);
  1449. isAudioPlaying.value = false;
  1450. }
  1451. }
  1452. // 停止音频播放
  1453. function stopAudio() {
  1454. if (emotionAudioStore.nowSound) {
  1455. emotionAudioStore.nowSound.stop();
  1456. }
  1457. isAudioPlaying.value = false;
  1458. }
  1459. // 触发图片旋转的方法
  1460. function startImageRotation() {
  1461. isRotating.value = true;
  1462. // 如果你想在一段时间后停止旋转,可以添加以下代码
  1463. setTimeout(() => {
  1464. isRotating.value = false;
  1465. }, 5000); // 5 秒后停止旋转
  1466. }
  1467. // 显示思考过程
  1468. async function showThinkingProcess(stockName = null) {
  1469. // 第一步:正在思考
  1470. const thinkingMessage1 = reactive({
  1471. sender: 'ai',
  1472. text: 'AI情绪大模型正在思考......',
  1473. gif: '/src/assets/img/gif/思考.gif'
  1474. });
  1475. messages.value.push(thinkingMessage1);
  1476. await new Promise(resolve => setTimeout(resolve, 1500));
  1477. messages.value.pop();
  1478. // 第二步:正在解析关键数据(持续显示直到获取到股票名称)
  1479. const thinkingMessage2 = reactive({
  1480. sender: 'ai',
  1481. text: 'AI情绪大模型正在解析关键数据......',
  1482. gif: '/src/assets/img/gif/解析.gif'
  1483. });
  1484. messages.value.push(thinkingMessage2);
  1485. // 如果没有股票名称,保持第二步显示
  1486. if (!stockName) {
  1487. return thinkingMessage2; // 返回消息引用,以便后续更新
  1488. }
  1489. // 有股票名称后,继续后续步骤
  1490. await new Promise(resolve => setTimeout(resolve, 1500));
  1491. messages.value.pop();
  1492. // 第三步:生成具体股票的量子四维矩阵图
  1493. const thinkingMessage3 = reactive({
  1494. sender: 'ai',
  1495. text: `AI情绪大模型正在生成${stockName}量子四维矩阵图......`,
  1496. gif: '/src/assets/img/gif/生成.gif'
  1497. });
  1498. messages.value.push(thinkingMessage3);
  1499. await new Promise(resolve => setTimeout(resolve, 1500));
  1500. messages.value.pop();
  1501. // 第四步:报告已生成
  1502. const thinkingMessage4 = reactive({ sender: 'ai', text: '报告已生成!' });
  1503. messages.value.push(thinkingMessage4);
  1504. await new Promise(resolve => setTimeout(resolve, 1500));
  1505. messages.value.pop();
  1506. return null;
  1507. }
  1508. // 继续思考过程(当获取到股票名称后调用)
  1509. async function continueThinkingProcess(thinkingMessageRef, stockName) {
  1510. if (!thinkingMessageRef || !stockName) return;
  1511. // 等待一段时间后继续
  1512. await new Promise(resolve => setTimeout(resolve, 1500));
  1513. // 移除第二步消息
  1514. const index = messages.value.indexOf(thinkingMessageRef);
  1515. if (index > -1) {
  1516. messages.value.splice(index, 1);
  1517. }
  1518. // 第三步:生成具体股票的量子四维矩阵图
  1519. const thinkingMessage3 = reactive({
  1520. sender: 'ai',
  1521. text: `正在生成${stockName}量子四维矩阵图......`,
  1522. gif: '/src/assets/img/gif/生成.gif'
  1523. });
  1524. messages.value.push(thinkingMessage3);
  1525. // 返回第三步消息的引用,以便后续处理
  1526. return thinkingMessage3;
  1527. }
  1528. // 完成思考过程(当第二个工作流接口成功后调用)
  1529. async function finishThinkingProcess(thinkingMessage3Ref) {
  1530. if (!thinkingMessage3Ref) return;
  1531. // 等待一段时间
  1532. await new Promise(resolve => setTimeout(resolve, 1500));
  1533. // 移除第三步消息
  1534. const index = messages.value.indexOf(thinkingMessage3Ref);
  1535. if (index > -1) {
  1536. messages.value.splice(index, 1);
  1537. }
  1538. // 第四步:报告已生成
  1539. const thinkingMessage4 = reactive({ sender: 'ai', text: '报告已生成!' });
  1540. messages.value.push(thinkingMessage4);
  1541. await new Promise(resolve => setTimeout(resolve, 1500));
  1542. messages.value.pop();
  1543. }
  1544. // 发送消息方法
  1545. async function handleSendMessage(input, onComplete) {
  1546. console.log("发送内容:", input);
  1547. // 标记为用户主动搜索
  1548. isUserInitiated.value = true;
  1549. // 重置历史记录模式状态,确保正常对话时显示conversation-area
  1550. isHistoryMode.value = false;
  1551. // 检查用户输入内容是否为空
  1552. if (!input || !input.trim()) {
  1553. ElMessage.warning("输入内容不能为空");
  1554. // 调用完成回调,重新启用输入框
  1555. if (onComplete && typeof onComplete === 'function') {
  1556. onComplete();
  1557. // 清除保存的回调函数
  1558. currentOnCompleteCallback.value = null;
  1559. }
  1560. return;
  1561. }
  1562. // 用户输入不为空,立即清空页面内容并触发图片旋转逻辑
  1563. isPageLoaded.value = false; // 立即隐藏页面内容
  1564. isRotating.value = true;
  1565. const previousMessages = [...messages.value]; // 保存历史消息
  1566. messages.value = []; // 清空历史数据
  1567. // 添加用户消息(只添加一次)
  1568. const userMessage = reactive({ sender: 'user', text: input });
  1569. messages.value.push(userMessage);
  1570. // 将用户消息添加到emotion store中
  1571. emotionStore.addConversation({
  1572. sender: 'user',
  1573. text: input,
  1574. timestamp: new Date().toISOString()
  1575. });
  1576. // 检查用户剩余次数
  1577. await chatStore.getUserCount(); // 获取最新的用户次数
  1578. if (chatStore.UserCount <= 0) {
  1579. const aiMessage = reactive({ sender: 'ai', text: '您的剩余次数为0,无法使用情绪大模型,请联系客服或购买服务包。' });
  1580. messages.value.push(aiMessage);
  1581. // 将AI消息添加到emotion store中
  1582. emotionStore.addConversation({
  1583. sender: 'ai',
  1584. text: '您的剩余次数为0,无法使用情绪大模型,请联系客服或购买服务包。',
  1585. timestamp: new Date().toISOString()
  1586. });
  1587. // 停止图片旋转,恢复历史数据
  1588. isRotating.value = false;
  1589. messages.value = [...previousMessages, ...messages.value];
  1590. // 调用完成回调,重新启用输入框
  1591. if (onComplete && typeof onComplete === 'function') {
  1592. onComplete();
  1593. // 清除保存的回调函数
  1594. currentOnCompleteCallback.value = null;
  1595. }
  1596. return;
  1597. }
  1598. // 开始思考过程(不带股票名称)
  1599. const thinkingMessageRef = await showThinkingProcess();
  1600. let thinkingMessage3Ref = null;
  1601. try {
  1602. // 第一步:调用第一个接口验证用户输入内容是否合法
  1603. // const params = {
  1604. // content: userMessage.text,
  1605. // userData: {
  1606. // token: localStorage.getItem('localToken'),
  1607. // language: "cn",
  1608. // brainPrivilegeState: '1',
  1609. // swordPrivilegeState: '1',
  1610. // stockForecastPrivilegeState: '1',
  1611. // spaceForecastPrivilegeState: '1',
  1612. // aibullPrivilegeState: '1',
  1613. // aigoldBullPrivilegeState: '1',
  1614. // airadarPrivilegeState: '1',
  1615. // marketList: "hk,cn,usa,my,sg,vi,in,gb",
  1616. // },
  1617. // };
  1618. const result = await getReplyAPI({
  1619. "token": localStorage.getItem("localToken"),
  1620. "language": "cn",
  1621. "marketList": "hk,cn,usa,my,sg,vi,in,gb",
  1622. "content": userMessage.text
  1623. });
  1624. const response = result;
  1625. const parsedData = response.data;
  1626. console.log('第一个接口返回的完整数据:', parsedData);
  1627. // 检查用户输入是否合法
  1628. if (!parsedData || !parsedData.market || !parsedData.code) {
  1629. // 输入不合法,先清理思考过程消息
  1630. if (thinkingMessageRef) {
  1631. const index = messages.value.indexOf(thinkingMessageRef);
  1632. if (index > -1) {
  1633. messages.value.splice(index, 1);
  1634. }
  1635. }
  1636. // 关闭加载状态和等待提示,返回refuse信息,停止图片旋转,恢复历史数据
  1637. // isLoading.value = false;
  1638. isPageLoaded.value = false;
  1639. const aiMessage = reactive({ sender: 'ai', text: processRefuseMessage(parsedData.refuse) });
  1640. messages.value.push(aiMessage);
  1641. // 将AI消息添加到emotion store中
  1642. emotionStore.addConversation({
  1643. sender: 'ai',
  1644. text: processRefuseMessage(parsedData.refuse),
  1645. timestamp: new Date().toISOString()
  1646. });
  1647. isRotating.value = false;
  1648. messages.value = [...previousMessages, ...messages.value];
  1649. // 调用完成回调,重新启用输入框
  1650. if (onComplete && typeof onComplete === 'function') {
  1651. onComplete();
  1652. // 清除保存的回调函数
  1653. currentOnCompleteCallback.value = null;
  1654. }
  1655. return;
  1656. }
  1657. // 输入合法,继续执行后续处理
  1658. // 获取到股票名称后,继续思考过程
  1659. if (thinkingMessageRef && parsedData.name) {
  1660. thinkingMessage3Ref = await continueThinkingProcess(thinkingMessageRef, parsedData.name);
  1661. }
  1662. // 设置加载状态,隐藏图表页面
  1663. // isLoading.value = true;
  1664. isPageLoaded.value = false;
  1665. // 调用第二个工作流接口
  1666. const conclusionParams = {
  1667. recordId: parsedData.recordId,
  1668. parentId: parsedData.parentId,
  1669. stockId: parsedData.stockId,
  1670. token: localStorage.getItem('localToken'),
  1671. language: "cn",
  1672. };
  1673. console.log('第二个接口参数:', conclusionParams);
  1674. // 同时调用第二个数据流接口和fetchData方法
  1675. const [conclusionResult, fetchDataResult] = await Promise.all([
  1676. getConclusionAPI(conclusionParams),
  1677. fetchData(parsedData.code, parsedData.market, parsedData.name || "未知股票", input.trim(), parsedData.stockId)
  1678. ]);
  1679. // 处理结论接口返回的数据
  1680. const conclusionResponse = conclusionResult;
  1681. // 检查所有数据是否都加载成功
  1682. if (conclusionResponse && conclusionResponse.data && fetchDataResult) {
  1683. // 第二个工作流接口成功,完成思考过程
  1684. if (thinkingMessage3Ref) {
  1685. await finishThinkingProcess(thinkingMessage3Ref);
  1686. }
  1687. // 将结论数据存储到响应式变量和store中
  1688. conclusionData.value = conclusionResponse.data;
  1689. console.log('第二个接口返回的完整数据结构:', conclusionResponse.data);
  1690. // 将结论数据存储到store中的当前激活股票
  1691. emotionStore.updateActiveStockConclusion(conclusionResponse.data);
  1692. // 所有数据加载完成,关闭加载状态,显示页面
  1693. // isLoading.value = false;
  1694. isPageLoaded.value = true;
  1695. // 使用nextTick确保DOM更新后清空对话显示并启动高度监听器
  1696. nextTick(() => {
  1697. messages.value = [];
  1698. // 启动页面高度监听器,实时监听内容变化并自动滚动
  1699. startHeightObserver();
  1700. // 立即滚动到底部
  1701. scrollToBottom();
  1702. });
  1703. // 数据获取成功后,重新获取用户次数以实现实时更新
  1704. try {
  1705. await chatStore.getUserCount();
  1706. console.log('数据获取成功后,用户次数已更新');
  1707. } catch (error) {
  1708. console.error('更新用户次数失败:', error);
  1709. }
  1710. // 确保页面状态更新后触发图表渲染和音频文本
  1711. nextTick(() => {
  1712. if (currentStock.value && currentStock.value.apiData) {
  1713. renderCharts(currentStock.value.apiData);
  1714. // 只有在用户主动搜索时才自动触发音频和文本
  1715. if (isUserInitiated.value && parsedConclusion.value && audioUrl.value) {
  1716. const stockCode = currentStock.value.stockInfo?.code || currentStock.value.stockInfo?.symbol;
  1717. if (stockCode && !stockTypewriterShown.value.has(stockCode)) {
  1718. if (!stockAudioPlayed.value.has(stockCode)) {
  1719. stockAudioPlayed.value.set(stockCode, true);
  1720. playAudioQueue(parsedConclusion.value, true, onComplete);
  1721. } else {
  1722. // 如果音频已播放过,只启动打字机效果
  1723. startTypewriterEffect(parsedConclusion.value, onComplete);
  1724. }
  1725. stockTypewriterShown.value.set(stockCode, true);
  1726. } else {
  1727. // 如果不需要打字机效果,直接调用完成回调
  1728. if (onComplete && typeof onComplete === 'function') {
  1729. onComplete();
  1730. // 清除保存的回调函数
  1731. currentOnCompleteCallback.value = null;
  1732. }
  1733. }
  1734. }
  1735. // 重置用户主动搜索标志
  1736. isUserInitiated.value = false;
  1737. }
  1738. });
  1739. } else {
  1740. // 数据加载失败,清理第三步思考过程消息
  1741. if (thinkingMessage3Ref) {
  1742. const index = messages.value.indexOf(thinkingMessage3Ref);
  1743. if (index > -1) {
  1744. messages.value.splice(index, 1);
  1745. }
  1746. }
  1747. // 数据加载失败,停止图片旋转,恢复历史数据
  1748. // isLoading.value = false;
  1749. // 如果 fetchDataResult 为 false,说明数据不完整的错误信息已经在 fetchData 中添加到 messages
  1750. // 只有在 conclusionResponse 有问题时才添加通用错误信息
  1751. if (!conclusionResponse || !conclusionResponse.data) {
  1752. const aiMessage = reactive({ sender: 'ai', text: '网络加载失败,请重试' });
  1753. messages.value.push(aiMessage);
  1754. }
  1755. isRotating.value = false;
  1756. messages.value = [...previousMessages, ...messages.value];
  1757. // 如果有之前的股票数据且页面已加载,重新渲染图表
  1758. if (isPageLoaded.value && emotionStore.activeStock && emotionStore.activeStock.apiData) {
  1759. nextTick(() => {
  1760. renderCharts(emotionStore.activeStock.apiData);
  1761. console.log('搜索失败,恢复显示之前股票的图表:', emotionStore.activeStock.stockInfo.name);
  1762. });
  1763. }
  1764. // 调用完成回调,重新启用输入框
  1765. if (onComplete && typeof onComplete === 'function') {
  1766. onComplete();
  1767. // 清除保存的回调函数
  1768. currentOnCompleteCallback.value = null;
  1769. }
  1770. return;
  1771. }
  1772. } catch (error) {
  1773. // 请求失败,清理第三步思考过程消息
  1774. if (thinkingMessage3Ref) {
  1775. const index = messages.value.indexOf(thinkingMessage3Ref);
  1776. if (index > -1) {
  1777. messages.value.splice(index, 1);
  1778. }
  1779. }
  1780. // 请求失败时关闭加载状态
  1781. // isLoading.value = false;
  1782. // 如果有之前的股票数据,恢复显示状态;否则设置为false
  1783. if (emotionStore.stockList.length > 0 && emotionStore.activeStock) {
  1784. isPageLoaded.value = true;
  1785. // 使用nextTick确保DOM更新后清空对话显示并启动高度监听器
  1786. nextTick(() => {
  1787. messages.value = [];
  1788. // 启动页面高度监听器,实时监听内容变化并自动滚动
  1789. startHeightObserver();
  1790. // 立即滚动到底部
  1791. scrollToBottom();
  1792. });
  1793. console.log('请求工作流接口失败,但恢复显示之前的股票数据');
  1794. // 立即渲染之前股票的图表,提升用户体验
  1795. nextTick(() => {
  1796. renderCharts(emotionStore.activeStock.apiData);
  1797. console.log('立即恢复显示之前股票的图表:', emotionStore.activeStock.stockInfo.name);
  1798. });
  1799. } else {
  1800. isPageLoaded.value = false;
  1801. }
  1802. const aiMessage = reactive({ sender: 'ai', text: '请求工作流接口失败,请检查网络连接' });
  1803. messages.value.push(aiMessage);
  1804. // 将AI消息添加到emotion store中
  1805. emotionStore.addConversation({
  1806. sender: 'ai',
  1807. text: '请求工作流接口失败,请检查网络连接',
  1808. timestamp: new Date().toISOString()
  1809. });
  1810. // 请求失败时停止图片旋转,恢复历史数据
  1811. isRotating.value = false;
  1812. messages.value = [...previousMessages, ...messages.value];
  1813. // 如果有之前的股票数据且页面已加载,重新渲染图表
  1814. if (isPageLoaded.value && emotionStore.activeStock && emotionStore.activeStock.apiData) {
  1815. nextTick(() => {
  1816. renderCharts(emotionStore.activeStock.apiData);
  1817. console.log('请求失败,恢复显示之前股票的图表:', emotionStore.activeStock.stockInfo.name);
  1818. });
  1819. }
  1820. // 调用完成回调,重新启用输入框
  1821. if (onComplete && typeof onComplete === 'function') {
  1822. onComplete();
  1823. // 清除保存的回调函数
  1824. currentOnCompleteCallback.value = null;
  1825. }
  1826. return;
  1827. } finally {
  1828. // 停止图片旋转(只有在设置了旋转状态时才需要停止)
  1829. if (isRotating.value) {
  1830. isRotating.value = false;
  1831. }
  1832. }
  1833. }
  1834. // 请求数据接口
  1835. async function fetchData(code, market, stockName, queryText, stockId) {
  1836. try {
  1837. const stockDataParams = {
  1838. "stockId": stockId
  1839. };
  1840. const stockDataResult = await axios.post(
  1841. // "http://39.101.133.168:8828/link/api/aiEmotion/client/getAiEmotionData",
  1842. `${APIurl}/api/workflow/getStockData`,
  1843. stockDataParams,
  1844. {
  1845. headers: {
  1846. "Content-Type": "application/json",
  1847. },
  1848. }
  1849. );
  1850. const stockDataResponse = stockDataResult.data; // 获取返回所有的数据
  1851. if (stockDataResponse.code === 200 && stockDataResponse.data) {
  1852. // 检查关键数据字段是否完整
  1853. const validation = validateRequiredFields(stockDataResponse.data);
  1854. // 如果有关键数据缺失,返回失败,不添加到StockTabs
  1855. if (!validation.isValid) {
  1856. console.log('API返回数据不完整,缺失字段:', validation.missingFields);
  1857. // 关闭加载状态
  1858. // isLoading.value = false;
  1859. // 如果有之前的股票数据,恢复显示状态;否则设置为false
  1860. if (emotionStore.stockList.length > 0 && emotionStore.activeStock) {
  1861. isPageLoaded.value = true;
  1862. // 使用nextTick确保DOM更新后清空对话显示并启动高度监听器
  1863. nextTick(() => {
  1864. messages.value = [];
  1865. // 启动页面高度监听器,实时监听内容变化并自动滚动
  1866. startHeightObserver();
  1867. // 立即滚动到底部
  1868. scrollToBottom();
  1869. });
  1870. console.log('数据验证失败,但恢复显示之前的股票数据');
  1871. // 立即渲染之前股票的图表,提升用户体验
  1872. nextTick(() => {
  1873. renderCharts(emotionStore.activeStock.apiData);
  1874. console.log('立即恢复显示之前股票的图表:', emotionStore.activeStock.stockInfo.name);
  1875. });
  1876. } else {
  1877. isPageLoaded.value = false;
  1878. }
  1879. const aiMessage = reactive({
  1880. sender: 'ai',
  1881. text: `数据丢失了,请稍后重试。`
  1882. });
  1883. messages.value.push(aiMessage);
  1884. // 将AI消息添加到emotion store中
  1885. emotionStore.addConversation({
  1886. sender: 'ai',
  1887. text: '数据丢失了,请稍后重试。',
  1888. timestamp: new Date().toISOString()
  1889. });
  1890. return false; // 返回失败标识,不添加股票到标签
  1891. }
  1892. // 只有数据完整时才创建股票数据对象并添加到store
  1893. const stockData = {
  1894. queryText: queryText,
  1895. stockInfo: {
  1896. name: stockName,
  1897. code: code,
  1898. market: market
  1899. },
  1900. apiData: stockDataResponse.data,
  1901. conclusionData: conclusionData.value, // 包含结论数据
  1902. timestamp: new Date().toISOString()
  1903. };
  1904. // 将股票数据添加到store中,显示在StockTabs中
  1905. emotionStore.addStock(stockData);
  1906. return true; // 返回成功标识
  1907. } else {
  1908. // 关闭加载状态
  1909. // isLoading.value = false;
  1910. // 如果有之前的股票数据,恢复显示状态;否则设置为false
  1911. if (emotionStore.stockList.length > 0 && emotionStore.activeStock) {
  1912. isPageLoaded.value = true;
  1913. // 使用nextTick确保DOM更新后清空对话显示并启动高度监听器
  1914. nextTick(() => {
  1915. messages.value = [];
  1916. // 启动页面高度监听器,实时监听内容变化并自动滚动
  1917. startHeightObserver();
  1918. // 立即滚动到底部
  1919. scrollToBottom();
  1920. });
  1921. console.log('API请求失败,但恢复显示之前的股票数据');
  1922. // 立即渲染之前股票的图表,提升用户体验
  1923. nextTick(() => {
  1924. renderCharts(emotionStore.activeStock.apiData);
  1925. console.log('立即恢复显示之前股票的图表:', emotionStore.activeStock.stockInfo.name);
  1926. });
  1927. } else {
  1928. isPageLoaded.value = false;
  1929. }
  1930. const aiMessage = reactive({ sender: 'ai', text: '图表数据请求失败,请检查网络连接' });
  1931. messages.value.push(aiMessage);
  1932. // 将AI消息添加到emotion store中
  1933. emotionStore.addConversation({
  1934. sender: 'ai',
  1935. text: '图表数据请求失败,请检查网络连接',
  1936. timestamp: new Date().toISOString()
  1937. });
  1938. return false; // 返回失败标识
  1939. }
  1940. } catch (error) {
  1941. // 关闭加载状态
  1942. // isLoading.value = false;
  1943. // 如果有之前的股票数据,恢复显示状态;否则设置为false
  1944. if (emotionStore.stockList.length > 0 && emotionStore.activeStock) {
  1945. isPageLoaded.value = true;
  1946. // 使用nextTick确保DOM更新后清空对话显示并启动高度监听器
  1947. nextTick(() => {
  1948. messages.value = [];
  1949. // 启动页面高度监听器,实时监听内容变化并自动滚动
  1950. startHeightObserver();
  1951. // 立即滚动到底部
  1952. scrollToBottom();
  1953. });
  1954. console.log('网络异常,但恢复显示之前的股票数据');
  1955. // 立即渲染之前股票的图表,提升用户体验
  1956. nextTick(() => {
  1957. renderCharts(emotionStore.activeStock.apiData);
  1958. console.log('立即恢复显示之前股票的图表:', emotionStore.activeStock.stockInfo.name);
  1959. });
  1960. } else {
  1961. isPageLoaded.value = false;
  1962. }
  1963. const aiMessage = reactive({ sender: 'ai', text: '图表数据请求失败,请检查网络连接' });
  1964. messages.value.push(aiMessage);
  1965. // 将AI消息添加到emotion store中
  1966. emotionStore.addConversation({
  1967. sender: 'ai',
  1968. text: '图表数据请求失败,请检查网络连接',
  1969. timestamp: new Date().toISOString()
  1970. });
  1971. return false; // 返回失败标识
  1972. }
  1973. }
  1974. // 检查关键数据字段是否完整的函数
  1975. function validateRequiredFields(data) {
  1976. const requiredFields = ['GSWDJ', 'KLine20', 'QXJMQ', 'QXTDLD', 'WDRL'];
  1977. const missingFields = [];
  1978. for (const field of requiredFields) {
  1979. if (!data[field] ||
  1980. (Array.isArray(data[field]) && data[field].length === 0) ||
  1981. (typeof data[field] === 'object' && !hasValidData(data[field]))) {
  1982. missingFields.push(field);
  1983. }
  1984. }
  1985. return {
  1986. isValid: missingFields.length === 0,
  1987. missingFields: missingFields
  1988. };
  1989. }
  1990. // 检查对象是否包含有效数据的辅助函数
  1991. function hasValidData(obj) {
  1992. if (!obj || typeof obj !== 'object') {
  1993. return false;
  1994. }
  1995. // 定义可以为空的数组字段
  1996. const allowedEmptyArrays = ['lowxh', 'qixh', 'topxh'];
  1997. // 检查对象的所有属性值
  1998. for (const key in obj) {
  1999. if (obj.hasOwnProperty(key)) {
  2000. const value = obj[key];
  2001. // 如果是字符串字段
  2002. if (typeof value === 'string') {
  2003. // 字符串字段必须有内容,为空则表示异常
  2004. if (value.trim() !== '') {
  2005. return true;
  2006. }
  2007. }
  2008. // 如果是数组字段
  2009. else if (Array.isArray(value)) {
  2010. // 数组字段可以为空,但如果有内容则表示有效
  2011. if (value.length > 0) {
  2012. return true;
  2013. }
  2014. }
  2015. // 如果是数字且不为0
  2016. else if (typeof value === 'number' && value !== 0) {
  2017. return true;
  2018. }
  2019. // 如果是布尔值且为true
  2020. else if (typeof value === 'boolean' && value === true) {
  2021. return true;
  2022. }
  2023. // 如果是对象且包含有效数据(递归检查)
  2024. else if (typeof value === 'object' && value !== null) {
  2025. if (hasValidData(value)) {
  2026. return true;
  2027. }
  2028. }
  2029. }
  2030. }
  2031. return false;
  2032. }
  2033. // 依次渲染图表的方法 - 支持多个股票
  2034. async function renderChartsSequentially(clonedData, stockIndex = 0) {
  2035. console.log(`开始渲染第${stockIndex}个股票的图表`);
  2036. // 定义图表渲染顺序和配置
  2037. const chartConfigs = [
  2038. {
  2039. name: '股市温度计',
  2040. ref: marketTemperatureRef.value[stockIndex],
  2041. visibility: chartVisibility.value.marketTemperature,
  2042. method: 'initChart',
  2043. params: [clonedData.GSWDJ, clonedData.KLine20, clonedData.WDRL]
  2044. },
  2045. {
  2046. name: '情绪解码器',
  2047. ref: emotionDecodRef.value[stockIndex],
  2048. visibility: chartVisibility.value.emotionDecod,
  2049. method: 'initQXNLZHEcharts',
  2050. params: [clonedData.KLine20, clonedData.QXJMQ]
  2051. },
  2052. {
  2053. name: '情绪探底雷达',
  2054. ref: emotionalBottomRadarRef.value[stockIndex],
  2055. visibility: chartVisibility.value.emotionalBottomRadar,
  2056. method: 'initEmotionalBottomRadar',
  2057. params: [clonedData.KLine20, clonedData.QXTDLD]
  2058. },
  2059. {
  2060. name: '情绪能量转化器',
  2061. ref: emoEnergyConverterRef.value[stockIndex],
  2062. visibility: chartVisibility.value.emoEnergyConverter,
  2063. method: 'initQXNLZHEcharts',
  2064. params: [clonedData.KLine20, clonedData.QXNLZHQ]
  2065. }
  2066. ];
  2067. // 依次渲染每个图表
  2068. for (const config of chartConfigs) {
  2069. if (config.ref && config.visibility) {
  2070. console.log(`开始渲染第${stockIndex}个股票的${config.name}图表`);
  2071. console.log(`${config.name}Ref方法:`, typeof config.ref[config.method]);
  2072. if (typeof config.ref[config.method] === 'function') {
  2073. try {
  2074. // 等待DOM元素完全渲染
  2075. await new Promise(resolve => setTimeout(resolve, 100));
  2076. config.ref[config.method](...config.params);
  2077. console.log(`${stockIndex}个股票的${config.name}图表渲染成功`);
  2078. // 每个图表渲染完成后等待一段时间再渲染下一个
  2079. await new Promise(resolve => setTimeout(resolve, 800));
  2080. } catch (error) {
  2081. console.error(`${stockIndex}个股票的${config.name}图表渲染失败:`, error);
  2082. }
  2083. } else {
  2084. console.error(`${stockIndex}个股票的${config.name}Ref.${config.method} 方法不存在`);
  2085. }
  2086. } else {
  2087. console.log(`${stockIndex}个股票的${config.name}图表未渲染,ref存在:`, !!config.ref, '数据存在:', config.visibility);
  2088. }
  2089. }
  2090. console.log(`${stockIndex}个股票的所有图表依次渲染完成`);
  2091. }
  2092. // 渲染组件图表的方法
  2093. function renderCharts(data) {
  2094. console.log('开始渲染图表,数据:', data);
  2095. // 深拷贝数据避免污染原始数据
  2096. const clonedData = JSON.parse(JSON.stringify(data));
  2097. // 检查关键数据字段是否完整
  2098. const validation = validateRequiredFields(clonedData);
  2099. // 如果有任何关键数据缺失,不渲染页面并返回提示
  2100. if (!validation.isValid) {
  2101. console.log('关键数据缺失:', validation.missingFields);
  2102. const aiMessage = reactive({
  2103. sender: 'ai',
  2104. text: `数据不完整,缺少以下关键数据:${validation.missingFields.join('、')}。请稍后重试或联系客服。`
  2105. });
  2106. messages.value.push(aiMessage);
  2107. // 将AI消息添加到emotion store中
  2108. emotionStore.addConversation({
  2109. sender: 'ai',
  2110. text: `数据不完整,缺少以下关键数据:${validation.missingFields.join('、')}。请稍后重试或联系客服。`,
  2111. timestamp: new Date().toISOString()
  2112. });
  2113. // 隐藏页面内容
  2114. isPageLoaded.value = false;
  2115. // isLoading.value = false;
  2116. return; // 直接返回,不进行后续渲染
  2117. }
  2118. // 先设置图表组件显示状态
  2119. chartVisibility.value = {
  2120. marketTemperature: !!(clonedData.GSWDJ && clonedData.GSWDJ.length > 0),
  2121. emotionDecod: !!(clonedData.QXJMQ && clonedData.QXJMQ.length > 0),
  2122. emotionalBottomRadar: !!(clonedData.QXTDLD && clonedData.QXTDLD.length > 0),
  2123. emoEnergyConverter: !!(clonedData.QXNLZHQ && (Array.isArray(clonedData.QXNLZHQ) ? clonedData.QXNLZHQ.length > 0 : hasValidData(clonedData.QXNLZHQ)))
  2124. };
  2125. console.log('图表显示状态:', chartVisibility.value);
  2126. console.log('数据检查:', {
  2127. GSWDJ: !!(clonedData.GSWDJ && clonedData.GSWDJ.length > 0),
  2128. QXJMQ: !!(clonedData.QXJMQ && clonedData.QXJMQ.length > 0),
  2129. QXTDLD: !!(clonedData.QXTDLD && clonedData.QXTDLD.length > 0),
  2130. QXNLZHQ: !!(clonedData.QXNLZHQ && (Array.isArray(clonedData.QXNLZHQ) ? clonedData.QXNLZHQ.length > 0 : hasValidData(clonedData.QXNLZHQ)))
  2131. });
  2132. console.log('QXNLZHQ数据详情:', clonedData.QXNLZHQ);
  2133. nextTick(() => {
  2134. // 增加延迟确保DOM完全更新和组件完全挂载
  2135. setTimeout(() => {
  2136. try {
  2137. console.log('图表组件ref状态:', {
  2138. marketTemperatureRef: !!marketTemperatureRef.value,
  2139. emotionDecodRef: !!emotionDecodRef.value,
  2140. emotionalBottomRadarRef: !!emotionalBottomRadarRef.value,
  2141. emoEnergyConverterRef: !!emoEnergyConverterRef.value
  2142. });
  2143. // 检查DOM元素是否存在
  2144. console.log('DOM元素检查:', {
  2145. marketTemperatureDOM: !!document.querySelector('.market-temperature-section'),
  2146. emotionDecodDOM: !!document.querySelector('.emotion-decoder-section'),
  2147. emotionalBottomRadarDOM: !!document.querySelector('.bottom-radar-section'),
  2148. emoEnergyConverterDOM: !!document.querySelector('.energy-converter-section')
  2149. });
  2150. // 检查具体的组件元素
  2151. const emoEnergyElement = document.querySelector('emo-energy-converter');
  2152. console.log('emoEnergyConverter元素:', emoEnergyElement);
  2153. // 等待更长时间再次检查ref
  2154. setTimeout(() => {
  2155. console.log('延迟检查emoEnergyConverterRef:', !!emoEnergyConverterRef.value);
  2156. if (emoEnergyConverterRef.value) {
  2157. console.log('emoEnergyConverter方法:', typeof emoEnergyConverterRef.value.initQXNLZHEcharts);
  2158. }
  2159. }, 1000);
  2160. // 开始依次渲染图表 - 为每个股票渲染
  2161. emotionStore.stockList.forEach((stock, index) => {
  2162. if (stock.apiData) {
  2163. renderChartsSequentially(stock.apiData, index);
  2164. }
  2165. });
  2166. console.log('图表渲染完成');
  2167. } catch (error) {
  2168. console.error('图表渲染错误:', error);
  2169. const aiMessage = reactive({ sender: 'ai', text: '图表渲染失败,请重试' });
  2170. messages.value.push(aiMessage);
  2171. // 将AI消息添加到emotion store中
  2172. emotionStore.addConversation({
  2173. sender: 'ai',
  2174. text: '图表渲染失败,请重试',
  2175. timestamp: new Date().toISOString()
  2176. });
  2177. }
  2178. }, 500); // 增加延迟到500ms确保DOM和组件完全稳定
  2179. });
  2180. }
  2181. // scrollToBottom函数已移除
  2182. // 处理用户滚动事件(用于其他滚动相关功能)
  2183. const handleUserScroll = () => {
  2184. // 用户滚动事件处理逻辑已简化,因为自动滚动功能已移除
  2185. };
  2186. // 处理滚轮事件
  2187. const handleWheel = (event) => {
  2188. handleUserScroll();
  2189. };
  2190. // 处理触摸滚动事件
  2191. const handleTouchMove = (event) => {
  2192. handleUserScroll();
  2193. };
  2194. // 检查数据是否已加载完成
  2195. function isDataLoaded() {
  2196. // 检查页面是否已加载
  2197. if (!isPageLoaded.value) {
  2198. console.log('页面数据尚未加载完成');
  2199. return false;
  2200. }
  2201. // 检查当前股票数据是否存在
  2202. if (!currentStock.value || !currentStock.value.apiData) {
  2203. console.log('股票数据尚未加载完成');
  2204. return false;
  2205. }
  2206. // 检查图表组件是否已渲染 - 检查所有股票的组件
  2207. const stockCount = emotionStore.stockList.length;
  2208. if (stockCount === 0) {
  2209. console.log('没有股票数据');
  2210. return false;
  2211. }
  2212. // 检查每个股票的图表组件是否都已加载
  2213. for (let i = 0; i < stockCount; i++) {
  2214. const requiredRefs = [
  2215. marketTemperatureRef.value[i],
  2216. emotionDecodRef.value[i],
  2217. emotionalBottomRadarRef.value[i],
  2218. emoEnergyConverterRef.value[i]
  2219. ];
  2220. const allRefsLoaded = requiredRefs.every(ref => ref !== null);
  2221. if (!allRefsLoaded) {
  2222. console.log(`${i}个股票的图表组件尚未完全加载`);
  2223. return false;
  2224. }
  2225. }
  2226. console.log('所有数据和组件已加载完成');
  2227. return true;
  2228. }
  2229. // 自动滚动函数已移除
  2230. // 设置Intersection Observer监听场景应用部分
  2231. function setupIntersectionObserver() {
  2232. // 检查 scenarioApplicationRef.value 是否存在且是有效的 DOM 元素
  2233. if (!scenarioApplicationRef.value || !(scenarioApplicationRef.value instanceof Element)) {
  2234. console.warn('scenarioApplicationRef.value 不是有效的 DOM 元素,跳过 IntersectionObserver 设置');
  2235. return;
  2236. }
  2237. const observer = new IntersectionObserver(
  2238. (entries) => {
  2239. entries.forEach((entry) => {
  2240. if (entry.isIntersecting) {
  2241. console.log('场景应用部分进入视口');
  2242. // 获取当前股票代码
  2243. const stockCode = currentStock.value?.stockInfo?.code || currentStock.value?.stockInfo?.symbol;
  2244. if (parsedConclusion.value && stockCode) {
  2245. // 检查该股票是否是第一次触发
  2246. if (!stockTypewriterShown.value.has(stockCode)) {
  2247. // 如果是用户主动搜索,启动打字机效果和音频播放
  2248. if (isUserInitiated.value && audioUrl.value) {
  2249. console.log('用户主动搜索,该股票第一次进入场景应用,开始打字机效果和音频播放');
  2250. if (!stockAudioPlayed.value.has(stockCode)) {
  2251. console.log('开始音频播放和打字机效果');
  2252. stockAudioPlayed.value.set(stockCode, true);
  2253. playAudioQueue(parsedConclusion.value, true);
  2254. } else {
  2255. // 如果音频已播放过,只启动打字机效果
  2256. startTypewriterEffect(parsedConclusion.value);
  2257. }
  2258. stockTypewriterShown.value.set(stockCode, true);
  2259. } else {
  2260. // 非用户主动搜索(如历史记录恢复),直接显示完整内容
  2261. console.log('非用户主动搜索,该股票第一次进入场景应用,直接显示完整内容');
  2262. const conclusion = parsedConclusion.value;
  2263. displayedTexts.value = {
  2264. one1: conclusion.one1 || '',
  2265. one2: conclusion.one2 || '',
  2266. two: conclusion.two || '',
  2267. three: conclusion.three || '',
  2268. four: conclusion.four || '',
  2269. disclaimer: '该内容由AI生成,请注意甄别'
  2270. };
  2271. displayedTitles.value = {
  2272. one: 'L1: 情绪监控',
  2273. two: 'L2: 情绪解码',
  2274. three: 'L3: 情绪推演',
  2275. four: 'L4: 情绪套利'
  2276. };
  2277. // 显示所有有内容的模块
  2278. moduleVisibility.value = {
  2279. one: !!(conclusion.one1 || conclusion.one2),
  2280. two: !!conclusion.two,
  2281. three: !!conclusion.three,
  2282. four: !!conclusion.four,
  2283. disclaimer: true
  2284. };
  2285. // 记录该股票已显示过
  2286. stockTypewriterShown.value.set(stockCode, true);
  2287. stockAudioPlayed.value.set(stockCode, true);
  2288. }
  2289. } else {
  2290. // 非第一次或已经触发过:直接显示完整内容,不播放音频和打字机效果
  2291. console.log('非第一次进入场景应用或已触发过,直接显示完整内容');
  2292. // 直接显示完整内容
  2293. const conclusion = parsedConclusion.value;
  2294. displayedTexts.value = {
  2295. one1: conclusion.one1 || '',
  2296. one2: conclusion.one2 || '',
  2297. two: conclusion.two || '',
  2298. three: conclusion.three || '',
  2299. four: conclusion.four || '',
  2300. disclaimer: '该内容由AI生成,请注意甄别'
  2301. };
  2302. displayedTitles.value = {
  2303. one: 'L1: 情绪监控',
  2304. two: 'L2: 情绪解码',
  2305. three: 'L3: 情绪推演',
  2306. four: 'L4: 情绪套利'
  2307. };
  2308. // 显示所有有内容的模块
  2309. moduleVisibility.value = {
  2310. one: !!(conclusion.one1 || conclusion.one2),
  2311. two: !!conclusion.two,
  2312. three: !!conclusion.three,
  2313. four: !!conclusion.four,
  2314. disclaimer: true
  2315. };
  2316. }
  2317. }
  2318. }
  2319. });
  2320. },
  2321. {
  2322. threshold: 0.3, // 当30%的元素进入视口时触发
  2323. rootMargin: '0px 0px -100px 0px' // 提前100px触发
  2324. }
  2325. );
  2326. observer.observe(scenarioApplicationRef.value);
  2327. intersectionObserver.value = observer;
  2328. }
  2329. // 手动触发自动滚动函数已移除
  2330. // 返回顶部功能
  2331. const scrollToTop = () => {
  2332. const topAnchor = document.getElementById('top-anchor');
  2333. if (topAnchor) {
  2334. topAnchor.scrollIntoView({
  2335. behavior: 'smooth',
  2336. block: 'start',
  2337. inline: 'nearest'
  2338. });
  2339. } else {
  2340. window.scrollTo({ top: 0, behavior: 'smooth' });
  2341. }
  2342. // 备用方案:直接滚动到页面顶部
  2343. setTimeout(() => {
  2344. const currentScrollTop = window.pageYOffset || document.documentElement.scrollTop;
  2345. if (currentScrollTop > 50) {
  2346. document.documentElement.scrollTop = 0;
  2347. document.body.scrollTop = 0;
  2348. }
  2349. }, 1000);
  2350. };
  2351. // 页面高度监听器
  2352. const heightObserver = ref(null);
  2353. const isAutoScrollEnabled = ref(false);
  2354. // 滚动到底部功能
  2355. const scrollToBottom = () => {
  2356. // 使用nextTick确保DOM已更新
  2357. nextTick(() => {
  2358. // 获取页面的总高度
  2359. const documentHeight = Math.max(
  2360. document.body.scrollHeight,
  2361. document.body.offsetHeight,
  2362. document.documentElement.clientHeight,
  2363. document.documentElement.scrollHeight,
  2364. document.documentElement.offsetHeight
  2365. );
  2366. // 平滑滚动到页面底部
  2367. window.scrollTo({
  2368. top: documentHeight,
  2369. behavior: 'smooth'
  2370. });
  2371. // 备用方案:直接设置滚动位置
  2372. setTimeout(() => {
  2373. document.documentElement.scrollTop = documentHeight;
  2374. document.body.scrollTop = documentHeight;
  2375. }, 1000);
  2376. });
  2377. };
  2378. // 防抖滚动函数
  2379. const debouncedScrollToBottom = (() => {
  2380. let timeoutId = null;
  2381. return () => {
  2382. if (timeoutId) {
  2383. clearTimeout(timeoutId);
  2384. }
  2385. timeoutId = setTimeout(() => {
  2386. if (isAutoScrollEnabled.value && isPageLoaded.value) {
  2387. scrollToBottom();
  2388. }
  2389. }, 150);
  2390. };
  2391. })();
  2392. // 启动页面高度监听器
  2393. const startHeightObserver = () => {
  2394. // 先停止之前的监听器
  2395. stopHeightObserver();
  2396. isAutoScrollEnabled.value = true;
  2397. // 创建ResizeObserver监听页面内容变化
  2398. heightObserver.value = new ResizeObserver((entries) => {
  2399. if (isAutoScrollEnabled.value && isPageLoaded.value) {
  2400. debouncedScrollToBottom();
  2401. }
  2402. });
  2403. // 监听document.body的尺寸变化
  2404. if (document.body) {
  2405. heightObserver.value.observe(document.body);
  2406. }
  2407. // 创建MutationObserver监听DOM结构变化
  2408. const mutationObserver = new MutationObserver((mutations) => {
  2409. let shouldScroll = false;
  2410. mutations.forEach((mutation) => {
  2411. if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
  2412. // 检查新增的节点是否包含实际内容
  2413. const hasContent = Array.from(mutation.addedNodes).some(node => {
  2414. if (node.nodeType === Node.ELEMENT_NODE) {
  2415. return node.offsetHeight > 0 || node.scrollHeight > 0;
  2416. }
  2417. return node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0;
  2418. });
  2419. if (hasContent) {
  2420. shouldScroll = true;
  2421. }
  2422. }
  2423. });
  2424. if (shouldScroll && isAutoScrollEnabled.value && isPageLoaded.value) {
  2425. debouncedScrollToBottom();
  2426. }
  2427. });
  2428. // 监听主要内容区域的DOM变化
  2429. const mainContainer = document.querySelector('.main') || document.body;
  2430. if (mainContainer) {
  2431. mutationObserver.observe(mainContainer, {
  2432. childList: true,
  2433. subtree: true,
  2434. attributes: false,
  2435. characterData: true
  2436. });
  2437. }
  2438. // 保存mutationObserver引用以便清理
  2439. heightObserver.value.mutationObserver = mutationObserver;
  2440. console.log('页面高度监听器已启动');
  2441. };
  2442. // 停止页面高度监听器
  2443. const stopHeightObserver = () => {
  2444. isAutoScrollEnabled.value = false;
  2445. if (heightObserver.value) {
  2446. // 清理ResizeObserver
  2447. heightObserver.value.disconnect();
  2448. // 清理MutationObserver
  2449. if (heightObserver.value.mutationObserver) {
  2450. heightObserver.value.mutationObserver.disconnect();
  2451. heightObserver.value.mutationObserver = null;
  2452. }
  2453. heightObserver.value = null;
  2454. }
  2455. console.log('页面高度监听器已停止');
  2456. };
  2457. // 监听页面滚动,控制返回顶部按钮显示
  2458. const handlePageScroll = () => {
  2459. const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
  2460. showBackToTop.value = scrollTop > 200;
  2461. };
  2462. // 监听容器滚动(备用方案)
  2463. const handleContainerScroll = () => {
  2464. const container = userInputDisplayRef.value;
  2465. if (container) {
  2466. const scrollTop = container.scrollTop;
  2467. if (scrollTop > 200) {
  2468. showBackToTop.value = true;
  2469. }
  2470. }
  2471. };
  2472. // 页面挂载完成后触发图片旋转和设置滚动监听
  2473. onMounted(async () => {
  2474. // 恢复对话记录
  2475. loadConversationsFromStore();
  2476. // 从stockList加载对话记录
  2477. loadConversationsFromStockList();
  2478. // 确保获取用户次数
  2479. // try {
  2480. // await chatStore.getUserCount();
  2481. // console.log('情绪大模型页面:用户次数获取成功');
  2482. // } catch (error) {
  2483. // console.error('情绪大模型页面:获取用户次数失败', error);
  2484. // }
  2485. // 添加全局resize监听器,确保所有图表和容器响应页面宽度变化
  2486. const globalResizeHandler = debounce(() => {
  2487. console.log('AiEmotion页面:窗口大小变化,触发容器和图表resize');
  2488. // 强制重新计算容器布局
  2489. const mainContainer = document.querySelector('.main');
  2490. if (mainContainer) {
  2491. // 触发重排,确保容器尺寸正确更新
  2492. mainContainer.style.display = 'none';
  2493. mainContainer.offsetHeight; // 强制重排
  2494. mainContainer.style.display = '';
  2495. }
  2496. // 触发所有图表组件的resize
  2497. const resizeHandlers = [
  2498. window.emoEnergyConverterResizeHandler,
  2499. window.marketTempResizeHandler,
  2500. window.emotionalBottomRadarResizeHandler,
  2501. window.emotionDecodResizeHandler
  2502. ];
  2503. resizeHandlers.forEach(handler => {
  2504. if (typeof handler === 'function') {
  2505. try {
  2506. handler();
  2507. } catch (error) {
  2508. console.error('AiEmotion页面:图表resize失败', error);
  2509. }
  2510. }
  2511. });
  2512. // 延迟再次触发图表resize,确保容器尺寸稳定后图表能正确适配
  2513. setTimeout(() => {
  2514. resizeHandlers.forEach(handler => {
  2515. if (typeof handler === 'function') {
  2516. try {
  2517. handler();
  2518. } catch (error) {
  2519. console.error('AiEmotion页面:延迟图表resize失败', error);
  2520. }
  2521. }
  2522. });
  2523. }, 100);
  2524. }, 150); // 150ms防抖延迟
  2525. // 移除之前的监听器(如果存在)
  2526. if (window.aiEmotionGlobalResizeHandler) {
  2527. window.removeEventListener('resize', window.aiEmotionGlobalResizeHandler);
  2528. }
  2529. // 添加新的监听器
  2530. window.addEventListener('resize', globalResizeHandler);
  2531. window.aiEmotionGlobalResizeHandler = globalResizeHandler;
  2532. // 添加滚动事件监听器
  2533. const container = userInputDisplayRef.value;
  2534. if (container) {
  2535. container.addEventListener('wheel', handleWheel, { passive: true });
  2536. container.addEventListener('touchmove', handleTouchMove, { passive: true });
  2537. container.addEventListener('scroll', handleUserScroll, { passive: true });
  2538. // 添加容器滚动监听器用于返回顶部按钮
  2539. container.addEventListener('scroll', handleContainerScroll, { passive: true });
  2540. }
  2541. // 添加页面滚动监听器,控制返回顶部按钮显示
  2542. window.addEventListener('scroll', handlePageScroll, { passive: true });
  2543. // 添加document滚动监听器(备用方案)
  2544. document.addEventListener('scroll', handlePageScroll, { passive: true });
  2545. // 防抖函数定义
  2546. function debounce(func, wait) {
  2547. let timeout;
  2548. return function executedFunction(...args) {
  2549. const later = () => {
  2550. clearTimeout(timeout);
  2551. func(...args);
  2552. };
  2553. clearTimeout(timeout);
  2554. timeout = setTimeout(later, wait);
  2555. };
  2556. }
  2557. startImageRotation();
  2558. // 检查是否有已保存的股票数据需要恢复
  2559. if (emotionStore.stockList.length > 0 && emotionStore.activeStock) {
  2560. console.log('检测到已保存的股票数据,开始恢复页面状态(不自动播放音频)');
  2561. // 恢复页面加载状态
  2562. isPageLoaded.value = true;
  2563. // 使用nextTick确保DOM更新后清空对话显示并启动高度监听器
  2564. nextTick(() => {
  2565. messages.value = [];
  2566. // 启动页面高度监听器,实时监听内容变化并自动滚动
  2567. startHeightObserver();
  2568. // 立即滚动到底部
  2569. scrollToBottom();
  2570. });
  2571. // 等待DOM渲染后恢复图表和数据
  2572. nextTick(() => {
  2573. const currentStockData = emotionStore.activeStock;
  2574. if (currentStockData && currentStockData.apiData) {
  2575. console.log('恢复图表数据:', currentStockData.stockInfo.name);
  2576. renderCharts(currentStockData.apiData);
  2577. // 恢复结论数据并显示内容
  2578. if (currentStockData.conclusionData) {
  2579. conclusionData.value = currentStockData.conclusionData;
  2580. // 直接显示所有内容,不使用打字机效果
  2581. const conclusion = currentStockData.conclusionData;
  2582. displayedTexts.value = {
  2583. one1: conclusion.one1 || '',
  2584. one2: conclusion.one2 || '',
  2585. two: conclusion.two || '',
  2586. three: conclusion.three || '',
  2587. four: conclusion.four || '',
  2588. disclaimer: '该内容由AI生成,请注意甄别'
  2589. };
  2590. displayedTitles.value = {
  2591. one: conclusion.one1 || conclusion.one2 ? 'L1: 情绪监控' : '',
  2592. two: conclusion.two ? 'L2: 情绪解码' : '',
  2593. three: conclusion.three ? 'L3: 情绪推演' : '',
  2594. four: conclusion.four ? 'L4: 情绪套利' : ''
  2595. };
  2596. moduleVisibility.value = {
  2597. one: !!(conclusion.one1 || conclusion.one2),
  2598. two: !!conclusion.two,
  2599. three: !!conclusion.three,
  2600. four: !!conclusion.four,
  2601. disclaimer: true
  2602. };
  2603. // 标记该股票的打字机效果和音频已经显示过,避免后续自动触发
  2604. const stockCode = currentStockData.stockInfo?.code || currentStockData.stockInfo?.symbol;
  2605. if (stockCode) {
  2606. stockTypewriterShown.value.set(stockCode, true);
  2607. stockAudioPlayed.value.set(stockCode, true);
  2608. }
  2609. }
  2610. }
  2611. setupIntersectionObserver();
  2612. });
  2613. } else {
  2614. // 没有保存的数据,正常设置监听器
  2615. nextTick(() => {
  2616. setupIntersectionObserver();
  2617. });
  2618. }
  2619. });
  2620. // 组件卸载时清理定时器、音频和observer
  2621. onUnmounted(() => {
  2622. clearTypewriterTimers();
  2623. // 如果有未完成的回调函数,调用它来重新启用输入框
  2624. if (currentOnCompleteCallback.value && typeof currentOnCompleteCallback.value === 'function') {
  2625. currentOnCompleteCallback.value();
  2626. currentOnCompleteCallback.value = null;
  2627. }
  2628. stopAudio();
  2629. // 重置触发状态
  2630. hasTriggeredAudio.value = false;
  2631. hasTriggeredTypewriter.value = false;
  2632. // 清理页面高度监听器
  2633. stopHeightObserver();
  2634. // 清理Intersection Observer
  2635. if (intersectionObserver.value) {
  2636. intersectionObserver.value.disconnect();
  2637. intersectionObserver.value = null;
  2638. }
  2639. // 清理全局resize监听器
  2640. if (window.aiEmotionGlobalResizeHandler) {
  2641. window.removeEventListener('resize', window.aiEmotionGlobalResizeHandler);
  2642. window.aiEmotionGlobalResizeHandler = null;
  2643. }
  2644. // 清理滚动事件监听器
  2645. const container = userInputDisplayRef.value;
  2646. if (container) {
  2647. container.removeEventListener('wheel', handleWheel);
  2648. container.removeEventListener('touchmove', handleTouchMove);
  2649. container.removeEventListener('scroll', handleUserScroll);
  2650. container.removeEventListener('scroll', handleContainerScroll);
  2651. }
  2652. // 清理页面滚动监听器
  2653. window.removeEventListener('scroll', handlePageScroll);
  2654. document.removeEventListener('scroll', handlePageScroll);
  2655. // 滚动相关清理已简化
  2656. });
  2657. // 声明组件可以触发的事件
  2658. const emit = defineEmits(['updateMessage', 'sendMessage', 'ensureAIchat']);
  2659. // 导出方法供外部使用(已在上方定义)
  2660. </script>
  2661. <style scoped>
  2662. .matrix-header {
  2663. width: 100%;
  2664. display: flex;
  2665. align-items: center;
  2666. }
  2667. .disclaimer-item p {
  2668. color: #ffffff !important;
  2669. font-size: 24px;
  2670. font-weight: bold;
  2671. }
  2672. .temperature-content {
  2673. padding-top: 8%;
  2674. display: flex;
  2675. align-items: center;
  2676. justify-content: center;
  2677. gap: 10rem;
  2678. }
  2679. .temperature-content .content1 {
  2680. display: flex;
  2681. flex-direction: column;
  2682. align-items: center;
  2683. }
  2684. .temperature-content .content1 img {
  2685. scale: 0.5;
  2686. }
  2687. .temperature-display {
  2688. display: flex;
  2689. flex-direction: column;
  2690. /* 竖向排列元素 */
  2691. /* margin-left: 15%; */
  2692. gap: 30px;
  2693. /* margin-top: -12%; */
  2694. /* width: 100%; */
  2695. /* height: auto; */
  2696. }
  2697. .temperature-display::after {
  2698. content: "";
  2699. display: table;
  2700. clear: both;
  2701. }
  2702. .temperature-content .temperature-hot {
  2703. background-image: url('@/assets/img/AiEmotion/redBorder.png');
  2704. background-repeat: no-repeat;
  2705. background-size: 100% 100%;
  2706. /* width: 50%; */
  2707. width: 22vw;
  2708. max-width: 400px;
  2709. min-width: 200px;
  2710. text-align: center;
  2711. font-size: 24px;
  2712. color: white;
  2713. display: flex;
  2714. justify-content: center;
  2715. align-items: center;
  2716. flex-shrink: 0;
  2717. }
  2718. .temperature-content .temperature-cold {
  2719. background-image: url('@/assets/img/AiEmotion/blueBorder.png');
  2720. background-repeat: no-repeat;
  2721. background-size: 100% 100%;
  2722. /* width: 35%; */
  2723. width: 22vw;
  2724. max-width: 400px;
  2725. min-width: 200px;
  2726. text-align: center;
  2727. font-size: 24px;
  2728. color: white;
  2729. display: flex;
  2730. justify-content: center;
  2731. align-items: center;
  2732. flex-shrink: 0;
  2733. }
  2734. .golden-wheel {
  2735. width: 100%;
  2736. display: flex;
  2737. justify-content: center;
  2738. }
  2739. .golden-wheel-img {
  2740. width: 60%;
  2741. max-width: 500px;
  2742. height: auto;
  2743. }
  2744. /* 定义旋转动画 */
  2745. @keyframes rotate {
  2746. from {
  2747. transform: rotate(0deg);
  2748. }
  2749. to {
  2750. transform: rotate(360deg);
  2751. }
  2752. }
  2753. /* 应用动画到图片 */
  2754. .rotating-image {
  2755. animation: rotate 5s linear;
  2756. /* 5 秒完成一次旋转,线性速度*/
  2757. will-change: transform;
  2758. /* 优化动画性能 */
  2759. }
  2760. .bk-image {
  2761. background-image: url("@/assets/img/AiEmotion/bk00000.png");
  2762. background-size: 100% 100%;
  2763. background-repeat: no-repeat;
  2764. width: 50vw;
  2765. height: auto;
  2766. margin: 0 auto;
  2767. margin-top: 20px;
  2768. }
  2769. .bk-image .conclusion-container {
  2770. padding: 20px;
  2771. border-radius: 15px;
  2772. margin: 20px;
  2773. }
  2774. .bk-image .conclusion-container .conclusion-item {
  2775. border-radius: 12px;
  2776. transition: all 0.3s ease;
  2777. overflow: hidden;
  2778. }
  2779. .bk-image .conclusion-container .conclusion-item:last-child {
  2780. margin-bottom: 0;
  2781. }
  2782. .bk-image .conclusion-container .conclusion-item .conclusion-title {
  2783. color: #FFD700;
  2784. font-size: 22px;
  2785. font-weight: bold;
  2786. margin: 0 0 15px 0;
  2787. text-align: center;
  2788. letter-spacing: 2px;
  2789. margin-top: 22px;
  2790. }
  2791. .bk-image .conclusion-container .conclusion-item .conclusion-text {
  2792. color: #ffffff;
  2793. font-size: 20px;
  2794. line-height: 1.8;
  2795. margin: 0 0 12px 0;
  2796. text-align: center;
  2797. word-wrap: break-word;
  2798. position: relative;
  2799. }
  2800. .bk-image .conclusion-container .conclusion-item .conclusion-text:last-child {
  2801. margin-bottom: 0;
  2802. }
  2803. .bk-image .conclusion-placeholder {
  2804. padding: 30px;
  2805. text-align: center;
  2806. border-radius: 12px;
  2807. }
  2808. .bk-image .conclusion-placeholder p {
  2809. color: #999999;
  2810. font-size: 16px;
  2811. margin: 0;
  2812. font-style: italic;
  2813. }
  2814. /* 最后文字的颜色 */
  2815. .text-container {
  2816. position: relative;
  2817. color: white;
  2818. text-align: left;
  2819. padding: 20px;
  2820. border-radius: 15px;
  2821. }
  2822. /* .text-container p {
  2823. margin: 0 auto;
  2824. font-size: 40px;
  2825. margin-left: 0%;
  2826. border-radius: 12px;
  2827. transition: all 0.3s ease;
  2828. overflow: hidden;
  2829. letter-spacing: 2px;
  2830. } */
  2831. .text-container .title {
  2832. display: block;
  2833. color: #FFD700;
  2834. font-weight: bold;
  2835. margin-top: 0px;
  2836. margin-bottom: 20px;
  2837. font-size: 22px;
  2838. text-align: center;
  2839. }
  2840. .text-container .content {
  2841. display: block;
  2842. color: white;
  2843. text-align: center;
  2844. font-size: 22px;
  2845. }
  2846. .core-logic-section {
  2847. background-image: url("@/assets/img/AiEmotion/bk00000.png");
  2848. background-size: cover;
  2849. background-repeat: no-repeat;
  2850. width: 95%;
  2851. height: auto;
  2852. min-height: 70rem;
  2853. margin: 0 auto;
  2854. }
  2855. .core-logic-header {
  2856. margin: 0 auto;
  2857. width: fit-content;
  2858. margin-top: 2%;
  2859. margin-bottom: 1%;
  2860. }
  2861. .core-highlights-header {
  2862. margin: 0 auto;
  2863. /* width: fit-content; */
  2864. margin-top: 2%;
  2865. margin-bottom: 1%;
  2866. display: flex;
  2867. align-items: center;
  2868. justify-content: center;
  2869. }
  2870. .emotion-decoder-content {
  2871. /* width: 80vw; */
  2872. margin: 0 auto;
  2873. }
  2874. .energy-converter-content {
  2875. min-width: 100%;
  2876. margin-top: 3%;
  2877. }
  2878. .bottom-radar-header {
  2879. display: flex;
  2880. justify-content: center;
  2881. align-items: center;
  2882. flex-direction: column;
  2883. }
  2884. .bottom-radar-header img {
  2885. scale: 0.5;
  2886. }
  2887. .energy-converter-header {
  2888. display: flex;
  2889. justify-content: center;
  2890. align-items: center;
  2891. flex-direction: column;
  2892. }
  2893. .energy-converter-header img {
  2894. scale: 0.5;
  2895. }
  2896. .bottom-radar-icon {
  2897. width: fit-content;
  2898. height: auto;
  2899. margin: 0 auto;
  2900. margin-top: 2%;
  2901. margin-bottom: 1%;
  2902. display: flex;
  2903. align-items: center;
  2904. justify-content: center;
  2905. }
  2906. .energy-converter-icon {
  2907. width: fit-content;
  2908. height: auto;
  2909. margin: 0 auto;
  2910. margin-top: 2%;
  2911. margin-bottom: 1%;
  2912. display: flex;
  2913. align-items: center;
  2914. justify-content: center;
  2915. }
  2916. .core-logic-content {
  2917. margin: 0 auto;
  2918. width: fit-content;
  2919. }
  2920. .market-temperature-icon {
  2921. width: fit-content;
  2922. height: auto;
  2923. margin: 0 auto;
  2924. margin-top: 2%;
  2925. margin-bottom: 1%;
  2926. display: flex;
  2927. align-items: center;
  2928. justify-content: center;
  2929. }
  2930. .bottom-radar-title {
  2931. margin: 0 auto;
  2932. width: fit-content;
  2933. margin-top: 2%;
  2934. margin-bottom: 1%;
  2935. }
  2936. .emotion-decoder-title {
  2937. margin: 0 auto;
  2938. width: fit-content;
  2939. margin-top: 2%;
  2940. margin-bottom: 1%;
  2941. }
  2942. .temperature-title {
  2943. margin: 0 auto;
  2944. width: fit-content;
  2945. margin-top: 2%;
  2946. margin-bottom: 1%;
  2947. }
  2948. .matrix-title {
  2949. margin: 0 auto;
  2950. width: fit-content;
  2951. margin-bottom: 1%;
  2952. }
  2953. .emotion-decoder-header {
  2954. display: flex;
  2955. flex-direction: column;
  2956. justify-content: center;
  2957. align-items: center;
  2958. }
  2959. .emotion-decoder-header img {
  2960. scale: 0.5;
  2961. }
  2962. .emotion-decoder-icon {
  2963. width: fit-content;
  2964. height: auto;
  2965. margin: 0 auto;
  2966. margin-top: 2%;
  2967. margin-bottom: 1%;
  2968. display: flex;
  2969. align-items: center;
  2970. justify-content: center;
  2971. }
  2972. .emotion-decoder-text {
  2973. color: white;
  2974. font-size: 20px;
  2975. font-weight: bold;
  2976. /* margin-left: 45%; */
  2977. }
  2978. .bottom-radar-text {
  2979. color: white;
  2980. font-size: 20px;
  2981. font-weight: bold;
  2982. /* margin-left: 44.6%; */
  2983. }
  2984. .energy-converter-text {
  2985. color: white;
  2986. font-size: 20px;
  2987. font-weight: bold;
  2988. /* margin-left: 44%; */
  2989. }
  2990. .scenario-application-section {
  2991. text-align: center;
  2992. margin-top: 2%;
  2993. margin-bottom: 1%;
  2994. }
  2995. /* 为需要放大的图片添加样式 */
  2996. .scaled-img {
  2997. background-image: url('@/assets/img/AiEmotion/tree00000.jpg');
  2998. background-size: 100% 100%;
  2999. background-position: center;
  3000. background-repeat: no-repeat;
  3001. width: 80%;
  3002. height: 350px;
  3003. margin: 0 auto;
  3004. margin-top: 2%;
  3005. margin-bottom: 3%;
  3006. }
  3007. .lz-img {
  3008. text-align: center;
  3009. padding-top: 30px;
  3010. }
  3011. .decision-tree-section {
  3012. background-image: url("@/assets/img/AiEmotion/bk00000.png");
  3013. background-size: 100% 100%;
  3014. background-repeat: no-repeat;
  3015. width: 50vw;
  3016. height: auto;
  3017. min-height: 30rem;
  3018. margin: 0 auto;
  3019. }
  3020. .energy-converter-section {
  3021. background-image: url('@/assets/img/AiEmotion/bk00000.png');
  3022. background-size: 100% 100%;
  3023. background-repeat: no-repeat;
  3024. width: 50vw;
  3025. height: auto;
  3026. margin: 0 auto;
  3027. box-sizing: border-box;
  3028. transition: all 0.3s ease;
  3029. min-height: 85vh;
  3030. }
  3031. .bottom-radar-section {
  3032. background-image: url("@/assets/img/AiEmotion/bk00000.png");
  3033. background-size: 100% 100%;
  3034. background-repeat: no-repeat;
  3035. width: 50vw;
  3036. max-width: 100%;
  3037. height: auto;
  3038. margin: 0 auto;
  3039. box-sizing: border-box;
  3040. transition: all 0.3s ease;
  3041. }
  3042. .emotion-decoder-section {
  3043. background-image: url('@/assets/img/AiEmotion/bk00000.png');
  3044. background-size: 100% 100%;
  3045. background-repeat: no-repeat;
  3046. width: 50vw;
  3047. max-width: 100%;
  3048. height: auto;
  3049. margin: 0 auto;
  3050. box-sizing: border-box;
  3051. transition: all 0.3s ease;
  3052. padding-bottom: 1rem;
  3053. }
  3054. .market-temperature-section {
  3055. background-image: url('@/assets/img/AiEmotion/bk00000.png');
  3056. background-size: 100% 100%;
  3057. background-repeat: no-repeat;
  3058. width: 50vw;
  3059. max-width: 100%;
  3060. height: auto;
  3061. min-height: 70rem;
  3062. margin: 0 auto;
  3063. box-sizing: border-box;
  3064. transition: all 0.3s ease;
  3065. }
  3066. .main-content-wrapper {
  3067. background-size: 100% 100%;
  3068. background-repeat: no-repeat;
  3069. width: 77vw;
  3070. max-width: 100%;
  3071. height: auto;
  3072. margin: 0 auto;
  3073. box-sizing: border-box;
  3074. transition: all 0.3s ease;
  3075. }
  3076. .matrix-content {
  3077. display: flex;
  3078. flex-direction: column;
  3079. align-items: center;
  3080. /* 竖向排列元素 */
  3081. /* gap: 1rem; */
  3082. /* margin-left: 10%; */
  3083. }
  3084. .matrix-main-title {
  3085. color: white;
  3086. font-size: 20px;
  3087. font-weight: bold;
  3088. margin-left: 0%;
  3089. }
  3090. .market-temperature-value {
  3091. font-size: 1.5rem;
  3092. font-weight: bold;
  3093. color: white;
  3094. margin-left: auto;
  3095. }
  3096. .market-temperature-label {
  3097. background-image: url('@/assets/img/AiEmotion/bk01.png');
  3098. background-size: 100% 100%;
  3099. background-repeat: no-repeat;
  3100. padding: 10px;
  3101. color: #fff;
  3102. font-size: 20px;
  3103. font-weight: bold;
  3104. text-align: center;
  3105. margin-left: 0;
  3106. width: 30%;
  3107. min-width: 200px;
  3108. max-width: 50%;
  3109. height: auto;
  3110. overflow: hidden;
  3111. text-overflow: ellipsis;
  3112. white-space: nowrap;
  3113. box-sizing: border-box;
  3114. }
  3115. .main {
  3116. width: 83%;
  3117. max-width: 1400px;
  3118. min-width: 320px;
  3119. min-height: 100px;
  3120. height: auto;
  3121. padding: 1rem;
  3122. box-sizing: border-box;
  3123. background-color: #2b378d;
  3124. margin: 0 auto;
  3125. transition: width 0.3s ease;
  3126. margin-bottom: 10rem;
  3127. }
  3128. .ai-emotion-container {
  3129. display: flex;
  3130. flex-direction: column;
  3131. align-items: center;
  3132. justify-content: center;
  3133. padding: 20px;
  3134. position: relative;
  3135. }
  3136. .user-input-display {
  3137. margin-top: 20px;
  3138. display: flex;
  3139. flex-direction: column;
  3140. width: 100%;
  3141. }
  3142. .message-container {
  3143. display: flex;
  3144. margin-bottom: 10px;
  3145. width: 100%;
  3146. }
  3147. /* 用户消息容器样式 */
  3148. .user-message-container {
  3149. display: flex;
  3150. align-items: center;
  3151. margin-left: auto;
  3152. gap: 10px;
  3153. }
  3154. .user-message-speaker {
  3155. width: 32px;
  3156. height: 32px;
  3157. object-fit: contain;
  3158. margin-top: 5px;
  3159. cursor: pointer;
  3160. transition: all 0.3s ease;
  3161. }
  3162. .user-message-speaker:hover {
  3163. transform: scale(1.1);
  3164. }
  3165. .user-message-speaker.speaker-active {
  3166. animation: pulse 1.5s infinite;
  3167. }
  3168. @keyframes pulse {
  3169. 0% {
  3170. transform: scale(1);
  3171. }
  3172. 50% {
  3173. transform: scale(1.1);
  3174. }
  3175. 100% {
  3176. transform: scale(1);
  3177. }
  3178. }
  3179. .user-message {
  3180. color: #6d22f8;
  3181. background: white;
  3182. font-weight: bold;
  3183. padding: 15px 20px;
  3184. border-radius: 15px;
  3185. max-width: 60%;
  3186. text-align: left;
  3187. box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
  3188. margin: 0;
  3189. /* 将用户消息推到右边 */
  3190. }
  3191. /* AI消息容器样式 */
  3192. .ai-message-container {
  3193. display: flex;
  3194. align-items: flex-start;
  3195. gap: 10px;
  3196. margin-right: auto;
  3197. max-width: 80%;
  3198. }
  3199. /* 思考过程动图样式 */
  3200. .thinking-gif {
  3201. width: 40px;
  3202. height: 40px;
  3203. object-fit: contain;
  3204. margin-top: 5px;
  3205. border-radius: 8px;
  3206. animation: float 2s ease-in-out infinite;
  3207. }
  3208. @keyframes float {
  3209. 0%,
  3210. 100% {
  3211. transform: translateY(0px);
  3212. }
  3213. 50% {
  3214. transform: translateY(-5px);
  3215. }
  3216. }
  3217. .ai-message {
  3218. background-color: #f1f1f1;
  3219. color: #333;
  3220. font-weight: bold;
  3221. padding: 20px 30px;
  3222. border-radius: 15px;
  3223. text-align: left;
  3224. margin-right: auto;
  3225. /* 将AI消息保持在左边 */
  3226. white-space: nowrap;
  3227. width: fit-content;
  3228. overflow: visible;
  3229. align-items: center;
  3230. display: flex;
  3231. }
  3232. .input-container {
  3233. display: flex;
  3234. align-items: center;
  3235. gap: 10px;
  3236. }
  3237. .fixed-bottom {
  3238. position: fixed;
  3239. bottom: 100px;
  3240. left: 0;
  3241. width: 100%;
  3242. background-color: #f8f9fa;
  3243. padding: 10px 20px;
  3244. box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
  3245. }
  3246. .input-box {
  3247. padding: 10px;
  3248. font-size: 16px;
  3249. border: 1px solid #ccc;
  3250. border-radius: 5px;
  3251. width: calc(100% - 120px);
  3252. }
  3253. .send-button {
  3254. padding: 10px 20px;
  3255. font-size: 16px;
  3256. color: #fff;
  3257. background-color: #007bff;
  3258. border: none;
  3259. border-radius: 5px;
  3260. cursor: pointer;
  3261. }
  3262. .send-button:hover {
  3263. background-color: #0056b3;
  3264. }
  3265. /* 响应式布局媒体查询 */
  3266. @media only screen and (max-width: 1200px) {
  3267. .main {
  3268. width: 95%;
  3269. padding: 0.8rem;
  3270. }
  3271. .market-temperature-label {
  3272. width: 40%;
  3273. min-width: 170px;
  3274. max-width: 60%;
  3275. font-size: 1.3rem;
  3276. overflow: hidden;
  3277. text-overflow: ellipsis;
  3278. white-space: nowrap;
  3279. }
  3280. .market-temperature-value {
  3281. font-size: 1.3rem;
  3282. }
  3283. /* 调整图表容器高度 */
  3284. /* .class00 {
  3285. min-height: 45rem;
  3286. } */
  3287. .market-temperature-section {
  3288. min-height: 60rem;
  3289. }
  3290. .decision-tree-section {
  3291. min-height: 42rem;
  3292. }
  3293. .scaled-img {
  3294. height: 350px;
  3295. min-height: 30rem;
  3296. }
  3297. }
  3298. @media only screen and (max-width: 992px) {
  3299. .main {
  3300. width: 98%;
  3301. padding: 0.6rem;
  3302. }
  3303. /* 调整图表容器高度 */
  3304. /* .class00 {
  3305. min-height: 40rem;
  3306. } */
  3307. .market-temperature-section {
  3308. min-height: 55rem;
  3309. }
  3310. .decision-tree-section {
  3311. min-height: 35rem;
  3312. }
  3313. .scaled-img {
  3314. height: 300px;
  3315. min-height: 25rem;
  3316. background-size: contain;
  3317. }
  3318. }
  3319. /* 手机端适配样式 */
  3320. @media only screen and (max-width: 768px) {
  3321. .text-container .content {
  3322. display: block;
  3323. color: white;
  3324. text-align: center;
  3325. font-size: 20px;
  3326. }
  3327. .market-temperature-icon {
  3328. width: auto;
  3329. height: auto;
  3330. display: flex;
  3331. align-items: center;
  3332. justify-content: center;
  3333. scale: 0.5;
  3334. }
  3335. .matrix-header {
  3336. display: flex;
  3337. align-items: center;
  3338. }
  3339. .main {
  3340. width: 100%;
  3341. padding: 0.5rem;
  3342. margin-bottom: 5rem;
  3343. }
  3344. .energy-converter-text {
  3345. color: white;
  3346. font-size: 20px;
  3347. font-weight: bold;
  3348. margin-left: 28%;
  3349. }
  3350. .energy-converter-content {
  3351. min-width: 100%;
  3352. /* margin-top: 25%; */
  3353. }
  3354. .scaled-img {
  3355. background-image: url('@/assets/img/AiEmotion/tree00000.jpg');
  3356. background-size: 100% 100%;
  3357. background-position: center;
  3358. background-repeat: no-repeat;
  3359. background-size: contain;
  3360. text-align: center;
  3361. width: 90%;
  3362. margin-top: 4%;
  3363. height: 200px;
  3364. min-height: 200px;
  3365. }
  3366. .bottom-radar-text {
  3367. color: white;
  3368. font-size: 20px;
  3369. font-weight: bold;
  3370. margin-left: 30%;
  3371. }
  3372. .emotion-decoder-text {
  3373. color: white;
  3374. font-size: 20px;
  3375. font-weight: bold;
  3376. margin-left: 30%;
  3377. }
  3378. /* 图片样式 */
  3379. .golden-wheel img {
  3380. width: 50%;
  3381. }
  3382. .matrix-title img {
  3383. width: 100%;
  3384. }
  3385. .temperature-title img {
  3386. width: 100%;
  3387. margin: 10px 10px;
  3388. }
  3389. .emotion-decoder-title img {
  3390. width: 100%;
  3391. margin: 10px 10px;
  3392. }
  3393. .bottom-radar-title img {
  3394. width: 100%;
  3395. margin: 10px 10px;
  3396. }
  3397. .core-highlights-header img {
  3398. scale: 0.5;
  3399. }
  3400. .core-logic-header img {
  3401. width: 100%;
  3402. margin: 10px 10px;
  3403. }
  3404. .scaled-img img {
  3405. width: 30%;
  3406. height: auto;
  3407. }
  3408. .scenario-application-section img {
  3409. width: 100%;
  3410. margin: 10px 10px;
  3411. }
  3412. .matrix-main-title {
  3413. font-size: 20px;
  3414. margin-left: 5%;
  3415. }
  3416. /* .span01 {
  3417. width: 50%;
  3418. min-width: 150px;
  3419. max-width: 70%;
  3420. font-size: 16px;
  3421. overflow: hidden;
  3422. text-overflow: ellipsis;
  3423. white-space: nowrap;
  3424. padding: 8px;
  3425. } */
  3426. .matrix-header .market-temperature-value {
  3427. font-size: 14px;
  3428. color: white;
  3429. float: right;
  3430. }
  3431. .market-temperature-section {
  3432. background-image: url('@/assets/img/AiEmotion/bk00000.png');
  3433. background-size: 100% 100%;
  3434. background-repeat: no-repeat;
  3435. width: 100%;
  3436. /* margin-left: -45px; */
  3437. height: auto;
  3438. }
  3439. .main {
  3440. min-height: 100px;
  3441. height: auto;
  3442. box-sizing: border-box;
  3443. background-color: #02107d;
  3444. margin-bottom: 10rem;
  3445. }
  3446. .emotion-decoder-section {
  3447. width: 80%;
  3448. height: auto;
  3449. margin: 0 auto;
  3450. /* min-height: 38rem; */
  3451. /* min-height: 51rem; */
  3452. /* margin-left: -39px; */
  3453. /* min-height: 38rem; */
  3454. }
  3455. /* 调整其他图表容器高度 */
  3456. /* .class00 {
  3457. min-height: 35rem;
  3458. } */
  3459. .market-temperature-section {
  3460. min-height: 45rem;
  3461. }
  3462. .matrix-header img {
  3463. width: 68%;
  3464. height: auto;
  3465. /* margin-top: 5%; */
  3466. margin-left: 0%;
  3467. }
  3468. .emotion-decoder-icon {
  3469. width: 25%;
  3470. height: auto;
  3471. scale: 0.5;
  3472. }
  3473. .bottom-radar-icon,
  3474. .energy-converter-icon {
  3475. width: 25%;
  3476. height: auto;
  3477. scale: 0.5;
  3478. }
  3479. .lz-img {
  3480. margin-bottom: 0;
  3481. padding-top: 0;
  3482. img {
  3483. width: 30%;
  3484. height: auto;
  3485. margin-top: 5%;
  3486. }
  3487. }
  3488. .decision-tree-section {
  3489. background-size: 100% 100%;
  3490. background-repeat: no-repeat;
  3491. width: 80vw;
  3492. height: auto;
  3493. min-height: 20rem;
  3494. margin: 0 auto;
  3495. }
  3496. .bk-image {
  3497. .conclusion-container {
  3498. padding: 15px;
  3499. border-radius: 8px;
  3500. margin: 8px;
  3501. .conclusion-item {
  3502. &:last-child {
  3503. margin-bottom: 0;
  3504. }
  3505. .conclusion-title {
  3506. color: #FFD700;
  3507. font-size: 16px;
  3508. font-weight: bold;
  3509. margin: 0 0 8px 0;
  3510. text-align: center;
  3511. }
  3512. .conclusion-text {
  3513. color: #ffffff;
  3514. font-size: 14px;
  3515. line-height: 1.5;
  3516. margin: 0 0 6px 0;
  3517. text-align: left;
  3518. word-wrap: break-word;
  3519. &:last-child {
  3520. margin-bottom: 0;
  3521. }
  3522. }
  3523. }
  3524. }
  3525. .conclusion-placeholder {
  3526. padding: 15px;
  3527. text-align: center;
  3528. p {
  3529. color: #999999;
  3530. font-size: 12px;
  3531. margin: 0;
  3532. }
  3533. }
  3534. }
  3535. .bk-image {
  3536. background-size: 100% 100%;
  3537. background-repeat: no-repeat;
  3538. width: 80vw;
  3539. height: auto;
  3540. margin: 0 auto;
  3541. .conclusion-container {
  3542. padding-top: 20px;
  3543. border-radius: 15px;
  3544. margin: 20px;
  3545. .conclusion-item {
  3546. border-radius: 12px;
  3547. transition: all 0.3s ease;
  3548. overflow: hidden;
  3549. }
  3550. .conclusion-item:last-child {
  3551. margin-bottom: 0;
  3552. }
  3553. .conclusion-item .conclusion-title {
  3554. color: #FFD700;
  3555. font-size: 22px;
  3556. font-weight: bold;
  3557. text-align: center;
  3558. letter-spacing: 2px;
  3559. margin-top: 22px;
  3560. }
  3561. .conclusion-item .conclusion-text {
  3562. color: #ffffff;
  3563. font-size: 20px;
  3564. line-height: 1.8;
  3565. margin: 0 0 12px 0;
  3566. text-align: center;
  3567. word-wrap: break-word;
  3568. position: relative;
  3569. }
  3570. .conclusion-item .conclusion-text:last-child {
  3571. margin-bottom: 0;
  3572. }
  3573. }
  3574. .conclusion-placeholder {
  3575. padding: 30px;
  3576. text-align: center;
  3577. border-radius: 12px;
  3578. background: rgba(255, 255, 255, 0.05);
  3579. border: 1px dashed rgba(153, 153, 153, 0.3);
  3580. }
  3581. .conclusion-placeholder p {
  3582. color: #999999;
  3583. font-size: 16px;
  3584. margin: 0;
  3585. font-style: italic;
  3586. }
  3587. .disclaimer-item {
  3588. /* margin-top: 30px; */
  3589. padding-bottom: 15%;
  3590. text-align: center;
  3591. }
  3592. .disclaimer-item p {
  3593. color: #ffffff !important;
  3594. font-size: 16px;
  3595. margin: 0;
  3596. letter-spacing: 1px;
  3597. }
  3598. }
  3599. .bottom-radar-section {
  3600. background-size: 100% 100%;
  3601. background-repeat: no-repeat;
  3602. width: 80%;
  3603. height: auto;
  3604. /* min-height: 48rem; */
  3605. }
  3606. .energy-converter-section {
  3607. background-size: 100% 100%;
  3608. background-repeat: no-repeat;
  3609. width: 80%;
  3610. height: auto;
  3611. margin: 0 auto;
  3612. min-height: 55vh;
  3613. }
  3614. .temperature-display {
  3615. display: flex;
  3616. flex-direction: column;
  3617. /* margin-left: 5rem; */
  3618. gap: 0;
  3619. /* margin-top: -6rem; */
  3620. /* width: 100%; */
  3621. height: auto;
  3622. }
  3623. .temperature-content .temperature-cold,
  3624. .temperature-content .temperature-hot {
  3625. min-width: 200px;
  3626. font-size: 16px;
  3627. }
  3628. .temperature-content {
  3629. padding-top: 8%;
  3630. display: flex;
  3631. align-items: center;
  3632. justify-content: center;
  3633. gap: 0rem;
  3634. }
  3635. .market-temperature-label {
  3636. background-image: url('@/assets/img/AiEmotion/bk01.png');
  3637. background-size: 100% 100%;
  3638. background-repeat: no-repeat;
  3639. display: inline-block;
  3640. padding: 10px;
  3641. color: #fff;
  3642. font-size: 14px;
  3643. text-align: center;
  3644. width: 50%;
  3645. }
  3646. .temperature-display {
  3647. flex-direction: column;
  3648. gap: 1rem;
  3649. align-items: center;
  3650. }
  3651. /* .span01 {
  3652. width: 60%;
  3653. font-size: 1.2rem;
  3654. padding: 8px;
  3655. } */
  3656. .market-temperature-value {
  3657. font-size: 1.2rem;
  3658. }
  3659. .matrix-main-title,
  3660. .emotion-decoder-text,
  3661. .bottom-radar-text,
  3662. .energy-converter-text {
  3663. font-size: 18px;
  3664. margin-left: 0;
  3665. }
  3666. }
  3667. /* 超小屏幕设备 */
  3668. @media only screen and (max-width: 480px) {
  3669. .main {
  3670. width: 100%;
  3671. padding: 0.3rem;
  3672. margin-bottom: 3rem;
  3673. }
  3674. .temperature-display {
  3675. flex-direction: column;
  3676. gap: 0.8rem;
  3677. align-items: center;
  3678. }
  3679. .temperature-content .temperature-cold,
  3680. .temperature-content .temperature-hot {
  3681. /* width: 90%; */
  3682. width: 10vw;
  3683. min-width: 120px;
  3684. font-size: 14px;
  3685. }
  3686. /* .span01 {
  3687. width: 50%;
  3688. font-size: 1rem;
  3689. padding: 6px;
  3690. } */
  3691. .market-temperature-value {
  3692. font-size: 1rem;
  3693. }
  3694. .golden-wheel-img {
  3695. width: 80%;
  3696. }
  3697. /* 调整图表容器高度适配超小屏幕 */
  3698. /* .class00 {
  3699. min-height: 25rem;
  3700. } */
  3701. .market-temperature-section {
  3702. min-height: 35rem;
  3703. }
  3704. .decision-tree-section {
  3705. min-height: 15rem;
  3706. }
  3707. .scaled-img {
  3708. height: 150px;
  3709. min-height: 150px;
  3710. }
  3711. }
  3712. /* 加载提示样式 */
  3713. /* .loading-container {
  3714. display: flex;
  3715. justify-content: center;
  3716. align-items: center;
  3717. min-height: 20vh;
  3718. padding: 20px 10px;
  3719. } */
  3720. /* .loading-content {
  3721. text-align: center;
  3722. background: linear-gradient(135deg, rgba(0, 212, 255, 0.15) 0%, rgba(0, 100, 200, 0.15) 100%);
  3723. border: 2px solid rgba(0, 212, 255, 0.4);
  3724. border-radius: 20px;
  3725. padding: 40px;
  3726. box-shadow: 0 8px 25px rgba(0, 212, 255, 0.3);
  3727. } */
  3728. /* .loading-spinner {
  3729. width: 60px;
  3730. height: 60px;
  3731. border: 4px solid rgba(0, 212, 255, 0.3);
  3732. border-top: 4px solid #00d4ff;
  3733. border-radius: 50%;
  3734. animation: spin 1s linear infinite;
  3735. margin: 0 auto 20px;
  3736. } */
  3737. @keyframes spin {
  3738. 0% {
  3739. transform: rotate(0deg);
  3740. }
  3741. 100% {
  3742. transform: rotate(360deg);
  3743. }
  3744. }
  3745. /* .loading-text {
  3746. color: #00d4ff;
  3747. font-size: 18px;
  3748. font-weight: bold;
  3749. text-shadow: 0 2px 8px rgba(0, 212, 255, 0.5);
  3750. letter-spacing: 1px;
  3751. } */
  3752. /* 对话区域样式 */
  3753. .conversation-area {
  3754. width: 100%;
  3755. padding: 0 20px;
  3756. }
  3757. .message-list {
  3758. display: flex;
  3759. flex-direction: column;
  3760. gap: 15px;
  3761. }
  3762. .message-item {
  3763. width: 100%;
  3764. display: flex;
  3765. }
  3766. .user-message-item {
  3767. justify-content: flex-end;
  3768. }
  3769. .ai-message-item {
  3770. justify-content: flex-start;
  3771. }
  3772. .user-message-wrapper {
  3773. display: flex;
  3774. justify-content: flex-end;
  3775. max-width: 70%;
  3776. }
  3777. .ai-message-wrapper {
  3778. display: flex;
  3779. justify-content: flex-start;
  3780. max-width: 80%;
  3781. }
  3782. /* 顶部锚点样式 */
  3783. .top-anchor {
  3784. position: relative;
  3785. top: 0;
  3786. left: 0;
  3787. width: 100%;
  3788. height: 1px;
  3789. display: block;
  3790. visibility: visible;
  3791. opacity: 0;
  3792. pointer-events: none;
  3793. }
  3794. /* 返回顶部按钮样式 */
  3795. .back-to-top {
  3796. position: sticky !important;
  3797. bottom: 20px !important;
  3798. left: calc(100% - 70px) !important;
  3799. width: 50px !important;
  3800. height: 50px !important;
  3801. background: linear-gradient(135deg, #00d4ff 0%, #0066cc 100%) !important;
  3802. border-radius: 50% !important;
  3803. display: flex !important;
  3804. align-items: center !important;
  3805. justify-content: center !important;
  3806. cursor: pointer !important;
  3807. transition: all 0.3s ease !important;
  3808. z-index: 100 !important;
  3809. color: white !important;
  3810. opacity: 1 !important;
  3811. visibility: visible !important;
  3812. margin-top: 20px !important;
  3813. margin-bottom: 20px !important;
  3814. }
  3815. .back-to-top:hover {
  3816. transform: translateY(-3px);
  3817. box-shadow: 0 6px 20px rgba(0, 212, 255, 0.5);
  3818. background: linear-gradient(135deg, #00e6ff 0%, #0077dd 100%);
  3819. }
  3820. .back-to-top:active {
  3821. transform: translateY(-1px);
  3822. }
  3823. /* 页面主容器样式 */
  3824. .page-container {
  3825. position: relative;
  3826. width: 100%;
  3827. min-height: 100vh;
  3828. }
  3829. .master:last-child {
  3830. border-bottom: none;
  3831. margin-bottom: 0;
  3832. }
  3833. /* class01容器样式 */
  3834. .main {
  3835. position: relative;
  3836. }
  3837. /* 移动端适配 */
  3838. @media only screen and (max-width: 768px) {
  3839. .back-to-top {
  3840. left: calc(100% - 65px) !important;
  3841. width: 45px !important;
  3842. height: 45px !important;
  3843. }
  3844. }
  3845. @media only screen and (max-width: 480px) {
  3846. .back-to-top {
  3847. left: calc(100% - 60px) !important;
  3848. width: 40px !important;
  3849. height: 40px !important;
  3850. }
  3851. .back-to-top svg {
  3852. width: 20px;
  3853. height: 20px;
  3854. }
  3855. }
  3856. </style>