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.

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