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
4173 lines
120 KiB
<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>
|