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.

4922 lines
142 KiB

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