|
|
<script setup> import { ref, onMounted, watch, nextTick, reactive, onUnmounted } from "vue"; import { ElDialog, ElMessage } from "element-plus"; import MessageItem from "@/components/deepNine/MessageItem.vue"; import ThinkingGif from "@/components/deepNine/ThinkingGif.vue"; import { dbqbFirstAPI, dbqbSecondOneAPI, dbqbSecondTwoAPI, dbqbSecondThreeAPI, dbqbSecondFourAPI, dataListAPI, } from "../api/AIxiaocaishen"; import { getNineTurnsAPI, deepNineFirstAPI, deepNineSecondOneAPI, deepNineSecondTwoAPI, deepNineSecondThreeAPI, } from "../api/deepNine"; import { useUserStore } from "../store/userPessionCode"; import { useDeepNineStore } from "../store/deepNine"; import { useDeepNineAudioStore } from "../store/deepNineAudio"; import { useDataStore } from "@/store/dataList.js"; import { marked } from "marked"; // 引入marked库
// 导入思考过程GIF
import thinkingGif from "@/assets/img/gif/思考.gif"; import analyzeGif from "@/assets/img/gif/解析.gif"; import generateGif from "@/assets/img/gif/生成.gif"; import katex from "katex"; // 引入 KaTeX 库
import { htmlToText } from "html-to-text"; import { Howl, Howler } from "howler"; import * as echarts from "echarts"; import _, { add } from "lodash"; import moment from "moment"; import title1 from "@/assets/img/AIchat/核心价值评估.png"; import title2 from "@/assets/img/AIchat/主力作战.png"; import title3 from "@/assets/img/AIchat/攻防三维.png"; import title4 from "@/assets/img/AIchat/综合作战.png"; import logo2 from "@/assets/img/AIchat/开启无限财富.png"; import getCountAll from "../assets/img/homePage/get-count-all.png"; import voice from "../assets/img/homePage/tail/voice.png"; import voiceNoActive from "../assets/img/homePage/tail/voice-no-active.png";
import { useChatStore } from "../store/chat"; const homepageChatStore = useChatStore();
const chatStore = useDeepNineStore(); const audioStore = useDeepNineAudioStore(); const dataStore = useDataStore();
// 将这些变量移到全局作用域
const audioQueue = ref([]); const isPlayingAudio = ref(false); let currentPlayIndex = 0; let isCallingPlayNext = false;
// 音频预加载状态
const audioPreloadStatus = { one: { loaded: false, url: null }, two: { loaded: false, url: null }, three: { loaded: false, url: null }, four: { loaded: false, url: null }, five: { loaded: false, url: null }, };
// 音频队列顺序管理
const audioQueueOrder = { "API1-第一个": 1, // 第一个接口的第一个音频 (link1)
"API1-第二个": 2, // 第一个接口的第二个音频 (link)
"API2-第一个": 3, // 第二个接口的第一个音频 (link3)
"API2-第二个": 4, // 第二个接口的第二个音频 (link1)
"API3-第一个": 5, // 第三个接口的音频 (link)
};
// 播放下一个音频的函数
const playNextAudio = () => { if (isCallingPlayNext) { console.log("playNextAudio已在执行中,跳过重复调用"); return; }
if (currentPlayIndex >= audioQueue.value.length) { console.log( "所有音频播放完成,重置到第一个音频 currentPlayIndex", currentPlayIndex ); // 播放完成后重置到第一个音频,但不自动播放
currentPlayIndex = 0; audioStore.isPlaying = false; audioStore.isPaused = false; audioStore.playbackPosition = 0; // 清除音频实例,确保下次点击从头开始
audioStore.soundInstance = null; audioStore.nowSound = null; if (audioQueue.value.length > 0) { audioStore.setCurrentAudioUrl(audioQueue.value[0]); } return; }
isCallingPlayNext = true; const audioInfo = audioQueue.value[currentPlayIndex];
if (!audioInfo || !audioInfo.url) { console.warn(`音频信息无效,跳过索引 ${currentPlayIndex}`); currentPlayIndex++; isCallingPlayNext = false; playNextAudio(); return; }
console.log(`开始播放 ${audioInfo.name},索引: ${currentPlayIndex}`);
const audio = new Howl({ src: [audioInfo.url], html5: false, format: ["mp3", "acc"], // rate: 2,
retryCount: 0, onplay: () => { audioStore.isPlaying = true; isPlayingAudio.value = true; isCallingPlayNext = false; console.log(`${audioInfo.name}音频开始播放111`); }, onpause: () => { audioStore.isPlaying = false; audioStore.isPaused = true; audioStore.playbackPosition = audio.seek() || 0; console.log(`${audioInfo.name}音频已暂停`); }, onresume: () => { audioStore.isPlaying = true; audioStore.isPaused = false; console.log(`${audioInfo.name}音频继续播放`); }, onend: () => { console.log(`${audioInfo.name}音频播放完成,准备播放下一个`); audioStore.isPlaying = false; audioStore.isPaused = false; audioStore.playbackPosition = 0; isPlayingAudio.value = false;
currentPlayIndex++;
console.log( "currentPlayIndex", currentPlayIndex, "audioQueue.value.length", audioQueue.value.length ); if (currentPlayIndex < audioQueue.value.length) { console.log( `队列中还有音频,500ms后播放下一个 (索引:${currentPlayIndex})` ); setTimeout(() => { isCallingPlayNext = false; playNextAudio(); }, 200); } else { console.log("🎉 所有音频播放完成,清除音频实例"); chatStore.messages[chatStore.currentUserIndex].audioStatus = false; audioStore.nowSound = null; audioStore.soundInstance = null; isCallingPlayNext = false; } }, onstop: () => { console.log(`${audioInfo.name}音频被停止`); audioStore.isPlaying = false; audioStore.isPaused = false; audioStore.playbackPosition = 0; }, onloaderror: (id, err) => { console.error(`${audioInfo.name}音频播放失败:`, err); isPlayingAudio.value = false; isCallingPlayNext = false; setTimeout(() => { playNextAudio(); }, 100); }, });
audioStore.setCurrentAudioUrl(audioInfo.url); audioStore.nowSound = audio; audioStore.setAudioInstance(audio);
console.log(`尝试播放${audioInfo.name}音频`); audio.play(); };
// 添加音频到播放队列(确保顺序)
const addToAudioQueue = (url, name) => { console.log(`=== 添加音频到队列 ===`); console.log("URL:", url); console.log("Name:", name); console.log("音频启用状态:", audioStore.isVoiceEnabled);
if (url && audioStore.isVoiceEnabled) { const audioItem = { url, name, order: audioQueueOrder[name] || 999, }; audioQueue.value.push(audioItem);
// 按顺序排序队列
audioQueue.value.sort((a, b) => a.order - b.order);
console.log(`音频${name}已添加到播放队列,顺序:${audioItem.order}`); console.log( "当前队列顺序:", audioQueue.value.map((item) => `${item.name}(${item.order})`) ); // 只有在确实没有音频在播放且这是第一个音频时才开始播放
if ( !isPlayingAudio.value && !audioStore.isPlaying && audioQueue.value.length === 1 ) { console.log("✅ 条件满足:没有音频在播放且这是第一个音频,立即开始播放", { isPlayingAudio: isPlayingAudio.value, audioStoreIsPlaying: audioStore.isPlaying, queueLength: audioQueue.value.length, }); playNextAudio(); } else { console.log("⏳ 等待条件:", { isPlayingAudio: isPlayingAudio.value, audioStoreIsPlaying: audioStore.isPlaying, queueLength: audioQueue.value.length, reason: audioQueue.value.length > 1 ? "队列中已有其他音频" : "有音频正在播放", }); } } else { console.log("❌ 跳过添加音频:", { hasUrl: !!url, voiceEnabled: audioStore.isVoiceEnabled, }); } console.log(`=== 添加音频完成 ===`); };
// 语音播放控制函数
const toggleVoiceForUser = (index) => { console.log( "上一个按钮坐标", chatStore.currentUserIndex, "当前按钮坐标", index );
if ( !chatStore.messages[index].audioArray[0] || !chatStore.messages[index].audioArray[1] || !chatStore.messages[index].audioArray[2] || !chatStore.messages[index].audioArray[3] ) { return; }
// 先把当前按钮状态修改
chatStore.messages[index].audioStatus = !chatStore.messages[index].audioStatus; // 如果当前按钮和之前的按钮不是同一个则再修改之前的按钮的状态
if (chatStore.currentUserIndex != index) { if (chatStore.currentUserIndex != null) { if (audioStore.isPlaying) { audioStore.togglePlayPause(); } chatStore.messages[chatStore.currentUserIndex].audioStatus = false; }
// 强制停止所有音频实例(移动端兼容)
if (audioStore.soundInstance) { audioStore.soundInstance.stop(); audioStore.soundInstance = null; }
audioPreloadStatus.one = { loaded: false, url: null }; audioPreloadStatus.two = { loaded: false, url: null }; audioPreloadStatus.three = { loaded: false, url: null }; audioPreloadStatus.four = { loaded: false, url: null }; audioPreloadStatus.five = { loaded: false, url: null }; if (chatStore.messages[index].audioArray[0]) { audioPreloadStatus.one.loaded = true; audioPreloadStatus.one.url = chatStore.messages[index].audioArray[0]; } if (chatStore.messages[index].audioArray[1]) { audioPreloadStatus.two.loaded = true; audioPreloadStatus.two.url = chatStore.messages[index].audioArray[1]; } if (chatStore.messages[index].audioArray[2]) { audioPreloadStatus.three.loaded = true; audioPreloadStatus.three.url = chatStore.messages[index].audioArray[2]; } if (chatStore.messages[index].audioArray[3]) { audioPreloadStatus.four.loaded = true; audioPreloadStatus.four.url = chatStore.messages[index].audioArray[3]; } chatStore.currentUserIndex = index; audioQueue.value = []; isPlayingAudio.value = false; audioStore.soundInstance = null; currentPlayIndex = 0; isCallingPlayNext = false; setTimeout(() => { addToAudioQueue(chatStore.messages[index].audioArray[0], "API1-第一个"); addToAudioQueue(chatStore.messages[index].audioArray[1], "API2-第二个"); addToAudioQueue(chatStore.messages[index].audioArray[2], "API3-第三个"); addToAudioQueue(chatStore.messages[index].audioArray[3], "API4-第四个");
if (!audioStore.isVoiceEnabled) { audioStore.toggleVoice(); } else { if (audioStore.currentAudioUrl || audioStore.ttsUrl) { // audioStore.togglePlayPause();
} else { audioStore.toggleVoice(); } } }, 100); // 100ms延迟足够移动端清理音频实例
} else { if (!audioStore.isVoiceEnabled) { console.log("1111"); audioStore.toggleVoice(); } else { if (currentPlayIndex >= audioQueue.value.length) { console.log("重新开始播放音频序列"); currentPlayIndex = 0; isPlayingAudio.value = false; isCallingPlayNext = false; audioStore.soundInstance = null; // 重新开始播放
if (audioQueue.value.length > 0) { playNextAudio(); } } else if (audioStore.currentAudioUrl || audioStore.ttsUrl) { console.log("2222"); audioStore.togglePlayPause(); } else { console.log("3333"); audioStore.toggleVoice(); } } } };
// 计算属性:判断语音是否启用
const isVoice = computed(() => { return audioStore.isVoiceEnabled; }); // 随机GIF
const currentGif = ref("");
const renderer = new marked.Renderer(); // 重写 del 方法,让删除线不生效
renderer.del = function (text) { // 处理各种数据类型
console.log("text", text); return "~" + text.tokens[0].raw + "<br>" + text.tokens[2].raw + "~"; };
// 定义自定义事件
const emit = defineEmits([ "updateMessage", "sendMessage", "enableInput", "ensureAIchat", "scrollToBottom", "showCount", ]);
// 音频播放方法
const playAudio = (url) => { // 添加空值校验
if (!url) { console.warn("音频URL为空,跳过播放"); audioStore.isPlaying = false; return; }
const handlePlay = () => { if (audioStore.isNewInstance) { const newSound = new Howl({ src: [url], html5: true, // 强制HTML5 Audio解决iOS兼容问题
format: ["mp3", "acc"], rate: 1.2, // 调整播放速度
onplay: () => { audioStore.isPlaying = true; // 改为更新store状态
newSound.volume(1); // 添加音量设置
}, onend: () => (audioStore.isPlaying = false), onstop: () => (audioStore.isPlaying = false), onloaderror: (id, err) => { console.error("音频加载失败:", err); ElMessage.error("音频播放失败,请检查网络连接"); }, }); if (audioStore.nowSound) { audioStore.nowSound.stop(); } audioStore.nowSound = newSound; audioStore.isNewInstance = false;
console.log("新音频"); } else { console.log("已经有音频"); }
const newSound = audioStore.nowSound;
// 添加立即播放逻辑
newSound.play();
audioStore.setAudioInstance(newSound); Howler._howls.push(newSound); // 强制注册到全局管理
};
handlePlay(); }; // 新增暂停方法
const pauseAudio = () => { if (audioStore.soundInstance) { audioStore.soundInstance.pause();
audioStore.isPlaying = false; } };
// 音频轮流播放方法
const playAudioSequence = (audioUrls) => { console.log("playAudioSequence被调用,参数:", audioUrls); if (!audioUrls || audioUrls.length === 0) { console.warn("音频URL列表为空,跳过播放"); return; }
let currentIndex = 0; let audioSequence = [...audioUrls]; // 保存音频序列
const playNext = () => { if (currentIndex >= audioSequence.length) { console.log("所有音频播放完成,重置到第一个音频"); // 播放完成后重置到第一个音频,但不自动播放
currentIndex = 0; audioStore.isPlaying = false; audioStore.isPaused = false; audioStore.playbackPosition = 0; // 清除音频实例,确保下次点击从头开始
audioStore.soundInstance = null; audioStore.nowSound = null; if (audioSequence.length > 0) { audioStore.setCurrentAudioUrl(audioSequence[0]); } return; }
const currentUrl = audioSequence[currentIndex]; console.log(`正在播放第${currentIndex + 1}个音频:`, currentUrl); console.log( "音频URL有效性检查:", !!currentUrl, "长度:", currentUrl?.length );
// 增强URL验证
if ( !currentUrl || typeof currentUrl !== "string" || currentUrl.trim() === "" ) { console.error(`音频 ${currentIndex + 1} URL无效,跳过该音频`); currentIndex++; setTimeout(() => { playNext(); }, 100); return; }
// 检查URL格式
try { new URL(currentUrl); } catch (e) { console.error(`音频 ${currentIndex + 1} URL格式错误:`, currentUrl); currentIndex++; setTimeout(() => { playNext(); }, 100); return; }
// 设置当前音频URL
audioStore.setCurrentAudioUrl(currentUrl);
// 停止当前播放的音频
if (audioStore.nowSound) { audioStore.nowSound.stop(); }
const sound = new Howl({ src: [currentUrl], html5: true, format: ["mp3", "acc"], rate: 1.2, onplay: () => { audioStore.isPlaying = true; audioStore.isPaused = false; console.log(`开始播放音频 ${currentIndex + 1}`); console.log("音频播放状态:", { duration: sound.duration(), state: sound.state(), playing: sound.playing(), }); }, onpause: () => { audioStore.isPlaying = false; audioStore.isPaused = true; audioStore.playbackPosition = sound.seek() || 0; console.log(`音频 ${currentIndex + 1} 已暂停`); }, onend: () => { audioStore.isPlaying = false; audioStore.isPaused = false; audioStore.playbackPosition = 0; console.log(`音频 ${currentIndex + 1} 播放完成`); currentIndex++; // 如果是最后一个音频播放完成,立即清除实例
if (currentIndex >= audioSequence.length) { console.log("最后一个音频播放完成,清除音频实例"); audioStore.soundInstance = null; audioStore.nowSound = null; currentIndex = 0; // 立即重置索引
} // 播放下一个音频
setTimeout(() => { playNext(); }, 500); // 间隔500ms播放下一个
}, onstop: () => { audioStore.isPlaying = false; audioStore.isPaused = false; audioStore.playbackPosition = 0; console.log(`音频 ${currentIndex + 1} 已停止`); }, onloaderror: (id, err) => { console.error(`音频 ${currentIndex + 1} 加载失败:`, err); console.error("失败的音频URL:", currentUrl); console.error("错误详情:", { id, err });
// 增加重试机制
if (!sound.retryCount) { sound.retryCount = 0; }
if (sound.retryCount < 2) { sound.retryCount++; console.log( `音频 ${currentIndex + 1} 第${sound.retryCount}次重试加载` ); setTimeout(() => { sound.load(); }, 1000 * sound.retryCount); // 递增延时重试
} else { console.warn(`音频 ${currentIndex + 1} 重试失败,跳过该音频`); currentIndex++; // 跳过失败的音频,播放下一个
setTimeout(() => { playNext(); }, 100); } }, });
audioStore.nowSound = sound; audioStore.setAudioInstance(sound);
// 添加播放超时检测
const playTimeout = setTimeout(() => { if (!audioStore.isPlaying && sound.state() === "loading") { console.warn(`音频 ${currentIndex + 1} 播放超时,可能网络问题`); sound.stop(); currentIndex++; playNext(); } }, 10000); // 10秒超时
// 播放成功后清除超时
sound.once("play", () => { clearTimeout(playTimeout); });
console.log(`尝试播放音频 ${currentIndex + 1},URL: ${currentUrl}`); sound.play(); };
// 重写togglePlayPause方法以支持音频序列控制
audioStore.togglePlayPause = () => { console.log("音频控制按钮被点击 11111111111"); console.log("当前播放状态:", audioStore.isPlaying); console.log("当前暂停状态:", audioStore.isPaused); console.log("当前音频实例:", audioStore.soundInstance); console.log( "当前索引:", currentIndex, "音频序列长度:", audioSequence.length );
if (audioStore.soundInstance) { if (audioStore.isPlaying) { // 暂停当前音频
console.log("暂停当前音频"); audioStore.pause(); } else if (audioStore.isPaused) { // 从暂停位置继续播放
console.log("从暂停位置继续播放"); audioStore.play(); } else { // 重新开始播放当前音频或从头开始播放序列
console.log("重新开始播放,当前索引:", currentIndex); if (currentIndex >= audioSequence.length) { console.log("所有音频已播放完成,从头开始"); currentIndex = 0; // 重置到第一个音频
} playNext(); } } else { // 没有音频实例时,从头开始播放
console.log("没有音频实例,从头开始播放"); currentIndex = 0; playNext(); } };
// 开始播放第一个音频
playNext(); };
// 获取消息
const chatMsg = computed(() => chatStore.messages); const props = defineProps({ messages: Array, chartData: { type: Object, default: null, }, index: { type: Number, required: true, }, });
// 打字机效果
const typewriterContent = ref(""); const isTyping = ref(false);
const typeWriter = (text, callback) => { let index = 0; isTyping.value = true; typewriterContent.value = "";
const typingInterval = setInterval(() => { if (index < text.length) { typewriterContent.value += text.charAt(index); index++; // 自动滚动到底部
nextTick(() => { const container = document.querySelector(".message-area"); if (container) container.scrollTop = container.scrollHeight; }); } else { clearInterval(typingInterval); isTyping.value = false; if (callback) callback(); } }, 50); // 调整速度(毫秒)
};
const typingQueue = ref([]); const isTypingInProgress = ref(false);
// 创建打字机效果的Promise函数
const createTypingEffect = (message, content, speed) => { return new Promise((resolve) => { chatStore.messages.push(message); if (Array.isArray(content) && content.length > 0) { message.content = ""; message.isTyping = true;
let currentIndex = 0;
const processNextElement = () => { if (currentIndex >= content.length) { if (message.isEnd) { if (message.isEnd == "1") { apiStatus.one.isEnd = true; } else if (message.isEnd == "2") { apiStatus.two.isEnd = true; } else if (message.isEnd == "3") { apiStatus.three.isEnd = true; } } if (message.error) { chatStore.messages.push({ class: "ing", type: "ing", flag: false, content: "系统正在为您努力加载中,请稍后再试", }); chatStore.isLoading = false; chatStore.chatInput = false; emit("enableInput"); if (message.error == "2") { apiStatus.two.isError = true; } else if (message.error == "3") { apiStatus.three.isError = true; } } if (message.end) { homepageChatStore.getUserCount(); chatStore.isLoading = false; console.log("打印完毕,接触输入框禁用状态"); chatStore.chatInput = false; emit("enableInput"); } message.isTyping = false; nextTick(() => { resolve(); // 完成后resolve
}); return; }
if (currentIndex % 2 === 0) { // 偶数下标:直接加入
message.content += content[currentIndex]; currentIndex++; processNextElement(); // 立即处理下一个元素
} else { // 奇数下标:打字机效果
const text = content[currentIndex]; let charIndex = 0;
const typingInterval = setInterval(() => { if (charIndex < text.length) { message.content += text.charAt(charIndex); charIndex++; } else { clearInterval(typingInterval); currentIndex++; processNextElement(); // 处理下一个元素
} }, speed); } };
processNextElement(); // 开始处理
} else { if (message.kline) { if (message.klineType == 2) { console.log("K线消息已添加到聊天列表");
// 在渲染完成后初始化图表
nextTick(() => { console.log("nextTick开始 - 准备渲染图表"); console.log("消息列表:", chatStore.messages);
// 寻找最新添加的K线消息索引
let klineIndex = -1; for (let i = 0; i < chatStore.messages.length; i++) { if (chatStore.messages[i].messageId === message.messageId) { klineIndex = i; break; } }
console.log("找到的K线消息索引:", klineIndex);
if (klineIndex !== -1) { const containerId = `kline-container-${klineIndex}`; console.log("图表容器ID:", containerId);
// 确保DOM已经渲染完成
setTimeout(() => { console.log("延时执行,确保DOM已渲染"); KlineCanvsEcharts(containerId); }, 100); // 短暂延时确保DOM已渲染
} else { console.warn("未找到K线消息"); } }); } if (message.isEnd) { if (message.isEnd == "1") { apiStatus.one.isEnd = true; } else if (message.isEnd == "2") { apiStatus.two.isEnd = true; } else if (message.isEnd == "3") { apiStatus.three.isEnd = true; } } if (message.error) { chatStore.messages.push({ class: "ing", type: "ing", flag: false, content: "系统正在为您努力加载中,请稍后再试", }); chatStore.isLoading = false; chatStore.chatInput = false; emit("enableInput"); if (message.error == "2") { apiStatus.two.isError = true; } else if (message.error == "3") { apiStatus.three.isError = true; } } // 延时1秒后resolve
setTimeout(() => { resolve(); }, 1000); } else { if (message.isEnd) { if (message.isEnd == "1") { apiStatus.one.isEnd = true; } else if (message.isEnd == "2") { apiStatus.two.isEnd = true; } else if (message.isEnd == "3") { apiStatus.three.isEnd = true; } } if (message.error) { chatStore.messages.push({ class: "ing", type: "ing", flag: false, content: "系统正在为您努力加载中,请稍后再试", }); chatStore.isLoading = false; chatStore.chatInput = false; emit("enableInput"); if (message.error == "2") { apiStatus.two.isError = true; } else if (message.error == "3") { apiStatus.three.isError = true; } } // 延时1秒后resolve
setTimeout(() => { resolve(); }, 1000); } } }); };
let apiStatus = {};
// 队列处理函数
const processTypingQueue = async () => { if (isTypingInProgress.value || typingQueue.value.length === 0) { return; }
isTypingInProgress.value = true;
while (typingQueue.value.length > 0) { const task = typingQueue.value.shift(); await createTypingEffect(task.message, task.content, task.speed); }
isTypingInProgress.value = false; };
// 添加打字机任务到队列
const addTypingTask = (message, content, speed) => { typingQueue.value.push({ message, content, speed }); processTypingQueue(); };
// 显示思考过程
async function showThinkingProcess(stockName = null) { // 第一步:正在思考
const thinkingMessage1 = reactive({ sender: "ai", class: "ing", type: "ing", flag: true, content: "深度九大模型正在思考", gif: thinkingGif, nowrap: true, }); chatStore.messages.push(thinkingMessage1); await new Promise((resolve) => setTimeout(resolve, 1500)); chatStore.messages.pop();
// 第二步:正在解析关键数据(持续显示直到获取到股票名称)
const thinkingMessage2 = reactive({ sender: "ai", class: "ing", type: "ing", flag: true, content: "正在解析关键数据", gif: analyzeGif, nowrap: true, }); chatStore.messages.push(thinkingMessage2);
// 如果没有股票名称,保持第二步显示
if (!stockName) { return thinkingMessage2; // 返回消息引用,以便后续更新
}
// 有股票名称后,继续后续步骤
await new Promise((resolve) => setTimeout(resolve, 1500)); chatStore.messages.pop();
// 第三步:生成具体股票的深度共振分析图谱
const thinkingMessage3 = reactive({ sender: "ai", class: "ing", type: "ing", flag: true, content: `正在生成${stockName}深度共振分析图谱`, gif: generateGif, nowrap: true, }); chatStore.messages.push(thinkingMessage3); await new Promise((resolve) => setTimeout(resolve, 1500)); chatStore.messages.pop();
// 第四步:报告已生成
const thinkingMessage4 = reactive({ sender: "ai", class: "ing", type: "ing", content: "报告已生成!", nowrap: true, }); chatStore.messages.push(thinkingMessage4); await new Promise((resolve) => setTimeout(resolve, 1500)); chatStore.messages.pop();
return null; }
// 继续思考过程(当获取到股票名称后调用)
async function continueThinkingProcess(thinkingMessageRef, stockName) { if (!thinkingMessageRef || !stockName) return;
// 等待一段时间后继续
await new Promise((resolve) => setTimeout(resolve, 1500));
// 移除第二步消息
const index = chatStore.messages.indexOf(thinkingMessageRef); if (index > -1) { chatStore.messages.splice(index, 1); }
// 第三步:生成具体股票的深度共振分析图谱
const thinkingMessage3 = reactive({ sender: "ai", class: "ing", type: "ing", flag: true, content: `正在生成${stockName}深度共振分析图谱`, gif: generateGif, }); chatStore.messages.push(thinkingMessage3); await new Promise((resolve) => setTimeout(resolve, 1500)); chatStore.messages.pop();
// 第四步:报告已生成
const thinkingMessage4 = reactive({ sender: "ai", class: "ing", type: "ing", content: "报告已生成!", }); chatStore.messages.push(thinkingMessage4); await new Promise((resolve) => setTimeout(resolve, 1500)); chatStore.messages.pop(); }
const hasValidData = ref(false);
// 创建一个非响应式的对象来存储图表实例
const chartInstancesMap = {};
// 存储上一次的消息的length
const previousMessagesLength = ref(0);
watch( () => props.messages, async (newVal, oldVal) => { // // 添加空值判断
if (!newVal?.length || newVal === previousMessagesLength.value) return;
chatStore.firstAPICall = true; console.log("第一阶段,意图识别,获取回复,历史记录禁止点击");
previousMessagesLength.value = newVal.length; if (newVal.length > 0) { // 清理语音下标
console.log("chatStore.currentUserIndex", chatStore.currentUserIndex); if (chatStore.currentUserIndex != null) { chatStore.messages[chatStore.currentUserIndex].audioStatus = false; } chatStore.currentUserIndex = null; audioStore.stop(); // 暂停语音
// 🔧 新增:重置音频队列状态,确保新音频能够自动播放
audioQueue.value = []; isPlayingAudio.value = false; currentPlayIndex = 0; isCallingPlayNext = false;
// 重置音频预加载状态
audioPreloadStatus.one = { loaded: false, url: null }; audioPreloadStatus.two = { loaded: false, url: null }; audioPreloadStatus.three = { loaded: false, url: null }; audioPreloadStatus.four = { loaded: false, url: null };
// 清除音频实例
audioStore.soundInstance = null; audioStore.nowSound = null; audioStore.isPlaying = false; audioStore.isPaused = false; audioStore.playbackPosition = 0;
console.log("消息列表已更新,最新消息:", newVal[newVal.length - 1]); chatStore.messages.push(newVal[newVal.length - 1]); chatStore.currentUserIndex = chatStore.messages.length - 1; chatStore.inputUserIndex = chatStore.messages.length - 1; console.log( "消息列表已更新,最新消息:", chatStore.messages[chatStore.messages.length - 1], "最新用户坐标", chatStore.currentUserIndex );
// 获取权限
const userStore = useUserStore();
const params1 = { language: "cn", marketList: "usa,sg,my,hk,cn,can,vi,th,in", content: newVal[newVal.length - 1].content,
token: localStorage.getItem("localToken"), model: 1, // language: "cn",
// marketList: "hk,cn,usa,my,sg,vi,in,gb"
// token: "+SsksARQgUHIbIG3rRnnbZi0+fEeMx8pywnIlrmTxo5EOPR/wjWDV7w7+ZUseiBtf9kFa/atmNx6QfSpv5w",
}; // 标志
let flag = true; const codeData = ref(); // 开始思考过程(不带股票名称)
const thinkingMessageRef = await showThinkingProcess();
// 第一阶段,意图识别
try { // 调用工作流获取回复
const result = await deepNineFirstAPI(params1); codeData.value = result.data; console.log(codeData.value, "codeData"); // 根据意图识别结果判断
if (result.code == 200) { // 意图识别成功后,更新历史记录状态
chatStore.searchRecord = true; // 获取到股票名称后,继续思考过程
if (thinkingMessageRef && codeData.value.name) { await continueThinkingProcess( thinkingMessageRef, codeData.value.name ); } // for (let i = chatStore.messages.length - 1; i >= 0; --i) {
// if (chatStore.messages[i].sender == "user") {
// chatStore.messages[i].audioStatus = true;
// break;
// }
// }
chatStore.messages.push({ // class: "ing",
// type: "ing",
// flag: flag,
// content: result.data.kaishi,
}); } else { // 意图识别失败,先清理思考过程消息
if (thinkingMessageRef) { const index = chatStore.messages.indexOf(thinkingMessageRef); if (index > -1) { chatStore.messages.splice(index, 1); } }
flag = false; console.log("执行回绝话术"); const AIcontent = ref(result.msg); // 修改后的消息处理逻辑
const processedContent = marked(AIcontent.value); const katexRegex = /\$\$(.*?)\$\$/g; let aiContent = processedContent.replace( katexRegex, (match, formula) => { try { return katex.renderToString(formula, { throwOnError: false }); } catch (error) { console.error("KaTeX 渲染错误:", error); return match; } } ); console.log(AIcontent, "AIcontent");
if (result.code == 406) { AIcontent.value = `<p>尊敬的用户您好,您当前的“深度九大模型专属Token”数量为0,无法进行股票查询,可联系客服团队进行充值,感谢您的理解与支持</p>`; }
const aiMsg = { class: "ing", type: "ing", flag: flag, content: AIcontent, };
chatStore.messages.push(aiMsg);
chatStore.isLoading = false; chatStore.chatInput = false; chatStore.firstAPICall = false;
console.log("历史记录可以点击"); emit("enableInput"); } } catch (e) { // 意图识别异常,先清理思考过程消息
if (thinkingMessageRef) { const index = chatStore.messages.indexOf(thinkingMessageRef); if (index > -1) { chatStore.messages.splice(index, 1); } }
console.log(e, "意图识别失败"); chatStore.messages.push({ class: "ing", type: "ing", flag: false, content: "系统正在为您努力加载中,请稍后再试", }); chatStore.isLoading = false; chatStore.chatInput = false; chatStore.firstAPICall = false; console.log("历史记录可以点击"); emit("enableInput"); }
if (flag) { const params2 = { language: "cn", token: localStorage.getItem("localToken"), parentId: codeData.value.parentId, stockId: codeData.value.stockId, recordId: codeData.value.recordId,
// content: newVal[newVal.length - 1].content,
// marketList: "usa,sg,my,hk,cn,can,vi,th,in",
// name: codeData.value.name,
// code: codeData.value.code,
// market: codeData.value.market,
// language: "cn",
// marketList: "hk,cn,usa,my,sg,vi,in,gb"
// token: "+SsksARQgUHIbIG3rRnnbZi0+fEeMx8pywnIlrmTxo5EOPR/wjWDV7w7+ZUseiBtf9kFa/atmNx6QfSpv5w",
};
try { const env = import.meta.env.VITE_ENV;
const result20 = await getNineTurnsAPI( { token: env == "development" || env == "test" ? "8Csj5VVX1UbIb4C3oxrnbZi0+fEeMx8pywnIlrmTm45Cb/EllzWACLto9J9+fCFsfdgBOvKvyY94FvqlvM0" : "8nkj4QBV1RPIb4CzoRTnbZi0+fEeMx8pywnIlrmTxdwROKkuwWqAWu9orpkpeXVqL98DPfeonNYpHv+mucA", }, { market: codeData.value.market, language: "cn", code: codeData.value.code, } );
// 添加空值检查防止访问null对象的属性
const nineTurns = result20.data ? result20.data : null;
const isNineTurns = nineTurns && nineTurns.DXT && nineTurns.JZJG && nineTurns.KLine20 && nineTurns.StockInformation && nineTurns.ZJQS ? true : false;
const katexRegex = /\$\$(.*?)\$\$/g;
let result21; let result22; let result23; let result24; // 用于跟踪API完成状态和结果
apiStatus = { one: { completed: false, result: null, error: null, isError: false, isEnd: false, }, two: { completed: false, result: null, error: null, isError: false, isEnd: false, }, three: { completed: false, result: null, error: null, isError: false, isEnd: false, }, four: { completed: false, result: null, error: null, isError: false, isEnd: false, }, };
// 预加载音频函数
const preloadAudio = (url, apiKey) => { if (!url || !audioStore.isVoiceEnabled) { audioPreloadStatus[apiKey].loaded = true; return Promise.resolve(); }
// 立即设置URL,确保即使预加载失败也能使用
audioPreloadStatus[apiKey].url = url; console.log(`设置音频${apiKey}的URL:`, url);
return new Promise((resolve) => { const audio = new Howl({ src: [url], html5: true, format: ["mp3", "acc"], rate: 1.2, preload: true, onload: () => { console.log(`音频${apiKey}预加载完成:`, url); audioPreloadStatus[apiKey].loaded = true; resolve(); }, onloaderror: (id, err) => { console.error(`音频${apiKey}预加载失败:`, err); audioPreloadStatus[apiKey].loaded = true; // 标记为已处理,避免阻塞
// URL已经在上面设置了,即使预加载失败也保留URL
resolve(); }, }); }); };
// 检查第一个接口是否可以开始输出(文本和音频都准备好)
const canStartFirstOutput = () => { return apiStatus.one.completed && audioPreloadStatus.one.loaded; };
// 检查并按顺序执行代码的函数
const checkAndExecuteInOrder = () => { // 检查OneAPI - 只有当文本和音频都准备好时才开始输出
if (canStartFirstOutput() && !apiStatus.one.executed) { if (apiStatus.one.result) { apiStatus.one.executed = true; console.log( "执行OneAPI代码(文本和音频同步开始):", apiStatus.one.result );
// 将第一个音频添加到播放队列(确保顺序:API1)
if (audioPreloadStatus.one.url) { chatStore.messages[chatStore.inputUserIndex].audioArray.push( audioPreloadStatus.one.url ); if (chatStore.currentUserIndex == chatStore.inputUserIndex) { chatStore.messages[ chatStore.inputUserIndex ].audioStatus = true; addToAudioQueue(audioPreloadStatus.one.url, "API1-第一个"); } else { chatStore.messages[ chatStore.inputUserIndex ].audioStatus = false; } console.log( "音频队列:添加API1-1音频(link1),当前队列长度:", audioQueue.value.length ); }
// 添加第二个音频(link)到队列
if (audioPreloadStatus.two.url) { chatStore.messages[chatStore.inputUserIndex].audioArray.push( audioPreloadStatus.two.url ); if (chatStore.currentUserIndex == chatStore.inputUserIndex) { addToAudioQueue(audioPreloadStatus.two.url, "API1-第二个"); } else { chatStore.messages[ chatStore.inputUserIndex ].audioStatus = false; } console.log( "音频队列:添加API1-2音频(link),当前队列长度:", audioQueue.value.length ); }
// 在这里添加OneAPI成功后需要执行的代码
// 删除正在为您生成信息
chatStore.messages.pop(); // 添加报告头和时间
addTypingTask( { sender: "ai", class: "title1", type: "title1", content: codeData.value.name + "深度共振分析图谱", date: result21.data.date, }, "", 50 );
chatStore.firstAPICall = false; console.log("历史记录可以点击");
const pc1 = marked( result21.data.name + "\n" + result21.data.price + "\n" + result21.data.date ); const ac1 = pc1.replace(katexRegex, (match, formula) => { try { return katex.renderToString(formula, { throwOnError: false, }); } catch (error) { console.error("KaTeX 渲染错误:", error); return match; } });
// 先推送初始消息
const aiMessage1 = reactive({ sender: "ai", class: "content1", type: "content1", content: "", isTyping: true, });
addTypingTask(aiMessage1, ["", ac1], 130);
// 九转结构K线图
if ( nineTurns && nineTurns.DXT && nineTurns.JZJG && nineTurns.KLine20 && nineTurns.StockInformation && nineTurns.ZJQS ) { const nineTurnsData = JSON.parse( JSON.stringify(toRaw(nineTurns)) ); console.log("处理 K 线数据 - 开始"); console.log("nineTurnsData", nineTurnsData);
const Kline20 = { name: nineTurnsData.StockInformation.Name, Kline: nineTurnsData, };
// 打印K线数据结构
console.log("K线数据结构:", Kline20); console.log("K线数据名称:", Kline20.name); console.log("K线数据:", Kline20.Kline ? Kline20.Kline : null);
// 设置数据有效标志
hasValidData.value = true; console.log("hasValidData设置为:", hasValidData.value);
// 先推送K线图消息
const klineMessageId2 = `kline-${Date.now() + 1}`; console.log("生成K线消息ID:", klineMessageId2);
// 添加九转结构图表
addTypingTask( { sender: "ai", class: "content2", type: "content2", kline: true, chartData: Kline20, messageId: klineMessageId2, hasValidData: true, // 添加hasValidData标志
klineType: 2, }, "", 50 );
// 添加标题-数据分析时代下的认知变现
addTypingTask( { sender: "ai", class: "title2", type: "title2", content: "", }, "", 50 );
// 添加图片-数据分析时代下的认知变现
addTypingTask( { sender: "ai", class: "content3", type: "img1", content: "https://d31zlh4on95l9h.cloudfront.net/images/5baa0a449cf74fb6a1afb1c909a21194.png", }, "", 50 );
// 添加标题-结构框架分析
addTypingTask( { sender: "ai", class: "title3", type: "title3", content: "https://d31zlh4on95l9h.cloudfront.net/images/9ab9d76b6906eb914fa1842dbcd56841.png", }, "", 50 );
// 添加内容框1
const ac2 = `<p>${result21.data.jgkjfx}</p>`;
// 先推送初始消息
const aiMessage2 = reactive({ sender: "ai", class: "content3", type: "content3", content: "", isTyping: true, error: apiStatus.two.error ? "2" : "", isEnd: "1", });
addTypingTask(aiMessage2, ["", ac2], 130); } } else { chatStore.messages.push({ class: "ing", type: "ing", flag: false, content: "系统正在为您努力加载中,请稍后再试", }); chatStore.isLoading = false; chatStore.chatInput = false; emit("enableInput"); } }
// 检查TwoAPI(需要OneAPI已执行)
if ( apiStatus.one.executed && apiStatus.two.completed && !apiStatus.two.executed ) { if (apiStatus.two.result) { apiStatus.two.executed = true; console.log("执行TwoAPI代码:", apiStatus.two.result);
// 将第二个接口的音频添加到播放队列(确保顺序:API2)
if (audioPreloadStatus.three.url) { chatStore.messages[chatStore.inputUserIndex].audioArray.push( audioPreloadStatus.three.url ); if (chatStore.currentUserIndex == chatStore.inputUserIndex) { addToAudioQueue( audioPreloadStatus.three.url, "API2-第一个" ); } else { chatStore.messages[ chatStore.inputUserIndex ].audioStatus = false; } console.log( "音频队列:添加API2-1音频(link3),当前队列长度:", audioQueue.value.length ); }
if (audioPreloadStatus.four.url) { chatStore.messages[chatStore.inputUserIndex].audioArray.push( audioPreloadStatus.four.url ); if (chatStore.currentUserIndex == chatStore.inputUserIndex) { addToAudioQueue(audioPreloadStatus.four.url, "API2-第二个"); } else { chatStore.messages[ chatStore.inputUserIndex ].audioStatus = false; } console.log( "音频队列:添加API2-2音频(link1),当前队列长度:", audioQueue.value.length ); }
// 在这里添加TwoAPI成功后需要执行的代码
// 添加标题-资金动向监控
addTypingTask( { sender: "ai", class: "title3", type: "title3", content: " https://d31zlh4on95l9h.cloudfront.net/images/f95c44f83b3e3c52e88964631c199060.png", }, "", 50 ); const ac31 = `<p style="margin:0;color:#FFD700;font-weight:bold;display:flex;justify-content:center;font-size:22px">【资金异动信号】</p><p>`; const ac32 = result22.data.dxtsc;
// const ac33 = result22.data.zjqssc1;
const ac34 = `<p style="margin:0;color:#FFD700;font-weight:bold;display:flex;justify-content:center;font-size:22px">【资金趋势导航】</p><p>`; const ac35 = result22.data.zjqssc1;
// const ac3 = `<p>${result23.data.DXTSC}</p><p>${result23.data.DXTSC2}</p><p>${result23.data.ZJQSSC1}</p>`;
const ac3Arr = []; ac3Arr.push(ac31); if (ac32 != "") { ac3Arr.push(`<p>${ac32}</p>`); } // if (ac33 != "") {
// ac3Arr.push("");
// ac3Arr.push(`<p>${ac33}</p>`);
// }
ac3Arr.push(ac34); if (ac35 != "") { ac3Arr.push(`<p>${ac35}</p>`); } // 先推送初始消息
const aiMessage3 = reactive({ sender: "ai", class: "content3", type: "content3", content: "", isTyping: true, error: apiStatus.three.error ? "3" : "", isEnd: "2", }); addTypingTask(aiMessage3, ac3Arr, 200); } else { if ( apiStatus.one.isEnd && apiStatus.two.error && !apiStatus.two.isError ) { apiStatus.two.isError = true; chatStore.messages.push({ class: "ing", type: "ing", flag: false, content: "系统正在为您努力加载中,请稍后再试", }); chatStore.isLoading = false; chatStore.chatInput = false; emit("enableInput"); } } }
// 检查ThreeAPI(需要TwoAPI已执行)
if ( apiStatus.two.executed && apiStatus.three.completed && !apiStatus.three.executed ) { if (apiStatus.three.result) { apiStatus.three.executed = true; console.log("执行ThreeAPI代码:", apiStatus.three.result);
// 将第三个接口的音频添加到播放队列(确保顺序:API3)
if (audioPreloadStatus.five.url) { chatStore.messages[chatStore.inputUserIndex].audioArray.push( audioPreloadStatus.five.url ); if (chatStore.currentUserIndex == chatStore.inputUserIndex) { addToAudioQueue(audioPreloadStatus.five.url, "API3-第一个"); } else { chatStore.messages[ chatStore.inputUserIndex ].audioStatus = false; } console.log( "音频队列:添加API3音频(link),当前队列长度:", audioQueue.value.length ); }
// 在这里添加ThreeAPI成功后需要执行的代码
// 添加标题-策略共振决策模型
addTypingTask( { sender: "ai", class: "title3", type: "title3", content: "https://d31zlh4on95l9h.cloudfront.net/images/d1fa1f4cbd6452796a4c5368d9f57c4d.png", }, "", 50 );
// 添加内容框4
const ac5 = `<p>${result23.data.zjqssc2}</p>`;
// 先推送初始消息
const aiMessage5 = reactive({ sender: "ai", class: "content3", type: "content3", content: "", isTyping: true, }); addTypingTask(aiMessage5, ["", ac5], 240);
const ac6 = "该内容由AI生成,请注意甄别"; // 先推送初始消息
const aiMessage6 = reactive({ sender: "ai", class: "mianze", type: "mianze", content: "", isTyping: true, end: true, }); addTypingTask(aiMessage6, ["", ac6], 210); } else { if ( apiStatus.two.isEnd && apiStatus.three.error && !apiStatus.three.isError ) { apiStatus.three.isError = true; chatStore.messages.push({ class: "ing", type: "ing", flag: false, content: "系统正在为您努力加载中,请稍后再试", }); chatStore.isLoading = false; chatStore.chatInput = false; emit("enableInput"); } } }
// 检查是否所有API都已完成并执行
if ( apiStatus.one.completed && apiStatus.two.completed && apiStatus.three.completed ) { console.log("所有API已完成,开始收集预加载的音频URL"); // 收集所有预加载的音频URL
const audioUrls = []; console.log("预加载音频状态检查:"); console.log("audioPreloadStatus:", audioPreloadStatus);
if (audioPreloadStatus.one.url) { console.log( "添加预加载音频URL one:", audioPreloadStatus.one.url ); audioUrls.push(audioPreloadStatus.one.url); } if (audioPreloadStatus.two.url) { console.log( "添加预加载音频URL two:", audioPreloadStatus.two.url ); audioUrls.push(audioPreloadStatus.two.url); } if (audioPreloadStatus.three.url) { console.log( "添加预加载音频URL three:", audioPreloadStatus.three.url ); audioUrls.push(audioPreloadStatus.three.url); } if (audioPreloadStatus.four.url) { console.log( "添加预加载音频URL four:", audioPreloadStatus.four.url ); audioUrls.push(audioPreloadStatus.four.url); }
console.log("收集到的预加载音频URLs:", audioUrls); console.log("语音是否启用:", audioStore.isVoiceEnabled);
// 音频播放逻辑已移至各个接口的执行代码中
console.log("所有接口执行完成,音频已在各接口中单独播放"); } };
const handleOneAPI = async () => { try { result21 = await deepNineSecondOneAPI(params2); if (result21.code == 400) { throw new Error("API返回错误码400,请求失败"); } console.log("OneAPI成功返回:", result21);
apiStatus.one.completed = true; apiStatus.one.result = result21;
// 预加载第一个接口的音频 - link1和link
if (result21?.data?.link1) { await preloadAudio(result21.data.link1.trim(), "one"); } else { audioPreloadStatus.one.loaded = true; }
if (result21?.data?.link) { await preloadAudio(result21.data.link.trim(), "two"); } else { audioPreloadStatus.two.loaded = true; }
// 检查是否可以执行
checkAndExecuteInOrder(); } catch (error) { console.error("OneAPI失败:", error); apiStatus.one.completed = true; apiStatus.one.error = error; audioPreloadStatus.one.loaded = true; // 失败时也标记为已处理
audioPreloadStatus.two.loaded = true; // 失败时也标记为已处理
// 即使失败也要检查后续执行
checkAndExecuteInOrder(); } };
const handleTwoAPI = async () => { try { result22 = await deepNineSecondTwoAPI(params2); if (result22.code == 400) { throw new Error("API返回错误码400,请求失败"); } console.log("TwoAPI成功返回:", result22);
apiStatus.two.completed = true; apiStatus.two.result = result22;
// 预加载第二个接口的音频 - link3和link1
if (result22?.data?.link3) { await preloadAudio(result22.data.link3.trim(), "three"); } else { audioPreloadStatus.three.loaded = true; }
if (result22?.data?.link1) { await preloadAudio(result22.data.link1.trim(), "four"); } else { audioPreloadStatus.four.loaded = true; }
// 检查是否可以执行
checkAndExecuteInOrder(); } catch (error) { console.error("TwoAPI失败:", error); apiStatus.two.completed = true; apiStatus.two.error = error; audioPreloadStatus.three.loaded = true; audioPreloadStatus.four.loaded = true; checkAndExecuteInOrder(); } };
const handleThreeAPI = async () => { try { result23 = await deepNineSecondThreeAPI(params2); if (result23.code == 400) { throw new Error("API返回错误码400,请求失败"); } // result23 = await dbqbSecondThreeAPI();
console.log("ThreeAPI成功返回:", result23);
apiStatus.three.completed = true; apiStatus.three.result = result23;
// 预加载第三个接口的音频 - 只有link字段
if (result23?.data?.link) { await preloadAudio(result23.data.link.trim(), "five"); } else { audioPreloadStatus.three.loaded = true; }
// 检查是否可以执行
checkAndExecuteInOrder(); } catch (error) { console.error("ThreeAPI失败:", error); apiStatus.three.completed = true; apiStatus.three.error = error; audioPreloadStatus.three.loaded = true; checkAndExecuteInOrder(); } };
if (isNineTurns) { handleOneAPI(); handleTwoAPI(); handleThreeAPI(); } else { chatStore.messages.pop(); chatStore.messages.push({ class: "ing", type: "ing", flag: false, content: "数据缺失,请稍后重试", }); chatStore.isLoading = false; chatStore.chatInput = false; chatStore.firstAPICall = false; emit("enableInput"); } } catch (e) { console.error("请求失败:", e); chatStore.firstAPICall = false; hasValidData.value = false; // 请求失败时设置数据无效
} finally { // chatStore.setLoading(false);
await homepageChatStore.getUserCount(); } } } }, { deep: false } );
// 点击历史记录
watch( () => chatStore.dbqbClickRecord, (newValue, oldValue) => { console.log("new", newValue); if (!newValue || Object.keys(newValue).length === 0) { return; }
const clickRecord = ref(newValue); console.log("dbqbClickRecord 发生变化:", clickRecord.value);
// 🔧 新增:完整的任务停止逻辑
try { // 1. 停止所有音频相关任务
chatStore.currentUserIndex = null; audioStore.stop(); // 暂停语音
// 🔧 新增:重置音频队列状态,确保新音频能够自动播放
audioQueue.value = []; isPlayingAudio.value = false; currentPlayIndex = 0; isCallingPlayNext = false;
// 🔧 新增:重置音频预加载状态
audioPreloadStatus.one = { loaded: false, url: null }; audioPreloadStatus.two = { loaded: false, url: null }; audioPreloadStatus.three = { loaded: false, url: null }; audioPreloadStatus.four = { loaded: false, url: null }; audioPreloadStatus.five = { loaded: false, url: null };
// 🔧 新增:清理音频实例
if (audioStore.soundInstance) { audioStore.soundInstance.stop(); audioStore.soundInstance.unload(); audioStore.soundInstance = null; } audioStore.nowSound = null;
// 2. 停止API调用和状态重置
// 🔧 新增:重置API状态
apiStatus.one = { completed: false, result: null, error: null, isError: false, isEnd: false, }; apiStatus.two = { completed: false, result: null, error: null, isError: false, isEnd: false, }; apiStatus.three = { completed: false, result: null, error: null, isError: false, isEnd: false, };
// 🔧 新增:重置数据有效性标志
hasValidData.value = false;
// 3. 停止打字机效果
typingQueue.value = []; isTypingInProgress.value = false;
// 4. 重置加载状态
chatStore.isLoading = false; chatStore.chatInput = false; emit("enableInput");
// 在下一个事件循环中清空 dbqbClickRecord
setTimeout(() => { chatStore.dbqbClickRecord = {}; console.log("dbqbClickRecord 已清空"); }, 0); } catch (error) { console.error("停止任务时发生错误:", error); }
if ( !clickRecord.value.wokeFlowData.One || !clickRecord.value.wokeFlowData.Two || !clickRecord.value.wokeFlowData.Three ) { return; } try { // 清空聊天框内容
chatStore.messages = [];
const userAudioArray = [ clickRecord.value.wokeFlowData.One.link1, clickRecord.value.wokeFlowData.One.link, clickRecord.value.wokeFlowData.Two.link3, ]; // if (clickRecord.value.wokeFlowData.Two.link2) {
// userAudioArray.push(clickRecord.value.wokeFlowData.Two.link2);
// }
userAudioArray.push(clickRecord.value.wokeFlowData.Two.link1); userAudioArray.push(clickRecord.value.wokeFlowData.Three.link); const userContent = { sender: "user", timestamp: clickRecord.value.createdTime, content: clickRecord.value.keyword, audioArray: userAudioArray, audioStatus: false, };
chatStore.messages.push(userContent);
chatStore.messages.push({ sender: "ai", class: "title1", type: "title1", content: clickRecord.value.stockName + "深度共振分析图谱", date: clickRecord.value.wokeFlowData.One.date, });
const pc1 = marked( clickRecord.value.wokeFlowData.One.name + "\n" + clickRecord.value.wokeFlowData.One.price + "\n" + clickRecord.value.wokeFlowData.One.date );
chatStore.messages.push({ sender: "ai", class: "content1", type: "content1", content: pc1, });
const nineTurns = clickRecord.value.stockData.data; // 度牛尺K线图
if ( nineTurns && nineTurns.DXT && nineTurns.JZJG && nineTurns.KLine20 && nineTurns.StockInformation && nineTurns.ZJQS ) { const nineTurnsData = JSON.parse(JSON.stringify(toRaw(nineTurns))); console.log("处理 K 线数据 - 开始"); console.log("nineTurnsData", nineTurnsData);
const Kline20 = { name: nineTurnsData.StockInformation.Name, Kline: nineTurnsData, };
// 打印K线数据结构
console.log("K线数据结构:", Kline20); console.log("K线数据名称:", Kline20.name); console.log("K线数据:", Kline20.Kline ? Kline20.Kline : null);
// 设置数据有效标志
hasValidData.value = true; console.log("hasValidData设置为:", hasValidData.value);
// chatStore.messages.pop();
// 先推送K线图消息
const klineMessageId2 = `kline-${Date.now() + 1}`; console.log("生成K线消息ID:", klineMessageId2);
chatStore.messages.push({ sender: "ai", class: "content2", type: "content2", kline: true, chartData: Kline20, messageId: klineMessageId2, hasValidData: true, // 添加hasValidData标志
klineType: 2, });
// 在渲染完成后初始化图表
nextTick(() => { console.log("nextTick开始 - 准备渲染图表"); console.log("消息列表:", chatStore.messages);
// 寻找最新添加的K线消息索引
let klineIndex = -1; for (let i = 0; i < chatStore.messages.length; i++) { if (chatStore.messages[i].messageId === klineMessageId2) { klineIndex = i; break; } }
console.log("找到的K线消息索引:", klineIndex);
if (klineIndex !== -1) { const containerId = `kline-container-${klineIndex}`; console.log("图表容器ID:", containerId);
// 确保DOM已经渲染完成
setTimeout(() => { console.log("延时执行,确保DOM已渲染"); KlineCanvsEcharts(containerId); }, 100); // 短暂延时确保DOM已渲染
} else { console.warn("未找到K线消息"); } }); } // 添加标题-数据分析时代下的认知变现
chatStore.messages.push({ sender: "ai", class: "title2", type: "title2", content: "", });
// 添加图片-数据分析时代下的认知变现
chatStore.messages.push({ sender: "ai", class: "content3", type: "img1", content: "https://d31zlh4on95l9h.cloudfront.net/images/5baa0a449cf74fb6a1afb1c909a21194.png", });
// 添加标题-结构框架分析
chatStore.messages.push({ sender: "ai", class: "title3", type: "title3", content: "https://d31zlh4on95l9h.cloudfront.net/images/9ab9d76b6906eb914fa1842dbcd56841.png", });
// 添加内容框1
const pc2 = marked(clickRecord.value.wokeFlowData.One.jgkjfx); // 先推送初始消息
chatStore.messages.push({ sender: "ai", class: "content3", type: "content3", content: pc2, }); // 添加标题-资金动向监控
chatStore.messages.push({ sender: "ai", class: "title3", type: "title3", content: " https://d31zlh4on95l9h.cloudfront.net/images/f95c44f83b3e3c52e88964631c199060.png", });
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>`;
// 先推送初始消息
chatStore.messages.push({ sender: "ai", class: "content3", type: "content3", content: ac3, isTyping: true, });
// 添加标题-策略共振决策模型
chatStore.messages.push({ sender: "ai", class: "title3", type: "title3", content: "https://d31zlh4on95l9h.cloudfront.net/images/d1fa1f4cbd6452796a4c5368d9f57c4d.png", });
// 添加内容框4
const ac5 = `<p>${clickRecord.value.wokeFlowData.Three.zjqssc2}</p>`;
// 先推送初始消息
chatStore.messages.push({ sender: "ai", class: "content3", type: "content3", content: ac5, }); chatStore.messages.push({ sender: "ai", class: "mianze", type: "mianze", content: "该内容由AI生成,请注意甄别", end: true, }); } catch (e) { ElMessage.error("历史数据获取出错!"); console.error("e", e); } }, { deep: true, // 深度监听对象内部属性的变化
immediate: true, // 立即执行一次回调
} );
function KlineCanvsEcharts(containerId) { function vwToPx(vw) { console.log((window.innerWidth * vw) / 100, "vwToPx"); return (window.innerWidth * vw) / 100; }
// console.log("KLine渲染: 开始处理数据, 容器ID:", containerId);
// 从 chatStore 中获取数据
const messages = chatStore.messages; // console.log("KLine渲染: 获取到的消息:", messages);
let klineMessageIndex = -1; let klineData = null;
klineMessageIndex = containerId.split("-")[2]; // console.log("KLine渲染: 找到K线消息索引:", klineMessageIndex);
if ( messages[klineMessageIndex].kline && messages[klineMessageIndex].chartData ) { klineData = messages[klineMessageIndex].chartData; }
var KlineOption = {};
// 检测设备类型
const isMobile = window.innerWidth < 768; const isTablet = window.innerWidth >= 768 && window.innerWidth < 1024; console.log( "KLine渲染: 设备类型", isMobile ? "移动设备" : isTablet ? "平板设备" : "桌面设备" );
if (messages[klineMessageIndex].klineType == 2) { if (!klineData || !klineData.Kline) { // console.warn("KLine渲染: 数据无效 - 在chatStore中找不到有效的K线数据");
return; }
// 获取容器元素
const container = document.getElementById(containerId); if (!container) { // console.error("KLine渲染: 找不到容器元素:", containerId);
return; }
// 创建图表实例
// console.log("KLine渲染: 创建图表实例");
try { // 如果已有实例,先销毁
if (chartInstancesMap[containerId]) { // console.log("KLine渲染: 销毁已有图表实例");
chartInstancesMap[containerId].dispose(); delete chartInstancesMap[containerId]; }
// 使用普通变量存储实例
chartInstancesMap[containerId] = echarts.init(container); // console.log("KLine渲染: 图表实例创建成功");
} catch (error) { // console.error("KLine渲染: 图表实例创建失败:", error);
return; }
const nineTurns = klineData.Kline; console.log("KLine渲染: Kline数据", nineTurns);
// 拿到相应的数据
const splitData = (b) => { const a = JSON.parse(JSON.stringify(b)); let categoryData = []; let values = []; for (let i = 0; i < a.length; i++) { categoryData.push(a[i].splice(0, 1)[0]); values.push(a[i]); } return { categoryData, values, }; }; function vwToPx(vw) { return (window.innerWidth * vw) / 100; }
// k线的数据
var dealData = splitData(nineTurns.KLine20);
var markPointData = []; const getNineNum = (KLine20, JZJG) => { JZJG.forEach((item, index) => { let low = KLine20[index][3]; let high = KLine20[index][4]; if (item[1] != -1) { if (item[1] == 9) { markPointData.push({ name: "low", coord: [index, low], itemStyle: { normal: { color: "rgba(0,0,0,0)", // 标记点透明
}, }, label: { normal: { show: true, position: "bottom", formatter: `${item[1]}`, textStyle: { color: "green", fontSize: window.innerWidth > 769 ? 18 : 15, textBorderColor: "#FFFFFF", textBorderWidth: 2, fontWeight: "bold", }, }, }, }); } else { markPointData.push({ name: "low", coord: [index, low], itemStyle: { normal: { color: "rgba(0,0,0,0)", // 标记点透明
}, }, label: { normal: { show: true, position: "bottom", formatter: `${item[1]}`, textStyle: { color: "green", fontSize: window.innerWidth > 769 ? 12 : 9, textBorderColor: "#FFFFFF", textBorderWidth: 2, fontWeight: "bold", }, }, }, }); } } if (item[2] != -1) { if (item[2] == 9) { markPointData.push({ name: "high", coord: [index, high], itemStyle: { normal: { color: "rgba(0,0,0,0)", // 标记点透明
}, }, label: { normal: { show: true, position: "top", formatter: `${item[2]}`, textStyle: { color: "#0099FF", fontSize: window.innerWidth > 769 ? 18 : 15, textBorderColor: "#FFFFFF", textBorderWidth: 2, fontWeight: "bold", }, }, }, }); } else { markPointData.push({ name: "high", coord: [index, high], itemStyle: { normal: { color: "rgba(0,0,0,0)", // 标记点透明
}, }, label: { normal: { show: true, position: "top", formatter: `${item[2]}`, textStyle: { color: "#0099FF", fontSize: window.innerWidth > 769 ? 12 : 9, textBorderColor: "#FFFFFF", textBorderWidth: 2, fontWeight: "bold", }, }, }, }); } } }); }; getNineNum(nineTurns.KLine20, nineTurns.JZJG); // console.log("markPointData", markPointData);
var arrRange = []; var arrSwing = []; var arrDXTBar = []; var arrInvisibleBar = []; var markPointDXT = []; var arr5DXTBar = []; var arr6DXTBar = []; var arr7DXTBar = []; var DXTmin = 0; var DXTmax = 100; const getDXT = (DXT) => { DXT.forEach((item, index) => { arrRange.push(item[1]); arrSwing.push(item[2]); if (item[2] > 0) { arrDXTBar.push(item[2] * 2); arrInvisibleBar.push(50); DXTmax = Math.max(DXTmax, item[2] * 2 + 50); } else { arrDXTBar.push(-item[2] * 2); arrInvisibleBar.push(item[2] * 2 + 50); }
if (item[5] == 1) { arr5DXTBar.push(100); } else { arr5DXTBar.push(0); } if (item[6] == 1) { arr6DXTBar.push(70); } else { arr6DXTBar.push(0); } if (item[7] == 1) { arr7DXTBar.push(40); } else { arr7DXTBar.push(0); }
if (item[8] == 1) { markPointDXT.push({ name: "DTX-8", coord: [index, 30], symbol: "triangle", // 三角形符号
symbolSize: 10, // 符号大小
symbolRotate: 0, // 向上的三角形(0度)
itemStyle: { normal: { color: "#FFFF00", // 标记点透明
borderColor: "#000000", // 边框颜色
borderWidth: 1, }, }, label: { normal: { show: false, }, }, }); }
if (item[9] == 1) { markPointDXT.push({ name: "DTX-9", coord: [index, 20], symbolSize: 7, // 正方形大小
itemStyle: { normal: { color: "red", // 标记点透明
// borderColor: "#000000", // 边框颜色
// borderWidth: 1,
}, }, label: { normal: { show: false, }, }, }); }
if (item[10] == 1) { markPointDXT.push({ name: "DTX-10", coord: [index, 15], symbol: "rect", // 设置为正方形
symbolSize: 7, // 正方形大小
itemStyle: { normal: { color: "grey", // 标记点透明
// borderColor: "#000000", // 边框颜色
// borderWidth: 1,
}, }, label: { normal: { show: false, }, }, }); } }); }; getDXT(nineTurns.DXT);
var arrBlue = []; var arrRed = []; var arrWhite = []; var arrYellow = []; const getZJQS = (ZJQS) => { ZJQS.forEach((item, index) => { arrBlue.push(item[1]); arrRed.push(item[2]); arrWhite.push(item[3]); arrYellow.push(item[4]); }); }; getZJQS(nineTurns.ZJQS); // console.log("arrBlue", arrBlue);
// console.log("arrRed", arrRed);
// console.log("arrWhite", arrWhite);
// console.log("arrYellow", arrYellow);
KlineOption = { // 手放上去显示的内容
tooltip: { position: function (point, params) { if (params[0].seriesIndex == 1) { return window.innerWidth > 768 ? ["12%", "42%"] : ["18%", "40%"]; } }, // 调用接口之后方法
formatter: function (a, b, d) { if (a[0].seriesIndex == 0) { const KlineTag = ref([]); // 判断几根K线
const AIBullTag = ref([]);
// 找到第一个满足条件的数据
KlineTag.value = a.find((item) => item.data[1])?.data || [];
// 找到第一个满足条件的非 '-' 数据
AIBullTag.value = a.slice(4).find((item) => item.data[1] !== "-")?.data || []; return ( a[0].name + "<br/>" + "开" + ":" + KlineTag.value[1] + "<br/>" + "收" + ":" + KlineTag.value[2] + "<br/>" + "低" + ":" + KlineTag.value[3] + "<br/>" + "高" + ":" + KlineTag.value[4] ); } else if (a[0].seriesIndex == 1) { return ( `<span style='color:red;'>RANGE: </span>` + a[0].data + " <span style='color:yellow'>SWING: </span>" + a[1].data ); } else { return null; // 格式化成交量显示
let formattedVolume; if (a[0].data.value >= 10000) { formattedVolume = (a[0].data.value / 10000).toFixed(2) + "w"; } else { formattedVolume = a[0].data.value; } return a[0].name + "<br/>" + "量" + ":" + formattedVolume; } }, trigger: "axis", axisPointer: { //坐标轴指示器配置项
type: "cross", //‘line’直线指示器,‘cross’十字准星指示器,‘shadow’阴影指示器
}, backgroundColor: "rgba(119, 120, 125, 0.6)", // 提示框浮层的边框颜色。
borderWidth: 1, // 提示框浮层的边框宽。
borderColor: "#77787D", // 提示框浮层的边框颜色。
padding: 10, // 提示框浮层内边距,
textStyle: { fontSize: window.innerWidth > 768 ? 12 : 8, //提示框浮层上的文字样式
color: "#fff", }, }, // 手放上去时拉的框
axisPointer: { link: [ { xAxisIndex: "all", // 同时触发所有图形的 x 坐标轴指示器
}, ], label: { backgroundColor: "#77787D", // 文本标签的背景颜色
}, }, toolbox: { show: false, }, grid: [ { left: window.innerWidth > 768 ? "12%" : "18%", right: window.innerWidth > 768 ? "10%" : "12%", top: window.innerWidth > 768 ? "8%" : "8%", height: window.innerWidth > 768 ? "34%" : "32%", containLabel: false, }, { left: window.innerWidth > 768 ? "12%" : "18%", right: window.innerWidth > 768 ? "10%" : "12%", top: window.innerWidth > 768 ? "48%" : "46%", height: window.innerWidth > 768 ? "18%" : "20%", containLabel: false, }, { left: window.innerWidth > 768 ? "12%" : "18%", right: window.innerWidth > 768 ? "10%" : "12%", top: window.innerWidth > 768 ? "68%" : "68%", height: window.innerWidth > 768 ? "18%" : "20%", containLabel: false, }, { left: window.innerWidth > 768 ? "12%" : "18%", right: window.innerWidth > 768 ? "10%" : "12%", top: window.innerWidth > 768 ? "48%" : "46%", height: window.innerWidth > 768 ? "18%" : "20%", containLabel: false, }, ], xAxis: [ { type: "category", data: dealData.categoryData, boundaryGap: true, // 坐标轴两边是否留空,false表示不留空(通常用于K线图)
axisLine: { onZero: false }, // 设置坐标轴是否通过零点,onZero:false表示不强制穿过零点
splitLine: { show: false }, // 是否显示分隔线,false表示不显示
min: "dataMin", // 坐标轴最小值,'dataMin'表示从数据的最小值开始
max: "dataMax", // 坐标轴最大值,'dataMax'表示从数据的最大值开始
axisPointer: { label: { show: false, }, z: 100, // 坐标轴指示器的层级,较大的值会让它显示在其他元素上方
}, axisLine: { lineStyle: { normal: { color: "black", // 坐标轴线的颜色
}, }, }, //
axisLabel: { show: false }, // 隐藏刻度标签
axisTick: { show: false }, // 隐藏刻度线
}, // 短线通1
{ type: "category", gridIndex: 1, data: dealData.categoryData, boundaryGap: true, axisPointer: { label: { show: false, }, }, axisLine: { lineStyle: { normal: { color: "black" } } }, axisLabel: { show: false, interval: "auto", }, axisTick: { show: true }, // 隐藏刻度线
}, // 下方成交量图的X轴
{ type: "category", gridIndex: 2, data: dealData.categoryData, boundaryGap: true, axisLine: { lineStyle: { normal: { color: "black" } } }, axisLabel: { show: true, fontSize: window.innerWidth > 768 ? 12 : 9, interval: "auto", }, axisTick: { show: false }, // 隐藏刻度线
}, // 短线通2
{ type: "category", gridIndex: 3, data: dealData.categoryData, boundaryGap: true, axisPointer: { label: { show: false, }, }, axisLine: { show: false }, axisLabel: { show: false }, axisTick: { show: false }, // 隐藏刻度线
}, ], // 控制纵坐标展示数据
yAxis: [ { scale: true, gridIndex: 0, position: "left", axisLabel: { inside: false, align: "right", fontSize: window.innerWidth > 768 ? 12 : 9, }, axisLine: { show: true, lineStyle: { normal: { color: "black", }, }, }, axisTick: { show: false }, splitLine: { show: false }, }, { scale: true, gridIndex: 1, min: DXTmin, max: DXTmax, axisLabel: { show: true, fontSize: window.innerWidth > 768 ? 12 : 9, margin: 8, // 添加边距以获得更好的间距
}, axisLine: { show: true, lineStyle: { normal: { color: "black" } } }, axisTick: { show: false }, splitLine: { show: true, lineStyle: { normal: { type: "dashed" } } }, // 添加分割线以提高可读性
boundaryGap: ["20%", "20%"], // 为坐标轴边界添加内边距
}, { scale: true, gridIndex: 2, splitNumber: 4, // 增加分割数以获得更好的间距
minInterval: 1, // 确保标签之间的最小间隔
axisLabel: { show: true, fontSize: window.innerWidth > 768 ? 12 : 9, margin: 8, // 添加边距以获得更好的间距
}, axisLine: { show: true, lineStyle: { normal: { color: "black" } } }, axisTick: { show: false }, splitLine: { show: true, lineStyle: { normal: { type: "dashed" } } }, // 添加分割线以提高可读性
boundaryGap: ["20%", "20%"], // 为坐标轴边界添加内边距
}, { scale: true, gridIndex: 3, min: DXTmin, max: DXTmax, axisLabel: { show: false }, axisLine: { show: false }, axisTick: { show: false }, splitLine: { show: false }, // 添加分割线以提高可读性
boundaryGap: ["20%", "20%"], // 为坐标轴边界添加内边距
}, ],
// 下拉条
dataZoom: [ { type: "inside", xAxisIndex: [0, 1, 2, 3], }, { show: true, xAxisIndex: [0, 1, 2, 3], type: "slider", top: window.innerWidth > 768 ? "90%" : "92%", height: window.innerWidth > 768 ? "25" : "20", // left: window.innerWidth > 768 ? "10%" : "8%",
start: 98, end: 100, }, ], series: [ { name: "九转结构", type: "candlestick", barWidth: "50%", // 设置和上方图表一致的柱子宽度
data: dealData.values, xAxisIndex: 0, // 使用第一个 X 轴
yAxisIndex: 0, // 使用第一个 Y 轴
markPoint: { symbol: "circle", symbolSize: 10, data: markPointData, z: 5, // 确保标记显示在最上层
}, itemStyle: { normal: { color: "red", // 默认颜色
color0: "green", borderColor: "red", borderColor0: "green", }, }, gridIndex: 0, }, { name: "短线通-RANGE", type: "line", xAxisIndex: 1, yAxisIndex: 1, data: arrRange, smooth: false, symbol: "none", itemStyle: { normal: { color: "#FA0096", }, }, lineStyle: { normal: { width: 2, type: "solid", }, }, }, { name: "短线通-SWING", type: "line", xAxisIndex: 1, yAxisIndex: 1, data: arrSwing, smooth: false, symbol: "none", show: false, itemStyle: { normal: { color: "transparent", // 设置为透明
opacity: 0, // 透明度为0
}, }, lineStyle: { normal: { width: 0, // 线宽为0
opacity: 0, // 透明度为0
}, }, }, { name: "短线通-隐形柱", type: "bar", stack: "Total", xAxisIndex: 1, yAxisIndex: 1, data: arrInvisibleBar, barWidth: "2", // 设置柱子宽度
// barGap: "-100%", // 完全重叠
z: 5, // 确保标记显示在最上层
itemStyle: { normal: { color: "transparent", // color: "#000",
}, }, lineStyle: { normal: { width: 2, type: "solid", }, }, }, { name: "短线通-红蓝柱", type: "bar", stack: "Total", xAxisIndex: 1, yAxisIndex: 1, data: arrDXTBar, barWidth: "20%", // 设置柱子宽度
barGap: "-40%", // 完全重叠
z: 5, // 确保标记显示在最上层
itemStyle: { normal: { color: function (params) { // console.log(params.dataIndex);
if (nineTurns.DXT[params.dataIndex][4] == 1) { return "#FA0096"; } else if (nineTurns.DXT[params.dataIndex][4] == 2) { return "#0099FF"; } return "transparent"; }, }, }, lineStyle: { normal: { width: 2, type: "solid", }, }, }, { name: "短线通-黄柱", type: "bar", xAxisIndex: 3, yAxisIndex: 3, data: arr5DXTBar, barWidth: "40%", // 设置柱子宽度
z: 6, // 确保标记显示在最上层
itemStyle: { normal: { color: "rgb(154,154,16)", }, }, lineStyle: { normal: { width: 2, type: "solid", }, }, }, { name: "短线通-灰柱", type: "bar", xAxisIndex: 3, yAxisIndex: 3, data: arr6DXTBar, barWidth: "40%", // 设置柱子宽度
z: 6, // 确保标记显示在最上层
itemStyle: { normal: { color: "rgb(140,140,140)", }, }, lineStyle: { normal: { width: 2, type: "solid", }, }, }, { name: "短线通-蓝柱", type: "bar", xAxisIndex: 3, yAxisIndex: 3, data: arr7DXTBar, barWidth: "40%", // 设置柱子宽度
barGap: "-100%", // 完全重叠
z: 6, // 确保标记显示在最上层
itemStyle: { normal: { color: "rgb(110,200,255)", }, }, lineStyle: { normal: { width: 2, type: "solid", }, }, }, { name: "短线通-其他指标", type: "line", xAxisIndex: 1, yAxisIndex: 1, markPoint: { symbol: "circle", symbolSize: 10, data: markPointDXT, z: 7, // 确保标记显示在最上层
}, data: [], smooth: false, symbol: "none", itemStyle: { normal: { color: function (params) { // params.value 是当前数据点的值
// params.dataIndex 是当前数据点的索引
return params.value > 0 ? "#FA0096" : "#0099FF"; }, }, }, lineStyle: { normal: { width: 2, type: "solid", }, }, }, { name: "资金趋势-蓝色线", type: "line", xAxisIndex: 2, yAxisIndex: 2, data: arrBlue, smooth: false, symbol: "none", itemStyle: { normal: { color: "#00A2A2", }, }, lineStyle: { normal: { width: 2, type: "solid", }, }, }, { name: "资金趋势-红色线", type: "line", xAxisIndex: 2, yAxisIndex: 2, data: arrRed, smooth: false, symbol: "none", itemStyle: { normal: { color: "#FF0000", }, }, lineStyle: { normal: { width: 2, type: "solid", }, }, }, { name: "资金趋势-白色线", type: "line", xAxisIndex: 2, yAxisIndex: 2, data: arrWhite, smooth: false, symbol: "none", itemStyle: { normal: { color: "grey", }, }, lineStyle: { normal: { width: 2, type: "solid", }, }, }, { name: "资金趋势-黄色线", type: "line", xAxisIndex: 2, yAxisIndex: 2, data: arrYellow, smooth: false, symbol: "none", itemStyle: { normal: { color: "#FFFF00", }, }, lineStyle: { normal: { width: 2, type: "solid", }, }, }, ], }; }
// console.log("KLine渲染: 图表配置完成");
try { // 应用配置
// console.log("KLine渲染: 开始设置图表选项");
chartInstancesMap[containerId].setOption(KlineOption); // console.log("KLine渲染: 图表选项设置成功");
// 窗口大小变化时重新渲染图表
const resizeFunc = _.throttle( function () { console.log("窗口大小改变,调整图表大小"); if ( chartInstancesMap[containerId] && !chartInstancesMap[containerId].isDisposed() ) { // 如果设备类型发生变化,重新渲染
const newIsMobile = window.innerWidth < 768; const newIsTablet = window.innerWidth >= 768 && window.innerWidth < 1024;
if (newIsMobile !== isMobile || newIsTablet !== isTablet) { console.log("设备类型变化,重新渲染图表"); KlineCanvsEcharts(containerId); return; }
chartInstancesMap[containerId].resize(); } }, 1000, { trailing: false } );
// 给resize事件绑定一个特定的函数名,便于后续移除
window[`resize_${containerId}`] = resizeFunc;
// 绑定resize事件
window.removeEventListener("resize", window[`resize_${containerId}`]); window.addEventListener("resize", window[`resize_${containerId}`]);
// console.log("KLine渲染: 图表渲染完成");
} catch (error) { // console.error("KLine渲染: 图表渲染出错", error);
} }
watch( () => audioStore.isVoiceEnabled, (newVal) => { // 添加状态锁定逻辑
if (newVal === audioStore.lastVoiceState) return; audioStore.lastVoiceState = newVal;
if (newVal) { console.log("开启语音播放"); // 添加重试机制
const tryPlay = () => { if (!audioStore.ttsUrl) return; // 新增空值判断
if (audioStore.soundInstance?.playing()) return; playAudio(audioStore.ttsUrl); setTimeout(() => { if (!audioStore.soundInstance?.playing()) { Howler.unload(); } }, 1000); }; tryPlay(); } else { console.log("关闭语音播放"); pauseAudio(); } }, { immediate: true } );
watch( () => dataStore.activeTabIndex, (newVal) => { setTimeout(() => { console.log("activeTabIndex变化:", newVal); // 当标签页切换回来时,重新渲染所有图表
if (newVal === 0) { console.log("切换到AI聊天页,重新渲染图表"); // 延迟执行以确保DOM已渲染
renderAllKlineCharts(); } }, 1000); }, { immediate: true } // 添加immediate属性,确保初始化时执行一次
);
const scrollToTop = () => { homepageChatStore.dbqbScrollToTop = !homepageChatStore.dbqbScrollToTop; };
// 添加渲染所有K线图的方法
async function renderAllKlineCharts() { console.log("重新渲染所有K线图");
// 查找所有K线消息
const messages = chatStore.messages; for (let i = 0; i < messages.length; i++) { if (messages[i].kline && messages[i].chartData) { const containerId = `kline-container-${i}`; console.log(`尝试渲染K线图: ${containerId}`);
// 确保DOM已经渲染
const container = document.getElementById(containerId); console.log("container", container); await nextTick(); if (container) { // 渲染图表
KlineCanvsEcharts(containerId); } else { console.warn(`找不到容器: ${containerId}`); } } } }
const clearAudio = () => { // 停止音频播放
if (audioStore.isPlaying) { audioStore.stop(); console.log("组件卸载,音频已停止"); } // 清理队列系统
audioQueue.value = []; isPlayingAudio.value = false; currentPlayIndex = 0; isCallingPlayNext = false;
// 清理音频实例
audioStore.soundInstance = null; audioStore.nowSound = null;
// 停止所有 Howler 实例
Howler.stop(); Howler.unload();
// 重置音频状态
audioStore.isPlaying = false; audioStore.isPaused = false; audioStore.playbackPosition = 0;
// 清理预加载状态
Object.keys(audioPreloadStatus).forEach((key) => { audioPreloadStatus[key] = { loaded: false, url: null }; });
chatStore.currentUserIndex = -1; };
// 初始化随机GIF
onMounted(() => { clearAudio(); // 检测移动设备
const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent );
if (isMobile) { // 强制移动设备使用 Web Audio API
Howler.html5PoolSize = 1; // 限制HTML5音频池大小
Howler.autoSuspend = false; // 禁用自动挂起
Howler.usingWebAudio = true; // 强制使用Web Audio API
// 激活音频上下文
const activateAudioContext = () => { if (Howler.ctx && Howler.ctx.state === "suspended") { Howler.ctx.resume(); console.log("音频上下文已激活"); } };
// 监听用户交互以激活音频上下文
document.addEventListener("touchstart", activateAudioContext, { once: true, }); document.addEventListener("click", activateAudioContext, { once: true }); } // 初始化marked组件
marked.setOptions({ breaks: true, // 支持换行符转换为 <br>
gfm: true, // 启用 GitHub Flavored Markdown
sanitize: false, // 不清理 HTML(谨慎使用)
smartLists: true, // 智能列表
smartypants: true, // 智能标点符号
xhtml: false, // 不使用 XHTML 输出
renderer: renderer, }); renderAllKlineCharts(); console.log("组件挂载完成"); // 重置音频下标
chatStore.currentUserIndex = null;
chatStore.messages.forEach((item) => { if (item.sender == "user") { item.audioStatus = false; } });
// 添加页面可见性变化监听器
document.addEventListener("visibilitychange", handleVisibilityChange);
// 添加DOM变化监听器
const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === "childList" && mutation.addedNodes.length) { // 检查是否添加了图表容器
const containers = document.querySelectorAll( '[id^="kline-container-"]' ); if (containers.length) { // console.log("DOM变化监听到K线容器:", Array.from(containers).map(el => el.id));
} } }); });
// 开始监听DOM变化
observer.observe(document.body, { childList: true, subtree: true }); });
// 页面可见性变化处理
let wasPlayingBeforeHidden = false;
const handleVisibilityChange = () => { if (document.hidden) { // 页面被隐藏时,如果音频正在播放,则暂停并记录状态
if (audioStore.isPlaying) { wasPlayingBeforeHidden = true; audioStore.pause(); console.log("页面切换离开,音频已暂停"); } else { wasPlayingBeforeHidden = false; } } else { // 页面重新可见时,如果之前在播放,则恢复播放
if (wasPlayingBeforeHidden && !audioStore.isPlaying) { audioStore.play(); console.log("页面切换回来,音频已恢复播放"); wasPlayingBeforeHidden = false; } } };
// 组件卸载时清理所有图表实例和事件监听器
onUnmounted(() => { // 移除页面可见性变化监听器
document.removeEventListener("visibilitychange", handleVisibilityChange);
clearAudio();
// 清理所有图表实例
Object.keys(chartInstancesMap).forEach((key) => { if (chartInstancesMap[key]) { // 移除resize事件监听
if (window[`resize_${key}`]) { window.removeEventListener("resize", window[`resize_${key}`]); delete window[`resize_${key}`]; }
// 销毁图表实例
chartInstancesMap[key].dispose(); delete chartInstancesMap[key]; } }); }); </script>
<template> <div class="chat-container"> <div id="deepNine-top-anchor"></div> <!-- GIF区域 --> <div class="gif-area"> <img src="https://d31zlh4on95l9h.cloudfront.net/images/a686991d0a26bdbafd938a15da93d5b4.png" alt="深度九大模型logo" class="bgc" /> <img src="https://d31zlh4on95l9h.cloudfront.net/images/35bf808538183be0062e4647570a9abd.png" alt="深度九大模型logo" class="logo1" /> <img src="https://d31zlh4on95l9h.cloudfront.net/images/0c09c892051e7ae16cbff6091075aee9.png" alt="深度九大模型标题logo" class="logo2" /> </div>
<div v-for="(msg, index) in chatMsg" :key="index"> <!-- 用户消息容器,包含喇叭按钮 --> <div v-if="msg.sender === 'user'" class="user-message-container"> <div class="user-msg"> <div class="user-content"> <img :src="msg.audioStatus ? voice : voiceNoActive" class="user-message-speaker" :class="{ 'speaker-active': msg.audioStatus, }" @click="toggleVoiceForUser(index)" alt="喇叭" /> <div :class="{ 'message-bubble': true, [msg.sender]: msg.sender, [msg.class]: msg.class, }" > <div v-html="msg.content"></div> </div> </div> <div v-if="msg.timestamp" class="user-sendTime"> {{ moment(msg.timestamp).format("YYYY-MM-DD HH:mm:ss") }} </div> </div> </div>
<!-- AI消息和其他类型消息 --> <div v-else :class="{ 'message-bubble': true, [msg.sender]: msg.sender, [msg.class]: msg.class, }" > <div v-if="msg.type === 'kline'" class="kline-container"> <div :id="'kline-container-' + index" class="chart-mount-point"> <div v-if="!msg.hasValidData" class="no-data-message"> <p>暂无K线数据</p> </div> </div> </div> <div v-else-if="msg.type == 'ing'" class="ai-message-container"> <img v-if="msg.gif" :src="msg.gif" alt="思考过程" class="thinking-gif" /> <div class="ai-message-content" :class="{ fourStep: msg.nowrap }"> <div v-if="msg.flag"> <span>{{ msg.content }}</span> <span class="loading-dots"> <span class="dot">.</span> <span class="dot">.</span> <span class="dot">.</span> <span class="dot">.</span> <span class="dot">.</span> <span class="dot">.</span> </span> </div> <div v-else v-html="msg.content"></div> </div> </div> <div v-else-if="msg.type == 'title1'" style="display: flex; width: 100%" > <div class="mainTitle"> {{ msg.content }} </div> <div class="date"> {{ msg.date }} </div> </div> <div v-else-if="msg.type == 'title2'" class="title2"> <img class="title1Img" src="https://d31zlh4on95l9h.cloudfront.net/images/c5c2887c56cde033c362cecae4cda4b4.png" alt="出错了" /> </div> <div v-else-if="msg.type == 'title3'" class="title3"> <img class="title2Img" :src="msg.content" alt="出错了" /> </div> <div v-else-if="msg.type == 'content1'" class="content1"> <div v-if="msg.kline" class="kline-container content1chart"> <div :id="'kline-container-' + index" class="chart-mount-point"> <div v-if="!msg.hasValidData" class="no-data-message"> <p>暂无数据</p> </div> </div> </div> <div v-else class="content1Text"> <div v-html="msg.content" class="text1"></div> </div> </div> <div v-else-if="msg.type == 'content2'" class="content2"> <div class="kline-container content2chart"> <div :id="'kline-container-' + index" class="chart-mount-pointJN"> <div v-if="!msg.hasValidData" class="no-data-message"> <p>暂无数据</p> </div> </div> </div> </div> <div v-else-if="msg.type == 'img1'" class="img1"> <div class="img1Bk"> <img class="img1Img" :src="msg.content" alt="出错了" /> </div> </div> <div v-else-if="msg.type == 'content3'" class="content3"> <div class="content3Text"> <div v-html="msg.content" class="text3"></div> </div> </div> <div v-else-if="msg.type == 'mianze'" class="mianze"> <div v-html="msg.content"></div> </div> <div v-else v-html="msg.content"></div> </div> </div> </div> <!-- 全局返回顶部按钮 --> <div v-if="chatMsg.length > 0" class="back-to-top" @click="scrollToTop"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" > <path d="M12 4L12 20M12 4L6 10M12 4L18 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> </svg> </div> </template>
<style scoped> p { font-size: 20px; }
.bgc { position: absolute; z-index: -1; max-width: 440px; min-width: 300px; top: -15px; width: 40%; height: auto; /* 添加旋转动画 */ animation: rotate 10s linear infinite reverse; }
/* 定义旋转动画 */ @keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.logo1 { max-width: 350px; min-width: 200px; width: 15%; }
.logo2 { margin-top: 20px; max-width: 350px; min-width: 200px; width: 30%; /* position: relative; */ }
.chat-container { display: flex; flex-direction: column; overflow: hidden; }
.gif-area { padding: 70px 0px; position: relative; /* height: 30vh; */ display: flex; flex-direction: column; justify-content: center; align-items: center; flex-shrink: 0;
/* 防止GIF区域被压缩 */ }
.message-area { margin-top: 2%; flex: 1; /* 消息区域占据剩余空间 */ overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 15px; }
.marquee-container { /* position: absolute; */ bottom: 0; width: 100%; /* ga */ }
.marquee-row { white-space: nowrap; overflow: visible; padding: 8px 0; width: 100%; }
.marquee-item { display: inline-block; margin: 0 15px; padding: 8px 20px; background: rgba(255, 255, 255, 0.9); /* 白色背景 */ border-radius: 10px; /* 圆角矩形 */ color: #333; /* 文字颜色改为深色 */ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* 添加阴影 */ transition: all 0.3s; transition: color 0.3s; }
.top { animation: marquee 25s linear infinite; /* 默认动画是运行状态 */ animation-play-state: running; }
.bottom { animation: marquee 15s linear infinite reverse; /* 默认动画是运行状态 */ animation-play-state: running; }
/* 返回顶部按钮样式 */ .back-to-top { position: sticky !important; bottom: 20px !important; left: calc(100% - 70px) !important; width: 50px !important; height: 50px !important; background: linear-gradient(135deg, #00d4ff 0%, #0066cc 100%) !important; border-radius: 50% !important; display: flex !important; align-items: center !important; justify-content: center !important; cursor: pointer !important; transition: all 0.3s ease !important; z-index: 100 !important; color: white !important; opacity: 1 !important; visibility: visible !important; margin-top: 20px !important; margin-bottom: 20px !important; }
.back-to-top:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(0, 212, 255, 0.5); background: linear-gradient(135deg, #00e6ff 0%, #0077dd 100%); }
.back-to-top:active { transform: translateY(-1px); }
/* 添加PC端专用速度 */ @media (min-width: 768px) { .top { animation-duration: 35s; /* PC端改为35秒 */ }
.bottom { animation-duration: 35s; /* PC端改为35秒 */ } }
@keyframes marquee { 0% { transform: translateX(100%); }
100% { transform: translateX(-250%); } }
.loading-dots { display: inline-block; }
.dot { opacity: 0.4; animation: loading 1.4s infinite; }
.dot:nth-child(1) { animation-delay: 0s; }
.dot:nth-child(2) { animation-delay: 0.2s; }
.dot:nth-child(3) { animation-delay: 0.4s; }
.dot:nth-child(4) { animation-delay: 0.6s; }
.dot:nth-child(5) { animation-delay: 0.8s; }
.dot:nth-child(6) { animation-delay: 1s; }
@keyframes loading { 0%, 60%, 100% { opacity: 0.4; }
30% { opacity: 1; } }
.message-bubble { max-width: 80%; margin: 10px 0px; padding: 15px 20px; position: relative; }
/* 用户消息容器样式 */ .user-message-container { display: flex; align-items: flex-end; margin: 10px 0px; justify-content: flex-end; gap: 10px; /* align-items: center; */ flex-direction: column; }
.user-msg { margin-left: auto; display: flex; flex-direction: column; }
.user-content { display: flex; height: 100%; align-items: center; margin-right: 5px; justify-content: flex-end; padding: 0 20px; }
.user-sendTime { width: 100%; text-align: center; color: rgba(255, 255, 255, 0.6); font-size: 0.8rem; }
.user-message-speaker { width: 32px; height: 32px; object-fit: contain; margin-right: 5px; cursor: pointer; transition: all 0.3s ease; }
.user-message-speaker:hover { transform: scale(1.1); }
.user-message-speaker.speaker-active { animation: pulse 1.5s infinite; }
@keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } }
.message-bubble.user { color: #6d22f8; background: white; font-weight: bold; border-radius: 10px; margin: 0; display: flex; align-items: center; word-break: break-word; /* 启用强制换行 */ }
.message-bubble.ai { background: #2b378d; color: #ffffff; margin: 0 auto; /* border-bottom-left-radius: 5px; */ }
.message-bubble.ing { background: #ffffff; color: #000000; font-weight: bold; border-radius: 10px; margin-left: 20px; margin-right: auto; display: flex; align-items: center; width: fit-content; }
.message-bubble.ai.title1 { width: 100%; display: flex; border-radius: 10px 10px 0px 0px; /* border-bottom-left-radius: 5px; */ }
.mainTitle { font-size: 16px; font-weight: bold; background-image: url("@/assets/img/AiEmotion/bk01.png"); background-repeat: no-repeat; background-size: 100% 100%; min-width: 200px; width: 20vw; max-width: 50%; height: 50px; padding: 15px 10px; text-align: center; line-height: 20px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; box-sizing: border-box; }
.date { font-size: 1.5rem; font-weight: bold; margin-left: auto; /* width: 100px; */ display: flex; justify-content: center; align-items: center; }
.message-bubble.ai.title2 { width: 100%; display: flex; justify-content: center; align-items: center; }
.title1Img { max-width: 500px; width: 90vw; }
.message-bubble.ai.title3 { width: 100%; display: flex; justify-content: center; align-items: center; }
.title2Img { max-width: 500px; width: 90vw; }
.message-bubble.ai.content1 { width: 100%; display: flex; justify-content: center; align-items: center; }
.content1chart { background-image: url("@/assets/img/AIchat/罗盘边框.png"); background-repeat: no-repeat; background-size: 100% 100%; width: 50vw; min-width: 350px; display: flex; justify-content: center; align-items: center; }
.content1Text { background-image: url("@/assets/img/AIchat/框.png"); background-repeat: no-repeat; background-size: 100% 100%; width: 50vw; min-width: 350px; /* height: 20vw; */ /* max-height: 400px; */ padding: 5% 0; }
.text1 { font-weight: bold; /* margin-left: 6%; */ /* margin-bottom: 10px; */ margin: 0px 6% 10px 6%; font-size: 20px; }
.message-bubble.ai.content2 { width: 100%; display: flex; justify-content: center; align-items: center; }
.content2chart { background-image: url("@/assets/img/AIchat/PCbackPic.png"); background-repeat: no-repeat; background-size: 100% 100%; width: 50vw; min-width: 350px; display: flex; justify-content: center; align-items: center; height: calc(500px + 10vw) !important; }
.message-bubble.ai.img1 { width: 100%; display: flex; justify-content: center; align-items: center; }
.img1Bk { background-image: url("@/assets/img/AIchat/边框.png"); background-repeat: no-repeat; background-size: 100% 100%; width: 50vw; min-width: 350px; /* height: 20vw; */ /* max-height: 400px; */ padding: 5% 0px; display: flex; justify-content: center; align-items: center; }
.img1Img { width: 90%; }
.message-bubble.ai.content3 { width: 100%; display: flex; justify-content: center; align-items: center; }
.content3Text { background-image: url("@/assets/img/AIchat/边框.png"); background-repeat: no-repeat; background-size: 100% 100%; width: 50vw; min-width: 350px; /* height: 20vw; */ /* max-height: 400px; */ padding: 5% 0px; }
.text3 { /* font-weight: bold; */ /* margin-left: 6%; */ /* margin-bottom: 10px; */ margin: 0px 6% 10px 6%; font-size: 20px; }
.message-bubble.ai.mianze { width: 100%; text-align: center; font-weight: bold; font-size: 24px; border-radius: 0px 0px 10px 10px; }
.kline-container { margin-top: 10px; /* 最小高度 */ min-height: 320px; /* 视口高度单位 */ height: 40vh; width: 50vw; }
@media (max-width: 768px) { .gif-area { padding: 0px 0px; position: relative; /* height: 30vh; */ display: flex; flex-direction: column; justify-content: center; align-items: center; flex-shrink: 0; }
.logo1 { max-width: 350px; min-width: 200px; width: 15%; scale: 0.8; }
.logo2 { margin-top: 20px; max-width: 350px; min-width: 200px; width: 80%; /* position: relative; */ }
.kline-container { min-width: 75vw; }
.content1Text { width: 77vw; min-width: 0px; /* height: 20vw; */ /* min-height: 150px; */ }
.date { font-size: 14px; text-align: end; }
.text1 { font-size: 20px; }
.content2chart { background-image: url("@/assets/img/AIchat/new-app-bgc.png") !important; height: 100vw; }
.img1Bk { width: 77vw; min-width: 0px; /* height: 20vw; */ /* min-height: 150px; */ }
.content3Text { width: 77vw; min-width: 0px; /* height: 20vw; */ /* min-height: 150px; */ }
.text3 { font-size: 20px; }
.message-bubble.ai.mianze { font-size: 18px; }
.back-to-top { left: calc(100% - 65px) !important; width: 45px !important; height: 45px !important; } }
.kline-container .chart-mount-point { display: flex; justify-content: center; align-items: center; height: 80%; width: 90%; }
.kline-container .chart-mount-pointJN { display: flex; justify-content: center; align-items: center; height: 100%; width: 100%; }
/* AI消息容器样式 */ .ai-message-container { display: flex; align-items: center; gap: 10px; margin-right: auto; /* max-width: 80%; */ }
/* 思考过程动图样式 */ .thinking-gif { width: 40px; height: 40px; object-fit: contain; margin-top: 5px; border-radius: 8px; animation: float 2s ease-in-out infinite; }
@keyframes float { 0%, 100% { transform: translateY(0px); } 50% { transform: translateY(-5px); } }
/* AI消息内容样式 */ .ai-message-content { display: flex; align-items: center; /* white-space: nowrap; */ width: fit-content; overflow: visible; }
.fourStep { white-space: nowrap; }
@media only screen and (max-width: 480px) { .back-to-top { left: calc(100% - 60px) !important; width: 40px !important; height: 40px !important; }
.back-to-top svg { width: 20px; height: 20px; } } </style>
|