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.

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