diff --git a/src/assets/img/AIchat/AIgif5.gif b/src/assets/img/AIchat/AIgif5.gif new file mode 100644 index 0000000..0ab0e4a Binary files /dev/null and b/src/assets/img/AIchat/AIgif5.gif differ diff --git a/src/assets/img/AIchat/AIgif6.gif b/src/assets/img/AIchat/AIgif6.gif new file mode 100644 index 0000000..51aafca Binary files /dev/null and b/src/assets/img/AIchat/AIgif6.gif differ diff --git a/src/assets/img/AIchat/AIgif7.gif b/src/assets/img/AIchat/AIgif7.gif new file mode 100644 index 0000000..1f05ee9 Binary files /dev/null and b/src/assets/img/AIchat/AIgif7.gif differ diff --git a/src/store/chat.js b/src/store/chat.js index 1209909..2e91462 100644 --- a/src/store/chat.js +++ b/src/store/chat.js @@ -1,11 +1,19 @@ import { defineStore } from 'pinia'; - +import { getUserCountAPI } from '@/api/AIxiaocaishen' export const useChatStore = defineStore('chat', { state: () => ({ messages: [], - isLoading: false // 新增加载状态 + isLoading: false, // 新增加载状态 + UserCount: 0, + chartData: [], }), actions: { + async getUserCount() { + const result = await getUserCountAPI({ + token: localStorage.getItem('localToken') + }) + this.UserCount = result.data.hasCount + }, setLoading(status) { this.isLoading = status }, @@ -13,7 +21,10 @@ export const useChatStore = defineStore('chat', { this.isLoading = true; }, isLoadingF() { - this.isLoading = false; + this.isLoading = false; + }, + addKLineData(data) { + this.kLineData.push(data); } }, persist: { @@ -21,7 +32,8 @@ export const useChatStore = defineStore('chat', { strategies: [ { key: 'chat_messages', - storage: localStorage + storage: localStorage, + paths: ['messages', 'kLineData'] } ] } diff --git a/src/views/AIchat.vue b/src/views/AIchat.vue index 57ca884..816273d 100644 --- a/src/views/AIchat.vue +++ b/src/views/AIchat.vue @@ -10,7 +10,8 @@ import { marked } from 'marked'; // 引入marked库 import katex from 'katex'; // 引入 KaTeX 库 import { htmlToText } from 'html-to-text'; import { Howl, Howler } from 'howler'; -import KLine from './Echarts/KLine.vue' +import KLine from './Echarts/KLine.vue'; +import * as echarts from 'echarts' const chatStore = useChatStore() const audioStore = useAudioStore() const dataStore = useDataStore() @@ -179,127 +180,152 @@ watch( const AIcontent = ref(""); // 处理不同的 answer 字段 - if (ans.value.answerG !== "") { - AIcontent.value = ans.value.answerG; - const code = ans.value.code; - const market = ans.value.market; - const data = JSON.parse(ans.value.duobaoData); - - console.log(data, "data"); - const Kline20 = { - name: data.data.HomePage.StockInformation.Name, - Kline: data.data.AIBull.KLine20 - } - dataStore.setKlineData(Kline20); + const status = JSON.parse(ans.value.resp); + console.log(status, "status") + if (status.code == 200) { + if (ans.value.answerG !== "") { + AIcontent.value = ans.value.answerG; + const code = ans.value.code; + const market = ans.value.market; + const data = JSON.parse(ans.value.duobaoData); + + console.log(data, "data"); + + const Kline20 = { + name: data.data.HomePage.StockInformation.Name, + Kline: data.data.AIBull.KLine20 + } - chatStore.messages.pop(); + dataStore.setKlineData(Kline20); - chatStore.messages.push({ - sender: "ai", - type: "kline", - }); + // chatStore.chartData.push({ + // data: Kline20.Kline + // }); + // for (let i = 0; i < chatStore.chartData.length; i++) { + // console.log(chatStore.chartData[i], "chatStore.chartData[i]") + // } - chatStore.messages.push({ - sender: "ai", - content: "AI正在思考中..." - }); + chatStore.messages.pop(); - console.log(Kline20, "Kline20"); + chatStore.messages.push({ + sender: "ai", + type: "kline", + chartRef: Kline20.name, // 唯一标识符 + chartData: Kline20.Kline, // 图表数据 + }); - console.log(code, "code"); - console.log(market, "market"); - console.log(data, "data"); - } else if (ans.value.answerN !== "") { - AIcontent.value = ans.value.answerN; - } else if (ans.value.answerO !== "") { - AIcontent.value = ans.value.answerO; - } + chatStore.messages.push({ + sender: "ai", + content: "AI正在思考中..." + }); - // // 使用marked库将Markdown转换为HTML - // AIcontent.value = marked(AIcontent.value,); - // // 使用 KaTeX 渲染数学公式 - // const katexRegex = /\$\$(.*?)\$\$/g; - // AIcontent.value = AIcontent.value.replace(katexRegex, (match, formula) => { - // try { - // return katex.renderToString(formula, { throwOnError: false }); - // } catch (error) { - // console.error('KaTeX 渲染错误:', error); - // return match; - // } - // }); - - // chatStore.messages.push({ - // sender: "ai", - // content: AIcontent.value - // }) - - // 修改后的消息处理逻辑 - const processedContent = marked(AIcontent.value); - const katexRegex = /\$\$(.*?)\$\$/g; - const plainTextContent = htmlToText(processedContent); - - // 获取音频数据 - const TTSResult = (await TTSAPI({ - language: "cn", - content: plainTextContent - })).json() + console.log(Kline20, "Kline20"); - const tts = ref(); - await TTSResult.then((res) => { - tts.value = JSON.parse(res.data); - }) + console.log(code, "code"); + console.log(market, "market"); + console.log(data, "data"); - const ttsUrl = ref(); - if (tts.value.tts_cn !== null) { - ttsUrl.value = tts.value.tts_cn.url; - } else if (tts.value.tts_en !== null) { - ttsUrl.value = tts.value.tts_en.url; - } + } else if (ans.value.answerN !== "") { + AIcontent.value = ans.value.answerN; + } else if (ans.value.answerO !== "") { + AIcontent.value = ans.value.answerO; + } + + // // 使用marked库将Markdown转换为HTML + // AIcontent.value = marked(AIcontent.value,); + // // 使用 KaTeX 渲染数学公式 + // const katexRegex = /\$\$(.*?)\$\$/g; + // AIcontent.value = AIcontent.value.replace(katexRegex, (match, formula) => { + // try { + // return katex.renderToString(formula, { throwOnError: false }); + // } catch (error) { + // console.error('KaTeX 渲染错误:', error); + // return match; + // } + // }); + + // chatStore.messages.push({ + // sender: "ai", + // content: AIcontent.value + // }) + + // 修改后的消息处理逻辑 + const processedContent = marked(AIcontent.value); + const katexRegex = /\$\$(.*?)\$\$/g; + const plainTextContent = htmlToText(processedContent); + + // 获取音频数据 + const TTSResult = (await TTSAPI({ + language: "cn", + content: plainTextContent + })).json() + + const tts = ref(); + await TTSResult.then((res) => { + tts.value = JSON.parse(res.data); + }) + + const ttsUrl = ref(); + if (tts.value.tts_cn !== null) { + ttsUrl.value = tts.value.tts_cn.url; + } else if (tts.value.tts_en !== null) { + ttsUrl.value = tts.value.tts_en.url; + } - if (ttsUrl.value) { - nextTick(() => { - if (audioStore.isVoiceEnabled) { - console.log("ttsUrl.value", ttsUrl.value) - // 播放音频 - playAudio(ttsUrl.value) + if (ttsUrl.value) { + nextTick(() => { + if (audioStore.isVoiceEnabled) { + console.log("ttsUrl.value", ttsUrl.value) + // 播放音频 + playAudio(ttsUrl.value) + } + }); + } + + chatStore.messages.pop(); + // 先推送初始消息 + const aiMessage = reactive({ + sender: "ai", + content: "", + isTyping: true, + }); + chatStore.messages.push(aiMessage); + + let index = 0; + const typingInterval = setInterval(() => { + if (index < processedContent.length) { + aiMessage.content += processedContent.charAt(index); + index++; + + } else { + clearInterval(typingInterval); + aiMessage.isTyping = false; + + // 延迟处理KaTeX确保DOM已更新 + nextTick(() => { + aiMessage.content = aiMessage.content.replace(katexRegex, (match, formula) => { + try { + return katex.renderToString(formula, { throwOnError: false }); + } catch (error) { + console.error('KaTeX 渲染错误:', error); + return match; + } + }); + chatStore.setLoading(false); + }); } + }, 50); // 调整速度为50ms/字符 + } else { + chatStore.messages.pop(); + chatStore.messages.push({ + sender: "ai", + content: status.msg }); + chatStore.setLoading(false); } - chatStore.messages.pop(); - // 先推送初始消息 - const aiMessage = reactive({ - sender: "ai", - content: "", - isTyping: true, - }); - chatStore.messages.push(aiMessage); - - let index = 0; - const typingInterval = setInterval(() => { - if (index < processedContent.length) { - aiMessage.content += processedContent.charAt(index); - index++; - - } else { - clearInterval(typingInterval); - aiMessage.isTyping = false; - // 延迟处理KaTeX确保DOM已更新 - nextTick(() => { - aiMessage.content = aiMessage.content.replace(katexRegex, (match, formula) => { - try { - return katex.renderToString(formula, { throwOnError: false }); - } catch (error) { - console.error('KaTeX 渲染错误:', error); - return match; - } - }); - chatStore.setLoading(false); - }); - } - }, 50); // 调整速度为50ms/字符 } catch (e) { console.error('请求失败:', e); @@ -309,14 +335,38 @@ watch( }); chatStore.setLoading(false); } + finally { + await chatStore.getUserCount(); + } } }, { deep: true } ); +// var kLineCharts = document.getElementsByClassName("kLineChart"); +// for (var i = 0; i < kLineCharts.length; i++) { +// (function (i) { +// const data = datatok.Kline +// // 切割数据方法 +// const spliteDate = (a) => { +// const categoryData = [] +// let value = [] +// for (let i = 0; i < a.length; i++) { +// categoryData.push(a[i][0]) +// value.push([a[i][1], a[i][2], a[i][3], a[i][4]]) +// } +// return { categoryData, value } +// } +// const dealData = spliteDate(data) +// var myChart = echarts.init(kLineCharts[i]); + +// }) +// } + + // 初始化随机GIF onMounted(() => { - const random = Math.floor(Math.random() * 4) + 1; + const random = Math.floor(Math.random() * 6) + 1; currentGif.value = `src/assets/img/AIchat/AIgif${random}.gif`; getQuestionsList(); @@ -362,6 +412,8 @@ onMounted(() => { <div v-for="(msg, index) in chatMsg" :key="index" :class="['message-bubble', msg.sender]"> <div v-if="msg.type === 'kline'" class="kline-container"> <KLine /> + <!-- <KLine :key="msg.timestamp" /> --> + <!-- <div class="kLineChart"></div> --> </div> <div v-else v-html="msg.content"></div> </div> @@ -377,6 +429,7 @@ onMounted(() => { .chat-container { display: flex; flex-direction: column; + overflow-x: hidden; } .gif-area { @@ -445,20 +498,34 @@ onMounted(() => { } .top { - animation: marquee 15s linear infinite; + animation: marquee 25s linear infinite; } .bottom { animation: marquee 15s linear infinite reverse; } +/* 添加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(-150%); + transform: translateX(-250%); } } diff --git a/src/views/Echarts/KLine.vue b/src/views/Echarts/KLine.vue index 95198d2..78b9a62 100644 --- a/src/views/Echarts/KLine.vue +++ b/src/views/Echarts/KLine.vue @@ -46,7 +46,7 @@ function KlineCanvsEcharts(datatok) { let value = [] for (let i = 0; i < a.length; i++) { categoryData.push(a[i][0]) - value.push([a[i][1],a[i][2],a[i][3],a[i][4]]) + value.push([a[i][1], a[i][2], a[i][3], a[i][4]]) } return { categoryData, value } } @@ -58,7 +58,7 @@ function KlineCanvsEcharts(datatok) { // Canvs的间隔大小 text: datatok.name, top: 20, - left: 20 + left: 20 }, tooltip: { trigger: 'axis', // 触发类型 坐标轴触发 @@ -206,6 +206,7 @@ function KlineCanvsEcharts(datatok) { onMounted(() => { // fnGetData() }) + </script> <style scoped> diff --git a/src/views/homePage.vue b/src/views/homePage.vue index 11c4dff..f75a455 100644 --- a/src/views/homePage.vue +++ b/src/views/homePage.vue @@ -39,6 +39,7 @@ const tabs = computed(() => [ // 修改 setActiveTab 方法,添加一个可选参数 forceAIchat const setActiveTab = (tab, index, forceAIchat = false) => { + isScrolling.value = false; //回复滚动到底部方法 isAnnouncementVisible.value = false; if (forceAIchat && activeTab.value !== "AIchat") { activeTab.value = "AIchat"; @@ -51,6 +52,7 @@ const setActiveTab = (tab, index, forceAIchat = false) => { sessionStorage.setItem("activeTabAI", tab); sessionStorage.setItem("activeIndexAI", index.toString()); } + console.log(tab, index, "tab, index"); setHeight(document.getElementById("testId")); // 给父组件发送窗口高度 }; @@ -68,12 +70,13 @@ const ensureAIchat = () => { }; // 获取次数 -const UserCount = ref(0); -const getUserCount = async () => { - const result = await getUserCountAPI({ token: localStorage.getItem('localToken') }); - UserCount.value = result.data.hasCount; +// const UserCount = ref(0); +// const getUserCount = async () => { +// const result = await getUserCountAPI({ token: localStorage.getItem('localToken') }); +// UserCount.value = result.data.hasCount; +// }; +const UserCount = computed(() => chatStore.UserCount) -}; const getCount = () => { console.log('点击了获取次数的按钮') @@ -98,6 +101,7 @@ const updateMessage = (title) => { // console.log("updateMessage 的值:", title); }; const sendMessage = async () => { + isScrolling.value = false; // 调用 ensureAIchat 确保跳转到 AIchat 页面 ensureAIchat(); @@ -132,8 +136,10 @@ const isAnnouncementVisible = ref(false); const showAnnouncement = () => { console.log("打开公告"); + isScrolling.value = false; //回复滚动到底部方法 + setActiveTab('', -1); // 清空当前选中状态 + isAnnouncementVisible.value = true; // 显示公告页面 - activeTab.value = 'Announcement`' }; // 点击剩余次数会弹出的弹窗 @@ -147,48 +153,39 @@ const showCount = () => { console.log("dialogVisible 的值:", dialogVisible.value); // 添加日志确认 }; - // 保证发送消息时,滚动屏在底部 const chatStore = useChatStore() const tabContent = ref(null); const isScrolling = ref(false); //判断用户是否在滚动 const smoothScrollToBottom = async () => { - await nextTick(); + console.log('调用滚动到底部的方法') + // await nextTick(); const container = tabContent.value; + console.log(container, 'container') + console.log(isScrolling.value, 'isScrolling.value') if (!container) return; - if (!isScrolling.value) - container.scrollTop = container.scrollHeight - container.offsetHeight; - requestAnimationFrame(() => { + await nextTick(); // 确保在DOM更新后执行 + + if (!isScrolling.value) { container.scrollTop = container.scrollHeight - container.offsetHeight; - }); + // container.scrollTop = container.scrollHeight; + // container.scrollTop = container.offsetHeight; + // container.scrollTop = container.scrollHeight + container.offsetHeight; + console.log(container.scrollHeight, container.offsetHeight,container.scrollHeight-container.offsetHeight, container.scrollTop, "总长度", "可视长度", "位置") + } } const throttledSmoothScrollToBottom = _.throttle(smoothScrollToBottom, 500, { trailing: false }); -const handleScroll = function () { - const scrollContainer = tabContent.value - const scrollTop = scrollContainer.scrollTop - const scrollHeight = scrollContainer.scrollHeight - const offsetHeight = scrollContainer.offsetHeight - // console.log(scrollTop, scrollHeight, offsetHeight, "scrollTop, scrollHeight, offsetHeight"); - if (scrollTop + offsetHeight < scrollHeight) { - // 用户开始滚动并在最底部之上,取消保持在最底部的效果 - isScrolling.value = true - } else { - // 用户停止滚动并滚动到最底部,开启保持到最底部的效果 - isScrolling.value = false - } - // console.log(isScrolling.value) - -} - watch( () => chatStore.messages, () => { + console.log('messages变化了') throttledSmoothScrollToBottom(); + // setTimeout(throttledSmoothScrollToBottom, 100); }, { deep: true, immediate: true } ); @@ -196,9 +193,15 @@ watch( watch( activeTab, - async (newVal) => { + async () => { + console.log('activeTab变化了', activeTab.value) + isScrolling.value = false; //回复滚动到底部方法 + await nextTick(); // 等待DOM更新 throttledSmoothScrollToBottom(); - }); + // setTimeout(throttledSmoothScrollToBottom, 100); + }, + { deep: true, immediate: true } +); // 在setTimeout中延迟执行 @@ -234,12 +237,60 @@ const fnGetToken = () => { useAppBridge().packageFun('JWwebReady', () => { }, 5, {}) } +// const tabContainer = tabContent.value +// let befortop = 0 +// tabContainer.addEventListener('scroll', () => { +// const aftertop = tabContainer.scrollTop +// if (aftertop - befortop > 0) { +// console.log('向下滚动') +// isScrolling.value = true +// } else { +// console.log('向上滚动') +// isScrolling.value = false +// } +// befortop = aftertop +// }) + +const heightListener = () => { + const tabContainer = tabContent.value; + let befortop = 0; + + const scrollHandler = () => { + const aftertop = tabContainer.scrollTop; + + // 新增底部判断逻辑 + const isBottom = aftertop + tabContainer.offsetHeight + 50 >= tabContainer.scrollHeight; + + if (activeTab.value === 'AIchat') { + if (aftertop - befortop > 0) { + console.log('向下滚动'); + isScrolling.value = true; + } else { + console.log('向上滚动'); + isScrolling.value = true; + } + + // 添加底部状态检测 + if (isBottom) { + console.log('滚动到底部'); + isScrolling.value = false; + } + } + befortop = aftertop; + }; + + console.log(isScrolling.value, 'isScrolling.value') + + tabContainer.addEventListener('scroll', scrollHandler); +}; + +const throttledHeightListener = _.throttle(heightListener, 500, { trailing: false }); + onMounted(async () => { setHeight(document.getElementById("testId")); // 给父组件发送窗口高度 - getUserCount(); + await chatStore.getUserCount(); throttledSmoothScrollToBottom(); - // 监听滚动事件,判断用户滚动状态 - tabContent.value.addEventListener('scroll', handleScroll) + throttledHeightListener(); }) </script> @@ -270,7 +321,7 @@ onMounted(async () => { <section class="tab-section"> <div class="tab-container"> <div v-for="(tab, index) in tabs" :key="tab.name" @click="setActiveTab(tab.name, index)" - :class="['tab-item', { active: activeIndex === index }]"> + :class="['tab-item', { active: activeIndex === index && !isAnnouncementVisible }]"> <span>{{ tab.label }}</span> </div> </div> @@ -398,6 +449,7 @@ onMounted(async () => { background-repeat: no-repeat; background-position: center; display: flex; + overflow: auto; } .homepage .el-container { @@ -405,7 +457,7 @@ onMounted(async () => { flex-direction: column; /* 明确纵向排列 */ display: flex; - overflow: hidden; + overflow: auto; } .el-container .el-header { @@ -469,11 +521,19 @@ onMounted(async () => { .homepage-right-group .announcement-btn { cursor: pointer; - transition: transform 0.2s; + transition: transform 0.3s; } +/* @keyframes tilt { + 0% { transform: rotate(0deg); } + 50% { transform: rotate(10deg); } + 100% { transform: rotate(-10deg); } + 130% { transform: rotate(0deg); } +} */ + .homepage-right-group .announcement-btn:hover { - transform: scale(1.05); + transform: scale(1.3); + /* animation: tilt 1s ease-in-out; */ } .homepage-body {