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.

3322 lines
94 KiB

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