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.

2866 lines
76 KiB

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