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.

4173 lines
120 KiB

2 weeks ago
2 weeks ago
2 weeks ago
1 week ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
2 weeks ago
2 weeks ago
1 week ago
2 weeks ago
2 weeks ago
2 weeks ago
1 week ago
2 weeks ago
2 weeks ago
1 week ago
2 weeks ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
1 week ago
1 week ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
2 weeks ago
1 week ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
1 week ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
1 week ago
2 weeks ago
2 weeks ago
1 week ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
1 week ago
2 weeks ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
1 week ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
2 weeks ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
2 weeks ago
1 week ago
2 weeks ago
1 week ago
1 week ago
1 week ago
1 week ago
2 weeks ago
1 week ago
2 weeks ago
  1. <script setup>
  2. import { ref, onMounted, watch, nextTick, reactive, onUnmounted } from "vue";
  3. import { ElDialog, ElMessage } from "element-plus";
  4. import MessageItem from "@/components/deepNine/MessageItem.vue";
  5. import ThinkingGif from "@/components/deepNine/ThinkingGif.vue";
  6. import {
  7. dbqbFirstAPI,
  8. dbqbSecondOneAPI,
  9. dbqbSecondTwoAPI,
  10. dbqbSecondThreeAPI,
  11. dbqbSecondFourAPI,
  12. dataListAPI,
  13. } from "../api/AIxiaocaishen";
  14. import {
  15. getNineTurnsAPI,
  16. deepNineFirstAPI,
  17. deepNineSecondOneAPI,
  18. deepNineSecondTwoAPI,
  19. deepNineSecondThreeAPI,
  20. } from "../api/deepNine";
  21. import { useUserStore } from "../store/userPessionCode";
  22. import { useDeepNineStore } from "../store/deepNine";
  23. import { useDeepNineAudioStore } from "../store/deepNineAudio";
  24. import { useDataStore } from "@/store/dataList.js";
  25. import { marked } from "marked"; // 引入marked库
  26. // 导入思考过程GIF
  27. import thinkingGif from "@/assets/img/gif/思考.gif";
  28. import analyzeGif from "@/assets/img/gif/解析.gif";
  29. import generateGif from "@/assets/img/gif/生成.gif";
  30. import katex from "katex"; // 引入 KaTeX 库
  31. import { htmlToText } from "html-to-text";
  32. import { Howl, Howler } from "howler";
  33. import * as echarts from "echarts";
  34. import _, { add } from "lodash";
  35. import moment from "moment";
  36. import title1 from "@/assets/img/AIchat/核心价值评估.png";
  37. import title2 from "@/assets/img/AIchat/主力作战.png";
  38. import title3 from "@/assets/img/AIchat/攻防三维.png";
  39. import title4 from "@/assets/img/AIchat/综合作战.png";
  40. import logo2 from "@/assets/img/AIchat/开启无限财富.png";
  41. import getCountAll from "../assets/img/homePage/get-count-all.png";
  42. import voice from "../assets/img/homePage/tail/voice.png";
  43. import voiceNoActive from "../assets/img/homePage/tail/voice-no-active.png";
  44. import { useChatStore } from "../store/chat";
  45. const homepageChatStore = useChatStore();
  46. const chatStore = useDeepNineStore();
  47. const audioStore = useDeepNineAudioStore();
  48. const dataStore = useDataStore();
  49. // 将这些变量移到全局作用域
  50. const audioQueue = ref([]);
  51. const isPlayingAudio = ref(false);
  52. let currentPlayIndex = 0;
  53. let isCallingPlayNext = false;
  54. // 音频预加载状态
  55. const audioPreloadStatus = {
  56. one: { loaded: false, url: null },
  57. two: { loaded: false, url: null },
  58. three: { loaded: false, url: null },
  59. four: { loaded: false, url: null },
  60. five: { loaded: false, url: null },
  61. };
  62. // 音频队列顺序管理
  63. const audioQueueOrder = {
  64. "API1-第一个": 1, // 第一个接口的第一个音频 (link1)
  65. "API1-第二个": 2, // 第一个接口的第二个音频 (link)
  66. "API2-第一个": 3, // 第二个接口的第一个音频 (link3)
  67. "API2-第二个": 4, // 第二个接口的第二个音频 (link1)
  68. "API3-第一个": 5, // 第三个接口的音频 (link)
  69. };
  70. // 播放下一个音频的函数
  71. const playNextAudio = () => {
  72. if (isCallingPlayNext) {
  73. console.log("playNextAudio已在执行中,跳过重复调用");
  74. return;
  75. }
  76. if (currentPlayIndex >= audioQueue.value.length) {
  77. console.log(
  78. "所有音频播放完成,重置到第一个音频 currentPlayIndex",
  79. currentPlayIndex
  80. );
  81. // 播放完成后重置到第一个音频,但不自动播放
  82. currentPlayIndex = 0;
  83. audioStore.isPlaying = false;
  84. audioStore.isPaused = false;
  85. audioStore.playbackPosition = 0;
  86. // 清除音频实例,确保下次点击从头开始
  87. audioStore.soundInstance = null;
  88. audioStore.nowSound = null;
  89. if (audioQueue.value.length > 0) {
  90. audioStore.setCurrentAudioUrl(audioQueue.value[0]);
  91. }
  92. return;
  93. }
  94. isCallingPlayNext = true;
  95. const audioInfo = audioQueue.value[currentPlayIndex];
  96. if (!audioInfo || !audioInfo.url) {
  97. console.warn(`音频信息无效,跳过索引 ${currentPlayIndex}`);
  98. currentPlayIndex++;
  99. isCallingPlayNext = false;
  100. playNextAudio();
  101. return;
  102. }
  103. console.log(`开始播放 ${audioInfo.name},索引: ${currentPlayIndex}`);
  104. const audio = new Howl({
  105. src: [audioInfo.url],
  106. html5: false,
  107. format: ["mp3", "acc"],
  108. // rate: 2,
  109. retryCount: 0,
  110. onplay: () => {
  111. audioStore.isPlaying = true;
  112. isPlayingAudio.value = true;
  113. isCallingPlayNext = false;
  114. console.log(`${audioInfo.name}音频开始播放111`);
  115. },
  116. onpause: () => {
  117. audioStore.isPlaying = false;
  118. audioStore.isPaused = true;
  119. audioStore.playbackPosition = audio.seek() || 0;
  120. console.log(`${audioInfo.name}音频已暂停`);
  121. },
  122. onresume: () => {
  123. audioStore.isPlaying = true;
  124. audioStore.isPaused = false;
  125. console.log(`${audioInfo.name}音频继续播放`);
  126. },
  127. onend: () => {
  128. console.log(`${audioInfo.name}音频播放完成,准备播放下一个`);
  129. audioStore.isPlaying = false;
  130. audioStore.isPaused = false;
  131. audioStore.playbackPosition = 0;
  132. isPlayingAudio.value = false;
  133. currentPlayIndex++;
  134. console.log(
  135. "currentPlayIndex",
  136. currentPlayIndex,
  137. "audioQueue.value.length",
  138. audioQueue.value.length
  139. );
  140. if (currentPlayIndex < audioQueue.value.length) {
  141. console.log(
  142. `队列中还有音频,500ms后播放下一个 (索引:${currentPlayIndex})`
  143. );
  144. setTimeout(() => {
  145. isCallingPlayNext = false;
  146. playNextAudio();
  147. }, 200);
  148. } else {
  149. console.log("🎉 所有音频播放完成,清除音频实例");
  150. chatStore.messages[chatStore.currentUserIndex].audioStatus = false;
  151. audioStore.nowSound = null;
  152. audioStore.soundInstance = null;
  153. isCallingPlayNext = false;
  154. }
  155. },
  156. onstop: () => {
  157. console.log(`${audioInfo.name}音频被停止`);
  158. audioStore.isPlaying = false;
  159. audioStore.isPaused = false;
  160. audioStore.playbackPosition = 0;
  161. },
  162. onloaderror: (id, err) => {
  163. console.error(`${audioInfo.name}音频播放失败:`, err);
  164. isPlayingAudio.value = false;
  165. isCallingPlayNext = false;
  166. setTimeout(() => {
  167. playNextAudio();
  168. }, 100);
  169. },
  170. });
  171. audioStore.setCurrentAudioUrl(audioInfo.url);
  172. audioStore.nowSound = audio;
  173. audioStore.setAudioInstance(audio);
  174. console.log(`尝试播放${audioInfo.name}音频`);
  175. audio.play();
  176. };
  177. // 添加音频到播放队列(确保顺序)
  178. const addToAudioQueue = (url, name) => {
  179. console.log(`=== 添加音频到队列 ===`);
  180. console.log("URL:", url);
  181. console.log("Name:", name);
  182. console.log("音频启用状态:", audioStore.isVoiceEnabled);
  183. if (url && audioStore.isVoiceEnabled) {
  184. const audioItem = {
  185. url,
  186. name,
  187. order: audioQueueOrder[name] || 999,
  188. };
  189. audioQueue.value.push(audioItem);
  190. // 按顺序排序队列
  191. audioQueue.value.sort((a, b) => a.order - b.order);
  192. console.log(`音频${name}已添加到播放队列,顺序:${audioItem.order}`);
  193. console.log(
  194. "当前队列顺序:",
  195. audioQueue.value.map((item) => `${item.name}(${item.order})`)
  196. );
  197. // 只有在确实没有音频在播放且这是第一个音频时才开始播放
  198. if (
  199. !isPlayingAudio.value &&
  200. !audioStore.isPlaying &&
  201. audioQueue.value.length === 1
  202. ) {
  203. console.log("✅ 条件满足:没有音频在播放且这是第一个音频,立即开始播放", {
  204. isPlayingAudio: isPlayingAudio.value,
  205. audioStoreIsPlaying: audioStore.isPlaying,
  206. queueLength: audioQueue.value.length,
  207. });
  208. playNextAudio();
  209. } else {
  210. console.log("⏳ 等待条件:", {
  211. isPlayingAudio: isPlayingAudio.value,
  212. audioStoreIsPlaying: audioStore.isPlaying,
  213. queueLength: audioQueue.value.length,
  214. reason:
  215. audioQueue.value.length > 1 ? "队列中已有其他音频" : "有音频正在播放",
  216. });
  217. }
  218. } else {
  219. console.log("❌ 跳过添加音频:", {
  220. hasUrl: !!url,
  221. voiceEnabled: audioStore.isVoiceEnabled,
  222. });
  223. }
  224. console.log(`=== 添加音频完成 ===`);
  225. };
  226. // 语音播放控制函数
  227. const toggleVoiceForUser = (index) => {
  228. console.log(
  229. "上一个按钮坐标",
  230. chatStore.currentUserIndex,
  231. "当前按钮坐标",
  232. index
  233. );
  234. if (
  235. !chatStore.messages[index].audioArray[0] ||
  236. !chatStore.messages[index].audioArray[1] ||
  237. !chatStore.messages[index].audioArray[2] ||
  238. !chatStore.messages[index].audioArray[3]
  239. ) {
  240. return;
  241. }
  242. // 先把当前按钮状态修改
  243. chatStore.messages[index].audioStatus =
  244. !chatStore.messages[index].audioStatus;
  245. // 如果当前按钮和之前的按钮不是同一个则再修改之前的按钮的状态
  246. if (chatStore.currentUserIndex != index) {
  247. if (chatStore.currentUserIndex != null) {
  248. if (audioStore.isPlaying) {
  249. audioStore.togglePlayPause();
  250. }
  251. chatStore.messages[chatStore.currentUserIndex].audioStatus = false;
  252. }
  253. // 强制停止所有音频实例(移动端兼容)
  254. if (audioStore.soundInstance) {
  255. audioStore.soundInstance.stop();
  256. audioStore.soundInstance = null;
  257. }
  258. audioPreloadStatus.one = { loaded: false, url: null };
  259. audioPreloadStatus.two = { loaded: false, url: null };
  260. audioPreloadStatus.three = { loaded: false, url: null };
  261. audioPreloadStatus.four = { loaded: false, url: null };
  262. audioPreloadStatus.five = { loaded: false, url: null };
  263. if (chatStore.messages[index].audioArray[0]) {
  264. audioPreloadStatus.one.loaded = true;
  265. audioPreloadStatus.one.url = chatStore.messages[index].audioArray[0];
  266. }
  267. if (chatStore.messages[index].audioArray[1]) {
  268. audioPreloadStatus.two.loaded = true;
  269. audioPreloadStatus.two.url = chatStore.messages[index].audioArray[1];
  270. }
  271. if (chatStore.messages[index].audioArray[2]) {
  272. audioPreloadStatus.three.loaded = true;
  273. audioPreloadStatus.three.url = chatStore.messages[index].audioArray[2];
  274. }
  275. if (chatStore.messages[index].audioArray[3]) {
  276. audioPreloadStatus.four.loaded = true;
  277. audioPreloadStatus.four.url = chatStore.messages[index].audioArray[3];
  278. }
  279. chatStore.currentUserIndex = index;
  280. audioQueue.value = [];
  281. isPlayingAudio.value = false;
  282. audioStore.soundInstance = null;
  283. currentPlayIndex = 0;
  284. isCallingPlayNext = false;
  285. setTimeout(() => {
  286. addToAudioQueue(chatStore.messages[index].audioArray[0], "API1-第一个");
  287. addToAudioQueue(chatStore.messages[index].audioArray[1], "API2-第二个");
  288. addToAudioQueue(chatStore.messages[index].audioArray[2], "API3-第三个");
  289. addToAudioQueue(chatStore.messages[index].audioArray[3], "API4-第四个");
  290. if (!audioStore.isVoiceEnabled) {
  291. audioStore.toggleVoice();
  292. } else {
  293. if (audioStore.currentAudioUrl || audioStore.ttsUrl) {
  294. // audioStore.togglePlayPause();
  295. } else {
  296. audioStore.toggleVoice();
  297. }
  298. }
  299. }, 100); // 100ms延迟足够移动端清理音频实例
  300. } else {
  301. if (!audioStore.isVoiceEnabled) {
  302. console.log("1111");
  303. audioStore.toggleVoice();
  304. } else {
  305. if (currentPlayIndex >= audioQueue.value.length) {
  306. console.log("重新开始播放音频序列");
  307. currentPlayIndex = 0;
  308. isPlayingAudio.value = false;
  309. isCallingPlayNext = false;
  310. audioStore.soundInstance = null;
  311. // 重新开始播放
  312. if (audioQueue.value.length > 0) {
  313. playNextAudio();
  314. }
  315. } else if (audioStore.currentAudioUrl || audioStore.ttsUrl) {
  316. console.log("2222");
  317. audioStore.togglePlayPause();
  318. } else {
  319. console.log("3333");
  320. audioStore.toggleVoice();
  321. }
  322. }
  323. }
  324. };
  325. // 计算属性:判断语音是否启用
  326. const isVoice = computed(() => {
  327. return audioStore.isVoiceEnabled;
  328. });
  329. // 随机GIF
  330. const currentGif = ref("");
  331. const renderer = new marked.Renderer();
  332. // 重写 del 方法,让删除线不生效
  333. renderer.del = function (text) {
  334. // 处理各种数据类型
  335. console.log("text", text);
  336. return "~" + text.tokens[0].raw + "<br>" + text.tokens[2].raw + "~";
  337. };
  338. // 定义自定义事件
  339. const emit = defineEmits([
  340. "updateMessage",
  341. "sendMessage",
  342. "enableInput",
  343. "ensureAIchat",
  344. "scrollToBottom",
  345. "showCount",
  346. ]);
  347. // 音频播放方法
  348. const playAudio = (url) => {
  349. // 添加空值校验
  350. if (!url) {
  351. console.warn("音频URL为空,跳过播放");
  352. audioStore.isPlaying = false;
  353. return;
  354. }
  355. const handlePlay = () => {
  356. if (audioStore.isNewInstance) {
  357. const newSound = new Howl({
  358. src: [url],
  359. html5: true, // 强制HTML5 Audio解决iOS兼容问题
  360. format: ["mp3", "acc"],
  361. rate: 1.2, // 调整播放速度
  362. onplay: () => {
  363. audioStore.isPlaying = true; // 改为更新store状态
  364. newSound.volume(1); // 添加音量设置
  365. },
  366. onend: () => (audioStore.isPlaying = false),
  367. onstop: () => (audioStore.isPlaying = false),
  368. onloaderror: (id, err) => {
  369. console.error("音频加载失败:", err);
  370. ElMessage.error("音频播放失败,请检查网络连接");
  371. },
  372. });
  373. if (audioStore.nowSound) {
  374. audioStore.nowSound.stop();
  375. }
  376. audioStore.nowSound = newSound;
  377. audioStore.isNewInstance = false;
  378. console.log("新音频");
  379. } else {
  380. console.log("已经有音频");
  381. }
  382. const newSound = audioStore.nowSound;
  383. // 添加立即播放逻辑
  384. newSound.play();
  385. audioStore.setAudioInstance(newSound);
  386. Howler._howls.push(newSound); // 强制注册到全局管理
  387. };
  388. handlePlay();
  389. };
  390. // 新增暂停方法
  391. const pauseAudio = () => {
  392. if (audioStore.soundInstance) {
  393. audioStore.soundInstance.pause();
  394. audioStore.isPlaying = false;
  395. }
  396. };
  397. // 音频轮流播放方法
  398. const playAudioSequence = (audioUrls) => {
  399. console.log("playAudioSequence被调用,参数:", audioUrls);
  400. if (!audioUrls || audioUrls.length === 0) {
  401. console.warn("音频URL列表为空,跳过播放");
  402. return;
  403. }
  404. let currentIndex = 0;
  405. let audioSequence = [...audioUrls]; // 保存音频序列
  406. const playNext = () => {
  407. if (currentIndex >= audioSequence.length) {
  408. console.log("所有音频播放完成,重置到第一个音频");
  409. // 播放完成后重置到第一个音频,但不自动播放
  410. currentIndex = 0;
  411. audioStore.isPlaying = false;
  412. audioStore.isPaused = false;
  413. audioStore.playbackPosition = 0;
  414. // 清除音频实例,确保下次点击从头开始
  415. audioStore.soundInstance = null;
  416. audioStore.nowSound = null;
  417. if (audioSequence.length > 0) {
  418. audioStore.setCurrentAudioUrl(audioSequence[0]);
  419. }
  420. return;
  421. }
  422. const currentUrl = audioSequence[currentIndex];
  423. console.log(`正在播放第${currentIndex + 1}个音频:`, currentUrl);
  424. console.log(
  425. "音频URL有效性检查:",
  426. !!currentUrl,
  427. "长度:",
  428. currentUrl?.length
  429. );
  430. // 增强URL验证
  431. if (
  432. !currentUrl ||
  433. typeof currentUrl !== "string" ||
  434. currentUrl.trim() === ""
  435. ) {
  436. console.error(`音频 ${currentIndex + 1} URL无效,跳过该音频`);
  437. currentIndex++;
  438. setTimeout(() => {
  439. playNext();
  440. }, 100);
  441. return;
  442. }
  443. // 检查URL格式
  444. try {
  445. new URL(currentUrl);
  446. } catch (e) {
  447. console.error(`音频 ${currentIndex + 1} URL格式错误:`, currentUrl);
  448. currentIndex++;
  449. setTimeout(() => {
  450. playNext();
  451. }, 100);
  452. return;
  453. }
  454. // 设置当前音频URL
  455. audioStore.setCurrentAudioUrl(currentUrl);
  456. // 停止当前播放的音频
  457. if (audioStore.nowSound) {
  458. audioStore.nowSound.stop();
  459. }
  460. const sound = new Howl({
  461. src: [currentUrl],
  462. html5: true,
  463. format: ["mp3", "acc"],
  464. rate: 1.2,
  465. onplay: () => {
  466. audioStore.isPlaying = true;
  467. audioStore.isPaused = false;
  468. console.log(`开始播放音频 ${currentIndex + 1}`);
  469. console.log("音频播放状态:", {
  470. duration: sound.duration(),
  471. state: sound.state(),
  472. playing: sound.playing(),
  473. });
  474. },
  475. onpause: () => {
  476. audioStore.isPlaying = false;
  477. audioStore.isPaused = true;
  478. audioStore.playbackPosition = sound.seek() || 0;
  479. console.log(`音频 ${currentIndex + 1} 已暂停`);
  480. },
  481. onend: () => {
  482. audioStore.isPlaying = false;
  483. audioStore.isPaused = false;
  484. audioStore.playbackPosition = 0;
  485. console.log(`音频 ${currentIndex + 1} 播放完成`);
  486. currentIndex++;
  487. // 如果是最后一个音频播放完成,立即清除实例
  488. if (currentIndex >= audioSequence.length) {
  489. console.log("最后一个音频播放完成,清除音频实例");
  490. audioStore.soundInstance = null;
  491. audioStore.nowSound = null;
  492. currentIndex = 0; // 立即重置索引
  493. }
  494. // 播放下一个音频
  495. setTimeout(() => {
  496. playNext();
  497. }, 500); // 间隔500ms播放下一个
  498. },
  499. onstop: () => {
  500. audioStore.isPlaying = false;
  501. audioStore.isPaused = false;
  502. audioStore.playbackPosition = 0;
  503. console.log(`音频 ${currentIndex + 1} 已停止`);
  504. },
  505. onloaderror: (id, err) => {
  506. console.error(`音频 ${currentIndex + 1} 加载失败:`, err);
  507. console.error("失败的音频URL:", currentUrl);
  508. console.error("错误详情:", { id, err });
  509. // 增加重试机制
  510. if (!sound.retryCount) {
  511. sound.retryCount = 0;
  512. }
  513. if (sound.retryCount < 2) {
  514. sound.retryCount++;
  515. console.log(
  516. `音频 ${currentIndex + 1}${sound.retryCount}次重试加载`
  517. );
  518. setTimeout(() => {
  519. sound.load();
  520. }, 1000 * sound.retryCount); // 递增延时重试
  521. } else {
  522. console.warn(`音频 ${currentIndex + 1} 重试失败,跳过该音频`);
  523. currentIndex++;
  524. // 跳过失败的音频,播放下一个
  525. setTimeout(() => {
  526. playNext();
  527. }, 100);
  528. }
  529. },
  530. });
  531. audioStore.nowSound = sound;
  532. audioStore.setAudioInstance(sound);
  533. // 添加播放超时检测
  534. const playTimeout = setTimeout(() => {
  535. if (!audioStore.isPlaying && sound.state() === "loading") {
  536. console.warn(`音频 ${currentIndex + 1} 播放超时,可能网络问题`);
  537. sound.stop();
  538. currentIndex++;
  539. playNext();
  540. }
  541. }, 10000); // 10秒超时
  542. // 播放成功后清除超时
  543. sound.once("play", () => {
  544. clearTimeout(playTimeout);
  545. });
  546. console.log(`尝试播放音频 ${currentIndex + 1},URL: ${currentUrl}`);
  547. sound.play();
  548. };
  549. // 重写togglePlayPause方法以支持音频序列控制
  550. audioStore.togglePlayPause = () => {
  551. console.log("音频控制按钮被点击 11111111111");
  552. console.log("当前播放状态:", audioStore.isPlaying);
  553. console.log("当前暂停状态:", audioStore.isPaused);
  554. console.log("当前音频实例:", audioStore.soundInstance);
  555. console.log(
  556. "当前索引:",
  557. currentIndex,
  558. "音频序列长度:",
  559. audioSequence.length
  560. );
  561. if (audioStore.soundInstance) {
  562. if (audioStore.isPlaying) {
  563. // 暂停当前音频
  564. console.log("暂停当前音频");
  565. audioStore.pause();
  566. } else if (audioStore.isPaused) {
  567. // 从暂停位置继续播放
  568. console.log("从暂停位置继续播放");
  569. audioStore.play();
  570. } else {
  571. // 重新开始播放当前音频或从头开始播放序列
  572. console.log("重新开始播放,当前索引:", currentIndex);
  573. if (currentIndex >= audioSequence.length) {
  574. console.log("所有音频已播放完成,从头开始");
  575. currentIndex = 0; // 重置到第一个音频
  576. }
  577. playNext();
  578. }
  579. } else {
  580. // 没有音频实例时,从头开始播放
  581. console.log("没有音频实例,从头开始播放");
  582. currentIndex = 0;
  583. playNext();
  584. }
  585. };
  586. // 开始播放第一个音频
  587. playNext();
  588. };
  589. // 获取消息
  590. const chatMsg = computed(() => chatStore.messages);
  591. const props = defineProps({
  592. messages: Array,
  593. chartData: {
  594. type: Object,
  595. default: null,
  596. },
  597. index: {
  598. type: Number,
  599. required: true,
  600. },
  601. });
  602. // 打字机效果
  603. const typewriterContent = ref("");
  604. const isTyping = ref(false);
  605. const typeWriter = (text, callback) => {
  606. let index = 0;
  607. isTyping.value = true;
  608. typewriterContent.value = "";
  609. const typingInterval = setInterval(() => {
  610. if (index < text.length) {
  611. typewriterContent.value += text.charAt(index);
  612. index++;
  613. // 自动滚动到底部
  614. nextTick(() => {
  615. const container = document.querySelector(".message-area");
  616. if (container) container.scrollTop = container.scrollHeight;
  617. });
  618. } else {
  619. clearInterval(typingInterval);
  620. isTyping.value = false;
  621. if (callback) callback();
  622. }
  623. }, 50); // 调整速度(毫秒)
  624. };
  625. const typingQueue = ref([]);
  626. const isTypingInProgress = ref(false);
  627. // 创建打字机效果的Promise函数
  628. const createTypingEffect = (message, content, speed) => {
  629. return new Promise((resolve) => {
  630. chatStore.messages.push(message);
  631. if (Array.isArray(content) && content.length > 0) {
  632. message.content = "";
  633. message.isTyping = true;
  634. let currentIndex = 0;
  635. const processNextElement = () => {
  636. if (currentIndex >= content.length) {
  637. if (message.isEnd) {
  638. if (message.isEnd == "1") {
  639. apiStatus.one.isEnd = true;
  640. } else if (message.isEnd == "2") {
  641. apiStatus.two.isEnd = true;
  642. } else if (message.isEnd == "3") {
  643. apiStatus.three.isEnd = true;
  644. }
  645. }
  646. if (message.error) {
  647. chatStore.messages.push({
  648. class: "ing",
  649. type: "ing",
  650. flag: false,
  651. content: "系统正在为您努力加载中,请稍后再试",
  652. });
  653. chatStore.isLoading = false;
  654. chatStore.chatInput = false;
  655. emit("enableInput");
  656. if (message.error == "2") {
  657. apiStatus.two.isError = true;
  658. } else if (message.error == "3") {
  659. apiStatus.three.isError = true;
  660. }
  661. }
  662. if (message.end) {
  663. homepageChatStore.getUserCount();
  664. chatStore.isLoading = false;
  665. console.log("打印完毕,接触输入框禁用状态");
  666. chatStore.chatInput = false;
  667. emit("enableInput");
  668. }
  669. message.isTyping = false;
  670. nextTick(() => {
  671. resolve(); // 完成后resolve
  672. });
  673. return;
  674. }
  675. if (currentIndex % 2 === 0) {
  676. // 偶数下标:直接加入
  677. message.content += content[currentIndex];
  678. currentIndex++;
  679. processNextElement(); // 立即处理下一个元素
  680. } else {
  681. // 奇数下标:打字机效果
  682. const text = content[currentIndex];
  683. let charIndex = 0;
  684. const typingInterval = setInterval(() => {
  685. if (charIndex < text.length) {
  686. message.content += text.charAt(charIndex);
  687. charIndex++;
  688. } else {
  689. clearInterval(typingInterval);
  690. currentIndex++;
  691. processNextElement(); // 处理下一个元素
  692. }
  693. }, speed);
  694. }
  695. };
  696. processNextElement(); // 开始处理
  697. } else {
  698. if (message.kline) {
  699. if (message.klineType == 2) {
  700. console.log("K线消息已添加到聊天列表");
  701. // 在渲染完成后初始化图表
  702. nextTick(() => {
  703. console.log("nextTick开始 - 准备渲染图表");
  704. console.log("消息列表:", chatStore.messages);
  705. // 寻找最新添加的K线消息索引
  706. let klineIndex = -1;
  707. for (let i = 0; i < chatStore.messages.length; i++) {
  708. if (chatStore.messages[i].messageId === message.messageId) {
  709. klineIndex = i;
  710. break;
  711. }
  712. }
  713. console.log("找到的K线消息索引:", klineIndex);
  714. if (klineIndex !== -1) {
  715. const containerId = `kline-container-${klineIndex}`;
  716. console.log("图表容器ID:", containerId);
  717. // 确保DOM已经渲染完成
  718. setTimeout(() => {
  719. console.log("延时执行,确保DOM已渲染");
  720. KlineCanvsEcharts(containerId);
  721. }, 100); // 短暂延时确保DOM已渲染
  722. } else {
  723. console.warn("未找到K线消息");
  724. }
  725. });
  726. }
  727. if (message.isEnd) {
  728. if (message.isEnd == "1") {
  729. apiStatus.one.isEnd = true;
  730. } else if (message.isEnd == "2") {
  731. apiStatus.two.isEnd = true;
  732. } else if (message.isEnd == "3") {
  733. apiStatus.three.isEnd = true;
  734. }
  735. }
  736. if (message.error) {
  737. chatStore.messages.push({
  738. class: "ing",
  739. type: "ing",
  740. flag: false,
  741. content: "系统正在为您努力加载中,请稍后再试",
  742. });
  743. chatStore.isLoading = false;
  744. chatStore.chatInput = false;
  745. emit("enableInput");
  746. if (message.error == "2") {
  747. apiStatus.two.isError = true;
  748. } else if (message.error == "3") {
  749. apiStatus.three.isError = true;
  750. }
  751. }
  752. // 延时1秒后resolve
  753. setTimeout(() => {
  754. resolve();
  755. }, 1000);
  756. } else {
  757. if (message.isEnd) {
  758. if (message.isEnd == "1") {
  759. apiStatus.one.isEnd = true;
  760. } else if (message.isEnd == "2") {
  761. apiStatus.two.isEnd = true;
  762. } else if (message.isEnd == "3") {
  763. apiStatus.three.isEnd = true;
  764. }
  765. }
  766. if (message.error) {
  767. chatStore.messages.push({
  768. class: "ing",
  769. type: "ing",
  770. flag: false,
  771. content: "系统正在为您努力加载中,请稍后再试",
  772. });
  773. chatStore.isLoading = false;
  774. chatStore.chatInput = false;
  775. emit("enableInput");
  776. if (message.error == "2") {
  777. apiStatus.two.isError = true;
  778. } else if (message.error == "3") {
  779. apiStatus.three.isError = true;
  780. }
  781. }
  782. // 延时1秒后resolve
  783. setTimeout(() => {
  784. resolve();
  785. }, 1000);
  786. }
  787. }
  788. });
  789. };
  790. let apiStatus = {};
  791. // 队列处理函数
  792. const processTypingQueue = async () => {
  793. if (isTypingInProgress.value || typingQueue.value.length === 0) {
  794. return;
  795. }
  796. isTypingInProgress.value = true;
  797. while (typingQueue.value.length > 0) {
  798. const task = typingQueue.value.shift();
  799. await createTypingEffect(task.message, task.content, task.speed);
  800. }
  801. isTypingInProgress.value = false;
  802. };
  803. // 添加打字机任务到队列
  804. const addTypingTask = (message, content, speed) => {
  805. typingQueue.value.push({ message, content, speed });
  806. processTypingQueue();
  807. };
  808. // 显示思考过程
  809. async function showThinkingProcess(stockName = null) {
  810. // 第一步:正在思考
  811. const thinkingMessage1 = reactive({
  812. sender: "ai",
  813. class: "ing",
  814. type: "ing",
  815. flag: true,
  816. content: "深度九大模型正在思考",
  817. gif: thinkingGif,
  818. nowrap: true,
  819. });
  820. chatStore.messages.push(thinkingMessage1);
  821. await new Promise((resolve) => setTimeout(resolve, 1500));
  822. chatStore.messages.pop();
  823. // 第二步:正在解析关键数据(持续显示直到获取到股票名称)
  824. const thinkingMessage2 = reactive({
  825. sender: "ai",
  826. class: "ing",
  827. type: "ing",
  828. flag: true,
  829. content: "正在解析关键数据",
  830. gif: analyzeGif,
  831. nowrap: true,
  832. });
  833. chatStore.messages.push(thinkingMessage2);
  834. // 如果没有股票名称,保持第二步显示
  835. if (!stockName) {
  836. return thinkingMessage2; // 返回消息引用,以便后续更新
  837. }
  838. // 有股票名称后,继续后续步骤
  839. await new Promise((resolve) => setTimeout(resolve, 1500));
  840. chatStore.messages.pop();
  841. // 第三步:生成具体股票的深度共振分析图谱
  842. const thinkingMessage3 = reactive({
  843. sender: "ai",
  844. class: "ing",
  845. type: "ing",
  846. flag: true,
  847. content: `正在生成${stockName}深度共振分析图谱`,
  848. gif: generateGif,
  849. nowrap: true,
  850. });
  851. chatStore.messages.push(thinkingMessage3);
  852. await new Promise((resolve) => setTimeout(resolve, 1500));
  853. chatStore.messages.pop();
  854. // 第四步:报告已生成
  855. const thinkingMessage4 = reactive({
  856. sender: "ai",
  857. class: "ing",
  858. type: "ing",
  859. content: "报告已生成!",
  860. nowrap: true,
  861. });
  862. chatStore.messages.push(thinkingMessage4);
  863. await new Promise((resolve) => setTimeout(resolve, 1500));
  864. chatStore.messages.pop();
  865. return null;
  866. }
  867. // 继续思考过程(当获取到股票名称后调用)
  868. async function continueThinkingProcess(thinkingMessageRef, stockName) {
  869. if (!thinkingMessageRef || !stockName) return;
  870. // 等待一段时间后继续
  871. await new Promise((resolve) => setTimeout(resolve, 1500));
  872. // 移除第二步消息
  873. const index = chatStore.messages.indexOf(thinkingMessageRef);
  874. if (index > -1) {
  875. chatStore.messages.splice(index, 1);
  876. }
  877. // 第三步:生成具体股票的深度共振分析图谱
  878. const thinkingMessage3 = reactive({
  879. sender: "ai",
  880. class: "ing",
  881. type: "ing",
  882. flag: true,
  883. content: `正在生成${stockName}深度共振分析图谱`,
  884. gif: generateGif,
  885. });
  886. chatStore.messages.push(thinkingMessage3);
  887. await new Promise((resolve) => setTimeout(resolve, 1500));
  888. chatStore.messages.pop();
  889. // 第四步:报告已生成
  890. const thinkingMessage4 = reactive({
  891. sender: "ai",
  892. class: "ing",
  893. type: "ing",
  894. content: "报告已生成!",
  895. });
  896. chatStore.messages.push(thinkingMessage4);
  897. await new Promise((resolve) => setTimeout(resolve, 1500));
  898. chatStore.messages.pop();
  899. }
  900. const hasValidData = ref(false);
  901. // 创建一个非响应式的对象来存储图表实例
  902. const chartInstancesMap = {};
  903. // 存储上一次的消息的length
  904. const previousMessagesLength = ref(0);
  905. watch(
  906. () => props.messages,
  907. async (newVal, oldVal) => {
  908. // // 添加空值判断
  909. if (!newVal?.length || newVal === previousMessagesLength.value) return;
  910. chatStore.firstAPICall = true;
  911. console.log("第一阶段,意图识别,获取回复,历史记录禁止点击");
  912. previousMessagesLength.value = newVal.length;
  913. if (newVal.length > 0) {
  914. // 清理语音下标
  915. console.log("chatStore.currentUserIndex", chatStore.currentUserIndex);
  916. if (chatStore.currentUserIndex != null) {
  917. chatStore.messages[chatStore.currentUserIndex].audioStatus = false;
  918. }
  919. chatStore.currentUserIndex = null;
  920. audioStore.stop(); // 暂停语音
  921. // 🔧 新增:重置音频队列状态,确保新音频能够自动播放
  922. audioQueue.value = [];
  923. isPlayingAudio.value = false;
  924. currentPlayIndex = 0;
  925. isCallingPlayNext = false;
  926. // 重置音频预加载状态
  927. audioPreloadStatus.one = { loaded: false, url: null };
  928. audioPreloadStatus.two = { loaded: false, url: null };
  929. audioPreloadStatus.three = { loaded: false, url: null };
  930. audioPreloadStatus.four = { loaded: false, url: null };
  931. // 清除音频实例
  932. audioStore.soundInstance = null;
  933. audioStore.nowSound = null;
  934. audioStore.isPlaying = false;
  935. audioStore.isPaused = false;
  936. audioStore.playbackPosition = 0;
  937. console.log("消息列表已更新,最新消息:", newVal[newVal.length - 1]);
  938. chatStore.messages.push(newVal[newVal.length - 1]);
  939. chatStore.currentUserIndex = chatStore.messages.length - 1;
  940. chatStore.inputUserIndex = chatStore.messages.length - 1;
  941. console.log(
  942. "消息列表已更新,最新消息:",
  943. chatStore.messages[chatStore.messages.length - 1],
  944. "最新用户坐标",
  945. chatStore.currentUserIndex
  946. );
  947. // 获取权限
  948. const userStore = useUserStore();
  949. const params1 = {
  950. language: "cn",
  951. marketList: "usa,sg,my,hk,cn,can,vi,th,in",
  952. content: newVal[newVal.length - 1].content,
  953. token: localStorage.getItem("localToken"),
  954. model: 1,
  955. // language: "cn",
  956. // marketList: "hk,cn,usa,my,sg,vi,in,gb"
  957. // token: "+SsksARQgUHIbIG3rRnnbZi0+fEeMx8pywnIlrmTxo5EOPR/wjWDV7w7+ZUseiBtf9kFa/atmNx6QfSpv5w",
  958. };
  959. // 标志
  960. let flag = true;
  961. const codeData = ref();
  962. // 开始思考过程(不带股票名称)
  963. const thinkingMessageRef = await showThinkingProcess();
  964. // 第一阶段,意图识别
  965. try {
  966. // 调用工作流获取回复
  967. const result = await deepNineFirstAPI(params1);
  968. codeData.value = result.data;
  969. console.log(codeData.value, "codeData");
  970. // 根据意图识别结果判断
  971. if (result.code == 200) {
  972. // 意图识别成功后,更新历史记录状态
  973. chatStore.searchRecord = true;
  974. // 获取到股票名称后,继续思考过程
  975. if (thinkingMessageRef && codeData.value.name) {
  976. await continueThinkingProcess(
  977. thinkingMessageRef,
  978. codeData.value.name
  979. );
  980. }
  981. // for (let i = chatStore.messages.length - 1; i >= 0; --i) {
  982. // if (chatStore.messages[i].sender == "user") {
  983. // chatStore.messages[i].audioStatus = true;
  984. // break;
  985. // }
  986. // }
  987. chatStore.messages.push({
  988. // class: "ing",
  989. // type: "ing",
  990. // flag: flag,
  991. // content: result.data.kaishi,
  992. });
  993. } else {
  994. // 意图识别失败,先清理思考过程消息
  995. if (thinkingMessageRef) {
  996. const index = chatStore.messages.indexOf(thinkingMessageRef);
  997. if (index > -1) {
  998. chatStore.messages.splice(index, 1);
  999. }
  1000. }
  1001. flag = false;
  1002. console.log("执行回绝话术");
  1003. const AIcontent = ref(result.msg);
  1004. // 修改后的消息处理逻辑
  1005. const processedContent = marked(AIcontent.value);
  1006. const katexRegex = /\$\$(.*?)\$\$/g;
  1007. let aiContent = processedContent.replace(
  1008. katexRegex,
  1009. (match, formula) => {
  1010. try {
  1011. return katex.renderToString(formula, { throwOnError: false });
  1012. } catch (error) {
  1013. console.error("KaTeX 渲染错误:", error);
  1014. return match;
  1015. }
  1016. }
  1017. );
  1018. console.log(AIcontent, "AIcontent");
  1019. if (result.code == 406) {
  1020. AIcontent.value = `<p>尊敬的用户您好,您当前的“深度九大模型专属Token”数量为0,无法进行股票查询,可联系客服团队进行充值,感谢您的理解与支持</p>`;
  1021. }
  1022. const aiMsg = {
  1023. class: "ing",
  1024. type: "ing",
  1025. flag: flag,
  1026. content: AIcontent,
  1027. };
  1028. chatStore.messages.push(aiMsg);
  1029. chatStore.isLoading = false;
  1030. chatStore.chatInput = false;
  1031. chatStore.firstAPICall = false;
  1032. console.log("历史记录可以点击");
  1033. emit("enableInput");
  1034. }
  1035. } catch (e) {
  1036. // 意图识别异常,先清理思考过程消息
  1037. if (thinkingMessageRef) {
  1038. const index = chatStore.messages.indexOf(thinkingMessageRef);
  1039. if (index > -1) {
  1040. chatStore.messages.splice(index, 1);
  1041. }
  1042. }
  1043. console.log(e, "意图识别失败");
  1044. chatStore.messages.push({
  1045. class: "ing",
  1046. type: "ing",
  1047. flag: false,
  1048. content: "系统正在为您努力加载中,请稍后再试",
  1049. });
  1050. chatStore.isLoading = false;
  1051. chatStore.chatInput = false;
  1052. chatStore.firstAPICall = false;
  1053. console.log("历史记录可以点击");
  1054. emit("enableInput");
  1055. }
  1056. if (flag) {
  1057. const params2 = {
  1058. language: "cn",
  1059. token: localStorage.getItem("localToken"),
  1060. parentId: codeData.value.parentId,
  1061. stockId: codeData.value.stockId,
  1062. recordId: codeData.value.recordId,
  1063. // content: newVal[newVal.length - 1].content,
  1064. // marketList: "usa,sg,my,hk,cn,can,vi,th,in",
  1065. // name: codeData.value.name,
  1066. // code: codeData.value.code,
  1067. // market: codeData.value.market,
  1068. // language: "cn",
  1069. // marketList: "hk,cn,usa,my,sg,vi,in,gb"
  1070. // token: "+SsksARQgUHIbIG3rRnnbZi0+fEeMx8pywnIlrmTxo5EOPR/wjWDV7w7+ZUseiBtf9kFa/atmNx6QfSpv5w",
  1071. };
  1072. try {
  1073. const env = import.meta.env.VITE_ENV;
  1074. const result20 = await getNineTurnsAPI(
  1075. {
  1076. token:
  1077. env == "development" || env == "test"
  1078. ? "8Csj5VVX1UbIb4C3oxrnbZi0+fEeMx8pywnIlrmTm45Cb/EllzWACLto9J9+fCFsfdgBOvKvyY94FvqlvM0"
  1079. : "8nkj4QBV1RPIb4CzoRTnbZi0+fEeMx8pywnIlrmTxdwROKkuwWqAWu9orpkpeXVqL98DPfeonNYpHv+mucA",
  1080. },
  1081. {
  1082. market: codeData.value.market,
  1083. language: "cn",
  1084. code: codeData.value.code,
  1085. }
  1086. );
  1087. // 添加空值检查防止访问null对象的属性
  1088. const nineTurns = result20.data ? result20.data : null;
  1089. const isNineTurns =
  1090. nineTurns &&
  1091. nineTurns.DXT &&
  1092. nineTurns.JZJG &&
  1093. nineTurns.KLine20 &&
  1094. nineTurns.StockInformation &&
  1095. nineTurns.ZJQS
  1096. ? true
  1097. : false;
  1098. const katexRegex = /\$\$(.*?)\$\$/g;
  1099. let result21;
  1100. let result22;
  1101. let result23;
  1102. let result24;
  1103. // 用于跟踪API完成状态和结果
  1104. apiStatus = {
  1105. one: {
  1106. completed: false,
  1107. result: null,
  1108. error: null,
  1109. isError: false,
  1110. isEnd: false,
  1111. },
  1112. two: {
  1113. completed: false,
  1114. result: null,
  1115. error: null,
  1116. isError: false,
  1117. isEnd: false,
  1118. },
  1119. three: {
  1120. completed: false,
  1121. result: null,
  1122. error: null,
  1123. isError: false,
  1124. isEnd: false,
  1125. },
  1126. four: {
  1127. completed: false,
  1128. result: null,
  1129. error: null,
  1130. isError: false,
  1131. isEnd: false,
  1132. },
  1133. };
  1134. // 预加载音频函数
  1135. const preloadAudio = (url, apiKey) => {
  1136. if (!url || !audioStore.isVoiceEnabled) {
  1137. audioPreloadStatus[apiKey].loaded = true;
  1138. return Promise.resolve();
  1139. }
  1140. // 立即设置URL,确保即使预加载失败也能使用
  1141. audioPreloadStatus[apiKey].url = url;
  1142. console.log(`设置音频${apiKey}的URL:`, url);
  1143. return new Promise((resolve) => {
  1144. const audio = new Howl({
  1145. src: [url],
  1146. html5: true,
  1147. format: ["mp3", "acc"],
  1148. rate: 1.2,
  1149. preload: true,
  1150. onload: () => {
  1151. console.log(`音频${apiKey}预加载完成:`, url);
  1152. audioPreloadStatus[apiKey].loaded = true;
  1153. resolve();
  1154. },
  1155. onloaderror: (id, err) => {
  1156. console.error(`音频${apiKey}预加载失败:`, err);
  1157. audioPreloadStatus[apiKey].loaded = true; // 标记为已处理,避免阻塞
  1158. // URL已经在上面设置了,即使预加载失败也保留URL
  1159. resolve();
  1160. },
  1161. });
  1162. });
  1163. };
  1164. // 检查第一个接口是否可以开始输出(文本和音频都准备好)
  1165. const canStartFirstOutput = () => {
  1166. return apiStatus.one.completed && audioPreloadStatus.one.loaded;
  1167. };
  1168. // 检查并按顺序执行代码的函数
  1169. const checkAndExecuteInOrder = () => {
  1170. // 检查OneAPI - 只有当文本和音频都准备好时才开始输出
  1171. if (canStartFirstOutput() && !apiStatus.one.executed) {
  1172. if (apiStatus.one.result) {
  1173. apiStatus.one.executed = true;
  1174. console.log(
  1175. "执行OneAPI代码(文本和音频同步开始):",
  1176. apiStatus.one.result
  1177. );
  1178. // 将第一个音频添加到播放队列(确保顺序:API1)
  1179. if (audioPreloadStatus.one.url) {
  1180. chatStore.messages[chatStore.inputUserIndex].audioArray.push(
  1181. audioPreloadStatus.one.url
  1182. );
  1183. if (chatStore.currentUserIndex == chatStore.inputUserIndex) {
  1184. chatStore.messages[
  1185. chatStore.inputUserIndex
  1186. ].audioStatus = true;
  1187. addToAudioQueue(audioPreloadStatus.one.url, "API1-第一个");
  1188. } else {
  1189. chatStore.messages[
  1190. chatStore.inputUserIndex
  1191. ].audioStatus = false;
  1192. }
  1193. console.log(
  1194. "音频队列:添加API1-1音频(link1),当前队列长度:",
  1195. audioQueue.value.length
  1196. );
  1197. }
  1198. // 添加第二个音频(link)到队列
  1199. if (audioPreloadStatus.two.url) {
  1200. chatStore.messages[chatStore.inputUserIndex].audioArray.push(
  1201. audioPreloadStatus.two.url
  1202. );
  1203. if (chatStore.currentUserIndex == chatStore.inputUserIndex) {
  1204. addToAudioQueue(audioPreloadStatus.two.url, "API1-第二个");
  1205. } else {
  1206. chatStore.messages[
  1207. chatStore.inputUserIndex
  1208. ].audioStatus = false;
  1209. }
  1210. console.log(
  1211. "音频队列:添加API1-2音频(link),当前队列长度:",
  1212. audioQueue.value.length
  1213. );
  1214. }
  1215. // 在这里添加OneAPI成功后需要执行的代码
  1216. // 删除正在为您生成信息
  1217. chatStore.messages.pop();
  1218. // 添加报告头和时间
  1219. addTypingTask(
  1220. {
  1221. sender: "ai",
  1222. class: "title1",
  1223. type: "title1",
  1224. content: codeData.value.name + "深度共振分析图谱",
  1225. date: result21.data.date,
  1226. },
  1227. "",
  1228. 50
  1229. );
  1230. chatStore.firstAPICall = false;
  1231. console.log("历史记录可以点击");
  1232. const pc1 = marked(
  1233. result21.data.name +
  1234. "\n" +
  1235. result21.data.price +
  1236. "\n" +
  1237. result21.data.date
  1238. );
  1239. const ac1 = pc1.replace(katexRegex, (match, formula) => {
  1240. try {
  1241. return katex.renderToString(formula, {
  1242. throwOnError: false,
  1243. });
  1244. } catch (error) {
  1245. console.error("KaTeX 渲染错误:", error);
  1246. return match;
  1247. }
  1248. });
  1249. // 先推送初始消息
  1250. const aiMessage1 = reactive({
  1251. sender: "ai",
  1252. class: "content1",
  1253. type: "content1",
  1254. content: "",
  1255. isTyping: true,
  1256. });
  1257. addTypingTask(aiMessage1, ["", ac1], 130);
  1258. // 九转结构K线图
  1259. if (
  1260. nineTurns &&
  1261. nineTurns.DXT &&
  1262. nineTurns.JZJG &&
  1263. nineTurns.KLine20 &&
  1264. nineTurns.StockInformation &&
  1265. nineTurns.ZJQS
  1266. ) {
  1267. const nineTurnsData = JSON.parse(
  1268. JSON.stringify(toRaw(nineTurns))
  1269. );
  1270. console.log("处理 K 线数据 - 开始");
  1271. console.log("nineTurnsData", nineTurnsData);
  1272. const Kline20 = {
  1273. name: nineTurnsData.StockInformation.Name,
  1274. Kline: nineTurnsData,
  1275. };
  1276. // 打印K线数据结构
  1277. console.log("K线数据结构:", Kline20);
  1278. console.log("K线数据名称:", Kline20.name);
  1279. console.log("K线数据:", Kline20.Kline ? Kline20.Kline : null);
  1280. // 设置数据有效标志
  1281. hasValidData.value = true;
  1282. console.log("hasValidData设置为:", hasValidData.value);
  1283. // 先推送K线图消息
  1284. const klineMessageId2 = `kline-${Date.now() + 1}`;
  1285. console.log("生成K线消息ID:", klineMessageId2);
  1286. // 添加九转结构图表
  1287. addTypingTask(
  1288. {
  1289. sender: "ai",
  1290. class: "content2",
  1291. type: "content2",
  1292. kline: true,
  1293. chartData: Kline20,
  1294. messageId: klineMessageId2,
  1295. hasValidData: true, // 添加hasValidData标志
  1296. klineType: 2,
  1297. },
  1298. "",
  1299. 50
  1300. );
  1301. // 添加标题-数据分析时代下的认知变现
  1302. addTypingTask(
  1303. {
  1304. sender: "ai",
  1305. class: "title2",
  1306. type: "title2",
  1307. content: "",
  1308. },
  1309. "",
  1310. 50
  1311. );
  1312. // 添加图片-数据分析时代下的认知变现
  1313. addTypingTask(
  1314. {
  1315. sender: "ai",
  1316. class: "content3",
  1317. type: "img1",
  1318. content:
  1319. "https://d31zlh4on95l9h.cloudfront.net/images/5baa0a449cf74fb6a1afb1c909a21194.png",
  1320. },
  1321. "",
  1322. 50
  1323. );
  1324. // 添加标题-结构框架分析
  1325. addTypingTask(
  1326. {
  1327. sender: "ai",
  1328. class: "title3",
  1329. type: "title3",
  1330. content:
  1331. "https://d31zlh4on95l9h.cloudfront.net/images/9ab9d76b6906eb914fa1842dbcd56841.png",
  1332. },
  1333. "",
  1334. 50
  1335. );
  1336. // 添加内容框1
  1337. const ac2 = `<p>${result21.data.jgkjfx}</p>`;
  1338. // 先推送初始消息
  1339. const aiMessage2 = reactive({
  1340. sender: "ai",
  1341. class: "content3",
  1342. type: "content3",
  1343. content: "",
  1344. isTyping: true,
  1345. error: apiStatus.two.error ? "2" : "",
  1346. isEnd: "1",
  1347. });
  1348. addTypingTask(aiMessage2, ["", ac2], 130);
  1349. }
  1350. } else {
  1351. chatStore.messages.push({
  1352. class: "ing",
  1353. type: "ing",
  1354. flag: false,
  1355. content: "系统正在为您努力加载中,请稍后再试",
  1356. });
  1357. chatStore.isLoading = false;
  1358. chatStore.chatInput = false;
  1359. emit("enableInput");
  1360. }
  1361. }
  1362. // 检查TwoAPI(需要OneAPI已执行)
  1363. if (
  1364. apiStatus.one.executed &&
  1365. apiStatus.two.completed &&
  1366. !apiStatus.two.executed
  1367. ) {
  1368. if (apiStatus.two.result) {
  1369. apiStatus.two.executed = true;
  1370. console.log("执行TwoAPI代码:", apiStatus.two.result);
  1371. // 将第二个接口的音频添加到播放队列(确保顺序:API2)
  1372. if (audioPreloadStatus.three.url) {
  1373. chatStore.messages[chatStore.inputUserIndex].audioArray.push(
  1374. audioPreloadStatus.three.url
  1375. );
  1376. if (chatStore.currentUserIndex == chatStore.inputUserIndex) {
  1377. addToAudioQueue(
  1378. audioPreloadStatus.three.url,
  1379. "API2-第一个"
  1380. );
  1381. } else {
  1382. chatStore.messages[
  1383. chatStore.inputUserIndex
  1384. ].audioStatus = false;
  1385. }
  1386. console.log(
  1387. "音频队列:添加API2-1音频(link3),当前队列长度:",
  1388. audioQueue.value.length
  1389. );
  1390. }
  1391. if (audioPreloadStatus.four.url) {
  1392. chatStore.messages[chatStore.inputUserIndex].audioArray.push(
  1393. audioPreloadStatus.four.url
  1394. );
  1395. if (chatStore.currentUserIndex == chatStore.inputUserIndex) {
  1396. addToAudioQueue(audioPreloadStatus.four.url, "API2-第二个");
  1397. } else {
  1398. chatStore.messages[
  1399. chatStore.inputUserIndex
  1400. ].audioStatus = false;
  1401. }
  1402. console.log(
  1403. "音频队列:添加API2-2音频(link1),当前队列长度:",
  1404. audioQueue.value.length
  1405. );
  1406. }
  1407. // 在这里添加TwoAPI成功后需要执行的代码
  1408. // 添加标题-资金动向监控
  1409. addTypingTask(
  1410. {
  1411. sender: "ai",
  1412. class: "title3",
  1413. type: "title3",
  1414. content:
  1415. " https://d31zlh4on95l9h.cloudfront.net/images/f95c44f83b3e3c52e88964631c199060.png",
  1416. },
  1417. "",
  1418. 50
  1419. );
  1420. const ac31 = `<p style="margin:0;color:#FFD700;font-weight:bold;display:flex;justify-content:center;font-size:22px">【资金异动信号】</p><p>`;
  1421. const ac32 = result22.data.dxtsc;
  1422. // const ac33 = result22.data.zjqssc1;
  1423. const ac34 = `<p style="margin:0;color:#FFD700;font-weight:bold;display:flex;justify-content:center;font-size:22px">【资金趋势导航】</p><p>`;
  1424. const ac35 = result22.data.zjqssc1;
  1425. // const ac3 = `<p>${result23.data.DXTSC}</p><p>${result23.data.DXTSC2}</p><p>${result23.data.ZJQSSC1}</p>`;
  1426. const ac3Arr = [];
  1427. ac3Arr.push(ac31);
  1428. if (ac32 != "") {
  1429. ac3Arr.push(`<p>${ac32}</p>`);
  1430. }
  1431. // if (ac33 != "") {
  1432. // ac3Arr.push("");
  1433. // ac3Arr.push(`<p>${ac33}</p>`);
  1434. // }
  1435. ac3Arr.push(ac34);
  1436. if (ac35 != "") {
  1437. ac3Arr.push(`<p>${ac35}</p>`);
  1438. }
  1439. // 先推送初始消息
  1440. const aiMessage3 = reactive({
  1441. sender: "ai",
  1442. class: "content3",
  1443. type: "content3",
  1444. content: "",
  1445. isTyping: true,
  1446. error: apiStatus.three.error ? "3" : "",
  1447. isEnd: "2",
  1448. });
  1449. addTypingTask(aiMessage3, ac3Arr, 200);
  1450. } else {
  1451. if (
  1452. apiStatus.one.isEnd &&
  1453. apiStatus.two.error &&
  1454. !apiStatus.two.isError
  1455. ) {
  1456. apiStatus.two.isError = true;
  1457. chatStore.messages.push({
  1458. class: "ing",
  1459. type: "ing",
  1460. flag: false,
  1461. content: "系统正在为您努力加载中,请稍后再试",
  1462. });
  1463. chatStore.isLoading = false;
  1464. chatStore.chatInput = false;
  1465. emit("enableInput");
  1466. }
  1467. }
  1468. }
  1469. // 检查ThreeAPI(需要TwoAPI已执行)
  1470. if (
  1471. apiStatus.two.executed &&
  1472. apiStatus.three.completed &&
  1473. !apiStatus.three.executed
  1474. ) {
  1475. if (apiStatus.three.result) {
  1476. apiStatus.three.executed = true;
  1477. console.log("执行ThreeAPI代码:", apiStatus.three.result);
  1478. // 将第三个接口的音频添加到播放队列(确保顺序:API3)
  1479. if (audioPreloadStatus.five.url) {
  1480. chatStore.messages[chatStore.inputUserIndex].audioArray.push(
  1481. audioPreloadStatus.five.url
  1482. );
  1483. if (chatStore.currentUserIndex == chatStore.inputUserIndex) {
  1484. addToAudioQueue(audioPreloadStatus.five.url, "API3-第一个");
  1485. } else {
  1486. chatStore.messages[
  1487. chatStore.inputUserIndex
  1488. ].audioStatus = false;
  1489. }
  1490. console.log(
  1491. "音频队列:添加API3音频(link),当前队列长度:",
  1492. audioQueue.value.length
  1493. );
  1494. }
  1495. // 在这里添加ThreeAPI成功后需要执行的代码
  1496. // 添加标题-策略共振决策模型
  1497. addTypingTask(
  1498. {
  1499. sender: "ai",
  1500. class: "title3",
  1501. type: "title3",
  1502. content:
  1503. "https://d31zlh4on95l9h.cloudfront.net/images/d1fa1f4cbd6452796a4c5368d9f57c4d.png",
  1504. },
  1505. "",
  1506. 50
  1507. );
  1508. // 添加内容框4
  1509. const ac5 = `<p>${result23.data.zjqssc2}</p>`;
  1510. // 先推送初始消息
  1511. const aiMessage5 = reactive({
  1512. sender: "ai",
  1513. class: "content3",
  1514. type: "content3",
  1515. content: "",
  1516. isTyping: true,
  1517. });
  1518. addTypingTask(aiMessage5, ["", ac5], 240);
  1519. const ac6 = "该内容由AI生成,请注意甄别";
  1520. // 先推送初始消息
  1521. const aiMessage6 = reactive({
  1522. sender: "ai",
  1523. class: "mianze",
  1524. type: "mianze",
  1525. content: "",
  1526. isTyping: true,
  1527. end: true,
  1528. });
  1529. addTypingTask(aiMessage6, ["", ac6], 210);
  1530. } else {
  1531. if (
  1532. apiStatus.two.isEnd &&
  1533. apiStatus.three.error &&
  1534. !apiStatus.three.isError
  1535. ) {
  1536. apiStatus.three.isError = true;
  1537. chatStore.messages.push({
  1538. class: "ing",
  1539. type: "ing",
  1540. flag: false,
  1541. content: "系统正在为您努力加载中,请稍后再试",
  1542. });
  1543. chatStore.isLoading = false;
  1544. chatStore.chatInput = false;
  1545. emit("enableInput");
  1546. }
  1547. }
  1548. }
  1549. // 检查是否所有API都已完成并执行
  1550. if (
  1551. apiStatus.one.completed &&
  1552. apiStatus.two.completed &&
  1553. apiStatus.three.completed
  1554. ) {
  1555. console.log("所有API已完成,开始收集预加载的音频URL");
  1556. // 收集所有预加载的音频URL
  1557. const audioUrls = [];
  1558. console.log("预加载音频状态检查:");
  1559. console.log("audioPreloadStatus:", audioPreloadStatus);
  1560. if (audioPreloadStatus.one.url) {
  1561. console.log(
  1562. "添加预加载音频URL one:",
  1563. audioPreloadStatus.one.url
  1564. );
  1565. audioUrls.push(audioPreloadStatus.one.url);
  1566. }
  1567. if (audioPreloadStatus.two.url) {
  1568. console.log(
  1569. "添加预加载音频URL two:",
  1570. audioPreloadStatus.two.url
  1571. );
  1572. audioUrls.push(audioPreloadStatus.two.url);
  1573. }
  1574. if (audioPreloadStatus.three.url) {
  1575. console.log(
  1576. "添加预加载音频URL three:",
  1577. audioPreloadStatus.three.url
  1578. );
  1579. audioUrls.push(audioPreloadStatus.three.url);
  1580. }
  1581. if (audioPreloadStatus.four.url) {
  1582. console.log(
  1583. "添加预加载音频URL four:",
  1584. audioPreloadStatus.four.url
  1585. );
  1586. audioUrls.push(audioPreloadStatus.four.url);
  1587. }
  1588. console.log("收集到的预加载音频URLs:", audioUrls);
  1589. console.log("语音是否启用:", audioStore.isVoiceEnabled);
  1590. // 音频播放逻辑已移至各个接口的执行代码中
  1591. console.log("所有接口执行完成,音频已在各接口中单独播放");
  1592. }
  1593. };
  1594. const handleOneAPI = async () => {
  1595. try {
  1596. result21 = await deepNineSecondOneAPI(params2);
  1597. if (result21.code == 400) {
  1598. throw new Error("API返回错误码400,请求失败");
  1599. }
  1600. console.log("OneAPI成功返回:", result21);
  1601. apiStatus.one.completed = true;
  1602. apiStatus.one.result = result21;
  1603. // 预加载第一个接口的音频 - link1和link
  1604. if (result21?.data?.link1) {
  1605. await preloadAudio(result21.data.link1.trim(), "one");
  1606. } else {
  1607. audioPreloadStatus.one.loaded = true;
  1608. }
  1609. if (result21?.data?.link) {
  1610. await preloadAudio(result21.data.link.trim(), "two");
  1611. } else {
  1612. audioPreloadStatus.two.loaded = true;
  1613. }
  1614. // 检查是否可以执行
  1615. checkAndExecuteInOrder();
  1616. } catch (error) {
  1617. console.error("OneAPI失败:", error);
  1618. apiStatus.one.completed = true;
  1619. apiStatus.one.error = error;
  1620. audioPreloadStatus.one.loaded = true; // 失败时也标记为已处理
  1621. audioPreloadStatus.two.loaded = true; // 失败时也标记为已处理
  1622. // 即使失败也要检查后续执行
  1623. checkAndExecuteInOrder();
  1624. }
  1625. };
  1626. const handleTwoAPI = async () => {
  1627. try {
  1628. result22 = await deepNineSecondTwoAPI(params2);
  1629. if (result22.code == 400) {
  1630. throw new Error("API返回错误码400,请求失败");
  1631. }
  1632. console.log("TwoAPI成功返回:", result22);
  1633. apiStatus.two.completed = true;
  1634. apiStatus.two.result = result22;
  1635. // 预加载第二个接口的音频 - link3和link1
  1636. if (result22?.data?.link3) {
  1637. await preloadAudio(result22.data.link3.trim(), "three");
  1638. } else {
  1639. audioPreloadStatus.three.loaded = true;
  1640. }
  1641. if (result22?.data?.link1) {
  1642. await preloadAudio(result22.data.link1.trim(), "four");
  1643. } else {
  1644. audioPreloadStatus.four.loaded = true;
  1645. }
  1646. // 检查是否可以执行
  1647. checkAndExecuteInOrder();
  1648. } catch (error) {
  1649. console.error("TwoAPI失败:", error);
  1650. apiStatus.two.completed = true;
  1651. apiStatus.two.error = error;
  1652. audioPreloadStatus.three.loaded = true;
  1653. audioPreloadStatus.four.loaded = true;
  1654. checkAndExecuteInOrder();
  1655. }
  1656. };
  1657. const handleThreeAPI = async () => {
  1658. try {
  1659. result23 = await deepNineSecondThreeAPI(params2);
  1660. if (result23.code == 400) {
  1661. throw new Error("API返回错误码400,请求失败");
  1662. }
  1663. // result23 = await dbqbSecondThreeAPI();
  1664. console.log("ThreeAPI成功返回:", result23);
  1665. apiStatus.three.completed = true;
  1666. apiStatus.three.result = result23;
  1667. // 预加载第三个接口的音频 - 只有link字段
  1668. if (result23?.data?.link) {
  1669. await preloadAudio(result23.data.link.trim(), "five");
  1670. } else {
  1671. audioPreloadStatus.three.loaded = true;
  1672. }
  1673. // 检查是否可以执行
  1674. checkAndExecuteInOrder();
  1675. } catch (error) {
  1676. console.error("ThreeAPI失败:", error);
  1677. apiStatus.three.completed = true;
  1678. apiStatus.three.error = error;
  1679. audioPreloadStatus.three.loaded = true;
  1680. checkAndExecuteInOrder();
  1681. }
  1682. };
  1683. if (isNineTurns) {
  1684. handleOneAPI();
  1685. handleTwoAPI();
  1686. handleThreeAPI();
  1687. } else {
  1688. chatStore.messages.pop();
  1689. chatStore.messages.push({
  1690. class: "ing",
  1691. type: "ing",
  1692. flag: false,
  1693. content: "数据缺失,请稍后重试",
  1694. });
  1695. chatStore.isLoading = false;
  1696. chatStore.chatInput = false;
  1697. chatStore.firstAPICall = false;
  1698. emit("enableInput");
  1699. }
  1700. } catch (e) {
  1701. console.error("请求失败:", e);
  1702. chatStore.firstAPICall = false;
  1703. hasValidData.value = false; // 请求失败时设置数据无效
  1704. } finally {
  1705. // chatStore.setLoading(false);
  1706. await homepageChatStore.getUserCount();
  1707. }
  1708. }
  1709. }
  1710. },
  1711. { deep: false }
  1712. );
  1713. // 点击历史记录
  1714. watch(
  1715. () => chatStore.dbqbClickRecord,
  1716. (newValue, oldValue) => {
  1717. console.log("new", newValue);
  1718. if (!newValue || Object.keys(newValue).length === 0) {
  1719. return;
  1720. }
  1721. const clickRecord = ref(newValue);
  1722. console.log("dbqbClickRecord 发生变化:", clickRecord.value);
  1723. // 🔧 新增:完整的任务停止逻辑
  1724. try {
  1725. // 1. 停止所有音频相关任务
  1726. chatStore.currentUserIndex = null;
  1727. audioStore.stop(); // 暂停语音
  1728. // 🔧 新增:重置音频队列状态,确保新音频能够自动播放
  1729. audioQueue.value = [];
  1730. isPlayingAudio.value = false;
  1731. currentPlayIndex = 0;
  1732. isCallingPlayNext = false;
  1733. // 🔧 新增:重置音频预加载状态
  1734. audioPreloadStatus.one = { loaded: false, url: null };
  1735. audioPreloadStatus.two = { loaded: false, url: null };
  1736. audioPreloadStatus.three = { loaded: false, url: null };
  1737. audioPreloadStatus.four = { loaded: false, url: null };
  1738. audioPreloadStatus.five = { loaded: false, url: null };
  1739. // 🔧 新增:清理音频实例
  1740. if (audioStore.soundInstance) {
  1741. audioStore.soundInstance.stop();
  1742. audioStore.soundInstance.unload();
  1743. audioStore.soundInstance = null;
  1744. }
  1745. audioStore.nowSound = null;
  1746. // 2. 停止API调用和状态重置
  1747. // 🔧 新增:重置API状态
  1748. apiStatus.one = {
  1749. completed: false,
  1750. result: null,
  1751. error: null,
  1752. isError: false,
  1753. isEnd: false,
  1754. };
  1755. apiStatus.two = {
  1756. completed: false,
  1757. result: null,
  1758. error: null,
  1759. isError: false,
  1760. isEnd: false,
  1761. };
  1762. apiStatus.three = {
  1763. completed: false,
  1764. result: null,
  1765. error: null,
  1766. isError: false,
  1767. isEnd: false,
  1768. };
  1769. // 🔧 新增:重置数据有效性标志
  1770. hasValidData.value = false;
  1771. // 3. 停止打字机效果
  1772. typingQueue.value = [];
  1773. isTypingInProgress.value = false;
  1774. // 4. 重置加载状态
  1775. chatStore.isLoading = false;
  1776. chatStore.chatInput = false;
  1777. emit("enableInput");
  1778. // 在下一个事件循环中清空 dbqbClickRecord
  1779. setTimeout(() => {
  1780. chatStore.dbqbClickRecord = {};
  1781. console.log("dbqbClickRecord 已清空");
  1782. }, 0);
  1783. } catch (error) {
  1784. console.error("停止任务时发生错误:", error);
  1785. }
  1786. if (
  1787. !clickRecord.value.wokeFlowData.One ||
  1788. !clickRecord.value.wokeFlowData.Two ||
  1789. !clickRecord.value.wokeFlowData.Three
  1790. ) {
  1791. return;
  1792. }
  1793. try {
  1794. // 清空聊天框内容
  1795. chatStore.messages = [];
  1796. const userAudioArray = [
  1797. clickRecord.value.wokeFlowData.One.link1,
  1798. clickRecord.value.wokeFlowData.One.link,
  1799. clickRecord.value.wokeFlowData.Two.link3,
  1800. ];
  1801. // if (clickRecord.value.wokeFlowData.Two.link2) {
  1802. // userAudioArray.push(clickRecord.value.wokeFlowData.Two.link2);
  1803. // }
  1804. userAudioArray.push(clickRecord.value.wokeFlowData.Two.link1);
  1805. userAudioArray.push(clickRecord.value.wokeFlowData.Three.link);
  1806. const userContent = {
  1807. sender: "user",
  1808. timestamp: clickRecord.value.createdTime,
  1809. content: clickRecord.value.keyword,
  1810. audioArray: userAudioArray,
  1811. audioStatus: false,
  1812. };
  1813. chatStore.messages.push(userContent);
  1814. chatStore.messages.push({
  1815. sender: "ai",
  1816. class: "title1",
  1817. type: "title1",
  1818. content: clickRecord.value.stockName + "深度共振分析图谱",
  1819. date: clickRecord.value.wokeFlowData.One.date,
  1820. });
  1821. const pc1 = marked(
  1822. clickRecord.value.wokeFlowData.One.name +
  1823. "\n" +
  1824. clickRecord.value.wokeFlowData.One.price +
  1825. "\n" +
  1826. clickRecord.value.wokeFlowData.One.date
  1827. );
  1828. chatStore.messages.push({
  1829. sender: "ai",
  1830. class: "content1",
  1831. type: "content1",
  1832. content: pc1,
  1833. });
  1834. const nineTurns = clickRecord.value.stockData.data;
  1835. // 度牛尺K线图
  1836. if (
  1837. nineTurns &&
  1838. nineTurns.DXT &&
  1839. nineTurns.JZJG &&
  1840. nineTurns.KLine20 &&
  1841. nineTurns.StockInformation &&
  1842. nineTurns.ZJQS
  1843. ) {
  1844. const nineTurnsData = JSON.parse(JSON.stringify(toRaw(nineTurns)));
  1845. console.log("处理 K 线数据 - 开始");
  1846. console.log("nineTurnsData", nineTurnsData);
  1847. const Kline20 = {
  1848. name: nineTurnsData.StockInformation.Name,
  1849. Kline: nineTurnsData,
  1850. };
  1851. // 打印K线数据结构
  1852. console.log("K线数据结构:", Kline20);
  1853. console.log("K线数据名称:", Kline20.name);
  1854. console.log("K线数据:", Kline20.Kline ? Kline20.Kline : null);
  1855. // 设置数据有效标志
  1856. hasValidData.value = true;
  1857. console.log("hasValidData设置为:", hasValidData.value);
  1858. // chatStore.messages.pop();
  1859. // 先推送K线图消息
  1860. const klineMessageId2 = `kline-${Date.now() + 1}`;
  1861. console.log("生成K线消息ID:", klineMessageId2);
  1862. chatStore.messages.push({
  1863. sender: "ai",
  1864. class: "content2",
  1865. type: "content2",
  1866. kline: true,
  1867. chartData: Kline20,
  1868. messageId: klineMessageId2,
  1869. hasValidData: true, // 添加hasValidData标志
  1870. klineType: 2,
  1871. });
  1872. // 在渲染完成后初始化图表
  1873. nextTick(() => {
  1874. console.log("nextTick开始 - 准备渲染图表");
  1875. console.log("消息列表:", chatStore.messages);
  1876. // 寻找最新添加的K线消息索引
  1877. let klineIndex = -1;
  1878. for (let i = 0; i < chatStore.messages.length; i++) {
  1879. if (chatStore.messages[i].messageId === klineMessageId2) {
  1880. klineIndex = i;
  1881. break;
  1882. }
  1883. }
  1884. console.log("找到的K线消息索引:", klineIndex);
  1885. if (klineIndex !== -1) {
  1886. const containerId = `kline-container-${klineIndex}`;
  1887. console.log("图表容器ID:", containerId);
  1888. // 确保DOM已经渲染完成
  1889. setTimeout(() => {
  1890. console.log("延时执行,确保DOM已渲染");
  1891. KlineCanvsEcharts(containerId);
  1892. }, 100); // 短暂延时确保DOM已渲染
  1893. } else {
  1894. console.warn("未找到K线消息");
  1895. }
  1896. });
  1897. }
  1898. // 添加标题-数据分析时代下的认知变现
  1899. chatStore.messages.push({
  1900. sender: "ai",
  1901. class: "title2",
  1902. type: "title2",
  1903. content: "",
  1904. });
  1905. // 添加图片-数据分析时代下的认知变现
  1906. chatStore.messages.push({
  1907. sender: "ai",
  1908. class: "content3",
  1909. type: "img1",
  1910. content:
  1911. "https://d31zlh4on95l9h.cloudfront.net/images/5baa0a449cf74fb6a1afb1c909a21194.png",
  1912. });
  1913. // 添加标题-结构框架分析
  1914. chatStore.messages.push({
  1915. sender: "ai",
  1916. class: "title3",
  1917. type: "title3",
  1918. content:
  1919. "https://d31zlh4on95l9h.cloudfront.net/images/9ab9d76b6906eb914fa1842dbcd56841.png",
  1920. });
  1921. // 添加内容框1
  1922. const pc2 = marked(clickRecord.value.wokeFlowData.One.jgkjfx);
  1923. // 先推送初始消息
  1924. chatStore.messages.push({
  1925. sender: "ai",
  1926. class: "content3",
  1927. type: "content3",
  1928. content: pc2,
  1929. });
  1930. // 添加标题-资金动向监控
  1931. chatStore.messages.push({
  1932. sender: "ai",
  1933. class: "title3",
  1934. type: "title3",
  1935. content:
  1936. " https://d31zlh4on95l9h.cloudfront.net/images/f95c44f83b3e3c52e88964631c199060.png",
  1937. });
  1938. const ac3 = `<p style="margin:0;color:#FFD700;font-weight:bold;display:flex;justify-content:center;font-size:22px">【资金异动信号】</p><p>${clickRecord.value.wokeFlowData.Two.dxtsc}</p><p style="margin:0;color:#FFD700;font-weight:bold;display:flex;justify-content:center;font-size:22px">【资金趋势导航】</p><p>${clickRecord.value.wokeFlowData.Two.zjqssc1}</p>`;
  1939. // 先推送初始消息
  1940. chatStore.messages.push({
  1941. sender: "ai",
  1942. class: "content3",
  1943. type: "content3",
  1944. content: ac3,
  1945. isTyping: true,
  1946. });
  1947. // 添加标题-策略共振决策模型
  1948. chatStore.messages.push({
  1949. sender: "ai",
  1950. class: "title3",
  1951. type: "title3",
  1952. content:
  1953. "https://d31zlh4on95l9h.cloudfront.net/images/d1fa1f4cbd6452796a4c5368d9f57c4d.png",
  1954. });
  1955. // 添加内容框4
  1956. const ac5 = `<p>${clickRecord.value.wokeFlowData.Three.zjqssc2}</p>`;
  1957. // 先推送初始消息
  1958. chatStore.messages.push({
  1959. sender: "ai",
  1960. class: "content3",
  1961. type: "content3",
  1962. content: ac5,
  1963. });
  1964. chatStore.messages.push({
  1965. sender: "ai",
  1966. class: "mianze",
  1967. type: "mianze",
  1968. content: "该内容由AI生成,请注意甄别",
  1969. end: true,
  1970. });
  1971. } catch (e) {
  1972. ElMessage.error("历史数据获取出错!");
  1973. console.error("e", e);
  1974. }
  1975. },
  1976. {
  1977. deep: true, // 深度监听对象内部属性的变化
  1978. immediate: true, // 立即执行一次回调
  1979. }
  1980. );
  1981. function KlineCanvsEcharts(containerId) {
  1982. function vwToPx(vw) {
  1983. console.log((window.innerWidth * vw) / 100, "vwToPx");
  1984. return (window.innerWidth * vw) / 100;
  1985. }
  1986. // console.log("KLine渲染: 开始处理数据, 容器ID:", containerId);
  1987. // 从 chatStore 中获取数据
  1988. const messages = chatStore.messages;
  1989. // console.log("KLine渲染: 获取到的消息:", messages);
  1990. let klineMessageIndex = -1;
  1991. let klineData = null;
  1992. klineMessageIndex = containerId.split("-")[2];
  1993. // console.log("KLine渲染: 找到K线消息索引:", klineMessageIndex);
  1994. if (
  1995. messages[klineMessageIndex].kline &&
  1996. messages[klineMessageIndex].chartData
  1997. ) {
  1998. klineData = messages[klineMessageIndex].chartData;
  1999. }
  2000. var KlineOption = {};
  2001. // 检测设备类型
  2002. const isMobile = window.innerWidth < 768;
  2003. const isTablet = window.innerWidth >= 768 && window.innerWidth < 1024;
  2004. console.log(
  2005. "KLine渲染: 设备类型",
  2006. isMobile ? "移动设备" : isTablet ? "平板设备" : "桌面设备"
  2007. );
  2008. if (messages[klineMessageIndex].klineType == 2) {
  2009. if (!klineData || !klineData.Kline) {
  2010. // console.warn("KLine渲染: 数据无效 - 在chatStore中找不到有效的K线数据");
  2011. return;
  2012. }
  2013. // 获取容器元素
  2014. const container = document.getElementById(containerId);
  2015. if (!container) {
  2016. // console.error("KLine渲染: 找不到容器元素:", containerId);
  2017. return;
  2018. }
  2019. // 创建图表实例
  2020. // console.log("KLine渲染: 创建图表实例");
  2021. try {
  2022. // 如果已有实例,先销毁
  2023. if (chartInstancesMap[containerId]) {
  2024. // console.log("KLine渲染: 销毁已有图表实例");
  2025. chartInstancesMap[containerId].dispose();
  2026. delete chartInstancesMap[containerId];
  2027. }
  2028. // 使用普通变量存储实例
  2029. chartInstancesMap[containerId] = echarts.init(container);
  2030. // console.log("KLine渲染: 图表实例创建成功");
  2031. } catch (error) {
  2032. // console.error("KLine渲染: 图表实例创建失败:", error);
  2033. return;
  2034. }
  2035. const nineTurns = klineData.Kline;
  2036. console.log("KLine渲染: Kline数据", nineTurns);
  2037. // 拿到相应的数据
  2038. const splitData = (b) => {
  2039. const a = JSON.parse(JSON.stringify(b));
  2040. let categoryData = [];
  2041. let values = [];
  2042. for (let i = 0; i < a.length; i++) {
  2043. categoryData.push(a[i].splice(0, 1)[0]);
  2044. values.push(a[i]);
  2045. }
  2046. return {
  2047. categoryData,
  2048. values,
  2049. };
  2050. };
  2051. function vwToPx(vw) {
  2052. return (window.innerWidth * vw) / 100;
  2053. }
  2054. // k线的数据
  2055. var dealData = splitData(nineTurns.KLine20);
  2056. var markPointData = [];
  2057. const getNineNum = (KLine20, JZJG) => {
  2058. JZJG.forEach((item, index) => {
  2059. let low = KLine20[index][3];
  2060. let high = KLine20[index][4];
  2061. if (item[1] != -1) {
  2062. if (item[1] == 9) {
  2063. markPointData.push({
  2064. name: "low",
  2065. coord: [index, low],
  2066. itemStyle: {
  2067. normal: {
  2068. color: "rgba(0,0,0,0)", // 标记点透明
  2069. },
  2070. },
  2071. label: {
  2072. normal: {
  2073. show: true,
  2074. position: "bottom",
  2075. formatter: `${item[1]}`,
  2076. textStyle: {
  2077. color: "green",
  2078. fontSize: window.innerWidth > 769 ? 18 : 15,
  2079. textBorderColor: "#FFFFFF",
  2080. textBorderWidth: 2,
  2081. fontWeight: "bold",
  2082. },
  2083. },
  2084. },
  2085. });
  2086. } else {
  2087. markPointData.push({
  2088. name: "low",
  2089. coord: [index, low],
  2090. itemStyle: {
  2091. normal: {
  2092. color: "rgba(0,0,0,0)", // 标记点透明
  2093. },
  2094. },
  2095. label: {
  2096. normal: {
  2097. show: true,
  2098. position: "bottom",
  2099. formatter: `${item[1]}`,
  2100. textStyle: {
  2101. color: "green",
  2102. fontSize: window.innerWidth > 769 ? 12 : 9,
  2103. textBorderColor: "#FFFFFF",
  2104. textBorderWidth: 2,
  2105. fontWeight: "bold",
  2106. },
  2107. },
  2108. },
  2109. });
  2110. }
  2111. }
  2112. if (item[2] != -1) {
  2113. if (item[2] == 9) {
  2114. markPointData.push({
  2115. name: "high",
  2116. coord: [index, high],
  2117. itemStyle: {
  2118. normal: {
  2119. color: "rgba(0,0,0,0)", // 标记点透明
  2120. },
  2121. },
  2122. label: {
  2123. normal: {
  2124. show: true,
  2125. position: "top",
  2126. formatter: `${item[2]}`,
  2127. textStyle: {
  2128. color: "#0099FF",
  2129. fontSize: window.innerWidth > 769 ? 18 : 15,
  2130. textBorderColor: "#FFFFFF",
  2131. textBorderWidth: 2,
  2132. fontWeight: "bold",
  2133. },
  2134. },
  2135. },
  2136. });
  2137. } else {
  2138. markPointData.push({
  2139. name: "high",
  2140. coord: [index, high],
  2141. itemStyle: {
  2142. normal: {
  2143. color: "rgba(0,0,0,0)", // 标记点透明
  2144. },
  2145. },
  2146. label: {
  2147. normal: {
  2148. show: true,
  2149. position: "top",
  2150. formatter: `${item[2]}`,
  2151. textStyle: {
  2152. color: "#0099FF",
  2153. fontSize: window.innerWidth > 769 ? 12 : 9,
  2154. textBorderColor: "#FFFFFF",
  2155. textBorderWidth: 2,
  2156. fontWeight: "bold",
  2157. },
  2158. },
  2159. },
  2160. });
  2161. }
  2162. }
  2163. });
  2164. };
  2165. getNineNum(nineTurns.KLine20, nineTurns.JZJG);
  2166. // console.log("markPointData", markPointData);
  2167. var arrRange = [];
  2168. var arrSwing = [];
  2169. var arrDXTBar = [];
  2170. var arrInvisibleBar = [];
  2171. var markPointDXT = [];
  2172. var arr5DXTBar = [];
  2173. var arr6DXTBar = [];
  2174. var arr7DXTBar = [];
  2175. var DXTmin = 0;
  2176. var DXTmax = 100;
  2177. const getDXT = (DXT) => {
  2178. DXT.forEach((item, index) => {
  2179. arrRange.push(item[1]);
  2180. arrSwing.push(item[2]);
  2181. if (item[2] > 0) {
  2182. arrDXTBar.push(item[2] * 2);
  2183. arrInvisibleBar.push(50);
  2184. DXTmax = Math.max(DXTmax, item[2] * 2 + 50);
  2185. } else {
  2186. arrDXTBar.push(-item[2] * 2);
  2187. arrInvisibleBar.push(item[2] * 2 + 50);
  2188. }
  2189. if (item[5] == 1) {
  2190. arr5DXTBar.push(100);
  2191. } else {
  2192. arr5DXTBar.push(0);
  2193. }
  2194. if (item[6] == 1) {
  2195. arr6DXTBar.push(70);
  2196. } else {
  2197. arr6DXTBar.push(0);
  2198. }
  2199. if (item[7] == 1) {
  2200. arr7DXTBar.push(40);
  2201. } else {
  2202. arr7DXTBar.push(0);
  2203. }
  2204. if (item[8] == 1) {
  2205. markPointDXT.push({
  2206. name: "DTX-8",
  2207. coord: [index, 30],
  2208. symbol: "triangle", // 三角形符号
  2209. symbolSize: 10, // 符号大小
  2210. symbolRotate: 0, // 向上的三角形(0度)
  2211. itemStyle: {
  2212. normal: {
  2213. color: "#FFFF00", // 标记点透明
  2214. borderColor: "#000000", // 边框颜色
  2215. borderWidth: 1,
  2216. },
  2217. },
  2218. label: {
  2219. normal: {
  2220. show: false,
  2221. },
  2222. },
  2223. });
  2224. }
  2225. if (item[9] == 1) {
  2226. markPointDXT.push({
  2227. name: "DTX-9",
  2228. coord: [index, 20],
  2229. symbolSize: 7, // 正方形大小
  2230. itemStyle: {
  2231. normal: {
  2232. color: "red", // 标记点透明
  2233. // borderColor: "#000000", // 边框颜色
  2234. // borderWidth: 1,
  2235. },
  2236. },
  2237. label: {
  2238. normal: {
  2239. show: false,
  2240. },
  2241. },
  2242. });
  2243. }
  2244. if (item[10] == 1) {
  2245. markPointDXT.push({
  2246. name: "DTX-10",
  2247. coord: [index, 15],
  2248. symbol: "rect", // 设置为正方形
  2249. symbolSize: 7, // 正方形大小
  2250. itemStyle: {
  2251. normal: {
  2252. color: "grey", // 标记点透明
  2253. // borderColor: "#000000", // 边框颜色
  2254. // borderWidth: 1,
  2255. },
  2256. },
  2257. label: {
  2258. normal: {
  2259. show: false,
  2260. },
  2261. },
  2262. });
  2263. }
  2264. });
  2265. };
  2266. getDXT(nineTurns.DXT);
  2267. var arrBlue = [];
  2268. var arrRed = [];
  2269. var arrWhite = [];
  2270. var arrYellow = [];
  2271. const getZJQS = (ZJQS) => {
  2272. ZJQS.forEach((item, index) => {
  2273. arrBlue.push(item[1]);
  2274. arrRed.push(item[2]);
  2275. arrWhite.push(item[3]);
  2276. arrYellow.push(item[4]);
  2277. });
  2278. };
  2279. getZJQS(nineTurns.ZJQS);
  2280. // console.log("arrBlue", arrBlue);
  2281. // console.log("arrRed", arrRed);
  2282. // console.log("arrWhite", arrWhite);
  2283. // console.log("arrYellow", arrYellow);
  2284. KlineOption = {
  2285. // 手放上去显示的内容
  2286. tooltip: {
  2287. position: function (point, params) {
  2288. if (params[0].seriesIndex == 1) {
  2289. return window.innerWidth > 768 ? ["12%", "42%"] : ["18%", "40%"];
  2290. }
  2291. },
  2292. // 调用接口之后方法
  2293. formatter: function (a, b, d) {
  2294. if (a[0].seriesIndex == 0) {
  2295. const KlineTag = ref([]); // 判断几根K线
  2296. const AIBullTag = ref([]);
  2297. // 找到第一个满足条件的数据
  2298. KlineTag.value = a.find((item) => item.data[1])?.data || [];
  2299. // 找到第一个满足条件的非 '-' 数据
  2300. AIBullTag.value =
  2301. a.slice(4).find((item) => item.data[1] !== "-")?.data || [];
  2302. return (
  2303. a[0].name +
  2304. "<br/>" +
  2305. "开" +
  2306. ":" +
  2307. KlineTag.value[1] +
  2308. "<br/>" +
  2309. "收" +
  2310. ":" +
  2311. KlineTag.value[2] +
  2312. "<br/>" +
  2313. "低" +
  2314. ":" +
  2315. KlineTag.value[3] +
  2316. "<br/>" +
  2317. "高" +
  2318. ":" +
  2319. KlineTag.value[4]
  2320. );
  2321. } else if (a[0].seriesIndex == 1) {
  2322. return (
  2323. `<span style='color:red;'>RANGE: </span>` +
  2324. a[0].data +
  2325. " <span style='color:yellow'>SWING: </span>" +
  2326. a[1].data
  2327. );
  2328. } else {
  2329. return null;
  2330. // 格式化成交量显示
  2331. let formattedVolume;
  2332. if (a[0].data.value >= 10000) {
  2333. formattedVolume = (a[0].data.value / 10000).toFixed(2) + "w";
  2334. } else {
  2335. formattedVolume = a[0].data.value;
  2336. }
  2337. return a[0].name + "<br/>" + "量" + ":" + formattedVolume;
  2338. }
  2339. },
  2340. trigger: "axis",
  2341. axisPointer: {
  2342. //坐标轴指示器配置项
  2343. type: "cross", //‘line’直线指示器,‘cross’十字准星指示器,‘shadow’阴影指示器
  2344. },
  2345. backgroundColor: "rgba(119, 120, 125, 0.6)", // 提示框浮层的边框颜色。
  2346. borderWidth: 1, // 提示框浮层的边框宽。
  2347. borderColor: "#77787D", // 提示框浮层的边框颜色。
  2348. padding: 10, // 提示框浮层内边距,
  2349. textStyle: {
  2350. fontSize: window.innerWidth > 768 ? 12 : 8,
  2351. //提示框浮层上的文字样式
  2352. color: "#fff",
  2353. },
  2354. },
  2355. // 手放上去时拉的框
  2356. axisPointer: {
  2357. link: [
  2358. {
  2359. xAxisIndex: "all", // 同时触发所有图形的 x 坐标轴指示器
  2360. },
  2361. ],
  2362. label: {
  2363. backgroundColor: "#77787D", // 文本标签的背景颜色
  2364. },
  2365. },
  2366. toolbox: {
  2367. show: false,
  2368. },
  2369. grid: [
  2370. {
  2371. left: window.innerWidth > 768 ? "12%" : "18%",
  2372. right: window.innerWidth > 768 ? "10%" : "12%",
  2373. top: window.innerWidth > 768 ? "8%" : "8%",
  2374. height: window.innerWidth > 768 ? "34%" : "32%",
  2375. containLabel: false,
  2376. },
  2377. {
  2378. left: window.innerWidth > 768 ? "12%" : "18%",
  2379. right: window.innerWidth > 768 ? "10%" : "12%",
  2380. top: window.innerWidth > 768 ? "48%" : "46%",
  2381. height: window.innerWidth > 768 ? "18%" : "20%",
  2382. containLabel: false,
  2383. },
  2384. {
  2385. left: window.innerWidth > 768 ? "12%" : "18%",
  2386. right: window.innerWidth > 768 ? "10%" : "12%",
  2387. top: window.innerWidth > 768 ? "68%" : "68%",
  2388. height: window.innerWidth > 768 ? "18%" : "20%",
  2389. containLabel: false,
  2390. },
  2391. {
  2392. left: window.innerWidth > 768 ? "12%" : "18%",
  2393. right: window.innerWidth > 768 ? "10%" : "12%",
  2394. top: window.innerWidth > 768 ? "48%" : "46%",
  2395. height: window.innerWidth > 768 ? "18%" : "20%",
  2396. containLabel: false,
  2397. },
  2398. ],
  2399. xAxis: [
  2400. {
  2401. type: "category",
  2402. data: dealData.categoryData,
  2403. boundaryGap: true, // 坐标轴两边是否留空,false表示不留空(通常用于K线图)
  2404. axisLine: { onZero: false }, // 设置坐标轴是否通过零点,onZero:false表示不强制穿过零点
  2405. splitLine: { show: false }, // 是否显示分隔线,false表示不显示
  2406. min: "dataMin", // 坐标轴最小值,'dataMin'表示从数据的最小值开始
  2407. max: "dataMax", // 坐标轴最大值,'dataMax'表示从数据的最大值开始
  2408. axisPointer: {
  2409. label: {
  2410. show: false,
  2411. },
  2412. z: 100, // 坐标轴指示器的层级,较大的值会让它显示在其他元素上方
  2413. },
  2414. axisLine: {
  2415. lineStyle: {
  2416. normal: {
  2417. color: "black", // 坐标轴线的颜色
  2418. },
  2419. },
  2420. }, //
  2421. axisLabel: { show: false }, // 隐藏刻度标签
  2422. axisTick: { show: false }, // 隐藏刻度线
  2423. },
  2424. // 短线通1
  2425. {
  2426. type: "category",
  2427. gridIndex: 1,
  2428. data: dealData.categoryData,
  2429. boundaryGap: true,
  2430. axisPointer: {
  2431. label: {
  2432. show: false,
  2433. },
  2434. },
  2435. axisLine: { lineStyle: { normal: { color: "black" } } },
  2436. axisLabel: {
  2437. show: false,
  2438. interval: "auto",
  2439. },
  2440. axisTick: { show: true }, // 隐藏刻度线
  2441. },
  2442. // 下方成交量图的X轴
  2443. {
  2444. type: "category",
  2445. gridIndex: 2,
  2446. data: dealData.categoryData,
  2447. boundaryGap: true,
  2448. axisLine: { lineStyle: { normal: { color: "black" } } },
  2449. axisLabel: {
  2450. show: true,
  2451. fontSize: window.innerWidth > 768 ? 12 : 9,
  2452. interval: "auto",
  2453. },
  2454. axisTick: { show: false }, // 隐藏刻度线
  2455. },
  2456. // 短线通2
  2457. {
  2458. type: "category",
  2459. gridIndex: 3,
  2460. data: dealData.categoryData,
  2461. boundaryGap: true,
  2462. axisPointer: {
  2463. label: {
  2464. show: false,
  2465. },
  2466. },
  2467. axisLine: { show: false },
  2468. axisLabel: { show: false },
  2469. axisTick: { show: false }, // 隐藏刻度线
  2470. },
  2471. ],
  2472. // 控制纵坐标展示数据
  2473. yAxis: [
  2474. {
  2475. scale: true,
  2476. gridIndex: 0,
  2477. position: "left",
  2478. axisLabel: {
  2479. inside: false,
  2480. align: "right",
  2481. fontSize: window.innerWidth > 768 ? 12 : 9,
  2482. },
  2483. axisLine: {
  2484. show: true,
  2485. lineStyle: {
  2486. normal: {
  2487. color: "black",
  2488. },
  2489. },
  2490. },
  2491. axisTick: { show: false },
  2492. splitLine: { show: false },
  2493. },
  2494. {
  2495. scale: true,
  2496. gridIndex: 1,
  2497. min: DXTmin,
  2498. max: DXTmax,
  2499. axisLabel: {
  2500. show: true,
  2501. fontSize: window.innerWidth > 768 ? 12 : 9,
  2502. margin: 8, // 添加边距以获得更好的间距
  2503. },
  2504. axisLine: { show: true, lineStyle: { normal: { color: "black" } } },
  2505. axisTick: { show: false },
  2506. splitLine: { show: true, lineStyle: { normal: { type: "dashed" } } }, // 添加分割线以提高可读性
  2507. boundaryGap: ["20%", "20%"], // 为坐标轴边界添加内边距
  2508. },
  2509. {
  2510. scale: true,
  2511. gridIndex: 2,
  2512. splitNumber: 4, // 增加分割数以获得更好的间距
  2513. minInterval: 1, // 确保标签之间的最小间隔
  2514. axisLabel: {
  2515. show: true,
  2516. fontSize: window.innerWidth > 768 ? 12 : 9,
  2517. margin: 8, // 添加边距以获得更好的间距
  2518. },
  2519. axisLine: { show: true, lineStyle: { normal: { color: "black" } } },
  2520. axisTick: { show: false },
  2521. splitLine: { show: true, lineStyle: { normal: { type: "dashed" } } }, // 添加分割线以提高可读性
  2522. boundaryGap: ["20%", "20%"], // 为坐标轴边界添加内边距
  2523. },
  2524. {
  2525. scale: true,
  2526. gridIndex: 3,
  2527. min: DXTmin,
  2528. max: DXTmax,
  2529. axisLabel: { show: false },
  2530. axisLine: { show: false },
  2531. axisTick: { show: false },
  2532. splitLine: { show: false }, // 添加分割线以提高可读性
  2533. boundaryGap: ["20%", "20%"], // 为坐标轴边界添加内边距
  2534. },
  2535. ],
  2536. // 下拉条
  2537. dataZoom: [
  2538. {
  2539. type: "inside",
  2540. xAxisIndex: [0, 1, 2, 3],
  2541. },
  2542. {
  2543. show: true,
  2544. xAxisIndex: [0, 1, 2, 3],
  2545. type: "slider",
  2546. top: window.innerWidth > 768 ? "90%" : "92%",
  2547. height: window.innerWidth > 768 ? "25" : "20",
  2548. // left: window.innerWidth > 768 ? "10%" : "8%",
  2549. start: 98,
  2550. end: 100,
  2551. },
  2552. ],
  2553. series: [
  2554. {
  2555. name: "九转结构",
  2556. type: "candlestick",
  2557. barWidth: "50%", // 设置和上方图表一致的柱子宽度
  2558. data: dealData.values,
  2559. xAxisIndex: 0, // 使用第一个 X 轴
  2560. yAxisIndex: 0, // 使用第一个 Y 轴
  2561. markPoint: {
  2562. symbol: "circle",
  2563. symbolSize: 10,
  2564. data: markPointData,
  2565. z: 5, // 确保标记显示在最上层
  2566. },
  2567. itemStyle: {
  2568. normal: {
  2569. color: "red", // 默认颜色
  2570. color0: "green",
  2571. borderColor: "red",
  2572. borderColor0: "green",
  2573. },
  2574. },
  2575. gridIndex: 0,
  2576. },
  2577. {
  2578. name: "短线通-RANGE",
  2579. type: "line",
  2580. xAxisIndex: 1,
  2581. yAxisIndex: 1,
  2582. data: arrRange,
  2583. smooth: false,
  2584. symbol: "none",
  2585. itemStyle: {
  2586. normal: {
  2587. color: "#FA0096",
  2588. },
  2589. },
  2590. lineStyle: {
  2591. normal: {
  2592. width: 2,
  2593. type: "solid",
  2594. },
  2595. },
  2596. },
  2597. {
  2598. name: "短线通-SWING",
  2599. type: "line",
  2600. xAxisIndex: 1,
  2601. yAxisIndex: 1,
  2602. data: arrSwing,
  2603. smooth: false,
  2604. symbol: "none",
  2605. show: false,
  2606. itemStyle: {
  2607. normal: {
  2608. color: "transparent", // 设置为透明
  2609. opacity: 0, // 透明度为0
  2610. },
  2611. },
  2612. lineStyle: {
  2613. normal: {
  2614. width: 0, // 线宽为0
  2615. opacity: 0, // 透明度为0
  2616. },
  2617. },
  2618. },
  2619. {
  2620. name: "短线通-隐形柱",
  2621. type: "bar",
  2622. stack: "Total",
  2623. xAxisIndex: 1,
  2624. yAxisIndex: 1,
  2625. data: arrInvisibleBar,
  2626. barWidth: "2", // 设置柱子宽度
  2627. // barGap: "-100%", // 完全重叠
  2628. z: 5, // 确保标记显示在最上层
  2629. itemStyle: {
  2630. normal: {
  2631. color: "transparent",
  2632. // color: "#000",
  2633. },
  2634. },
  2635. lineStyle: {
  2636. normal: {
  2637. width: 2,
  2638. type: "solid",
  2639. },
  2640. },
  2641. },
  2642. {
  2643. name: "短线通-红蓝柱",
  2644. type: "bar",
  2645. stack: "Total",
  2646. xAxisIndex: 1,
  2647. yAxisIndex: 1,
  2648. data: arrDXTBar,
  2649. barWidth: "20%", // 设置柱子宽度
  2650. barGap: "-40%", // 完全重叠
  2651. z: 5, // 确保标记显示在最上层
  2652. itemStyle: {
  2653. normal: {
  2654. color: function (params) {
  2655. // console.log(params.dataIndex);
  2656. if (nineTurns.DXT[params.dataIndex][4] == 1) {
  2657. return "#FA0096";
  2658. } else if (nineTurns.DXT[params.dataIndex][4] == 2) {
  2659. return "#0099FF";
  2660. }
  2661. return "transparent";
  2662. },
  2663. },
  2664. },
  2665. lineStyle: {
  2666. normal: {
  2667. width: 2,
  2668. type: "solid",
  2669. },
  2670. },
  2671. },
  2672. {
  2673. name: "短线通-黄柱",
  2674. type: "bar",
  2675. xAxisIndex: 3,
  2676. yAxisIndex: 3,
  2677. data: arr5DXTBar,
  2678. barWidth: "40%", // 设置柱子宽度
  2679. z: 6, // 确保标记显示在最上层
  2680. itemStyle: {
  2681. normal: {
  2682. color: "rgb(154,154,16)",
  2683. },
  2684. },
  2685. lineStyle: {
  2686. normal: {
  2687. width: 2,
  2688. type: "solid",
  2689. },
  2690. },
  2691. },
  2692. {
  2693. name: "短线通-灰柱",
  2694. type: "bar",
  2695. xAxisIndex: 3,
  2696. yAxisIndex: 3,
  2697. data: arr6DXTBar,
  2698. barWidth: "40%", // 设置柱子宽度
  2699. z: 6, // 确保标记显示在最上层
  2700. itemStyle: {
  2701. normal: {
  2702. color: "rgb(140,140,140)",
  2703. },
  2704. },
  2705. lineStyle: {
  2706. normal: {
  2707. width: 2,
  2708. type: "solid",
  2709. },
  2710. },
  2711. },
  2712. {
  2713. name: "短线通-蓝柱",
  2714. type: "bar",
  2715. xAxisIndex: 3,
  2716. yAxisIndex: 3,
  2717. data: arr7DXTBar,
  2718. barWidth: "40%", // 设置柱子宽度
  2719. barGap: "-100%", // 完全重叠
  2720. z: 6, // 确保标记显示在最上层
  2721. itemStyle: {
  2722. normal: {
  2723. color: "rgb(110,200,255)",
  2724. },
  2725. },
  2726. lineStyle: {
  2727. normal: {
  2728. width: 2,
  2729. type: "solid",
  2730. },
  2731. },
  2732. },
  2733. {
  2734. name: "短线通-其他指标",
  2735. type: "line",
  2736. xAxisIndex: 1,
  2737. yAxisIndex: 1,
  2738. markPoint: {
  2739. symbol: "circle",
  2740. symbolSize: 10,
  2741. data: markPointDXT,
  2742. z: 7, // 确保标记显示在最上层
  2743. },
  2744. data: [],
  2745. smooth: false,
  2746. symbol: "none",
  2747. itemStyle: {
  2748. normal: {
  2749. color: function (params) {
  2750. // params.value 是当前数据点的值
  2751. // params.dataIndex 是当前数据点的索引
  2752. return params.value > 0 ? "#FA0096" : "#0099FF";
  2753. },
  2754. },
  2755. },
  2756. lineStyle: {
  2757. normal: {
  2758. width: 2,
  2759. type: "solid",
  2760. },
  2761. },
  2762. },
  2763. {
  2764. name: "资金趋势-蓝色线",
  2765. type: "line",
  2766. xAxisIndex: 2,
  2767. yAxisIndex: 2,
  2768. data: arrBlue,
  2769. smooth: false,
  2770. symbol: "none",
  2771. itemStyle: {
  2772. normal: {
  2773. color: "#00A2A2",
  2774. },
  2775. },
  2776. lineStyle: {
  2777. normal: {
  2778. width: 2,
  2779. type: "solid",
  2780. },
  2781. },
  2782. },
  2783. {
  2784. name: "资金趋势-红色线",
  2785. type: "line",
  2786. xAxisIndex: 2,
  2787. yAxisIndex: 2,
  2788. data: arrRed,
  2789. smooth: false,
  2790. symbol: "none",
  2791. itemStyle: {
  2792. normal: {
  2793. color: "#FF0000",
  2794. },
  2795. },
  2796. lineStyle: {
  2797. normal: {
  2798. width: 2,
  2799. type: "solid",
  2800. },
  2801. },
  2802. },
  2803. {
  2804. name: "资金趋势-白色线",
  2805. type: "line",
  2806. xAxisIndex: 2,
  2807. yAxisIndex: 2,
  2808. data: arrWhite,
  2809. smooth: false,
  2810. symbol: "none",
  2811. itemStyle: {
  2812. normal: {
  2813. color: "grey",
  2814. },
  2815. },
  2816. lineStyle: {
  2817. normal: {
  2818. width: 2,
  2819. type: "solid",
  2820. },
  2821. },
  2822. },
  2823. {
  2824. name: "资金趋势-黄色线",
  2825. type: "line",
  2826. xAxisIndex: 2,
  2827. yAxisIndex: 2,
  2828. data: arrYellow,
  2829. smooth: false,
  2830. symbol: "none",
  2831. itemStyle: {
  2832. normal: {
  2833. color: "#FFFF00",
  2834. },
  2835. },
  2836. lineStyle: {
  2837. normal: {
  2838. width: 2,
  2839. type: "solid",
  2840. },
  2841. },
  2842. },
  2843. ],
  2844. };
  2845. }
  2846. // console.log("KLine渲染: 图表配置完成");
  2847. try {
  2848. // 应用配置
  2849. // console.log("KLine渲染: 开始设置图表选项");
  2850. chartInstancesMap[containerId].setOption(KlineOption);
  2851. // console.log("KLine渲染: 图表选项设置成功");
  2852. // 窗口大小变化时重新渲染图表
  2853. const resizeFunc = _.throttle(
  2854. function () {
  2855. console.log("窗口大小改变,调整图表大小");
  2856. if (
  2857. chartInstancesMap[containerId] &&
  2858. !chartInstancesMap[containerId].isDisposed()
  2859. ) {
  2860. // 如果设备类型发生变化,重新渲染
  2861. const newIsMobile = window.innerWidth < 768;
  2862. const newIsTablet =
  2863. window.innerWidth >= 768 && window.innerWidth < 1024;
  2864. if (newIsMobile !== isMobile || newIsTablet !== isTablet) {
  2865. console.log("设备类型变化,重新渲染图表");
  2866. KlineCanvsEcharts(containerId);
  2867. return;
  2868. }
  2869. chartInstancesMap[containerId].resize();
  2870. }
  2871. },
  2872. 1000,
  2873. { trailing: false }
  2874. );
  2875. // 给resize事件绑定一个特定的函数名,便于后续移除
  2876. window[`resize_${containerId}`] = resizeFunc;
  2877. // 绑定resize事件
  2878. window.removeEventListener("resize", window[`resize_${containerId}`]);
  2879. window.addEventListener("resize", window[`resize_${containerId}`]);
  2880. // console.log("KLine渲染: 图表渲染完成");
  2881. } catch (error) {
  2882. // console.error("KLine渲染: 图表渲染出错", error);
  2883. }
  2884. }
  2885. watch(
  2886. () => audioStore.isVoiceEnabled,
  2887. (newVal) => {
  2888. // 添加状态锁定逻辑
  2889. if (newVal === audioStore.lastVoiceState) return;
  2890. audioStore.lastVoiceState = newVal;
  2891. if (newVal) {
  2892. console.log("开启语音播放");
  2893. // 添加重试机制
  2894. const tryPlay = () => {
  2895. if (!audioStore.ttsUrl) return; // 新增空值判断
  2896. if (audioStore.soundInstance?.playing()) return;
  2897. playAudio(audioStore.ttsUrl);
  2898. setTimeout(() => {
  2899. if (!audioStore.soundInstance?.playing()) {
  2900. Howler.unload();
  2901. }
  2902. }, 1000);
  2903. };
  2904. tryPlay();
  2905. } else {
  2906. console.log("关闭语音播放");
  2907. pauseAudio();
  2908. }
  2909. },
  2910. { immediate: true }
  2911. );
  2912. watch(
  2913. () => dataStore.activeTabIndex,
  2914. (newVal) => {
  2915. setTimeout(() => {
  2916. console.log("activeTabIndex变化:", newVal);
  2917. // 当标签页切换回来时,重新渲染所有图表
  2918. if (newVal === 0) {
  2919. console.log("切换到AI聊天页,重新渲染图表");
  2920. // 延迟执行以确保DOM已渲染
  2921. renderAllKlineCharts();
  2922. }
  2923. }, 1000);
  2924. },
  2925. { immediate: true } // 添加immediate属性,确保初始化时执行一次
  2926. );
  2927. const scrollToTop = () => {
  2928. homepageChatStore.dbqbScrollToTop = !homepageChatStore.dbqbScrollToTop;
  2929. };
  2930. // 添加渲染所有K线图的方法
  2931. async function renderAllKlineCharts() {
  2932. console.log("重新渲染所有K线图");
  2933. // 查找所有K线消息
  2934. const messages = chatStore.messages;
  2935. for (let i = 0; i < messages.length; i++) {
  2936. if (messages[i].kline && messages[i].chartData) {
  2937. const containerId = `kline-container-${i}`;
  2938. console.log(`尝试渲染K线图: ${containerId}`);
  2939. // 确保DOM已经渲染
  2940. const container = document.getElementById(containerId);
  2941. console.log("container", container);
  2942. await nextTick();
  2943. if (container) {
  2944. // 渲染图表
  2945. KlineCanvsEcharts(containerId);
  2946. } else {
  2947. console.warn(`找不到容器: ${containerId}`);
  2948. }
  2949. }
  2950. }
  2951. }
  2952. const clearAudio = () => {
  2953. // 停止音频播放
  2954. if (audioStore.isPlaying) {
  2955. audioStore.stop();
  2956. console.log("组件卸载,音频已停止");
  2957. }
  2958. // 清理队列系统
  2959. audioQueue.value = [];
  2960. isPlayingAudio.value = false;
  2961. currentPlayIndex = 0;
  2962. isCallingPlayNext = false;
  2963. // 清理音频实例
  2964. audioStore.soundInstance = null;
  2965. audioStore.nowSound = null;
  2966. // 停止所有 Howler 实例
  2967. Howler.stop();
  2968. Howler.unload();
  2969. // 重置音频状态
  2970. audioStore.isPlaying = false;
  2971. audioStore.isPaused = false;
  2972. audioStore.playbackPosition = 0;
  2973. // 清理预加载状态
  2974. Object.keys(audioPreloadStatus).forEach((key) => {
  2975. audioPreloadStatus[key] = { loaded: false, url: null };
  2976. });
  2977. chatStore.currentUserIndex = -1;
  2978. };
  2979. // 初始化随机GIF
  2980. onMounted(() => {
  2981. clearAudio();
  2982. // 检测移动设备
  2983. const isMobile =
  2984. /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
  2985. navigator.userAgent
  2986. );
  2987. if (isMobile) {
  2988. // 强制移动设备使用 Web Audio API
  2989. Howler.html5PoolSize = 1; // 限制HTML5音频池大小
  2990. Howler.autoSuspend = false; // 禁用自动挂起
  2991. Howler.usingWebAudio = true; // 强制使用Web Audio API
  2992. // 激活音频上下文
  2993. const activateAudioContext = () => {
  2994. if (Howler.ctx && Howler.ctx.state === "suspended") {
  2995. Howler.ctx.resume();
  2996. console.log("音频上下文已激活");
  2997. }
  2998. };
  2999. // 监听用户交互以激活音频上下文
  3000. document.addEventListener("touchstart", activateAudioContext, {
  3001. once: true,
  3002. });
  3003. document.addEventListener("click", activateAudioContext, { once: true });
  3004. }
  3005. // 初始化marked组件
  3006. marked.setOptions({
  3007. breaks: true, // 支持换行符转换为 <br>
  3008. gfm: true, // 启用 GitHub Flavored Markdown
  3009. sanitize: false, // 不清理 HTML(谨慎使用)
  3010. smartLists: true, // 智能列表
  3011. smartypants: true, // 智能标点符号
  3012. xhtml: false, // 不使用 XHTML 输出
  3013. renderer: renderer,
  3014. });
  3015. renderAllKlineCharts();
  3016. console.log("组件挂载完成");
  3017. // 重置音频下标
  3018. chatStore.currentUserIndex = null;
  3019. chatStore.messages.forEach((item) => {
  3020. if (item.sender == "user") {
  3021. item.audioStatus = false;
  3022. }
  3023. });
  3024. // 添加页面可见性变化监听器
  3025. document.addEventListener("visibilitychange", handleVisibilityChange);
  3026. // 添加DOM变化监听器
  3027. const observer = new MutationObserver((mutations) => {
  3028. mutations.forEach((mutation) => {
  3029. if (mutation.type === "childList" && mutation.addedNodes.length) {
  3030. // 检查是否添加了图表容器
  3031. const containers = document.querySelectorAll(
  3032. '[id^="kline-container-"]'
  3033. );
  3034. if (containers.length) {
  3035. // console.log("DOM变化监听到K线容器:", Array.from(containers).map(el => el.id));
  3036. }
  3037. }
  3038. });
  3039. });
  3040. // 开始监听DOM变化
  3041. observer.observe(document.body, { childList: true, subtree: true });
  3042. });
  3043. // 页面可见性变化处理
  3044. let wasPlayingBeforeHidden = false;
  3045. const handleVisibilityChange = () => {
  3046. if (document.hidden) {
  3047. // 页面被隐藏时,如果音频正在播放,则暂停并记录状态
  3048. if (audioStore.isPlaying) {
  3049. wasPlayingBeforeHidden = true;
  3050. audioStore.pause();
  3051. console.log("页面切换离开,音频已暂停");
  3052. } else {
  3053. wasPlayingBeforeHidden = false;
  3054. }
  3055. } else {
  3056. // 页面重新可见时,如果之前在播放,则恢复播放
  3057. if (wasPlayingBeforeHidden && !audioStore.isPlaying) {
  3058. audioStore.play();
  3059. console.log("页面切换回来,音频已恢复播放");
  3060. wasPlayingBeforeHidden = false;
  3061. }
  3062. }
  3063. };
  3064. // 组件卸载时清理所有图表实例和事件监听器
  3065. onUnmounted(() => {
  3066. // 移除页面可见性变化监听器
  3067. document.removeEventListener("visibilitychange", handleVisibilityChange);
  3068. clearAudio();
  3069. // 清理所有图表实例
  3070. Object.keys(chartInstancesMap).forEach((key) => {
  3071. if (chartInstancesMap[key]) {
  3072. // 移除resize事件监听
  3073. if (window[`resize_${key}`]) {
  3074. window.removeEventListener("resize", window[`resize_${key}`]);
  3075. delete window[`resize_${key}`];
  3076. }
  3077. // 销毁图表实例
  3078. chartInstancesMap[key].dispose();
  3079. delete chartInstancesMap[key];
  3080. }
  3081. });
  3082. });
  3083. </script>
  3084. <template>
  3085. <div class="chat-container">
  3086. <div id="deepNine-top-anchor"></div>
  3087. <!-- GIF区域 -->
  3088. <div class="gif-area">
  3089. <img
  3090. src="https://d31zlh4on95l9h.cloudfront.net/images/a686991d0a26bdbafd938a15da93d5b4.png"
  3091. alt="深度九大模型logo"
  3092. class="bgc"
  3093. />
  3094. <img
  3095. src="https://d31zlh4on95l9h.cloudfront.net/images/35bf808538183be0062e4647570a9abd.png"
  3096. alt="深度九大模型logo"
  3097. class="logo1"
  3098. />
  3099. <img
  3100. src="https://d31zlh4on95l9h.cloudfront.net/images/0c09c892051e7ae16cbff6091075aee9.png"
  3101. alt="深度九大模型标题logo"
  3102. class="logo2"
  3103. />
  3104. </div>
  3105. <div v-for="(msg, index) in chatMsg" :key="index">
  3106. <!-- 用户消息容器包含喇叭按钮 -->
  3107. <div v-if="msg.sender === 'user'" class="user-message-container">
  3108. <div class="user-msg">
  3109. <div class="user-content">
  3110. <img
  3111. :src="msg.audioStatus ? voice : voiceNoActive"
  3112. class="user-message-speaker"
  3113. :class="{
  3114. 'speaker-active': msg.audioStatus,
  3115. }"
  3116. @click="toggleVoiceForUser(index)"
  3117. alt="喇叭"
  3118. />
  3119. <div
  3120. :class="{
  3121. 'message-bubble': true,
  3122. [msg.sender]: msg.sender,
  3123. [msg.class]: msg.class,
  3124. }"
  3125. >
  3126. <div v-html="msg.content"></div>
  3127. </div>
  3128. </div>
  3129. <div v-if="msg.timestamp" class="user-sendTime">
  3130. {{ moment(msg.timestamp).format("YYYY-MM-DD HH:mm:ss") }}
  3131. </div>
  3132. </div>
  3133. </div>
  3134. <!-- AI消息和其他类型消息 -->
  3135. <div
  3136. v-else
  3137. :class="{
  3138. 'message-bubble': true,
  3139. [msg.sender]: msg.sender,
  3140. [msg.class]: msg.class,
  3141. }"
  3142. >
  3143. <div v-if="msg.type === 'kline'" class="kline-container">
  3144. <div :id="'kline-container-' + index" class="chart-mount-point">
  3145. <div v-if="!msg.hasValidData" class="no-data-message">
  3146. <p>暂无K线数据</p>
  3147. </div>
  3148. </div>
  3149. </div>
  3150. <div v-else-if="msg.type == 'ing'" class="ai-message-container">
  3151. <img
  3152. v-if="msg.gif"
  3153. :src="msg.gif"
  3154. alt="思考过程"
  3155. class="thinking-gif"
  3156. />
  3157. <div class="ai-message-content" :class="{ fourStep: msg.nowrap }">
  3158. <div v-if="msg.flag">
  3159. <span>{{ msg.content }}</span>
  3160. <span class="loading-dots">
  3161. <span class="dot">.</span>
  3162. <span class="dot">.</span>
  3163. <span class="dot">.</span>
  3164. <span class="dot">.</span>
  3165. <span class="dot">.</span>
  3166. <span class="dot">.</span>
  3167. </span>
  3168. </div>
  3169. <div v-else v-html="msg.content"></div>
  3170. </div>
  3171. </div>
  3172. <div
  3173. v-else-if="msg.type == 'title1'"
  3174. style="display: flex; width: 100%"
  3175. >
  3176. <div class="mainTitle">
  3177. {{ msg.content }}
  3178. </div>
  3179. <div class="date">
  3180. {{ msg.date }}
  3181. </div>
  3182. </div>
  3183. <div v-else-if="msg.type == 'title2'" class="title2">
  3184. <img
  3185. class="title1Img"
  3186. src="https://d31zlh4on95l9h.cloudfront.net/images/c5c2887c56cde033c362cecae4cda4b4.png"
  3187. alt="出错了"
  3188. />
  3189. </div>
  3190. <div v-else-if="msg.type == 'title3'" class="title3">
  3191. <img class="title2Img" :src="msg.content" alt="出错了" />
  3192. </div>
  3193. <div v-else-if="msg.type == 'content1'" class="content1">
  3194. <div v-if="msg.kline" class="kline-container content1chart">
  3195. <div :id="'kline-container-' + index" class="chart-mount-point">
  3196. <div v-if="!msg.hasValidData" class="no-data-message">
  3197. <p>暂无数据</p>
  3198. </div>
  3199. </div>
  3200. </div>
  3201. <div v-else class="content1Text">
  3202. <div v-html="msg.content" class="text1"></div>
  3203. </div>
  3204. </div>
  3205. <div v-else-if="msg.type == 'content2'" class="content2">
  3206. <div class="kline-container content2chart">
  3207. <div :id="'kline-container-' + index" class="chart-mount-pointJN">
  3208. <div v-if="!msg.hasValidData" class="no-data-message">
  3209. <p>暂无数据</p>
  3210. </div>
  3211. </div>
  3212. </div>
  3213. </div>
  3214. <div v-else-if="msg.type == 'img1'" class="img1">
  3215. <div class="img1Bk">
  3216. <img class="img1Img" :src="msg.content" alt="出错了" />
  3217. </div>
  3218. </div>
  3219. <div v-else-if="msg.type == 'content3'" class="content3">
  3220. <div class="content3Text">
  3221. <div v-html="msg.content" class="text3"></div>
  3222. </div>
  3223. </div>
  3224. <div v-else-if="msg.type == 'mianze'" class="mianze">
  3225. <div v-html="msg.content"></div>
  3226. </div>
  3227. <div v-else v-html="msg.content"></div>
  3228. </div>
  3229. </div>
  3230. </div>
  3231. <!-- 全局返回顶部按钮 -->
  3232. <div v-if="chatMsg.length > 0" class="back-to-top" @click="scrollToTop">
  3233. <svg
  3234. width="24"
  3235. height="24"
  3236. viewBox="0 0 24 24"
  3237. fill="none"
  3238. xmlns="http://www.w3.org/2000/svg"
  3239. >
  3240. <path
  3241. d="M12 4L12 20M12 4L6 10M12 4L18 10"
  3242. stroke="currentColor"
  3243. stroke-width="2"
  3244. stroke-linecap="round"
  3245. stroke-linejoin="round"
  3246. />
  3247. </svg>
  3248. </div>
  3249. </template>
  3250. <style scoped>
  3251. p {
  3252. font-size: 20px;
  3253. }
  3254. .bgc {
  3255. position: absolute;
  3256. z-index: -1;
  3257. max-width: 440px;
  3258. min-width: 300px;
  3259. top: -15px;
  3260. width: 40%;
  3261. height: auto;
  3262. /* 添加旋转动画 */
  3263. animation: rotate 10s linear infinite reverse;
  3264. }
  3265. /* 定义旋转动画 */
  3266. @keyframes rotate {
  3267. from {
  3268. transform: rotate(0deg);
  3269. }
  3270. to {
  3271. transform: rotate(360deg);
  3272. }
  3273. }
  3274. .logo1 {
  3275. max-width: 350px;
  3276. min-width: 200px;
  3277. width: 15%;
  3278. }
  3279. .logo2 {
  3280. margin-top: 20px;
  3281. max-width: 350px;
  3282. min-width: 200px;
  3283. width: 30%;
  3284. /* position: relative; */
  3285. }
  3286. .chat-container {
  3287. display: flex;
  3288. flex-direction: column;
  3289. overflow: hidden;
  3290. }
  3291. .gif-area {
  3292. padding: 70px 0px;
  3293. position: relative;
  3294. /* height: 30vh; */
  3295. display: flex;
  3296. flex-direction: column;
  3297. justify-content: center;
  3298. align-items: center;
  3299. flex-shrink: 0;
  3300. /* 防止GIF区域被压缩 */
  3301. }
  3302. .message-area {
  3303. margin-top: 2%;
  3304. flex: 1;
  3305. /* 消息区域占据剩余空间 */
  3306. overflow-y: auto;
  3307. padding: 20px;
  3308. display: flex;
  3309. flex-direction: column;
  3310. gap: 15px;
  3311. }
  3312. .marquee-container {
  3313. /* position: absolute; */
  3314. bottom: 0;
  3315. width: 100%;
  3316. /* ga */
  3317. }
  3318. .marquee-row {
  3319. white-space: nowrap;
  3320. overflow: visible;
  3321. padding: 8px 0;
  3322. width: 100%;
  3323. }
  3324. .marquee-item {
  3325. display: inline-block;
  3326. margin: 0 15px;
  3327. padding: 8px 20px;
  3328. background: rgba(255, 255, 255, 0.9);
  3329. /* 白色背景 */
  3330. border-radius: 10px;
  3331. /* 圆角矩形 */
  3332. color: #333;
  3333. /* 文字颜色改为深色 */
  3334. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  3335. /* 添加阴影 */
  3336. transition: all 0.3s;
  3337. transition: color 0.3s;
  3338. }
  3339. .top {
  3340. animation: marquee 25s linear infinite;
  3341. /* 默认动画是运行状态 */
  3342. animation-play-state: running;
  3343. }
  3344. .bottom {
  3345. animation: marquee 15s linear infinite reverse;
  3346. /* 默认动画是运行状态 */
  3347. animation-play-state: running;
  3348. }
  3349. /* 返回顶部按钮样式 */
  3350. .back-to-top {
  3351. position: sticky !important;
  3352. bottom: 20px !important;
  3353. left: calc(100% - 70px) !important;
  3354. width: 50px !important;
  3355. height: 50px !important;
  3356. background: linear-gradient(135deg, #00d4ff 0%, #0066cc 100%) !important;
  3357. border-radius: 50% !important;
  3358. display: flex !important;
  3359. align-items: center !important;
  3360. justify-content: center !important;
  3361. cursor: pointer !important;
  3362. transition: all 0.3s ease !important;
  3363. z-index: 100 !important;
  3364. color: white !important;
  3365. opacity: 1 !important;
  3366. visibility: visible !important;
  3367. margin-top: 20px !important;
  3368. margin-bottom: 20px !important;
  3369. }
  3370. .back-to-top:hover {
  3371. transform: translateY(-3px);
  3372. box-shadow: 0 6px 20px rgba(0, 212, 255, 0.5);
  3373. background: linear-gradient(135deg, #00e6ff 0%, #0077dd 100%);
  3374. }
  3375. .back-to-top:active {
  3376. transform: translateY(-1px);
  3377. }
  3378. /* 添加PC端专用速度 */
  3379. @media (min-width: 768px) {
  3380. .top {
  3381. animation-duration: 35s;
  3382. /* PC端改为35秒 */
  3383. }
  3384. .bottom {
  3385. animation-duration: 35s;
  3386. /* PC端改为35秒 */
  3387. }
  3388. }
  3389. @keyframes marquee {
  3390. 0% {
  3391. transform: translateX(100%);
  3392. }
  3393. 100% {
  3394. transform: translateX(-250%);
  3395. }
  3396. }
  3397. .loading-dots {
  3398. display: inline-block;
  3399. }
  3400. .dot {
  3401. opacity: 0.4;
  3402. animation: loading 1.4s infinite;
  3403. }
  3404. .dot:nth-child(1) {
  3405. animation-delay: 0s;
  3406. }
  3407. .dot:nth-child(2) {
  3408. animation-delay: 0.2s;
  3409. }
  3410. .dot:nth-child(3) {
  3411. animation-delay: 0.4s;
  3412. }
  3413. .dot:nth-child(4) {
  3414. animation-delay: 0.6s;
  3415. }
  3416. .dot:nth-child(5) {
  3417. animation-delay: 0.8s;
  3418. }
  3419. .dot:nth-child(6) {
  3420. animation-delay: 1s;
  3421. }
  3422. @keyframes loading {
  3423. 0%,
  3424. 60%,
  3425. 100% {
  3426. opacity: 0.4;
  3427. }
  3428. 30% {
  3429. opacity: 1;
  3430. }
  3431. }
  3432. .message-bubble {
  3433. max-width: 80%;
  3434. margin: 10px 0px;
  3435. padding: 15px 20px;
  3436. position: relative;
  3437. }
  3438. /* 用户消息容器样式 */
  3439. .user-message-container {
  3440. display: flex;
  3441. align-items: flex-end;
  3442. margin: 10px 0px;
  3443. justify-content: flex-end;
  3444. gap: 10px;
  3445. /* align-items: center; */
  3446. flex-direction: column;
  3447. }
  3448. .user-msg {
  3449. margin-left: auto;
  3450. display: flex;
  3451. flex-direction: column;
  3452. }
  3453. .user-content {
  3454. display: flex;
  3455. height: 100%;
  3456. align-items: center;
  3457. margin-right: 5px;
  3458. justify-content: flex-end;
  3459. padding: 0 20px;
  3460. }
  3461. .user-sendTime {
  3462. width: 100%;
  3463. text-align: center;
  3464. color: rgba(255, 255, 255, 0.6);
  3465. font-size: 0.8rem;
  3466. }
  3467. .user-message-speaker {
  3468. width: 32px;
  3469. height: 32px;
  3470. object-fit: contain;
  3471. margin-right: 5px;
  3472. cursor: pointer;
  3473. transition: all 0.3s ease;
  3474. }
  3475. .user-message-speaker:hover {
  3476. transform: scale(1.1);
  3477. }
  3478. .user-message-speaker.speaker-active {
  3479. animation: pulse 1.5s infinite;
  3480. }
  3481. @keyframes pulse {
  3482. 0% {
  3483. transform: scale(1);
  3484. }
  3485. 50% {
  3486. transform: scale(1.1);
  3487. }
  3488. 100% {
  3489. transform: scale(1);
  3490. }
  3491. }
  3492. .message-bubble.user {
  3493. color: #6d22f8;
  3494. background: white;
  3495. font-weight: bold;
  3496. border-radius: 10px;
  3497. margin: 0;
  3498. display: flex;
  3499. align-items: center;
  3500. word-break: break-word; /* 启用强制换行 */
  3501. }
  3502. .message-bubble.ai {
  3503. background: #2b378d;
  3504. color: #ffffff;
  3505. margin: 0 auto;
  3506. /* border-bottom-left-radius: 5px; */
  3507. }
  3508. .message-bubble.ing {
  3509. background: #ffffff;
  3510. color: #000000;
  3511. font-weight: bold;
  3512. border-radius: 10px;
  3513. margin-left: 20px;
  3514. margin-right: auto;
  3515. display: flex;
  3516. align-items: center;
  3517. width: fit-content;
  3518. }
  3519. .message-bubble.ai.title1 {
  3520. width: 100%;
  3521. display: flex;
  3522. border-radius: 10px 10px 0px 0px;
  3523. /* border-bottom-left-radius: 5px; */
  3524. }
  3525. .mainTitle {
  3526. font-size: 16px;
  3527. font-weight: bold;
  3528. background-image: url("@/assets/img/AiEmotion/bk01.png");
  3529. background-repeat: no-repeat;
  3530. background-size: 100% 100%;
  3531. min-width: 200px;
  3532. width: 20vw;
  3533. max-width: 50%;
  3534. height: 50px;
  3535. padding: 15px 10px;
  3536. text-align: center;
  3537. line-height: 20px;
  3538. overflow: hidden;
  3539. text-overflow: ellipsis;
  3540. white-space: nowrap;
  3541. box-sizing: border-box;
  3542. }
  3543. .date {
  3544. font-size: 1.5rem;
  3545. font-weight: bold;
  3546. margin-left: auto;
  3547. /* width: 100px; */
  3548. display: flex;
  3549. justify-content: center;
  3550. align-items: center;
  3551. }
  3552. .message-bubble.ai.title2 {
  3553. width: 100%;
  3554. display: flex;
  3555. justify-content: center;
  3556. align-items: center;
  3557. }
  3558. .title1Img {
  3559. max-width: 500px;
  3560. width: 90vw;
  3561. }
  3562. .message-bubble.ai.title3 {
  3563. width: 100%;
  3564. display: flex;
  3565. justify-content: center;
  3566. align-items: center;
  3567. }
  3568. .title2Img {
  3569. max-width: 500px;
  3570. width: 90vw;
  3571. }
  3572. .message-bubble.ai.content1 {
  3573. width: 100%;
  3574. display: flex;
  3575. justify-content: center;
  3576. align-items: center;
  3577. }
  3578. .content1chart {
  3579. background-image: url("@/assets/img/AIchat/罗盘边框.png");
  3580. background-repeat: no-repeat;
  3581. background-size: 100% 100%;
  3582. width: 50vw;
  3583. min-width: 350px;
  3584. display: flex;
  3585. justify-content: center;
  3586. align-items: center;
  3587. }
  3588. .content1Text {
  3589. background-image: url("@/assets/img/AIchat/框.png");
  3590. background-repeat: no-repeat;
  3591. background-size: 100% 100%;
  3592. width: 50vw;
  3593. min-width: 350px;
  3594. /* height: 20vw; */
  3595. /* max-height: 400px; */
  3596. padding: 5% 0;
  3597. }
  3598. .text1 {
  3599. font-weight: bold;
  3600. /* margin-left: 6%; */
  3601. /* margin-bottom: 10px; */
  3602. margin: 0px 6% 10px 6%;
  3603. font-size: 20px;
  3604. }
  3605. .message-bubble.ai.content2 {
  3606. width: 100%;
  3607. display: flex;
  3608. justify-content: center;
  3609. align-items: center;
  3610. }
  3611. .content2chart {
  3612. background-image: url("@/assets/img/AIchat/PCbackPic.png");
  3613. background-repeat: no-repeat;
  3614. background-size: 100% 100%;
  3615. width: 50vw;
  3616. min-width: 350px;
  3617. display: flex;
  3618. justify-content: center;
  3619. align-items: center;
  3620. height: calc(500px + 10vw) !important;
  3621. }
  3622. .message-bubble.ai.img1 {
  3623. width: 100%;
  3624. display: flex;
  3625. justify-content: center;
  3626. align-items: center;
  3627. }
  3628. .img1Bk {
  3629. background-image: url("@/assets/img/AIchat/边框.png");
  3630. background-repeat: no-repeat;
  3631. background-size: 100% 100%;
  3632. width: 50vw;
  3633. min-width: 350px;
  3634. /* height: 20vw; */
  3635. /* max-height: 400px; */
  3636. padding: 5% 0px;
  3637. display: flex;
  3638. justify-content: center;
  3639. align-items: center;
  3640. }
  3641. .img1Img {
  3642. width: 90%;
  3643. }
  3644. .message-bubble.ai.content3 {
  3645. width: 100%;
  3646. display: flex;
  3647. justify-content: center;
  3648. align-items: center;
  3649. }
  3650. .content3Text {
  3651. background-image: url("@/assets/img/AIchat/边框.png");
  3652. background-repeat: no-repeat;
  3653. background-size: 100% 100%;
  3654. width: 50vw;
  3655. min-width: 350px;
  3656. /* height: 20vw; */
  3657. /* max-height: 400px; */
  3658. padding: 5% 0px;
  3659. }
  3660. .text3 {
  3661. /* font-weight: bold; */
  3662. /* margin-left: 6%; */
  3663. /* margin-bottom: 10px; */
  3664. margin: 0px 6% 10px 6%;
  3665. font-size: 20px;
  3666. }
  3667. .message-bubble.ai.mianze {
  3668. width: 100%;
  3669. text-align: center;
  3670. font-weight: bold;
  3671. font-size: 24px;
  3672. border-radius: 0px 0px 10px 10px;
  3673. }
  3674. .kline-container {
  3675. margin-top: 10px;
  3676. /* 最小高度 */
  3677. min-height: 320px;
  3678. /* 视口高度单位 */
  3679. height: 40vh;
  3680. width: 50vw;
  3681. }
  3682. @media (max-width: 768px) {
  3683. .gif-area {
  3684. padding: 0px 0px;
  3685. position: relative;
  3686. /* height: 30vh; */
  3687. display: flex;
  3688. flex-direction: column;
  3689. justify-content: center;
  3690. align-items: center;
  3691. flex-shrink: 0;
  3692. }
  3693. .logo1 {
  3694. max-width: 350px;
  3695. min-width: 200px;
  3696. width: 15%;
  3697. scale: 0.8;
  3698. }
  3699. .logo2 {
  3700. margin-top: 20px;
  3701. max-width: 350px;
  3702. min-width: 200px;
  3703. width: 80%;
  3704. /* position: relative; */
  3705. }
  3706. .kline-container {
  3707. min-width: 75vw;
  3708. }
  3709. .content1Text {
  3710. width: 77vw;
  3711. min-width: 0px;
  3712. /* height: 20vw; */
  3713. /* min-height: 150px; */
  3714. }
  3715. .date {
  3716. font-size: 14px;
  3717. text-align: end;
  3718. }
  3719. .text1 {
  3720. font-size: 20px;
  3721. }
  3722. .content2chart {
  3723. background-image: url("@/assets/img/AIchat/new-app-bgc.png") !important;
  3724. height: 100vw;
  3725. }
  3726. .img1Bk {
  3727. width: 77vw;
  3728. min-width: 0px;
  3729. /* height: 20vw; */
  3730. /* min-height: 150px; */
  3731. }
  3732. .content3Text {
  3733. width: 77vw;
  3734. min-width: 0px;
  3735. /* height: 20vw; */
  3736. /* min-height: 150px; */
  3737. }
  3738. .text3 {
  3739. font-size: 20px;
  3740. }
  3741. .message-bubble.ai.mianze {
  3742. font-size: 18px;
  3743. }
  3744. .back-to-top {
  3745. left: calc(100% - 65px) !important;
  3746. width: 45px !important;
  3747. height: 45px !important;
  3748. }
  3749. }
  3750. .kline-container .chart-mount-point {
  3751. display: flex;
  3752. justify-content: center;
  3753. align-items: center;
  3754. height: 80%;
  3755. width: 90%;
  3756. }
  3757. .kline-container .chart-mount-pointJN {
  3758. display: flex;
  3759. justify-content: center;
  3760. align-items: center;
  3761. height: 100%;
  3762. width: 100%;
  3763. }
  3764. /* AI消息容器样式 */
  3765. .ai-message-container {
  3766. display: flex;
  3767. align-items: center;
  3768. gap: 10px;
  3769. margin-right: auto;
  3770. /* max-width: 80%; */
  3771. }
  3772. /* 思考过程动图样式 */
  3773. .thinking-gif {
  3774. width: 40px;
  3775. height: 40px;
  3776. object-fit: contain;
  3777. margin-top: 5px;
  3778. border-radius: 8px;
  3779. animation: float 2s ease-in-out infinite;
  3780. }
  3781. @keyframes float {
  3782. 0%,
  3783. 100% {
  3784. transform: translateY(0px);
  3785. }
  3786. 50% {
  3787. transform: translateY(-5px);
  3788. }
  3789. }
  3790. /* AI消息内容样式 */
  3791. .ai-message-content {
  3792. display: flex;
  3793. align-items: center;
  3794. /* white-space: nowrap; */
  3795. width: fit-content;
  3796. overflow: visible;
  3797. }
  3798. .fourStep {
  3799. white-space: nowrap;
  3800. }
  3801. @media only screen and (max-width: 480px) {
  3802. .back-to-top {
  3803. left: calc(100% - 60px) !important;
  3804. width: 40px !important;
  3805. height: 40px !important;
  3806. }
  3807. .back-to-top svg {
  3808. width: 20px;
  3809. height: 20px;
  3810. }
  3811. }
  3812. </style>