|
@ -1,6 +1,10 @@ |
|
|
<template> |
|
|
<template> |
|
|
<!-- 顶部锚点 --> |
|
|
<!-- 顶部锚点 --> |
|
|
<div id="top-anchor" class="top-anchor"></div> |
|
|
<div id="top-anchor" class="top-anchor"></div> |
|
|
|
|
|
|
|
|
|
|
|
<!-- 主容器:包含对话框和main容器 --> |
|
|
|
|
|
<div class="page-container"> |
|
|
|
|
|
<!-- 对话框区域 --> |
|
|
<div class="ai-emotion-container" ref="userInputDisplayRef"> |
|
|
<div class="ai-emotion-container" ref="userInputDisplayRef"> |
|
|
<!-- 金轮 --> |
|
|
<!-- 金轮 --> |
|
|
<div class="golden-wheel"> |
|
|
<div class="golden-wheel"> |
|
@ -8,38 +12,33 @@ |
|
|
:class="{ 'rotating-image': isRotating }" /> |
|
|
:class="{ 'rotating-image': isRotating }" /> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<!-- 消息显示区域 --> |
|
|
|
|
|
<div class="user-input-display"> |
|
|
|
|
|
<div v-for="(message, index) in messages" :key="index" class="message-container"> |
|
|
|
|
|
<!-- 用户输入内容 --> |
|
|
|
|
|
<div v-if="message.sender === 'user'" class="user-message-container"> |
|
|
|
|
|
<img |
|
|
|
|
|
:src="isVoice && emotionAudioStore.isPlaying ? voiceNoActive : voice" |
|
|
|
|
|
class="user-message-speaker" |
|
|
|
|
|
:class="{ |
|
|
|
|
|
'speaker-active': isVoice && emotionAudioStore.isPlaying |
|
|
|
|
|
}" |
|
|
|
|
|
@click="toggleVoiceForUser" |
|
|
|
|
|
alt="喇叭" |
|
|
|
|
|
/> |
|
|
|
|
|
|
|
|
<!-- 对话消息显示区域 --> |
|
|
|
|
|
<div class="conversation-area" v-if="messages.length > 0 && !isHistoryMode"> |
|
|
|
|
|
<div class="message-list"> |
|
|
|
|
|
<div v-for="(message, index) in messages" :key="index" class="message-item" |
|
|
|
|
|
:class="{ 'user-message-item': message.sender === 'user', 'ai-message-item': message.sender === 'ai' }"> |
|
|
|
|
|
|
|
|
|
|
|
<!-- 用户消息 --> |
|
|
|
|
|
<div v-if="message.sender === 'user'" class="user-message-wrapper"> |
|
|
<div class="message-bubble user-message"> |
|
|
<div class="message-bubble user-message"> |
|
|
{{ message.text }} |
|
|
{{ message.text }} |
|
|
|
|
|
|
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<!-- AI返回结果 --> |
|
|
|
|
|
<div v-if="message.sender === 'ai'" class="ai-message-container"> |
|
|
|
|
|
<!-- 思考过程动图 --> |
|
|
|
|
|
<!-- <img v-if="message.gif" :src="message.gif" class="thinking-gif" alt="思考动图" /> --> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- AI消息(包括思考过程) --> |
|
|
|
|
|
<div v-else class="ai-message-wrapper"> |
|
|
|
|
|
<div class="ai-message-container"> |
|
|
|
|
|
<img v-if="message.gif" :src="message.gif" alt="思考过程" class="thinking-gif" /> |
|
|
<div class="message-bubble ai-message"> |
|
|
<div class="message-bubble ai-message"> |
|
|
<!-- 思考过程动图 --> |
|
|
|
|
|
<img v-if="message.gif" :src="message.gif" class="thinking-gif" alt="思考动图" /> |
|
|
|
|
|
{{ message.text }} |
|
|
{{ message.text }} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
<!-- 加载提示 --> |
|
|
<!-- 加载提示 --> |
|
|
<div v-if="isLoading" class="loading-container"> |
|
|
<div v-if="isLoading" class="loading-container"> |
|
|
<div class="loading-content"> |
|
|
<div class="loading-content"> |
|
@ -47,9 +46,28 @@ |
|
|
<div class="loading-text">AI情绪大模型正在努力为您加载,请稍候...</div> |
|
|
<div class="loading-text">AI情绪大模型正在努力为您加载,请稍候...</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<!-- main容器区域 --> |
|
|
<!-- 移除股票标签页,改为对话形式展示 --> |
|
|
<!-- 移除股票标签页,改为对话形式展示 --> |
|
|
<!-- 渲染整个页面 - 遍历stockList显示所有股票 --> |
|
|
<!-- 渲染整个页面 - 遍历stockList显示所有股票 --> |
|
|
<div v-for="(stock, stockIndex) in emotionStore.stockList" :key="`stock-${stockIndex}-${stock.timestamp}`" v-if="isPageLoaded" class="main"> |
|
|
|
|
|
|
|
|
<div class="master" v-for="(stock, stockIndex) in emotionStore.stockList" :key="`stock-${stockIndex}-${stock.timestamp}`" v-if="isPageLoaded"> |
|
|
|
|
|
<!-- 对应股票的消息显示区域 --> |
|
|
|
|
|
<div class="user-input-display"> |
|
|
|
|
|
<div class="message-container"> |
|
|
|
|
|
<!-- 显示该股票对应的用户输入内容 --> |
|
|
|
|
|
<div class="user-message-container"> |
|
|
|
|
|
<img :src="isVoice && getStockAudioState(stock).isPlaying ? voiceNoActive : voice" class="user-message-speaker" |
|
|
|
|
|
:class="{ |
|
|
|
|
|
'speaker-active': isVoice && getStockAudioState(stock).isPlaying |
|
|
|
|
|
}" @click="() => toggleVoiceForUser(stock)" alt="喇叭" /> |
|
|
|
|
|
<div class="message-bubble user-message"> |
|
|
|
|
|
{{ stock.queryText }} |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<div class="main"> |
|
|
<div class="main-content-wrapper"> |
|
|
<div class="main-content-wrapper"> |
|
|
<!-- 四维矩阵图 --> |
|
|
<!-- 四维矩阵图 --> |
|
|
<div class="matrix-header"> |
|
|
<div class="matrix-header"> |
|
@ -74,7 +92,8 @@ |
|
|
<div class="temperature-hot">市场温度:{{ getStockData1(stock) }}</div> |
|
|
<div class="temperature-hot">市场温度:{{ getStockData1(stock) }}</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<marketTemperature :ref="el => marketTemperatureRef[stockIndex] = el" :companyName="stock.stockInfo.name" :stockCode="stock.stockInfo.code" /> |
|
|
|
|
|
|
|
|
<marketTemperature :ref="el => marketTemperatureRef[stockIndex] = el" :companyName="stock.stockInfo.name" |
|
|
|
|
|
:stockCode="stock.stockInfo.code" /> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="emotion-decoder-icon" v-if="chartVisibility.emotionDecod"> |
|
|
<div class="emotion-decoder-icon" v-if="chartVisibility.emotionDecod"> |
|
@ -152,8 +171,8 @@ |
|
|
<div class="scenario-application-section" ref="scenarioApplicationRef"> |
|
|
<div class="scenario-application-section" ref="scenarioApplicationRef"> |
|
|
<img src="@/assets/img/AiEmotion/场景应用.png" alt="场景应用标题"> |
|
|
<img src="@/assets/img/AiEmotion/场景应用.png" alt="场景应用标题"> |
|
|
<div class="bk-image"> |
|
|
<div class="bk-image"> |
|
|
<div class="conclusion-container" v-if="getStockConclusion(stock)"> |
|
|
|
|
|
<!-- 打字机效果显示的内容 --> |
|
|
|
|
|
|
|
|
<div class="conclusion-container"> |
|
|
|
|
|
<!-- 使用打字机效果显示结论内容 --> |
|
|
<div class="conclusion-item" v-if="moduleVisibility.one"> |
|
|
<div class="conclusion-item" v-if="moduleVisibility.one"> |
|
|
<h4 class="conclusion-title">{{ displayedTitles.one }}</h4> |
|
|
<h4 class="conclusion-title">{{ displayedTitles.one }}</h4> |
|
|
<p class="conclusion-text" v-if="displayedTexts.one1">{{ displayedTexts.one1 }}</p> |
|
|
<p class="conclusion-text" v-if="displayedTexts.one1">{{ displayedTexts.one1 }}</p> |
|
@ -176,7 +195,7 @@ |
|
|
<p class="disclaimer-text">{{ displayedTexts.disclaimer }}</p> |
|
|
<p class="disclaimer-text">{{ displayedTexts.disclaimer }}</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="conclusion-placeholder" v-else> |
|
|
|
|
|
|
|
|
<div class="conclusion-placeholder" v-if="!moduleVisibility.one && !moduleVisibility.two && !moduleVisibility.three && !moduleVisibility.four"> |
|
|
<p>等待股票分析结论...</p> |
|
|
<p>等待股票分析结论...</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
@ -190,6 +209,8 @@ |
|
|
</svg> |
|
|
</svg> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
</template> |
|
|
</template> |
|
|
|
|
|
|
|
|
<script setup> |
|
|
<script setup> |
|
@ -221,31 +242,47 @@ const emotionStore = useEmotionStore(); |
|
|
const emotionAudioStore = useEmotionAudioStore(); |
|
|
const emotionAudioStore = useEmotionAudioStore(); |
|
|
|
|
|
|
|
|
// 语音播放控制函数 |
|
|
// 语音播放控制函数 |
|
|
const toggleVoiceForUser = () => { |
|
|
|
|
|
|
|
|
const toggleVoiceForUser = (stock) => { |
|
|
if (!emotionAudioStore.isVoiceEnabled) { |
|
|
if (!emotionAudioStore.isVoiceEnabled) { |
|
|
// 如果语音功能关闭,先开启语音功能 |
|
|
// 如果语音功能关闭,先开启语音功能 |
|
|
emotionAudioStore.toggleVoice(); |
|
|
emotionAudioStore.toggleVoice(); |
|
|
} else { |
|
|
} else { |
|
|
// 如果语音功能开启,则切换播放/暂停状态 |
|
|
|
|
|
if (emotionAudioStore.isPlaying) { |
|
|
|
|
|
// 如果正在播放,则暂停 |
|
|
|
|
|
emotionAudioStore.togglePlayPause(); |
|
|
|
|
|
|
|
|
// 获取该股票的结论数据和当前状态 |
|
|
|
|
|
const stockConclusion = getStockConclusion(stock); |
|
|
|
|
|
const currentState = getStockAudioState(stock); |
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有任何音频正在播放(包括全局播放状态和当前股票状态) |
|
|
|
|
|
const isAnyAudioPlaying = emotionAudioStore.isPlaying || currentState.isPlaying; |
|
|
|
|
|
|
|
|
|
|
|
// 如果当前点击的股票正在播放,则暂停 |
|
|
|
|
|
if (currentState.isPlaying) { |
|
|
|
|
|
console.log('暂停当前股票音频:', stock.stockInfo?.name); |
|
|
|
|
|
stopAudio(); |
|
|
|
|
|
emotionAudioStore.resetAudioState(); |
|
|
|
|
|
setStockAudioState(stock, { isPlaying: false, isPaused: true }); |
|
|
} else { |
|
|
} else { |
|
|
// 如果没有在播放,先检查是否处于暂停状态 |
|
|
|
|
|
if (emotionAudioStore.isPaused && (emotionAudioStore.currentAudioUrl || emotionAudioStore.ttsUrl)) { |
|
|
|
|
|
// 如果处于暂停状态且有音频,则继续播放 |
|
|
|
|
|
console.log('从暂停状态继续播放'); |
|
|
|
|
|
emotionAudioStore.togglePlayPause(); |
|
|
|
|
|
} else if (parsedConclusion.value && (parsedConclusion.value.one1_url || parsedConclusion.value.two_url || parsedConclusion.value.three_url || parsedConclusion.value.four_url)) { |
|
|
|
|
|
// 有结论数据时,重新播放整个音频队列 |
|
|
|
|
|
console.log('用户点击播放,重新播放音频队列'); |
|
|
|
|
|
playAudioQueue(parsedConclusion.value, false); // 不启动打字机效果,因为内容已经显示 |
|
|
|
|
|
} else if (emotionAudioStore.currentAudioUrl || emotionAudioStore.ttsUrl) { |
|
|
|
|
|
// 有单个音频URL时切换播放/暂停 |
|
|
|
|
|
emotionAudioStore.togglePlayPause(); |
|
|
|
|
|
|
|
|
// 如果有其他音频正在播放,先停止 |
|
|
|
|
|
if (isAnyAudioPlaying) { |
|
|
|
|
|
console.log('停止其他正在播放的音频,准备播放新音频:', stock.stockInfo?.name); |
|
|
|
|
|
stopAudio(); |
|
|
|
|
|
emotionAudioStore.resetAudioState(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 清除所有股票的播放状态 |
|
|
|
|
|
clearAllStockAudioStates(); |
|
|
|
|
|
|
|
|
|
|
|
// 如果有音频数据,开始播放 |
|
|
|
|
|
if (stockConclusion && (stockConclusion.one1_url || stockConclusion.two_url || stockConclusion.three_url || stockConclusion.four_url)) { |
|
|
|
|
|
console.log('开始播放股票音频:', stock.stockInfo?.name); |
|
|
|
|
|
setStockAudioState(stock, { isPlaying: true, isPaused: false }); |
|
|
|
|
|
|
|
|
|
|
|
// 播放音频队列 |
|
|
|
|
|
playAudioQueue(stockConclusion, false, () => { |
|
|
|
|
|
// 音频播放完成后重置状态 |
|
|
|
|
|
setStockAudioState(stock, { isPlaying: false, isPaused: false }); |
|
|
|
|
|
}); |
|
|
} else { |
|
|
} else { |
|
|
// 没有音频时关闭语音功能 |
|
|
|
|
|
emotionAudioStore.toggleVoice(); |
|
|
|
|
|
|
|
|
console.log('该股票没有可播放的音频数据'); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
@ -355,12 +392,87 @@ const clearConversations = () => { |
|
|
addedStocks.value.clear(); |
|
|
addedStocks.value.clear(); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
// 暴露清空对话记录的方法给父组件 |
|
|
|
|
|
|
|
|
// 添加股票数据的包装方法 |
|
|
|
|
|
const addStock = (stockData) => { |
|
|
|
|
|
console.log('AiEmotion组件接收到股票数据:', stockData); |
|
|
|
|
|
|
|
|
|
|
|
// 设置为历史记录模式 |
|
|
|
|
|
isHistoryMode.value = true; |
|
|
|
|
|
|
|
|
|
|
|
// 1. 先清空页面显示节点和stockList中的数据 |
|
|
|
|
|
isPageLoaded.value = false; // 隐藏页面显示节点 |
|
|
|
|
|
emotionStore.clearAllStocks(); // 清空stockList中的数据 |
|
|
|
|
|
emotionStore.clearConversations(); // 清空对话记录 |
|
|
|
|
|
messages.value = []; // 清空页面对话显示 |
|
|
|
|
|
|
|
|
|
|
|
// 清理已添加股票的记录 |
|
|
|
|
|
addedStocks.value.clear(); |
|
|
|
|
|
|
|
|
|
|
|
// 停止音频播放和清理状态 |
|
|
|
|
|
stopAudio(); |
|
|
|
|
|
audioUrl.value = ''; |
|
|
|
|
|
emotionAudioStore.resetAudioState(); |
|
|
|
|
|
clearTypewriterTimers(); |
|
|
|
|
|
hasTriggeredAudio.value = false; |
|
|
|
|
|
hasTriggeredTypewriter.value = false; |
|
|
|
|
|
stockTypewriterShown.value.clear(); |
|
|
|
|
|
stockAudioPlayed.value.clear(); |
|
|
|
|
|
|
|
|
|
|
|
// 清理显示的文本和标题 |
|
|
|
|
|
displayedTexts.value = { |
|
|
|
|
|
one1: '', |
|
|
|
|
|
one2: '', |
|
|
|
|
|
two: '', |
|
|
|
|
|
three: '', |
|
|
|
|
|
four: '', |
|
|
|
|
|
disclaimer: '' |
|
|
|
|
|
}; |
|
|
|
|
|
displayedTitles.value = { |
|
|
|
|
|
one: '', |
|
|
|
|
|
two: '', |
|
|
|
|
|
three: '', |
|
|
|
|
|
four: '' |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// 隐藏所有模块 |
|
|
|
|
|
moduleVisibility.value = { |
|
|
|
|
|
one: false, |
|
|
|
|
|
two: false, |
|
|
|
|
|
three: false, |
|
|
|
|
|
four: false, |
|
|
|
|
|
disclaimer: false |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// 隐藏所有图表组件 |
|
|
|
|
|
chartVisibility.value = { |
|
|
|
|
|
marketTemperature: false, |
|
|
|
|
|
emotionDecod: false, |
|
|
|
|
|
emotionalBottomRadar: false, |
|
|
|
|
|
emoEnergyConverter: false |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// 2. 将新的数据存储到stockList中 |
|
|
|
|
|
emotionStore.addStock(stockData); |
|
|
|
|
|
// 3. 设置页面为已加载状态,重新渲染页面 |
|
|
|
|
|
isPageLoaded.value = true; |
|
|
|
|
|
|
|
|
|
|
|
// 4. 使用nextTick确保DOM更新后启动高度监听器并滚动到底部 |
|
|
|
|
|
nextTick(() => { |
|
|
|
|
|
// 启动页面高度监听器,实时监听内容变化并自动滚动 |
|
|
|
|
|
startHeightObserver(); |
|
|
|
|
|
// 立即滚动到底部 |
|
|
|
|
|
scrollToBottom(); |
|
|
|
|
|
}); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// 暴露方法给父组件 |
|
|
defineExpose({ |
|
|
defineExpose({ |
|
|
handleSendMessage, |
|
|
handleSendMessage, |
|
|
clearConversations |
|
|
|
|
|
|
|
|
clearConversations, |
|
|
|
|
|
addStock |
|
|
}); |
|
|
}); |
|
|
const isPageLoaded = ref(false); // 控制页面是否显示 |
|
|
const isPageLoaded = ref(false); // 控制页面是否显示 |
|
|
|
|
|
const isHistoryMode = ref(false); // 控制是否为历史记录模式 |
|
|
// const isLoading = ref(false); // 控制加载状态 |
|
|
// const isLoading = ref(false); // 控制加载状态 |
|
|
const isRotating = ref(false);//控制旋转 |
|
|
const isRotating = ref(false);//控制旋转 |
|
|
const version1 = ref(1); // 版本号 |
|
|
const version1 = ref(1); // 版本号 |
|
@ -415,6 +527,8 @@ const typewriterTimers = ref([]); |
|
|
const stockTypewriterShown = ref(new Map()); |
|
|
const stockTypewriterShown = ref(new Map()); |
|
|
// 记录每个股票是否已经播放过音频 |
|
|
// 记录每个股票是否已经播放过音频 |
|
|
const stockAudioPlayed = ref(new Map()); |
|
|
const stockAudioPlayed = ref(new Map()); |
|
|
|
|
|
// 跟踪每个股票的音频播放状态 |
|
|
|
|
|
const stockAudioStates = ref(new Map()); |
|
|
// 存储当前的完成回调函数 |
|
|
// 存储当前的完成回调函数 |
|
|
const currentOnCompleteCallback = ref(null); |
|
|
const currentOnCompleteCallback = ref(null); |
|
|
|
|
|
|
|
@ -422,6 +536,29 @@ const currentOnCompleteCallback = ref(null); |
|
|
const audioUrl = ref(''); |
|
|
const audioUrl = ref(''); |
|
|
const isAudioPlaying = ref(false); |
|
|
const isAudioPlaying = ref(false); |
|
|
|
|
|
|
|
|
|
|
|
// 获取股票的音频播放状态 |
|
|
|
|
|
const getStockAudioState = (stock) => { |
|
|
|
|
|
const stockCode = stock.stockInfo?.code || stock.stockInfo?.symbol; |
|
|
|
|
|
if (!stockCode) return { isPlaying: false, isPaused: false }; |
|
|
|
|
|
|
|
|
|
|
|
return stockAudioStates.value.get(stockCode) || { isPlaying: false, isPaused: false }; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// 设置股票的音频播放状态 |
|
|
|
|
|
const setStockAudioState = (stock, state) => { |
|
|
|
|
|
const stockCode = stock.stockInfo?.code || stock.stockInfo?.symbol; |
|
|
|
|
|
if (!stockCode) return; |
|
|
|
|
|
|
|
|
|
|
|
stockAudioStates.value.set(stockCode, { ...state }); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// 清除所有股票的播放状态(当开始播放新音频时) |
|
|
|
|
|
const clearAllStockAudioStates = () => { |
|
|
|
|
|
for (const [key, value] of stockAudioStates.value.entries()) { |
|
|
|
|
|
stockAudioStates.value.set(key, { isPlaying: false, isPaused: false }); |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
// 返回顶部按钮相关数据 |
|
|
// 返回顶部按钮相关数据 |
|
|
const showBackToTop = ref(false); |
|
|
const showBackToTop = ref(false); |
|
|
|
|
|
|
|
@ -746,7 +883,13 @@ watch(currentStock, (newStock) => { |
|
|
console.log('图表数据已准备完成,开始渲染:', newStock.apiData) |
|
|
console.log('图表数据已准备完成,开始渲染:', newStock.apiData) |
|
|
// 检查场景应用部分是否已经在视口中,如果是则立即触发效果 |
|
|
// 检查场景应用部分是否已经在视口中,如果是则立即触发效果 |
|
|
setTimeout(() => { |
|
|
setTimeout(() => { |
|
|
if (scenarioApplicationRef.value && parsedConclusion.value) { |
|
|
|
|
|
|
|
|
// 检查 scenarioApplicationRef.value 是否存在且是有效的 DOM 元素 |
|
|
|
|
|
if (!scenarioApplicationRef.value || !(scenarioApplicationRef.value instanceof Element)) { |
|
|
|
|
|
console.warn('scenarioApplicationRef.value 不是有效的 DOM 元素,跳过处理'); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (parsedConclusion.value) { |
|
|
const stockCode = newStock.stockInfo?.code || newStock.stockInfo?.symbol; |
|
|
const stockCode = newStock.stockInfo?.code || newStock.stockInfo?.symbol; |
|
|
|
|
|
|
|
|
// 如果该股票已经显示过,不需要再处理 |
|
|
// 如果该股票已经显示过,不需要再处理 |
|
@ -763,9 +906,9 @@ watch(currentStock, (newStock) => { |
|
|
if (stockCode) { |
|
|
if (stockCode) { |
|
|
// 检查该股票是否是第一次触发 |
|
|
// 检查该股票是否是第一次触发 |
|
|
if (!stockTypewriterShown.value.has(stockCode)) { |
|
|
if (!stockTypewriterShown.value.has(stockCode)) { |
|
|
// 该股票第一次:播放音频和打字机效果 |
|
|
|
|
|
if (audioUrl.value) { |
|
|
|
|
|
console.log('该股票第一次进入场景应用,开始打字机效果和音频播放'); |
|
|
|
|
|
|
|
|
// 如果是用户主动搜索,启动打字机效果和音频播放 |
|
|
|
|
|
if (isUserInitiated.value && audioUrl.value) { |
|
|
|
|
|
console.log('用户主动搜索,该股票第一次进入场景应用,开始打字机效果和音频播放'); |
|
|
hasTriggeredTypewriter.value = true; |
|
|
hasTriggeredTypewriter.value = true; |
|
|
hasTriggeredAudio.value = true; |
|
|
hasTriggeredAudio.value = true; |
|
|
|
|
|
|
|
@ -779,9 +922,38 @@ watch(currentStock, (newStock) => { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
stockTypewriterShown.value.set(stockCode, true); |
|
|
stockTypewriterShown.value.set(stockCode, true); |
|
|
} else { |
|
|
|
|
|
|
|
|
} else if (isUserInitiated.value && !audioUrl.value) { |
|
|
console.log('音频尚未准备好,等待音频加载完成后再触发效果(股票切换后)'); |
|
|
console.log('音频尚未准备好,等待音频加载完成后再触发效果(股票切换后)'); |
|
|
return; |
|
|
return; |
|
|
|
|
|
} else { |
|
|
|
|
|
// 非用户主动搜索(如历史记录恢复),直接显示完整内容 |
|
|
|
|
|
console.log('非用户主动搜索,该股票第一次进入场景应用,直接显示完整内容'); |
|
|
|
|
|
|
|
|
|
|
|
const conclusion = parsedConclusion.value; |
|
|
|
|
|
displayedTexts.value = { |
|
|
|
|
|
one1: conclusion.one1 || '', |
|
|
|
|
|
one2: conclusion.one2 || '', |
|
|
|
|
|
two: conclusion.two || '', |
|
|
|
|
|
three: conclusion.three || '', |
|
|
|
|
|
four: conclusion.four || '', |
|
|
|
|
|
disclaimer: '该内容由AI生成,请注意甄别' |
|
|
|
|
|
}; |
|
|
|
|
|
displayedTitles.value = { |
|
|
|
|
|
one: 'L1: 情绪监控', |
|
|
|
|
|
two: 'L2: 情绪解码', |
|
|
|
|
|
three: 'L3: 情绪推演', |
|
|
|
|
|
four: 'L4: 情绪套利' |
|
|
|
|
|
}; |
|
|
|
|
|
moduleVisibility.value = { |
|
|
|
|
|
one: !!(conclusion.one1 || conclusion.one2), |
|
|
|
|
|
two: !!conclusion.two, |
|
|
|
|
|
three: !!conclusion.three, |
|
|
|
|
|
four: !!conclusion.four, |
|
|
|
|
|
disclaimer: true |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
stockTypewriterShown.value.set(stockCode, true); |
|
|
|
|
|
stockAudioPlayed.value.set(stockCode, true); |
|
|
} |
|
|
} |
|
|
} else { |
|
|
} else { |
|
|
// 非第一次或已经触发过:直接显示完整内容,不播放音频和打字机效果 |
|
|
// 非第一次或已经触发过:直接显示完整内容,不播放音频和打字机效果 |
|
@ -1142,6 +1314,12 @@ const playNextAudio = () => { |
|
|
console.log("🎉 所有音频播放完成"); |
|
|
console.log("🎉 所有音频播放完成"); |
|
|
emotionAudioStore.nowSound = null; |
|
|
emotionAudioStore.nowSound = null; |
|
|
isCallingPlayNext = false; |
|
|
isCallingPlayNext = false; |
|
|
|
|
|
|
|
|
|
|
|
// 调用完成回调(如果有的话) |
|
|
|
|
|
if (audioInfo.onComplete && typeof audioInfo.onComplete === 'function') { |
|
|
|
|
|
console.log('调用音频播放完成回调'); |
|
|
|
|
|
audioInfo.onComplete(); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
onstop: () => { |
|
|
onstop: () => { |
|
@ -1483,6 +1661,8 @@ async function handleSendMessage(input, onComplete) { |
|
|
console.log("发送内容:", input); |
|
|
console.log("发送内容:", input); |
|
|
// 标记为用户主动搜索 |
|
|
// 标记为用户主动搜索 |
|
|
isUserInitiated.value = true; |
|
|
isUserInitiated.value = true; |
|
|
|
|
|
// 重置历史记录模式状态,确保正常对话时显示conversation-area |
|
|
|
|
|
isHistoryMode.value = false; |
|
|
|
|
|
|
|
|
// 检查用户输入内容是否为空 |
|
|
// 检查用户输入内容是否为空 |
|
|
if (!input || !input.trim()) { |
|
|
if (!input || !input.trim()) { |
|
@ -1496,7 +1676,8 @@ async function handleSendMessage(input, onComplete) { |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 用户输入不为空,立即触发图片旋转逻辑,隐藏历史数据 |
|
|
|
|
|
|
|
|
// 用户输入不为空,立即清空页面内容并触发图片旋转逻辑 |
|
|
|
|
|
isPageLoaded.value = false; // 立即隐藏页面内容 |
|
|
isRotating.value = true; |
|
|
isRotating.value = true; |
|
|
const previousMessages = [...messages.value]; // 保存历史消息 |
|
|
const previousMessages = [...messages.value]; // 保存历史消息 |
|
|
messages.value = []; // 清空历史数据 |
|
|
messages.value = []; // 清空历史数据 |
|
@ -1645,6 +1826,15 @@ async function handleSendMessage(input, onComplete) { |
|
|
// isLoading.value = false; |
|
|
// isLoading.value = false; |
|
|
isPageLoaded.value = true; |
|
|
isPageLoaded.value = true; |
|
|
|
|
|
|
|
|
|
|
|
// 使用nextTick确保DOM更新后清空对话显示并启动高度监听器 |
|
|
|
|
|
nextTick(() => { |
|
|
|
|
|
messages.value = []; |
|
|
|
|
|
// 启动页面高度监听器,实时监听内容变化并自动滚动 |
|
|
|
|
|
startHeightObserver(); |
|
|
|
|
|
// 立即滚动到底部 |
|
|
|
|
|
scrollToBottom(); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
// 数据获取成功后,重新获取用户次数以实现实时更新 |
|
|
// 数据获取成功后,重新获取用户次数以实现实时更新 |
|
|
try { |
|
|
try { |
|
|
await chatStore.getUserCount(); |
|
|
await chatStore.getUserCount(); |
|
@ -1736,6 +1926,14 @@ async function handleSendMessage(input, onComplete) { |
|
|
// 如果有之前的股票数据,恢复显示状态;否则设置为false |
|
|
// 如果有之前的股票数据,恢复显示状态;否则设置为false |
|
|
if (emotionStore.stockList.length > 0 && emotionStore.activeStock) { |
|
|
if (emotionStore.stockList.length > 0 && emotionStore.activeStock) { |
|
|
isPageLoaded.value = true; |
|
|
isPageLoaded.value = true; |
|
|
|
|
|
// 使用nextTick确保DOM更新后清空对话显示并启动高度监听器 |
|
|
|
|
|
nextTick(() => { |
|
|
|
|
|
messages.value = []; |
|
|
|
|
|
// 启动页面高度监听器,实时监听内容变化并自动滚动 |
|
|
|
|
|
startHeightObserver(); |
|
|
|
|
|
// 立即滚动到底部 |
|
|
|
|
|
scrollToBottom(); |
|
|
|
|
|
}); |
|
|
console.log('请求工作流接口失败,但恢复显示之前的股票数据'); |
|
|
console.log('请求工作流接口失败,但恢复显示之前的股票数据'); |
|
|
// 立即渲染之前股票的图表,提升用户体验 |
|
|
// 立即渲染之前股票的图表,提升用户体验 |
|
|
nextTick(() => { |
|
|
nextTick(() => { |
|
@ -1815,6 +2013,14 @@ async function fetchData(code, market, stockName, queryText, stockId) { |
|
|
// 如果有之前的股票数据,恢复显示状态;否则设置为false |
|
|
// 如果有之前的股票数据,恢复显示状态;否则设置为false |
|
|
if (emotionStore.stockList.length > 0 && emotionStore.activeStock) { |
|
|
if (emotionStore.stockList.length > 0 && emotionStore.activeStock) { |
|
|
isPageLoaded.value = true; |
|
|
isPageLoaded.value = true; |
|
|
|
|
|
// 使用nextTick确保DOM更新后清空对话显示并启动高度监听器 |
|
|
|
|
|
nextTick(() => { |
|
|
|
|
|
messages.value = []; |
|
|
|
|
|
// 启动页面高度监听器,实时监听内容变化并自动滚动 |
|
|
|
|
|
startHeightObserver(); |
|
|
|
|
|
// 立即滚动到底部 |
|
|
|
|
|
scrollToBottom(); |
|
|
|
|
|
}); |
|
|
console.log('数据验证失败,但恢复显示之前的股票数据'); |
|
|
console.log('数据验证失败,但恢复显示之前的股票数据'); |
|
|
// 立即渲染之前股票的图表,提升用户体验 |
|
|
// 立即渲染之前股票的图表,提升用户体验 |
|
|
nextTick(() => { |
|
|
nextTick(() => { |
|
@ -1862,6 +2068,14 @@ async function fetchData(code, market, stockName, queryText, stockId) { |
|
|
// 如果有之前的股票数据,恢复显示状态;否则设置为false |
|
|
// 如果有之前的股票数据,恢复显示状态;否则设置为false |
|
|
if (emotionStore.stockList.length > 0 && emotionStore.activeStock) { |
|
|
if (emotionStore.stockList.length > 0 && emotionStore.activeStock) { |
|
|
isPageLoaded.value = true; |
|
|
isPageLoaded.value = true; |
|
|
|
|
|
// 使用nextTick确保DOM更新后清空对话显示并启动高度监听器 |
|
|
|
|
|
nextTick(() => { |
|
|
|
|
|
messages.value = []; |
|
|
|
|
|
// 启动页面高度监听器,实时监听内容变化并自动滚动 |
|
|
|
|
|
startHeightObserver(); |
|
|
|
|
|
// 立即滚动到底部 |
|
|
|
|
|
scrollToBottom(); |
|
|
|
|
|
}); |
|
|
console.log('API请求失败,但恢复显示之前的股票数据'); |
|
|
console.log('API请求失败,但恢复显示之前的股票数据'); |
|
|
// 立即渲染之前股票的图表,提升用户体验 |
|
|
// 立即渲染之前股票的图表,提升用户体验 |
|
|
nextTick(() => { |
|
|
nextTick(() => { |
|
@ -1890,6 +2104,14 @@ async function fetchData(code, market, stockName, queryText, stockId) { |
|
|
// 如果有之前的股票数据,恢复显示状态;否则设置为false |
|
|
// 如果有之前的股票数据,恢复显示状态;否则设置为false |
|
|
if (emotionStore.stockList.length > 0 && emotionStore.activeStock) { |
|
|
if (emotionStore.stockList.length > 0 && emotionStore.activeStock) { |
|
|
isPageLoaded.value = true; |
|
|
isPageLoaded.value = true; |
|
|
|
|
|
// 使用nextTick确保DOM更新后清空对话显示并启动高度监听器 |
|
|
|
|
|
nextTick(() => { |
|
|
|
|
|
messages.value = []; |
|
|
|
|
|
// 启动页面高度监听器,实时监听内容变化并自动滚动 |
|
|
|
|
|
startHeightObserver(); |
|
|
|
|
|
// 立即滚动到底部 |
|
|
|
|
|
scrollToBottom(); |
|
|
|
|
|
}); |
|
|
console.log('网络异常,但恢复显示之前的股票数据'); |
|
|
console.log('网络异常,但恢复显示之前的股票数据'); |
|
|
// 立即渲染之前股票的图表,提升用户体验 |
|
|
// 立即渲染之前股票的图表,提升用户体验 |
|
|
nextTick(() => { |
|
|
nextTick(() => { |
|
@ -2024,6 +2246,9 @@ async function renderChartsSequentially(clonedData, stockIndex = 0) { |
|
|
|
|
|
|
|
|
if (typeof config.ref[config.method] === 'function') { |
|
|
if (typeof config.ref[config.method] === 'function') { |
|
|
try { |
|
|
try { |
|
|
|
|
|
// 等待DOM元素完全渲染 |
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 100)); |
|
|
|
|
|
|
|
|
config.ref[config.method](...config.params); |
|
|
config.ref[config.method](...config.params); |
|
|
console.log(`第${stockIndex}个股票的${config.name}图表渲染成功`); |
|
|
console.log(`第${stockIndex}个股票的${config.name}图表渲染成功`); |
|
|
|
|
|
|
|
@ -2229,10 +2454,24 @@ function setupIntersectionObserver() { |
|
|
if (parsedConclusion.value && stockCode) { |
|
|
if (parsedConclusion.value && stockCode) { |
|
|
// 检查该股票是否是第一次触发 |
|
|
// 检查该股票是否是第一次触发 |
|
|
if (!stockTypewriterShown.value.has(stockCode)) { |
|
|
if (!stockTypewriterShown.value.has(stockCode)) { |
|
|
// 该股票第一次进入视口:只显示文本,不自动播放音频和打字机效果 |
|
|
|
|
|
console.log('该股票第一次进入场景应用,直接显示完整内容,不自动播放'); |
|
|
|
|
|
|
|
|
// 如果是用户主动搜索,启动打字机效果和音频播放 |
|
|
|
|
|
if (isUserInitiated.value && audioUrl.value) { |
|
|
|
|
|
console.log('用户主动搜索,该股票第一次进入场景应用,开始打字机效果和音频播放'); |
|
|
|
|
|
|
|
|
|
|
|
if (!stockAudioPlayed.value.has(stockCode)) { |
|
|
|
|
|
console.log('开始音频播放和打字机效果'); |
|
|
|
|
|
stockAudioPlayed.value.set(stockCode, true); |
|
|
|
|
|
playAudioQueue(parsedConclusion.value, true); |
|
|
|
|
|
} else { |
|
|
|
|
|
// 如果音频已播放过,只启动打字机效果 |
|
|
|
|
|
startTypewriterEffect(parsedConclusion.value); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
stockTypewriterShown.value.set(stockCode, true); |
|
|
|
|
|
} else { |
|
|
|
|
|
// 非用户主动搜索(如历史记录恢复),直接显示完整内容 |
|
|
|
|
|
console.log('非用户主动搜索,该股票第一次进入场景应用,直接显示完整内容'); |
|
|
|
|
|
|
|
|
// 直接显示完整内容,不使用打字机效果 |
|
|
|
|
|
const conclusion = parsedConclusion.value; |
|
|
const conclusion = parsedConclusion.value; |
|
|
displayedTexts.value = { |
|
|
displayedTexts.value = { |
|
|
one1: conclusion.one1 || '', |
|
|
one1: conclusion.one1 || '', |
|
@ -2257,9 +2496,10 @@ function setupIntersectionObserver() { |
|
|
disclaimer: true |
|
|
disclaimer: true |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
// 记录该股票已显示过,但不播放音频 |
|
|
|
|
|
|
|
|
// 记录该股票已显示过 |
|
|
stockTypewriterShown.value.set(stockCode, true); |
|
|
stockTypewriterShown.value.set(stockCode, true); |
|
|
stockAudioPlayed.value.set(stockCode, true); // 标记为已播放,避免后续自动播放 |
|
|
|
|
|
|
|
|
stockAudioPlayed.value.set(stockCode, true); |
|
|
|
|
|
} |
|
|
} else { |
|
|
} else { |
|
|
// 非第一次或已经触发过:直接显示完整内容,不播放音频和打字机效果 |
|
|
// 非第一次或已经触发过:直接显示完整内容,不播放音频和打字机效果 |
|
|
console.log('非第一次进入场景应用或已触发过,直接显示完整内容'); |
|
|
console.log('非第一次进入场景应用或已触发过,直接显示完整内容'); |
|
@ -2328,6 +2568,131 @@ const scrollToTop = () => { |
|
|
}, 1000); |
|
|
}, 1000); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// 页面高度监听器 |
|
|
|
|
|
const heightObserver = ref(null); |
|
|
|
|
|
const isAutoScrollEnabled = ref(false); |
|
|
|
|
|
|
|
|
|
|
|
// 滚动到底部功能 |
|
|
|
|
|
const scrollToBottom = () => { |
|
|
|
|
|
// 使用nextTick确保DOM已更新 |
|
|
|
|
|
nextTick(() => { |
|
|
|
|
|
// 获取页面的总高度 |
|
|
|
|
|
const documentHeight = Math.max( |
|
|
|
|
|
document.body.scrollHeight, |
|
|
|
|
|
document.body.offsetHeight, |
|
|
|
|
|
document.documentElement.clientHeight, |
|
|
|
|
|
document.documentElement.scrollHeight, |
|
|
|
|
|
document.documentElement.offsetHeight |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// 平滑滚动到页面底部 |
|
|
|
|
|
window.scrollTo({ |
|
|
|
|
|
top: documentHeight, |
|
|
|
|
|
behavior: 'smooth' |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 备用方案:直接设置滚动位置 |
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
document.documentElement.scrollTop = documentHeight; |
|
|
|
|
|
document.body.scrollTop = documentHeight; |
|
|
|
|
|
}, 1000); |
|
|
|
|
|
}); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// 防抖滚动函数 |
|
|
|
|
|
const debouncedScrollToBottom = (() => { |
|
|
|
|
|
let timeoutId = null; |
|
|
|
|
|
return () => { |
|
|
|
|
|
if (timeoutId) { |
|
|
|
|
|
clearTimeout(timeoutId); |
|
|
|
|
|
} |
|
|
|
|
|
timeoutId = setTimeout(() => { |
|
|
|
|
|
if (isAutoScrollEnabled.value && isPageLoaded.value) { |
|
|
|
|
|
scrollToBottom(); |
|
|
|
|
|
} |
|
|
|
|
|
}, 150); |
|
|
|
|
|
}; |
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
|
|
|
|
// 启动页面高度监听器 |
|
|
|
|
|
const startHeightObserver = () => { |
|
|
|
|
|
// 先停止之前的监听器 |
|
|
|
|
|
stopHeightObserver(); |
|
|
|
|
|
|
|
|
|
|
|
isAutoScrollEnabled.value = true; |
|
|
|
|
|
|
|
|
|
|
|
// 创建ResizeObserver监听页面内容变化 |
|
|
|
|
|
heightObserver.value = new ResizeObserver((entries) => { |
|
|
|
|
|
if (isAutoScrollEnabled.value && isPageLoaded.value) { |
|
|
|
|
|
debouncedScrollToBottom(); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 监听document.body的尺寸变化 |
|
|
|
|
|
if (document.body) { |
|
|
|
|
|
heightObserver.value.observe(document.body); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 创建MutationObserver监听DOM结构变化 |
|
|
|
|
|
const mutationObserver = new MutationObserver((mutations) => { |
|
|
|
|
|
let shouldScroll = false; |
|
|
|
|
|
mutations.forEach((mutation) => { |
|
|
|
|
|
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { |
|
|
|
|
|
// 检查新增的节点是否包含实际内容 |
|
|
|
|
|
const hasContent = Array.from(mutation.addedNodes).some(node => { |
|
|
|
|
|
if (node.nodeType === Node.ELEMENT_NODE) { |
|
|
|
|
|
return node.offsetHeight > 0 || node.scrollHeight > 0; |
|
|
|
|
|
} |
|
|
|
|
|
return node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0; |
|
|
|
|
|
}); |
|
|
|
|
|
if (hasContent) { |
|
|
|
|
|
shouldScroll = true; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
if (shouldScroll && isAutoScrollEnabled.value && isPageLoaded.value) { |
|
|
|
|
|
debouncedScrollToBottom(); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 监听主要内容区域的DOM变化 |
|
|
|
|
|
const mainContainer = document.querySelector('.main') || document.body; |
|
|
|
|
|
if (mainContainer) { |
|
|
|
|
|
mutationObserver.observe(mainContainer, { |
|
|
|
|
|
childList: true, |
|
|
|
|
|
subtree: true, |
|
|
|
|
|
attributes: false, |
|
|
|
|
|
characterData: true |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 保存mutationObserver引用以便清理 |
|
|
|
|
|
heightObserver.value.mutationObserver = mutationObserver; |
|
|
|
|
|
|
|
|
|
|
|
console.log('页面高度监听器已启动'); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// 停止页面高度监听器 |
|
|
|
|
|
const stopHeightObserver = () => { |
|
|
|
|
|
isAutoScrollEnabled.value = false; |
|
|
|
|
|
|
|
|
|
|
|
if (heightObserver.value) { |
|
|
|
|
|
// 清理ResizeObserver |
|
|
|
|
|
heightObserver.value.disconnect(); |
|
|
|
|
|
|
|
|
|
|
|
// 清理MutationObserver |
|
|
|
|
|
if (heightObserver.value.mutationObserver) { |
|
|
|
|
|
heightObserver.value.mutationObserver.disconnect(); |
|
|
|
|
|
heightObserver.value.mutationObserver = null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
heightObserver.value = null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
console.log('页面高度监听器已停止'); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
// 监听页面滚动,控制返回顶部按钮显示 |
|
|
// 监听页面滚动,控制返回顶部按钮显示 |
|
|
const handlePageScroll = () => { |
|
|
const handlePageScroll = () => { |
|
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; |
|
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; |
|
@ -2453,6 +2818,14 @@ onMounted(async () => { |
|
|
|
|
|
|
|
|
// 恢复页面加载状态 |
|
|
// 恢复页面加载状态 |
|
|
isPageLoaded.value = true; |
|
|
isPageLoaded.value = true; |
|
|
|
|
|
// 使用nextTick确保DOM更新后清空对话显示并启动高度监听器 |
|
|
|
|
|
nextTick(() => { |
|
|
|
|
|
messages.value = []; |
|
|
|
|
|
// 启动页面高度监听器,实时监听内容变化并自动滚动 |
|
|
|
|
|
startHeightObserver(); |
|
|
|
|
|
// 立即滚动到底部 |
|
|
|
|
|
scrollToBottom(); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
// 等待DOM渲染后恢复图表和数据 |
|
|
// 等待DOM渲染后恢复图表和数据 |
|
|
nextTick(() => { |
|
|
nextTick(() => { |
|
@ -2526,6 +2899,9 @@ onUnmounted(() => { |
|
|
hasTriggeredAudio.value = false; |
|
|
hasTriggeredAudio.value = false; |
|
|
hasTriggeredTypewriter.value = false; |
|
|
hasTriggeredTypewriter.value = false; |
|
|
|
|
|
|
|
|
|
|
|
// 清理页面高度监听器 |
|
|
|
|
|
stopHeightObserver(); |
|
|
|
|
|
|
|
|
// 清理Intersection Observer |
|
|
// 清理Intersection Observer |
|
|
if (intersectionObserver.value) { |
|
|
if (intersectionObserver.value) { |
|
|
intersectionObserver.value.disconnect(); |
|
|
intersectionObserver.value.disconnect(); |
|
@ -3150,9 +3526,11 @@ const emit = defineEmits(['updateMessage', 'sendMessage', 'ensureAIchat']); |
|
|
0% { |
|
|
0% { |
|
|
transform: scale(1); |
|
|
transform: scale(1); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
50% { |
|
|
50% { |
|
|
transform: scale(1.1); |
|
|
transform: scale(1.1); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
100% { |
|
|
100% { |
|
|
transform: scale(1); |
|
|
transform: scale(1); |
|
|
} |
|
|
} |
|
@ -3191,9 +3569,12 @@ const emit = defineEmits(['updateMessage', 'sendMessage', 'ensureAIchat']); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@keyframes float { |
|
|
@keyframes float { |
|
|
0%, 100% { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
0%, |
|
|
|
|
|
100% { |
|
|
transform: translateY(0px); |
|
|
transform: translateY(0px); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
50% { |
|
|
50% { |
|
|
transform: translateY(-5px); |
|
|
transform: translateY(-5px); |
|
|
} |
|
|
} |
|
@ -3836,6 +4217,43 @@ const emit = defineEmits(['updateMessage', 'sendMessage', 'ensureAIchat']); |
|
|
letter-spacing: 1px; |
|
|
letter-spacing: 1px; |
|
|
} */ |
|
|
} */ |
|
|
|
|
|
|
|
|
|
|
|
/* 对话区域样式 */ |
|
|
|
|
|
.conversation-area { |
|
|
|
|
|
width: 100%; |
|
|
|
|
|
padding: 0 20px; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.message-list { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
flex-direction: column; |
|
|
|
|
|
gap: 15px; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.message-item { |
|
|
|
|
|
width: 100%; |
|
|
|
|
|
display: flex; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.user-message-item { |
|
|
|
|
|
justify-content: flex-end; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.ai-message-item { |
|
|
|
|
|
justify-content: flex-start; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.user-message-wrapper { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
justify-content: flex-end; |
|
|
|
|
|
max-width: 70%; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.ai-message-wrapper { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
justify-content: flex-start; |
|
|
|
|
|
max-width: 80%; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
/* 顶部锚点样式 */ |
|
|
/* 顶部锚点样式 */ |
|
|
.top-anchor { |
|
|
.top-anchor { |
|
|
position: relative; |
|
|
position: relative; |
|
@ -3881,6 +4299,18 @@ const emit = defineEmits(['updateMessage', 'sendMessage', 'ensureAIchat']); |
|
|
transform: translateY(-1px); |
|
|
transform: translateY(-1px); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* 页面主容器样式 */ |
|
|
|
|
|
.page-container { |
|
|
|
|
|
position: relative; |
|
|
|
|
|
width: 100%; |
|
|
|
|
|
min-height: 100vh; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.master:last-child { |
|
|
|
|
|
border-bottom: none; |
|
|
|
|
|
margin-bottom: 0; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
/* class01容器样式 */ |
|
|
/* class01容器样式 */ |
|
|
.main { |
|
|
.main { |
|
|
position: relative; |
|
|
position: relative; |
|
|