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.
4923 lines
142 KiB
4923 lines
142 KiB
<template>
|
|
<!-- 顶部锚点 -->
|
|
<div id="top-anchor" class="top-anchor"></div>
|
|
|
|
<!-- 主容器:包含对话框和main容器 -->
|
|
<div class="page-container">
|
|
<!-- 对话框区域 -->
|
|
<div class="ai-emotion-container" ref="userInputDisplayRef">
|
|
<!-- 金轮 -->
|
|
<div class="golden-wheel">
|
|
<img
|
|
src="@/assets/img/AiEmotion/金轮.png"
|
|
class="golden-wheel-img"
|
|
alt="金轮图标"
|
|
:class="{ 'rotating-image': isRotating }"
|
|
/>
|
|
</div>
|
|
|
|
<!-- 对话消息显示区域 -->
|
|
<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">
|
|
{{ message.text }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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 v-if="message.flag">
|
|
<span>{{ message.text }}</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>{{ message.text }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 加载提示 -->
|
|
<div v-if="isLoading" class="loading-container">
|
|
<div class="loading-content">
|
|
<div class="loading-spinner"></div>
|
|
<div class="loading-text">AI情绪大模型正在努力为您加载,请稍候...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- main容器区域 -->
|
|
<!-- 移除股票标签页,改为对话形式展示 -->
|
|
|
|
<!-- 渲染整个页面 - 遍历过滤后的股票列表显示所有股票 -->
|
|
<div
|
|
class="master"
|
|
v-for="(stock, stockIndex) in filteredStockList"
|
|
:key="`stock-${stockIndex}-${stock.timestamp}`"
|
|
v-if="isPageLoaded"
|
|
>
|
|
<!-- 对应股票的消息显示区域 -->
|
|
<div class="user-input-display">
|
|
<div class="message-container">
|
|
<!-- 显示该股票对应的用户输入内容 -->
|
|
<div class="user-message-container">
|
|
<div class="user-content">
|
|
<img
|
|
:src="
|
|
isVoice && getStockAudioState(stock).isPlaying
|
|
? voice
|
|
: voiceNoActive
|
|
"
|
|
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 class="user-sendTime">
|
|
{{ moment(stock.timestamp).format("YYYY-MM-DD HH:mm:ss") }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main">
|
|
<div class="main-content-wrapper">
|
|
<!-- 四维矩阵图 -->
|
|
<div class="matrix-header">
|
|
<!-- <img class="item" :src="item" alt="思维矩阵图片" /> -->
|
|
<div class="market-temperature-label">
|
|
{{ stock.stockInfo.name
|
|
}}{{ stock.stockInfo.name ? "量子四维矩阵图" : "" }}
|
|
</div>
|
|
<div class="market-temperature-value">
|
|
{{ getDisplayDate(stock) }}
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="market-temperature-icon"
|
|
v-if="chartVisibility.marketTemperature"
|
|
>
|
|
<img src="@/assets/img/AiEmotion/L1.png" alt="情绪监控图标" />
|
|
</div>
|
|
<!-- 温度计图表 -->
|
|
<div
|
|
class="market-temperature-section"
|
|
v-if="chartVisibility.marketTemperature"
|
|
>
|
|
<div class="temperature-content">
|
|
<div class="content1">
|
|
<img src="@/assets/img/AiEmotion/温度计.png" alt="温度计图标" />
|
|
<span class="matrix-main-title">股市温度计</span>
|
|
</div>
|
|
<div class="temperature-display">
|
|
<div class="temperature-cold">
|
|
股票温度:{{ getStockData2(stock) ?? "NA" }}
|
|
</div>
|
|
<div class="temperature-hot">
|
|
市场温度:{{ getStockData1(stock) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<marketTemperature
|
|
:ref="(el) => (marketTemperatureRef[stockIndex] = el)"
|
|
:companyName="stock.stockInfo.name"
|
|
:stockCode="stock.stockInfo.code"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="emotion-decoder-icon" v-if="chartVisibility.emotionDecod">
|
|
<img src="@/assets/img/AiEmotion/L2.png" alt="情绪解码图标" />
|
|
</div>
|
|
<!-- 情绪解码器图表 -->
|
|
<div
|
|
class="emotion-decoder-section"
|
|
v-if="chartVisibility.emotionDecod"
|
|
>
|
|
<div class="emotion-decoder-header">
|
|
<img
|
|
src="@/assets/img/AiEmotion/emotionDecod.png"
|
|
alt="情绪解码器图标"
|
|
/>
|
|
<span class="emotion-decoder-text">情绪解码器</span>
|
|
</div>
|
|
<div class="emotion-decoder-content">
|
|
<emotionDecod
|
|
:ref="(el) => (emotionDecodRef[stockIndex] = el)"
|
|
></emotionDecod>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="bottom-radar-icon"
|
|
v-if="chartVisibility.emotionalBottomRadar"
|
|
>
|
|
<img src="@/assets/img/AiEmotion/L3.png" alt="情绪推演图标" />
|
|
</div>
|
|
<!-- 情绪探底雷达图表 -->
|
|
<div
|
|
class="bottom-radar-section"
|
|
v-if="chartVisibility.emotionalBottomRadar"
|
|
>
|
|
<div class="bottom-radar-header">
|
|
<img src="@/assets/img/AiEmotion/探底雷达.png" alt="探底雷达图表" />
|
|
<span class="bottom-radar-text">情绪探底雷达</span>
|
|
</div>
|
|
<div class="bottom-radar-content">
|
|
<emotionalBottomRadar
|
|
:ref="(el) => (emotionalBottomRadarRef[stockIndex] = el)"
|
|
></emotionalBottomRadar>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="energy-converter-icon"
|
|
v-if="chartVisibility.emoEnergyConverter"
|
|
>
|
|
<img src="@/assets/img/AiEmotion/L4.png" alt="情绪套利" />
|
|
</div>
|
|
<!-- 情绪能量转化器图表 -->
|
|
<div
|
|
class="energy-converter-section"
|
|
v-if="chartVisibility.emoEnergyConverter"
|
|
>
|
|
<div class="energy-converter-header">
|
|
<img
|
|
src="@/assets/img/AiEmotion/能量转化器.png"
|
|
alt="能量转化器图标"
|
|
/>
|
|
<span class="energy-converter-text">情绪能量转化器</span>
|
|
</div>
|
|
<div class="energy-converter-content">
|
|
<emoEnergyConverter
|
|
:ref="(el) => (emoEnergyConverterRef[stockIndex] = el)"
|
|
></emoEnergyConverter>
|
|
</div>
|
|
</div>
|
|
<!-- 核心看点 -->
|
|
<div class="core-highlights-header">
|
|
<img src="@/assets/img/AiEmotion/核心看点.png" alt="核心看点字样" />
|
|
</div>
|
|
<div class="bk-image">
|
|
<div class="text-container">
|
|
<p>
|
|
<span class="title">情绪监控-金融宇宙的【量子检测网络】</span>
|
|
<span class="content"
|
|
>核心任务:构建全市场情绪引力场雷达,实时监测资金流向和情绪波动</span
|
|
>
|
|
</p>
|
|
<p>
|
|
<span class="title">情绪解码-主力思维的【神经破译矩阵】</span>
|
|
<span class="content"
|
|
>核心任务:解构资金行为的量子密码,破译主力操盘意图和策略布局</span
|
|
>
|
|
</p>
|
|
<p>
|
|
<span class="title">情绪推演-未来战争的【时空推演舱】</span>
|
|
<span class="content"
|
|
>核心任务:基于情绪数据推演未来走势,预测市场转折点和机会窗口</span
|
|
>
|
|
</p>
|
|
<p>
|
|
<span class="title">情绪套利-财富裂变的【粒子对撞机】</span>
|
|
<span class="content"
|
|
>核心任务:将情绪差转化为收益粒子流,实现情绪能量的价值转换</span
|
|
>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<!-- 核心逻辑 -->
|
|
<div class="core-logic-header">
|
|
<img src="@/assets/img/AiEmotion/核心逻辑.png" alt="核心逻辑字样" />
|
|
</div>
|
|
<div class="decision-tree-section">
|
|
<div class="lz-img">
|
|
<img src="@/assets/img/AiEmotion/量子神经决策树.png" alt="树标题" />
|
|
</div>
|
|
<div class="scaled-img">
|
|
<!-- <img src="@/assets/img/AiEmotion/tree02.jpg" alt="树图片"> -->
|
|
</div>
|
|
</div>
|
|
<!-- 场景应用 -->
|
|
<div class="scenario-application-section" ref="scenarioApplicationRef">
|
|
<img src="@/assets/img/AiEmotion/场景应用.png" alt="场景应用标题" />
|
|
<div class="bk-image">
|
|
<div class="conclusion-container">
|
|
<!-- 使用当前股票项的结论数据显示内容,支持打字机效果 -->
|
|
<!-- L1: 情绪监控 -->
|
|
<div class="conclusion-item" v-if="(getStockTypewriterVisibility(stock) && getStockTypewriterVisibility(stock).one) || (getStockConclusion(stock) && !getStockTypewriterTexts(stock) && (getStockConclusion(stock).one1 || getStockConclusion(stock).one2))">
|
|
<h4 class="conclusion-title">L1: 情绪监控</h4>
|
|
<!-- 打字机效果文本 -->
|
|
<p class="conclusion-text" v-if="getStockTypewriterTexts(stock) && getStockTypewriterTexts(stock).one1">
|
|
{{ getStockTypewriterTexts(stock).one1 }}
|
|
</p>
|
|
<p class="conclusion-text" v-if="getStockTypewriterTexts(stock) && getStockTypewriterTexts(stock).one2">
|
|
{{ getStockTypewriterTexts(stock).one2 }}
|
|
</p>
|
|
<!-- 完整文本 -->
|
|
<p class="conclusion-text" v-if="!getStockTypewriterTexts(stock) && getStockConclusion(stock) && getStockConclusion(stock).one1">
|
|
{{ getStockConclusion(stock).one1 }}
|
|
</p>
|
|
<p class="conclusion-text" v-if="!getStockTypewriterTexts(stock) && getStockConclusion(stock) && getStockConclusion(stock).one2">
|
|
{{ getStockConclusion(stock).one2 }}
|
|
</p>
|
|
</div>
|
|
<!-- L2: 情绪解码 -->
|
|
<div class="conclusion-item" v-if="(getStockTypewriterVisibility(stock) && getStockTypewriterVisibility(stock).two) || (getStockConclusion(stock) && !getStockTypewriterTexts(stock) && getStockConclusion(stock).two)">
|
|
<h4 class="conclusion-title">L2: 情绪解码</h4>
|
|
<!-- 打字机效果文本 -->
|
|
<p class="conclusion-text" v-if="getStockTypewriterTexts(stock) && getStockTypewriterTexts(stock).two">
|
|
{{ getStockTypewriterTexts(stock).two }}
|
|
</p>
|
|
<!-- 完整文本 -->
|
|
<p class="conclusion-text" v-if="!getStockTypewriterTexts(stock) && getStockConclusion(stock) && getStockConclusion(stock).two">
|
|
{{ getStockConclusion(stock).two }}
|
|
</p>
|
|
</div>
|
|
<!-- L3: 情绪推演 -->
|
|
<div class="conclusion-item" v-if="(getStockTypewriterVisibility(stock) && getStockTypewriterVisibility(stock).three) || (getStockConclusion(stock) && !getStockTypewriterTexts(stock) && getStockConclusion(stock).three)">
|
|
<h4 class="conclusion-title">L3: 情绪推演</h4>
|
|
<!-- 打字机效果文本 -->
|
|
<p class="conclusion-text" v-if="getStockTypewriterTexts(stock) && getStockTypewriterTexts(stock).three">
|
|
{{ getStockTypewriterTexts(stock).three }}
|
|
</p>
|
|
<!-- 完整文本 -->
|
|
<p class="conclusion-text" v-if="!getStockTypewriterTexts(stock) && getStockConclusion(stock) && getStockConclusion(stock).three">
|
|
{{ getStockConclusion(stock).three }}
|
|
</p>
|
|
</div>
|
|
<!-- L4: 情绪套利 -->
|
|
<div class="conclusion-item" v-if="(getStockTypewriterVisibility(stock) && getStockTypewriterVisibility(stock).four) || (getStockConclusion(stock) && !getStockTypewriterTexts(stock) && getStockConclusion(stock).four)">
|
|
<h4 class="conclusion-title">L4: 情绪套利</h4>
|
|
<!-- 打字机效果文本 -->
|
|
<p class="conclusion-text" v-if="getStockTypewriterTexts(stock) && getStockTypewriterTexts(stock).four">
|
|
{{ getStockTypewriterTexts(stock).four }}
|
|
</p>
|
|
<!-- 完整文本 -->
|
|
<p class="conclusion-text" v-if="!getStockTypewriterTexts(stock) && getStockConclusion(stock) && getStockConclusion(stock).four">
|
|
{{ getStockConclusion(stock).four }}
|
|
</p>
|
|
</div>
|
|
<!-- AI生成内容免责声明 -->
|
|
<div class="disclaimer-item" v-if="(getStockTypewriterVisibility(stock) && getStockTypewriterVisibility(stock).disclaimer) || (getStockConclusion(stock) && !getStockTypewriterTexts(stock))">
|
|
<!-- 打字机效果文本 -->
|
|
<p class="disclaimer-text" v-if="getStockTypewriterTexts(stock) && getStockTypewriterTexts(stock).disclaimer">
|
|
{{ getStockTypewriterTexts(stock).disclaimer }}
|
|
</p>
|
|
<!-- 完整文本 -->
|
|
<!-- <p class="disclaimer-text" v-if="!getStockTypewriterTexts(stock)">
|
|
该内容由AI生成,请注意甄别
|
|
</p> -->
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="conclusion-placeholder"
|
|
v-if="!getStockConclusion(stock) || (!getStockConclusion(stock).one1 && !getStockConclusion(stock).one2 && !getStockConclusion(stock).two && !getStockConclusion(stock).three && !getStockConclusion(stock).four)"
|
|
>
|
|
<p>等待股票分析结论...</p>
|
|
</div>
|
|
</div>
|
|
<p class="disclaimer-text" v-if="!getStockTypewriterTexts(stock)">
|
|
该内容由AI生成,请注意甄别
|
|
</p>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 全局返回顶部按钮 -->
|
|
<div class="back-to-top" @click="scrollToTop" v-if="(filteredStockList.length > 0 || messages.length > 0)">
|
|
<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>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from "vue";
|
|
import { getReplyAPI, getConclusionAPI } from "@/api/AiEmotionApi.js"; // 导入工作流接口方法
|
|
import axios from "axios";
|
|
import moment from "moment";
|
|
import item from "@/assets/img/AiEmotion/bk01.png"; // 导入思维矩阵图片
|
|
import emotionDecod from "@/views/components/emotionDecod.vue"; // 导入情绪解码组件
|
|
import emotionalBottomRadar from "@/views/components/emotionalBottomRadar.vue"; // 导入情绪探底雷达图组件
|
|
import emoEnergyConverter from "@/views/components/emoEnergyConverter.vue"; // 导入情绪能量转化器组件
|
|
import marketTemperature from "@/views/components/marketTemperature.vue";
|
|
import StockTabs from "@/views/components/StockTabs.vue"; // 导入股票标签页组件
|
|
import blueBorderImg from "@/assets/img/AiEmotion/blueBorder.png"; //导入蓝色背景框图片
|
|
import { ElMessage } from "element-plus"; // 接口失败提示已改为对话形式,保留用于输入验证
|
|
import { useEmotionStore } from "@/store/emotion"; // 导入Pinia store
|
|
import { useEmotionAudioStore } from "@/store/emotionAudio.js"; // 导入音频store
|
|
import { useChatStore } from "@/store/chat.js"; // 导入聊天store
|
|
// 导入思考过程GIF
|
|
import thinkingGif from "@/assets/img/gif/思考.gif";
|
|
import analyzeGif from "@/assets/img/gif/解析.gif";
|
|
import generateGif from "@/assets/img/gif/生成.gif";
|
|
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 { Howl, Howler } from "howler"; // 导入音频播放库
|
|
import { reactive } from "vue";
|
|
import { marked } from "marked"; // 引入marked库
|
|
import { useUserStore } from "../store/userPessionCode";
|
|
const APIurl = import.meta.env.VITE_APP_API_BASE_URL;
|
|
|
|
// 使用Pinia store
|
|
const emotionStore = useEmotionStore();
|
|
const emotionAudioStore = useEmotionAudioStore();
|
|
|
|
// 语音播放控制函数
|
|
const toggleVoiceForUser = (stock) => {
|
|
if (!emotionAudioStore.isVoiceEnabled) {
|
|
// 如果语音功能关闭,先开启语音功能
|
|
emotionAudioStore.toggleVoice();
|
|
} else {
|
|
// 获取该股票的结论数据和当前状态
|
|
const stockConclusion = getStockConclusion(stock);
|
|
const currentState = getStockAudioState(stock);
|
|
|
|
// 检查是否有任何音频正在播放(包括全局播放状态和当前股票状态)
|
|
const isAnyAudioPlaying =
|
|
emotionAudioStore.isPlaying || currentState.isPlaying;
|
|
|
|
// 如果当前点击的股票正在播放,则暂停
|
|
if (currentState.isPlaying) {
|
|
console.log("暂停当前股票音频:", stock.stockInfo?.name);
|
|
// 暂停音频而不是停止
|
|
if (emotionAudioStore.nowSound && emotionAudioStore.nowSound.playing()) {
|
|
emotionAudioStore.nowSound.pause();
|
|
emotionAudioStore.isPaused = true;
|
|
emotionAudioStore.isPlaying = false;
|
|
}
|
|
setStockAudioState(stock, { isPlaying: false, isPaused: true });
|
|
} else if (currentState.isPaused) {
|
|
// 如果当前股票处于暂停状态,则继续播放
|
|
console.log("继续播放当前股票音频:", stock.stockInfo?.name);
|
|
if (emotionAudioStore.nowSound) {
|
|
emotionAudioStore.nowSound.play();
|
|
emotionAudioStore.isPaused = false;
|
|
emotionAudioStore.isPlaying = true;
|
|
}
|
|
setStockAudioState(stock, { isPlaying: true, isPaused: false });
|
|
} else {
|
|
// 如果有其他音频正在播放,先停止
|
|
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 {
|
|
console.log("该股票没有可播放的音频数据");
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// 计算属性:判断语音是否启用
|
|
const isVoice = computed(() => {
|
|
return emotionAudioStore.isVoiceEnabled;
|
|
});
|
|
const chatStore = useChatStore();
|
|
// 获取权限
|
|
const userStore = useUserStore();
|
|
// 处理refuse数据的函数
|
|
function processRefuseMessage(refuseData) {
|
|
if (!refuseData) return "未知错误";
|
|
|
|
// 如果refuse数据包含Markdown格式,进行转换
|
|
try {
|
|
// 配置marked选项
|
|
marked.setOptions({
|
|
breaks: true, // 支持换行符转换为 <br>
|
|
gfm: true, // 启用 GitHub Flavored Markdown
|
|
sanitize: false, // 不清理 HTML
|
|
smartLists: true, // 智能列表
|
|
smartypants: true, // 智能标点符号
|
|
xhtml: false, // 不使用 XHTML 输出
|
|
});
|
|
|
|
// 将Markdown转换为HTML
|
|
const htmlContent = marked(refuseData);
|
|
|
|
// 移除HTML标签,只保留纯文本用于ElMessage显示
|
|
const tempDiv = document.createElement("div");
|
|
tempDiv.innerHTML = htmlContent;
|
|
return tempDiv.textContent || tempDiv.innerText || refuseData;
|
|
} catch (error) {
|
|
console.error("处理refuse消息时出错:", error);
|
|
return refuseData;
|
|
}
|
|
}
|
|
|
|
// 组件引用 - 修改为数组形式支持多个股票
|
|
const marketTemperatureRef = ref([]); // 引用市场温度计组件数组
|
|
const emoEnergyConverterRef = ref([]);
|
|
const emotionDecodRef = ref([]);
|
|
const emotionalBottomRadarRef = ref([]);
|
|
const userInputDisplayRef = ref(null); //消息区域的引用
|
|
|
|
// 响应式数据
|
|
const messages = ref([]);
|
|
|
|
// 从emotion store中恢复对话记录
|
|
const loadConversationsFromStore = () => {
|
|
const storedConversations = emotionStore.getConversations();
|
|
messages.value = storedConversations.map((conv) => ({
|
|
sender: conv.sender,
|
|
text: conv.text,
|
|
}));
|
|
};
|
|
|
|
// 记录已经添加到对话中的股票,避免重复添加
|
|
const addedStocks = ref(new Set());
|
|
|
|
// 从stockList生成对话历史
|
|
const loadConversationsFromStockList = () => {
|
|
// 检查是否有新的股票需要添加到对话中
|
|
emotionStore.stockList.forEach((stock) => {
|
|
const stockKey = `${stock.stockInfo.code}_${stock.timestamp}`;
|
|
|
|
// 如果这个股票还没有添加到对话中
|
|
if (!addedStocks.value.has(stockKey)) {
|
|
// 检查messages中是否已经存在相同的用户消息
|
|
const existingMessage = messages.value.find(
|
|
(msg) => msg.sender === "user" && msg.text === stock.queryText
|
|
);
|
|
|
|
// 只有当messages中不存在相同消息时才添加
|
|
if (!existingMessage) {
|
|
// 只添加用户输入消息,不添加AI回复
|
|
const userMessage = {
|
|
sender: "user",
|
|
text: stock.queryText,
|
|
};
|
|
messages.value.push(userMessage);
|
|
|
|
// 只将用户消息添加到emotion store中(如果store中也不存在)
|
|
const storeConversations = emotionStore.getConversations();
|
|
const existingInStore = storeConversations.find(
|
|
(conv) => conv.sender === "user" && conv.text === stock.queryText
|
|
);
|
|
if (!existingInStore) {
|
|
emotionStore.addConversation(userMessage);
|
|
}
|
|
}
|
|
|
|
// 将这个股票标记为已添加
|
|
addedStocks.value.add(stockKey);
|
|
}
|
|
});
|
|
};
|
|
|
|
// 清空对话记录
|
|
const clearConversations = () => {
|
|
messages.value = [];
|
|
emotionStore.clearConversations();
|
|
// 清空已添加股票的记录
|
|
addedStocks.value.clear();
|
|
};
|
|
|
|
// 添加股票数据的包装方法
|
|
const addStock = (stockData) => {
|
|
console.log("AiEmotion组件接收到股票数据:", stockData);
|
|
|
|
// 设置为历史记录模式,并重置用户主动搜索标志
|
|
isHistoryMode.value = true;
|
|
isUserInitiated.value = false;
|
|
|
|
// 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();
|
|
|
|
// 清理状态变量(保留用于其他功能的变量)
|
|
|
|
// 隐藏所有图表组件
|
|
chartVisibility.value = {
|
|
marketTemperature: false,
|
|
emotionDecod: false,
|
|
emotionalBottomRadar: false,
|
|
emoEnergyConverter: false,
|
|
};
|
|
|
|
// 2. 将新的数据存储到stockList中
|
|
emotionStore.addStock(stockData);
|
|
// 3. 设置页面为已加载状态,重新渲染页面
|
|
isPageLoaded.value = true;
|
|
|
|
// 4. 标记历史记录股票已显示过,避免重复触发
|
|
if (stockData.conclusionData) {
|
|
const stockCode =
|
|
stockData.stockInfo?.code || stockData.stockInfo?.symbol;
|
|
if (stockCode) {
|
|
stockTypewriterShown.value.set(stockCode, true);
|
|
stockAudioPlayed.value.set(stockCode, true);
|
|
}
|
|
console.log("历史记录股票已标记为已显示");
|
|
}
|
|
|
|
// 5. 使用nextTick确保DOM更新后启动高度监听器并滚动到底部
|
|
nextTick(() => {
|
|
// 启动页面高度监听器,实时监听内容变化并自动滚动
|
|
startHeightObserver();
|
|
// 立即滚动到底部
|
|
scrollToBottom();
|
|
|
|
// 6. 历史记录加载完成后,通知父组件重新启用输入框
|
|
emit('enableInput');
|
|
});
|
|
};
|
|
|
|
// 暴露方法给父组件
|
|
defineExpose({
|
|
handleSendMessage,
|
|
clearConversations,
|
|
addStock,
|
|
});
|
|
const isPageLoaded = ref(false); // 控制页面是否显示
|
|
const isHistoryMode = ref(false); // 控制是否为历史记录模式
|
|
// const isLoading = ref(false); // 控制加载状态
|
|
const isRotating = ref(false); //控制旋转
|
|
const version1 = ref(1); // 版本号
|
|
const conclusionData = ref(""); // 存储第二个工作流接口返回的结论数据
|
|
|
|
// 自动滚动相关数据
|
|
const isAutoScrolling = ref(false);
|
|
const currentSection = ref(0);
|
|
const sectionRefs = ref([]);
|
|
const scenarioApplicationRef = ref(null); // 场景应用部分的引用
|
|
const hasTriggeredAudio = ref(false); // 是否已触发音频播放
|
|
const hasTriggeredTypewriter = ref(false); // 是否已触发打字机效果
|
|
const intersectionObserver = ref(null); // 存储observer实例
|
|
const isUserInitiated = ref(false); // 标记是否为用户主动搜索
|
|
|
|
// 打字机效果相关的变量已移除,现在直接使用parsedConclusion显示内容
|
|
|
|
// 图表组件显示状态
|
|
const chartVisibility = ref({
|
|
marketTemperature: true,
|
|
emotionDecod: true,
|
|
emotionalBottomRadar: true,
|
|
emoEnergyConverter: true,
|
|
});
|
|
const typewriterTimers = ref([]);
|
|
// 记录每个股票是否已经显示过打字机效果
|
|
const stockTypewriterShown = ref(new Map());
|
|
// 记录每个股票的打字机显示状态
|
|
const stockTypewriterTexts = ref(new Map());
|
|
// 记录每个股票的打字机模块可见性
|
|
const stockTypewriterVisibility = ref(new Map());
|
|
// 记录每个股票是否已经播放过音频
|
|
const stockAudioPlayed = ref(new Map());
|
|
// 跟踪每个股票的音频播放状态
|
|
const stockAudioStates = ref(new Map());
|
|
// 存储当前的完成回调函数
|
|
const currentOnCompleteCallback = ref(null);
|
|
|
|
// 音频播放相关数据
|
|
const audioUrl = ref("");
|
|
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);
|
|
|
|
// 计算属性 - 从store获取当前股票数据
|
|
const currentStock = computed(() => emotionStore.activeStock);
|
|
const stockName = computed(() => currentStock.value?.stockInfo.name || "");
|
|
const stockCode = computed(
|
|
() =>
|
|
currentStock.value?.stockInfo.code ||
|
|
currentStock.value?.stockInfo.symbol ||
|
|
""
|
|
);
|
|
const displayDate = computed(() => {
|
|
if (!currentStock.value?.apiData) return "";
|
|
const lastData = currentStock.value.apiData.GSWDJ?.at(-1);
|
|
if (!lastData || !lastData[0]) return "";
|
|
|
|
const dateStr = lastData[0];
|
|
// 假设原格式为 YYYY-MM-DD 或 YYYY/MM/DD
|
|
const dateMatch = dateStr.match(/(\d{4})[\-\/](\d{1,2})[\-\/](\d{1,2})/);
|
|
if (dateMatch) {
|
|
const [, year, month, day] = dateMatch;
|
|
// 转换为 DD/MM/YYYY 格式
|
|
return `更新时间:${day.padStart(2, "0")}/${month.padStart(
|
|
2,
|
|
"0"
|
|
)}/${year}`;
|
|
}
|
|
|
|
// 如果不匹配预期格式,返回原始值
|
|
return dateStr;
|
|
});
|
|
const data1 = computed(() => {
|
|
if (!currentStock.value?.apiData) return null;
|
|
const lastData = currentStock.value.apiData.GSWDJ?.at(-1);
|
|
return lastData ? Math.round(lastData[1]) : null;
|
|
});
|
|
const data2 = computed(() => {
|
|
if (!currentStock.value?.apiData) return null;
|
|
const lastData = currentStock.value.apiData.GSWDJ?.at(-1);
|
|
return lastData ? Math.round(lastData[2]) : null;
|
|
});
|
|
const currentConclusion = computed(() => {
|
|
return currentStock.value?.conclusionData || "";
|
|
});
|
|
const parsedConclusion = computed(() => {
|
|
if (!currentConclusion.value) return null;
|
|
|
|
// 如果conclusionData已经是对象,直接返回
|
|
if (typeof currentConclusion.value === "object") {
|
|
return currentConclusion.value;
|
|
}
|
|
|
|
// 如果是字符串,尝试解析JSON
|
|
try {
|
|
return JSON.parse(currentConclusion.value);
|
|
} catch (error) {
|
|
console.error("解析结论数据失败:", error);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
// 多股票数据显示的计算属性
|
|
// 过滤和排序后的股票列表
|
|
const filteredStockList = computed(() => {
|
|
return emotionStore.stockList
|
|
.filter((stock) => {
|
|
// 过滤掉数据不完整的股票
|
|
return stock.stockInfo?.name && stock.apiData && stock.queryText;
|
|
})
|
|
.sort((a, b) => {
|
|
// 按时间戳降序排序,最新的在前
|
|
return new Date(a.timestamp) - new Date(b.timestamp);
|
|
});
|
|
});
|
|
|
|
// 辅助函数:获取股票的显示日期
|
|
const getDisplayDate = (stock) => {
|
|
if (!stock?.apiData) return "";
|
|
const lastData = stock.apiData.GSWDJ?.at(-1);
|
|
if (!lastData || !lastData[0]) return "";
|
|
|
|
const dateStr = lastData[0];
|
|
// 假设原格式为 YYYY-MM-DD 或 YYYY/MM/DD
|
|
const dateMatch = dateStr.match(/(\d{4})[\-\/](\d{1,2})[\-\/](\d{1,2})/);
|
|
if (dateMatch) {
|
|
const [, year, month, day] = dateMatch;
|
|
// 转换为 DD/MM/YYYY 格式
|
|
return `更新时间:${day.padStart(2, "0")}/${month.padStart(
|
|
2,
|
|
"0"
|
|
)}/${year}`;
|
|
}
|
|
|
|
// 如果不匹配预期格式,返回原始值
|
|
return dateStr;
|
|
};
|
|
|
|
// 辅助函数:获取股票的市场温度数据
|
|
const getStockData1 = (stock) => {
|
|
if (!stock?.apiData) return null;
|
|
const lastData = stock.apiData.GSWDJ?.at(-1);
|
|
return lastData ? Math.round(lastData[1]) : null;
|
|
};
|
|
|
|
// 辅助函数:获取股票的股票温度数据
|
|
const getStockData2 = (stock) => {
|
|
if (!stock?.apiData) return null;
|
|
const lastData = stock.apiData.GSWDJ?.at(-1);
|
|
return lastData ? Math.round(lastData[2]) : null;
|
|
};
|
|
|
|
// 辅助函数:获取股票的结论数据
|
|
const getStockConclusion = (stock) => {
|
|
if (!stock?.conclusionData) return null;
|
|
|
|
// 如果conclusionData已经是对象,直接返回
|
|
if (typeof stock.conclusionData === "object") {
|
|
return stock.conclusionData;
|
|
}
|
|
|
|
// 如果是字符串,尝试解析JSON
|
|
try {
|
|
return JSON.parse(stock.conclusionData);
|
|
} catch (error) {
|
|
console.error("解析股票结论数据失败:", error);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// 辅助函数:获取股票的打字机文本状态
|
|
const getStockTypewriterTexts = (stock) => {
|
|
const stockCode = stock.stockInfo?.code || stock.stockInfo?.symbol;
|
|
if (!stockCode) return null;
|
|
return stockTypewriterTexts.value.get(stockCode) || null;
|
|
};
|
|
|
|
// 辅助函数:获取股票的打字机可见性状态
|
|
const getStockTypewriterVisibility = (stock) => {
|
|
const stockCode = stock.stockInfo?.code || stock.stockInfo?.symbol;
|
|
if (!stockCode) return null;
|
|
return stockTypewriterVisibility.value.get(stockCode) || null;
|
|
};
|
|
|
|
// 辅助函数:检查股票是否正在进行打字机效果
|
|
const isStockTypewriting = (stock) => {
|
|
const stockCode = stock.stockInfo?.code || stock.stockInfo?.symbol;
|
|
if (!stockCode) return false;
|
|
return stockTypewriterShown.value.has(stockCode) && !stockTypewriterTexts.value.has(stockCode);
|
|
};
|
|
|
|
// 监听股票列表变化,当列表为空时隐藏页面数据
|
|
watch(
|
|
() => emotionStore.stockList,
|
|
(newStockList) => {
|
|
if (newStockList.length === 0) {
|
|
// 当股票列表为空时,隐藏页面数据
|
|
isPageLoaded.value = false;
|
|
// 停止音频播放
|
|
stopAudio();
|
|
// 清理音频URL
|
|
audioUrl.value = "";
|
|
emotionAudioStore.resetAudioState();
|
|
// 清理打字机效果
|
|
clearTypewriterTimers();
|
|
// 重置所有状态
|
|
hasTriggeredAudio.value = false;
|
|
hasTriggeredTypewriter.value = false;
|
|
stockTypewriterShown.value.clear();
|
|
stockAudioPlayed.value.clear();
|
|
// 清理已添加股票的记录
|
|
addedStocks.value.clear();
|
|
// 隐藏所有图表组件
|
|
chartVisibility.value = {
|
|
marketTemperature: false,
|
|
emotionDecod: false,
|
|
emotionalBottomRadar: false,
|
|
emoEnergyConverter: false,
|
|
};
|
|
console.log("股票列表已清空,页面数据已隐藏");
|
|
} else {
|
|
// 当stockList有数据时,更新对话记录
|
|
loadConversationsFromStockList();
|
|
}
|
|
},
|
|
{ deep: true }
|
|
);
|
|
|
|
// 监听当前股票变化,重新渲染图表和显示对应结论
|
|
watch(
|
|
currentStock,
|
|
(newStock) => {
|
|
if (newStock && newStock.apiData) {
|
|
// 页面加载状态现在由 handleSendMessage 统一控制
|
|
// 停止当前播放的音频
|
|
stopAudio();
|
|
// 清理音频URL,确保不会播放之前股票的音频
|
|
audioUrl.value = "";
|
|
// 清理store中的音频URL,确保不会播放之前股票的音频
|
|
emotionAudioStore.resetAudioState();
|
|
// 清理正在进行的打字机效果定时器
|
|
clearTypewriterTimers();
|
|
// 重置触发状态,让每个股票都能独立触发效果
|
|
hasTriggeredAudio.value = false;
|
|
hasTriggeredTypewriter.value = false;
|
|
|
|
// 获取股票代码作为唯一标识
|
|
const stockCode = newStock.stockInfo?.code || newStock.stockInfo?.symbol;
|
|
|
|
// 处理当前股票的音频URL
|
|
if (newStock.conclusionData) {
|
|
try {
|
|
// 如果conclusionData已经是对象,直接使用;否则解析JSON
|
|
const conclusion =
|
|
typeof newStock.conclusionData === "object"
|
|
? newStock.conclusionData
|
|
: JSON.parse(newStock.conclusionData);
|
|
|
|
// 检查该股票是否已经显示过打字机效果
|
|
if (stockCode && stockTypewriterShown.value.has(stockCode)) {
|
|
// 如果已经显示过,直接显示完整内容,不需要打字机效果
|
|
|
|
// 提取音频URL但不自动播放,等待用户手动点击
|
|
let voiceUrl = null;
|
|
// 优先使用one1_url,如果没有则尝试其他音频URL
|
|
if (conclusion.one1_url) {
|
|
voiceUrl = conclusion.one1_url
|
|
.toString()
|
|
.trim()
|
|
.replace(/[`\s]/g, "");
|
|
} else if (conclusion.one2_url) {
|
|
voiceUrl = conclusion.one2_url
|
|
.toString()
|
|
.trim()
|
|
.replace(/[`\s]/g, "");
|
|
} else if (conclusion.two_url) {
|
|
voiceUrl = conclusion.two_url
|
|
.toString()
|
|
.trim()
|
|
.replace(/[`\s]/g, "");
|
|
} else if (conclusion.three_url) {
|
|
voiceUrl = conclusion.three_url
|
|
.toString()
|
|
.trim()
|
|
.replace(/[`\s]/g, "");
|
|
} else if (conclusion.four_url) {
|
|
voiceUrl = conclusion.four_url
|
|
.toString()
|
|
.trim()
|
|
.replace(/[`\s]/g, "");
|
|
} else if (conclusion.url) {
|
|
voiceUrl = conclusion.url.toString().trim().replace(/[`\s]/g, "");
|
|
} else if (conclusion.audioUrl) {
|
|
voiceUrl = conclusion.audioUrl
|
|
.toString()
|
|
.trim()
|
|
.replace(/[`\s]/g, "");
|
|
} else if (conclusion.voice_url) {
|
|
voiceUrl = conclusion.voice_url
|
|
.toString()
|
|
.trim()
|
|
.replace(/[`\s]/g, "");
|
|
} else if (conclusion.audio) {
|
|
voiceUrl = conclusion.audio
|
|
.toString()
|
|
.trim()
|
|
.replace(/[`\s]/g, "");
|
|
} else if (conclusion.tts_url) {
|
|
voiceUrl = conclusion.tts_url
|
|
.toString()
|
|
.trim()
|
|
.replace(/[`\s]/g, "");
|
|
}
|
|
|
|
if (voiceUrl && voiceUrl.startsWith("http")) {
|
|
console.log(
|
|
"切换到已显示股票,准备音频URL但不自动播放:",
|
|
voiceUrl
|
|
);
|
|
audioUrl.value = voiceUrl;
|
|
// 同时更新store中的音频URL
|
|
emotionAudioStore.setCurrentAudioUrl(voiceUrl);
|
|
// 不自动播放,等待用户手动点击
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("解析股票结论数据失败:", error);
|
|
}
|
|
} else {
|
|
// 如果没有结论数据,清空音频URL
|
|
audioUrl.value = "";
|
|
emotionAudioStore.resetAudioState();
|
|
console.log("当前股票没有结论数据,已清空音频");
|
|
}
|
|
|
|
// 只有在页面已加载的情况下才渲染图表
|
|
if (isPageLoaded.value) {
|
|
nextTick(() => {
|
|
renderCharts(newStock.apiData);
|
|
console.log("图表数据已准备完成,开始渲染:", newStock.apiData);
|
|
// 检查场景应用部分是否已经在视口中,如果是则立即触发效果
|
|
setTimeout(() => {
|
|
// 检查 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;
|
|
|
|
// 如果该股票已经显示过,不需要再处理
|
|
if (stockCode && stockTypewriterShown.value.has(stockCode)) {
|
|
return;
|
|
}
|
|
|
|
const rect = scenarioApplicationRef.value.getBoundingClientRect();
|
|
const isInViewport =
|
|
rect.top < window.innerHeight && rect.bottom > 0;
|
|
|
|
if (isInViewport) {
|
|
console.log("股票切换后检测到场景应用部分在视口中");
|
|
|
|
if (stockCode) {
|
|
// 检查该股票是否是第一次触发
|
|
if (!stockTypewriterShown.value.has(stockCode)) {
|
|
// 如果是用户主动搜索,启动打字机效果和音频播放
|
|
if (isUserInitiated.value && audioUrl.value) {
|
|
console.log(
|
|
"用户主动搜索,该股票第一次进入场景应用,开始打字机效果和音频播放"
|
|
);
|
|
hasTriggeredTypewriter.value = true;
|
|
hasTriggeredAudio.value = true;
|
|
|
|
if (!stockAudioPlayed.value.has(stockCode)) {
|
|
console.log("开始音频播放和打字机效果");
|
|
stockAudioPlayed.value.set(stockCode, true);
|
|
playAudioQueue(parsedConclusion.value, true);
|
|
} else {
|
|
// 如果音频已播放过,只启动打字机效果
|
|
startTypewriterEffect(parsedConclusion.value, stockCode);
|
|
}
|
|
|
|
stockTypewriterShown.value.set(stockCode, true);
|
|
} else if (isUserInitiated.value && !audioUrl.value) {
|
|
console.log(
|
|
"音频尚未准备好,等待音频加载完成后再触发效果(股票切换后)"
|
|
);
|
|
return;
|
|
} else {
|
|
// 非用户主动搜索(如历史记录恢复),直接显示完整内容
|
|
console.log(
|
|
"非用户主动搜索,该股票第一次进入场景应用,直接显示完整内容"
|
|
);
|
|
|
|
const conclusion = parsedConclusion.value;
|
|
// 结论内容现在直接通过parsedConclusion计算属性显示
|
|
|
|
stockTypewriterShown.value.set(stockCode, true);
|
|
stockAudioPlayed.value.set(stockCode, true);
|
|
}
|
|
} 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,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, 500); // 延迟500ms确保数据完全加载
|
|
});
|
|
} else {
|
|
console.log("页面尚未加载完成,等待数据加载完成后再渲染图表");
|
|
}
|
|
} else {
|
|
console.log("股票数据不存在或API数据未加载");
|
|
// 隐藏所有图表组件
|
|
chartVisibility.value = {
|
|
marketTemperature: false,
|
|
emotionDecod: false,
|
|
emotionalBottomRadar: false,
|
|
emoEnergyConverter: false,
|
|
};
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
// 监听parsedConclusion变化,准备数据但不立即触发打字机效果
|
|
watch(
|
|
parsedConclusion,
|
|
(newConclusion) => {
|
|
if (newConclusion) {
|
|
console.log("场景应用结论数据:", newConclusion);
|
|
// 不再立即开始打字机效果,等待滚动到场景应用部分时触发
|
|
|
|
// 尝试多种可能的语音URL字段名,优先使用新的数据结构
|
|
let voiceUrl = null;
|
|
if (newConclusion.one1_url) {
|
|
voiceUrl = newConclusion.one1_url
|
|
.toString()
|
|
.trim()
|
|
.replace(/[`\s]/g, "");
|
|
} else if (newConclusion.one2_url) {
|
|
voiceUrl = newConclusion.one2_url
|
|
.toString()
|
|
.trim()
|
|
.replace(/[`\s]/g, "");
|
|
} else if (newConclusion.two_url) {
|
|
voiceUrl = newConclusion.two_url
|
|
.toString()
|
|
.trim()
|
|
.replace(/[`\s]/g, "");
|
|
} else if (newConclusion.three_url) {
|
|
voiceUrl = newConclusion.three_url
|
|
.toString()
|
|
.trim()
|
|
.replace(/[`\s]/g, "");
|
|
} else if (newConclusion.four_url) {
|
|
voiceUrl = newConclusion.four_url
|
|
.toString()
|
|
.trim()
|
|
.replace(/[`\s]/g, "");
|
|
} else if (newConclusion.url) {
|
|
// 清理URL字符串,去除空格、反引号等特殊字符
|
|
voiceUrl = newConclusion.url.toString().trim().replace(/[`\s]/g, "");
|
|
} else if (newConclusion.audioUrl) {
|
|
voiceUrl = newConclusion.audioUrl
|
|
.toString()
|
|
.trim()
|
|
.replace(/[`\s]/g, "");
|
|
} else if (newConclusion.voice_url) {
|
|
voiceUrl = newConclusion.voice_url
|
|
.toString()
|
|
.trim()
|
|
.replace(/[`\s]/g, "");
|
|
} else if (newConclusion.audio) {
|
|
voiceUrl = newConclusion.audio.toString().trim().replace(/[`\s]/g, "");
|
|
} else if (newConclusion.tts_url) {
|
|
voiceUrl = newConclusion.tts_url
|
|
.toString()
|
|
.trim()
|
|
.replace(/[`\s]/g, "");
|
|
}
|
|
|
|
if (voiceUrl && voiceUrl.startsWith("http")) {
|
|
console.log("找到并清理后的语音URL:", voiceUrl);
|
|
audioUrl.value = voiceUrl;
|
|
// 同时更新store中的音频URL
|
|
emotionAudioStore.setCurrentAudioUrl(voiceUrl);
|
|
console.log("音频URL已准备,检查是否需要立即触发效果");
|
|
|
|
// 音频准备好后,只有在用户主动搜索时才自动触发效果
|
|
// 数据恢复时不自动播放音频和打字机效果
|
|
console.log("音频URL已准备完成,等待用户手动触发播放");
|
|
} else {
|
|
console.log("未找到有效的语音URL,原始URL:", newConclusion.url);
|
|
console.log("结论数据中的所有字段:", Object.keys(newConclusion));
|
|
}
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
// 打字机效果函数
|
|
function startTypewriterEffect(conclusion, stockId, onComplete) {
|
|
// 如果没有传入stockId,使用当前活跃股票
|
|
if (!stockId && emotionStore.activeStock) {
|
|
const stock = emotionStore.activeStock;
|
|
stockId = stock.stockInfo?.code || stock.stockInfo?.symbol;
|
|
}
|
|
|
|
if (!stockId) {
|
|
console.warn("无法确定股票ID,跳过打字机效果");
|
|
return;
|
|
}
|
|
|
|
console.log("开始打字机效果,结论数据:", conclusion, "股票ID:", stockId);
|
|
|
|
// 保存当前的完成回调函数
|
|
currentOnCompleteCallback.value = onComplete;
|
|
|
|
// 详细调试各个字段
|
|
console.log("L1字段 - one1:", conclusion.one1);
|
|
console.log("L1字段 - one2:", conclusion.one2);
|
|
console.log("L2字段 - two:", conclusion.two);
|
|
console.log("L3字段 - three:", conclusion.three);
|
|
console.log("L4字段 - four:", conclusion.four);
|
|
|
|
// 清除之前的定时器
|
|
typewriterTimers.value.forEach((timer) => clearTimeout(timer));
|
|
typewriterTimers.value = [];
|
|
|
|
// 初始化该股票的打字机状态
|
|
if (!stockTypewriterTexts.value.has(stockId)) {
|
|
stockTypewriterTexts.value.set(stockId, {
|
|
one1: "",
|
|
one2: "",
|
|
two: "",
|
|
three: "",
|
|
four: "",
|
|
disclaimer: "",
|
|
});
|
|
}
|
|
if (!stockTypewriterVisibility.value.has(stockId)) {
|
|
stockTypewriterVisibility.value.set(stockId, {
|
|
one: false,
|
|
two: false,
|
|
three: false,
|
|
four: false,
|
|
disclaimer: false,
|
|
});
|
|
}
|
|
|
|
// 重置该股票的显示文本和状态
|
|
stockTypewriterTexts.value.set(stockId, {
|
|
one1: "",
|
|
one2: "",
|
|
two: "",
|
|
three: "",
|
|
four: "",
|
|
disclaimer: "",
|
|
});
|
|
|
|
stockTypewriterVisibility.value.set(stockId, {
|
|
one: false,
|
|
two: false,
|
|
three: false,
|
|
four: false,
|
|
disclaimer: false,
|
|
});
|
|
|
|
// 定义打字速度(毫秒)
|
|
const typeSpeed = 200;
|
|
let totalDelay = 0;
|
|
|
|
// 定义模块配置
|
|
const modules = [
|
|
{
|
|
key: "one",
|
|
title: "L1: 情绪监控",
|
|
contents: [
|
|
{ key: "one1", text: conclusion.one1 },
|
|
{ key: "one2", text: conclusion.one2 },
|
|
],
|
|
},
|
|
{
|
|
key: "two",
|
|
title: "L2: 情绪解码",
|
|
contents: [{ key: "two", text: conclusion.two }],
|
|
},
|
|
{
|
|
key: "three",
|
|
title: "L3: 情绪推演",
|
|
contents: [{ key: "three", text: conclusion.three }],
|
|
},
|
|
{
|
|
key: "four",
|
|
title: "L4: 情绪套利",
|
|
contents: [{ key: "four", text: conclusion.four }],
|
|
},
|
|
];
|
|
|
|
// 按模块顺序处理
|
|
modules.forEach((module) => {
|
|
// 检查模块是否有内容
|
|
const hasContent = module.contents.some(
|
|
(content) => content.text && content.text.trim()
|
|
);
|
|
console.log(
|
|
`模块 ${module.key} 是否有内容:`,
|
|
hasContent,
|
|
"内容:",
|
|
module.contents.map((c) => c.text)
|
|
);
|
|
if (!hasContent) return;
|
|
|
|
console.log(`开始显示模块 ${module.key}`);
|
|
|
|
// 显示模块
|
|
const showModuleTimer = setTimeout(() => {
|
|
const visibility = stockTypewriterVisibility.value.get(stockId);
|
|
if (visibility) {
|
|
visibility[module.key] = true;
|
|
stockTypewriterVisibility.value.set(stockId, { ...visibility });
|
|
}
|
|
}, totalDelay);
|
|
typewriterTimers.value.push(showModuleTimer);
|
|
totalDelay += 100;
|
|
|
|
// 为每个内容项添加打字机效果
|
|
module.contents.forEach((content) => {
|
|
if (content.text && content.text.trim()) {
|
|
for (let i = 0; i <= content.text.length; i++) {
|
|
const timer = setTimeout(() => {
|
|
const texts = stockTypewriterTexts.value.get(stockId);
|
|
if (texts) {
|
|
texts[content.key] = content.text.substring(0, i);
|
|
stockTypewriterTexts.value.set(stockId, { ...texts });
|
|
}
|
|
}, totalDelay + i * typeSpeed);
|
|
typewriterTimers.value.push(timer);
|
|
}
|
|
totalDelay += content.text.length * typeSpeed + 200; // 内容间间隔
|
|
}
|
|
});
|
|
|
|
totalDelay += 800; // 模块间间隔
|
|
});
|
|
|
|
// 添加免责声明的打字机效果(在所有模块显示完成后)
|
|
const disclaimerText = "该内容由AI生成,请注意甄别";
|
|
|
|
// 显示免责声明模块
|
|
const showDisclaimerTimer = setTimeout(() => {
|
|
const visibility = stockTypewriterVisibility.value.get(stockId);
|
|
if (visibility) {
|
|
visibility.disclaimer = true;
|
|
stockTypewriterVisibility.value.set(stockId, { ...visibility });
|
|
}
|
|
}, totalDelay);
|
|
typewriterTimers.value.push(showDisclaimerTimer);
|
|
totalDelay += 100;
|
|
|
|
// 打字机效果显示免责声明
|
|
for (let i = 0; i <= disclaimerText.length; i++) {
|
|
const timer = setTimeout(() => {
|
|
const texts = stockTypewriterTexts.value.get(stockId);
|
|
if (texts) {
|
|
texts.disclaimer = disclaimerText.substring(0, i);
|
|
stockTypewriterTexts.value.set(stockId, { ...texts });
|
|
}
|
|
// 在免责声明打字机效果完成后调用回调函数
|
|
if (i === disclaimerText.length) {
|
|
console.log("打字机效果完成,调用onComplete回调");
|
|
if (onComplete && typeof onComplete === "function") {
|
|
onComplete();
|
|
// 清除保存的回调函数
|
|
currentOnCompleteCallback.value = null;
|
|
}
|
|
}
|
|
}, totalDelay + i * typeSpeed);
|
|
typewriterTimers.value.push(timer);
|
|
}
|
|
}
|
|
|
|
// 清理定时器的函数
|
|
function clearTypewriterTimers() {
|
|
typewriterTimers.value.forEach((timer) => clearTimeout(timer));
|
|
typewriterTimers.value = [];
|
|
}
|
|
|
|
// 音频队列管理
|
|
const audioQueue = ref([]);
|
|
const isPlayingQueueAudio = ref(false);
|
|
let currentPlayIndex = 0;
|
|
let isCallingPlayNext = false;
|
|
|
|
// 音频队列顺序管理
|
|
const audioQueueOrder = {
|
|
one1_url: 1,
|
|
one2_url: 2,
|
|
two_url: 3,
|
|
three_url: 4,
|
|
four_url: 5,
|
|
url: 6,
|
|
audioUrl: 7,
|
|
voice_url: 8,
|
|
audio: 9,
|
|
tts_url: 10,
|
|
};
|
|
|
|
// 播放音频队列中的下一个音频
|
|
const playNextAudio = () => {
|
|
console.log("=== playNextAudio 被调用 ===");
|
|
console.log("当前队列状态:", {
|
|
queueLength: audioQueue.value.length,
|
|
queueItems: audioQueue.value.map((item) => item.name),
|
|
currentPlayIndex: currentPlayIndex,
|
|
isPlayingQueueAudio: isPlayingQueueAudio.value,
|
|
isCallingPlayNext: isCallingPlayNext,
|
|
audioStoreIsPlaying: emotionAudioStore.isPlaying,
|
|
});
|
|
|
|
if (
|
|
audioQueue.value.length === 0 ||
|
|
isPlayingQueueAudio.value ||
|
|
isCallingPlayNext
|
|
) {
|
|
console.log(
|
|
"❌ 播放条件不满足 - 队列长度:",
|
|
audioQueue.value.length,
|
|
"正在播放:",
|
|
isPlayingQueueAudio.value,
|
|
"正在调用:",
|
|
isCallingPlayNext
|
|
);
|
|
return;
|
|
}
|
|
|
|
// 检查是否已播放完所有音频
|
|
if (
|
|
currentPlayIndex >= audioQueue.value.length &&
|
|
audioQueue.value.length > 0
|
|
) {
|
|
console.log("🔄 所有音频播放完成,重置索引从第一个开始");
|
|
currentPlayIndex = 0;
|
|
isCallingPlayNext = false; // 重置调用标志
|
|
}
|
|
|
|
isCallingPlayNext = true;
|
|
isPlayingQueueAudio.value = true;
|
|
const audioInfo = audioQueue.value[currentPlayIndex];
|
|
|
|
console.log(
|
|
`✅ 开始播放${audioInfo.name}音频 (索引:${currentPlayIndex}),队列总长度:`,
|
|
audioQueue.value.length
|
|
);
|
|
|
|
// 停止之前的音频
|
|
if (emotionAudioStore.nowSound && emotionAudioStore.nowSound.playing()) {
|
|
emotionAudioStore.nowSound.stop();
|
|
}
|
|
|
|
// 创建新的音频实例
|
|
const audio = new Howl({
|
|
src: [audioInfo.url],
|
|
html5: true,
|
|
format: ["mp3", "wav"],
|
|
onplay: () => {
|
|
isAudioPlaying.value = true;
|
|
isPlayingQueueAudio.value = true;
|
|
emotionAudioStore.isPlaying = true;
|
|
console.log(`开始播放${audioInfo.name}音频`);
|
|
|
|
// 设置当前股票的音频播放状态为播放中
|
|
const currentStock = emotionStore.activeStock;
|
|
if (currentStock) {
|
|
setStockAudioState(currentStock, { isPlaying: true, isPaused: false });
|
|
console.log('设置当前股票音频状态为播放中:', currentStock.stockInfo?.name);
|
|
}
|
|
|
|
// 如果是第一个音频且需要启动打字机效果,则启动
|
|
if (
|
|
currentPlayIndex === 0 &&
|
|
audioInfo.shouldStartTypewriter &&
|
|
parsedConclusion.value
|
|
) {
|
|
console.log("🎬 第一个音频开始播放,同时启动打字机效果");
|
|
const stockId = currentStock?.stockInfo?.code || currentStock?.stockInfo?.symbol;
|
|
startTypewriterEffect(parsedConclusion.value, stockId, audioInfo.onComplete);
|
|
}
|
|
},
|
|
onpause: () => {
|
|
emotionAudioStore.isPlaying = false;
|
|
emotionAudioStore.isPaused = true;
|
|
// 保存当前播放位置
|
|
if (audio && audio.seek) {
|
|
emotionAudioStore.playbackPosition = audio.seek() || 0;
|
|
}
|
|
console.log(`${audioInfo.name}音频暂停播放,位置:`, emotionAudioStore.playbackPosition);
|
|
|
|
// 设置当前股票的音频播放状态为暂停
|
|
const currentStock = emotionStore.activeStock;
|
|
if (currentStock) {
|
|
setStockAudioState(currentStock, { isPlaying: false, isPaused: true });
|
|
console.log('设置当前股票音频状态为暂停:', currentStock.stockInfo?.name);
|
|
}
|
|
},
|
|
onresume: () => {
|
|
emotionAudioStore.isPlaying = true;
|
|
emotionAudioStore.isPaused = false;
|
|
// 如果有保存的播放位置,从该位置继续播放
|
|
if (emotionAudioStore.playbackPosition > 0 && audio && audio.seek) {
|
|
audio.seek(emotionAudioStore.playbackPosition);
|
|
}
|
|
console.log(`${audioInfo.name}音频继续播放,位置:`, emotionAudioStore.playbackPosition);
|
|
|
|
// 设置当前股票的音频播放状态为播放中
|
|
const currentStock = emotionStore.activeStock;
|
|
if (currentStock) {
|
|
setStockAudioState(currentStock, { isPlaying: true, isPaused: false });
|
|
console.log('设置当前股票音频状态为播放中:', currentStock.stockInfo?.name);
|
|
}
|
|
},
|
|
onend: () => {
|
|
console.log(`${audioInfo.name}音频播放完成,准备播放下一个`);
|
|
emotionAudioStore.isPaused = false;
|
|
emotionAudioStore.playbackPosition = 0;
|
|
|
|
// 移动到下一个音频索引
|
|
currentPlayIndex++;
|
|
|
|
// 确保只有在音频真正播放完成时才播放下一个
|
|
if (currentPlayIndex < audioQueue.value.length) {
|
|
console.log(
|
|
`队列中还有音频,500ms后播放下一个 (索引:${currentPlayIndex})`
|
|
);
|
|
// 重置调用标志,但保持播放状态,直到所有音频播放完成
|
|
setTimeout(() => {
|
|
isCallingPlayNext = false;
|
|
isPlayingQueueAudio.value = false; // 重置队列播放状态,允许播放下一个音频
|
|
// 保持emotionAudioStore.isPlaying为true,确保喇叭图片保持voice状态
|
|
emotionAudioStore.isPlaying = true;
|
|
// 设置下一个音频的URL,确保togglePlayPause能正确工作
|
|
const nextAudioInfo = audioQueue.value[currentPlayIndex];
|
|
if (nextAudioInfo && nextAudioInfo.url) {
|
|
emotionAudioStore.setCurrentAudioUrl(nextAudioInfo.url);
|
|
}
|
|
playNextAudio();
|
|
}, 500);
|
|
} else {
|
|
console.log("🎉 所有音频播放完成");
|
|
emotionAudioStore.nowSound = null;
|
|
isCallingPlayNext = false;
|
|
|
|
// 重置音频store的播放状态
|
|
emotionAudioStore.isPlaying = false;
|
|
emotionAudioStore.isPaused = false;
|
|
emotionAudioStore.playbackPosition = 0;
|
|
isAudioPlaying.value = false;
|
|
isPlayingQueueAudio.value = false;
|
|
|
|
// 重置当前股票的音频播放状态(所有音频播放完成后按钮应该变暗)
|
|
const currentStock = emotionStore.activeStock;
|
|
if (currentStock) {
|
|
console.log('所有音频播放完成,重置当前股票音频状态:', currentStock.stockInfo?.name);
|
|
setStockAudioState(currentStock, { isPlaying: false, isPaused: false });
|
|
}
|
|
|
|
// 调用完成回调(如果有的话)
|
|
if (
|
|
audioInfo.onComplete &&
|
|
typeof audioInfo.onComplete === "function"
|
|
) {
|
|
console.log("调用音频播放完成回调");
|
|
audioInfo.onComplete();
|
|
}
|
|
}
|
|
},
|
|
onstop: () => {
|
|
console.log(`${audioInfo.name}音频被停止`);
|
|
emotionAudioStore.isPlaying = false;
|
|
emotionAudioStore.isPaused = false;
|
|
emotionAudioStore.playbackPosition = 0;
|
|
isAudioPlaying.value = false;
|
|
isPlayingQueueAudio.value = false;
|
|
|
|
// 重置当前股票的音频播放状态(音频被停止时按钮应该变暗)
|
|
const currentStock = emotionStore.activeStock;
|
|
if (currentStock) {
|
|
console.log('音频被停止,重置当前股票音频状态:', currentStock.stockInfo?.name);
|
|
setStockAudioState(currentStock, { isPlaying: false, isPaused: false });
|
|
}
|
|
},
|
|
onerror: (error) => {
|
|
console.error(`${audioInfo.name}音频播放失败:`, error);
|
|
isAudioPlaying.value = false;
|
|
isPlayingQueueAudio.value = false;
|
|
isCallingPlayNext = false;
|
|
// 播放下一个音频
|
|
setTimeout(() => {
|
|
playNextAudio();
|
|
}, 100);
|
|
},
|
|
onload: () => {
|
|
emotionAudioStore.duration = audio.duration();
|
|
console.log(
|
|
`${audioInfo.name}音频加载完成,时长:`,
|
|
emotionAudioStore.duration
|
|
);
|
|
},
|
|
});
|
|
|
|
// 设置当前音频URL到store
|
|
emotionAudioStore.setCurrentAudioUrl(audioInfo.url);
|
|
emotionAudioStore.nowSound = audio;
|
|
emotionAudioStore.setAudioInstance(audio);
|
|
|
|
console.log(`尝试播放${audioInfo.name}音频`);
|
|
audio.play();
|
|
};
|
|
|
|
// 添加音频到播放队列
|
|
const addToAudioQueue = (
|
|
url,
|
|
name,
|
|
shouldStartTypewriter = false,
|
|
onComplete = null
|
|
) => {
|
|
console.log(`=== 添加音频到队列 ===`);
|
|
console.log("URL:", url);
|
|
console.log("Name:", name);
|
|
console.log("是否启动打字机效果:", shouldStartTypewriter);
|
|
console.log("音频启用状态:", emotionAudioStore.isVoiceEnabled);
|
|
|
|
if (url && emotionAudioStore.isVoiceEnabled) {
|
|
const audioItem = {
|
|
url,
|
|
name,
|
|
order: audioQueueOrder[name] || 999,
|
|
shouldStartTypewriter, // 添加打字机效果标志
|
|
onComplete, // 添加完成回调
|
|
};
|
|
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 (
|
|
!isPlayingQueueAudio.value &&
|
|
!emotionAudioStore.isPlaying &&
|
|
audioQueue.value.length === 1
|
|
) {
|
|
console.log("✅ 条件满足:没有音频在播放且这是第一个音频,立即开始播放");
|
|
playNextAudio();
|
|
} else {
|
|
console.log("⏳ 等待条件:", {
|
|
isPlayingQueueAudio: isPlayingQueueAudio.value,
|
|
audioStoreIsPlaying: emotionAudioStore.isPlaying,
|
|
queueLength: audioQueue.value.length,
|
|
reason:
|
|
audioQueue.value.length > 1 ? "队列中已有其他音频" : "有音频正在播放",
|
|
});
|
|
}
|
|
} else {
|
|
console.log("❌ 跳过添加音频:", {
|
|
hasUrl: !!url,
|
|
voiceEnabled: emotionAudioStore.isVoiceEnabled,
|
|
});
|
|
}
|
|
};
|
|
|
|
// 修改后的音频播放函数 - 支持多音频播放和同步打字机效果
|
|
function playAudioQueue(
|
|
conclusionData,
|
|
shouldStartTypewriter = false,
|
|
onComplete = null
|
|
) {
|
|
if (!conclusionData) {
|
|
console.log("没有结论数据,跳过播放");
|
|
return;
|
|
}
|
|
|
|
// 检查是否启用了语音功能
|
|
console.log("语音功能状态:", emotionAudioStore.isVoiceEnabled);
|
|
if (!emotionAudioStore.isVoiceEnabled) {
|
|
console.log("语音功能已关闭,跳过播放");
|
|
return;
|
|
}
|
|
|
|
console.log(
|
|
"开始处理多音频播放...",
|
|
shouldStartTypewriter ? "同时启动打字机效果" : ""
|
|
);
|
|
|
|
try {
|
|
// 解析结论数据
|
|
const conclusion =
|
|
typeof conclusionData === "object"
|
|
? conclusionData
|
|
: JSON.parse(conclusionData);
|
|
|
|
// 清空之前的音频队列
|
|
audioQueue.value = [];
|
|
currentPlayIndex = 0;
|
|
isCallingPlayNext = false;
|
|
isPlayingQueueAudio.value = false;
|
|
|
|
// 按优先级顺序检查并添加所有可用的音频URL
|
|
const audioSources = [
|
|
{ key: "one1_url", name: "one1_url" },
|
|
{ key: "one2_url", name: "one2_url" },
|
|
{ key: "two_url", name: "two_url" },
|
|
{ key: "three_url", name: "three_url" },
|
|
{ key: "four_url", name: "four_url" },
|
|
{ key: "url", name: "url" },
|
|
{ key: "audioUrl", name: "audioUrl" },
|
|
{ key: "voice_url", name: "voice_url" },
|
|
{ key: "audio", name: "audio" },
|
|
{ key: "tts_url", name: "tts_url" },
|
|
];
|
|
|
|
audioSources.forEach((source) => {
|
|
if (conclusion[source.key]) {
|
|
const voiceUrl = conclusion[source.key]
|
|
.toString()
|
|
.trim()
|
|
.replace(/[`\s]/g, "");
|
|
if (voiceUrl && voiceUrl.startsWith("http")) {
|
|
console.log(`找到音频URL: ${source.name} = ${voiceUrl}`);
|
|
addToAudioQueue(
|
|
voiceUrl,
|
|
source.name,
|
|
shouldStartTypewriter && audioQueue.value.length === 0,
|
|
onComplete
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (audioQueue.value.length === 0) {
|
|
console.log("未找到有效的音频URL");
|
|
// 如果没有音频但需要启动打字机效果,直接启动
|
|
if (shouldStartTypewriter) {
|
|
console.log("没有音频但需要启动打字机效果");
|
|
const currentStock = emotionStore.activeStock;
|
|
const stockId = currentStock?.stockInfo?.code || currentStock?.stockInfo?.symbol;
|
|
startTypewriterEffect(conclusion, stockId, onComplete);
|
|
}
|
|
} else {
|
|
console.log(`总共找到 ${audioQueue.value.length} 个音频,准备播放`);
|
|
|
|
// 设置当前股票的音频播放状态为播放中(自动播放时按钮应该是亮的)
|
|
const currentStock = emotionStore.activeStock;
|
|
if (currentStock) {
|
|
console.log('设置当前股票音频状态为播放中:', currentStock.stockInfo?.name);
|
|
setStockAudioState(currentStock, { isPlaying: true, isPaused: false });
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("处理音频播放失败:", error);
|
|
}
|
|
}
|
|
|
|
// 原有的单音频播放函数(保持兼容性)
|
|
function playAudio(url) {
|
|
console.log("尝试播放音频:", url);
|
|
|
|
if (!url) {
|
|
console.warn("音频URL为空,跳过播放");
|
|
isAudioPlaying.value = false;
|
|
return;
|
|
}
|
|
|
|
// 检查是否启用了语音功能
|
|
console.log("语音功能状态:", emotionAudioStore.isVoiceEnabled);
|
|
if (!emotionAudioStore.isVoiceEnabled) {
|
|
console.log("语音功能已关闭,跳过播放");
|
|
return;
|
|
}
|
|
|
|
console.log("开始创建音频实例...");
|
|
|
|
try {
|
|
// 设置当前音频URL
|
|
emotionAudioStore.setCurrentAudioUrl(url);
|
|
|
|
// 停止之前的音频
|
|
if (emotionAudioStore.nowSound && emotionAudioStore.nowSound.playing()) {
|
|
emotionAudioStore.nowSound.stop();
|
|
}
|
|
|
|
// 创建新的音频实例
|
|
const newSound = new Howl({
|
|
src: [url],
|
|
html5: true,
|
|
format: ["mp3", "wav"],
|
|
onplay: () => {
|
|
isAudioPlaying.value = true;
|
|
emotionAudioStore.isPlaying = true;
|
|
console.log("开始播放场景应用语音");
|
|
|
|
// 设置当前股票的音频播放状态为播放中
|
|
const currentStock = emotionStore.activeStock;
|
|
if (currentStock) {
|
|
setStockAudioState(currentStock, { isPlaying: true, isPaused: false });
|
|
console.log('设置当前股票音频状态为播放中:', currentStock.stockInfo?.name);
|
|
}
|
|
// 音频开始播放时的自动滚动已移除
|
|
},
|
|
onend: () => {
|
|
isAudioPlaying.value = false;
|
|
emotionAudioStore.isPlaying = false;
|
|
emotionAudioStore.isPaused = false;
|
|
emotionAudioStore.playbackPosition = 0;
|
|
console.log("场景应用语音播放结束");
|
|
|
|
// 重置当前股票的音频播放状态
|
|
const currentStock = emotionStore.activeStock;
|
|
if (currentStock) {
|
|
setStockAudioState(currentStock, { isPlaying: false, isPaused: false });
|
|
console.log('重置当前股票音频状态:', currentStock.stockInfo?.name);
|
|
}
|
|
},
|
|
onstop: () => {
|
|
isAudioPlaying.value = false;
|
|
emotionAudioStore.isPlaying = false;
|
|
console.log("场景应用语音播放停止");
|
|
|
|
// 重置当前股票的音频播放状态
|
|
const currentStock = emotionStore.activeStock;
|
|
if (currentStock) {
|
|
setStockAudioState(currentStock, { isPlaying: false, isPaused: false });
|
|
console.log('重置当前股票音频状态:', currentStock.stockInfo?.name);
|
|
}
|
|
},
|
|
onpause: () => {
|
|
isAudioPlaying.value = false;
|
|
emotionAudioStore.isPlaying = false;
|
|
console.log("场景应用语音播放暂停");
|
|
|
|
// 设置当前股票的音频播放状态为暂停
|
|
const currentStock = emotionStore.activeStock;
|
|
if (currentStock) {
|
|
setStockAudioState(currentStock, { isPlaying: false, isPaused: true });
|
|
console.log('设置当前股票音频状态为暂停:', currentStock.stockInfo?.name);
|
|
}
|
|
},
|
|
onerror: (error) => {
|
|
isAudioPlaying.value = false;
|
|
emotionAudioStore.isPlaying = false;
|
|
console.error("音频播放错误:", error);
|
|
},
|
|
onload: () => {
|
|
// 音频加载完成,获取时长
|
|
emotionAudioStore.duration = newSound.duration();
|
|
console.log("音频加载完成,时长:", emotionAudioStore.duration);
|
|
},
|
|
});
|
|
|
|
// 保存音频实例到store
|
|
emotionAudioStore.nowSound = newSound;
|
|
emotionAudioStore.setAudioInstance(newSound);
|
|
|
|
// 播放音频
|
|
newSound.play();
|
|
} catch (error) {
|
|
console.error("创建音频实例失败:", error);
|
|
isAudioPlaying.value = false;
|
|
}
|
|
}
|
|
|
|
// 停止音频播放
|
|
function stopAudio() {
|
|
if (emotionAudioStore.nowSound) {
|
|
emotionAudioStore.nowSound.stop();
|
|
}
|
|
isAudioPlaying.value = false;
|
|
}
|
|
// 触发图片旋转的方法
|
|
function startImageRotation() {
|
|
isRotating.value = true;
|
|
// 如果你想在一段时间后停止旋转,可以添加以下代码
|
|
setTimeout(() => {
|
|
isRotating.value = false;
|
|
}, 5000); // 5 秒后停止旋转
|
|
}
|
|
|
|
// 显示思考过程
|
|
async function showThinkingProcess(stockName = null) {
|
|
// 第一步:正在思考
|
|
const thinkingMessage1 = reactive({
|
|
sender: "ai",
|
|
text: "AI情绪大模型正在思考",
|
|
gif: thinkingGif,
|
|
flag: true, // 添加flag属性以显示动态加载点
|
|
});
|
|
messages.value.push(thinkingMessage1);
|
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
messages.value.pop();
|
|
|
|
// 第二步:正在解析关键数据(持续显示直到获取到股票名称)
|
|
const thinkingMessage2 = reactive({
|
|
sender: "ai",
|
|
text: "正在解析关键数据",
|
|
gif: analyzeGif,
|
|
flag: true, // 添加flag属性以显示动态加载点
|
|
});
|
|
messages.value.push(thinkingMessage2);
|
|
|
|
// 如果没有股票名称,保持第二步显示
|
|
if (!stockName) {
|
|
return thinkingMessage2; // 返回消息引用,以便后续更新
|
|
}
|
|
|
|
// 有股票名称后,继续后续步骤
|
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
messages.value.pop();
|
|
|
|
// 第三步:生成具体股票的量子四维矩阵图
|
|
const thinkingMessage3 = reactive({
|
|
sender: "ai",
|
|
text: `正在生成${stockName}量子四维矩阵图`,
|
|
gif: generateGif,
|
|
flag: true, // 添加flag属性以显示动态加载点
|
|
});
|
|
messages.value.push(thinkingMessage3);
|
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
messages.value.pop();
|
|
|
|
// 第四步:报告已生成
|
|
const thinkingMessage4 = reactive({ sender: "ai", text: "报告已生成!" });
|
|
messages.value.push(thinkingMessage4);
|
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
messages.value.pop();
|
|
|
|
return null;
|
|
}
|
|
|
|
// 继续思考过程(当获取到股票名称后调用)
|
|
async function continueThinkingProcess(thinkingMessageRef, stockName) {
|
|
if (!thinkingMessageRef || !stockName) return;
|
|
|
|
// 等待一段时间后继续
|
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
|
|
// 移除第二步消息
|
|
const index = messages.value.indexOf(thinkingMessageRef);
|
|
if (index > -1) {
|
|
messages.value.splice(index, 1);
|
|
}
|
|
|
|
// 第三步:生成具体股票的量子四维矩阵图
|
|
const thinkingMessage3 = reactive({
|
|
sender: "ai",
|
|
text: `正在生成${stockName}量子四维矩阵图`,
|
|
gif: generateGif,
|
|
flag: true, // 添加flag属性以显示动态加载点
|
|
});
|
|
messages.value.push(thinkingMessage3);
|
|
|
|
// 返回第三步消息的引用,以便后续处理
|
|
return thinkingMessage3;
|
|
}
|
|
|
|
// 完成思考过程(当第二个工作流接口成功后调用)
|
|
async function finishThinkingProcess(thinkingMessage3Ref) {
|
|
if (!thinkingMessage3Ref) return;
|
|
|
|
// 等待一段时间
|
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
|
|
// 移除第三步消息
|
|
const index = messages.value.indexOf(thinkingMessage3Ref);
|
|
if (index > -1) {
|
|
messages.value.splice(index, 1);
|
|
}
|
|
|
|
// 第四步:报告已生成
|
|
const thinkingMessage4 = reactive({ sender: "ai", text: "报告已生成!" });
|
|
messages.value.push(thinkingMessage4);
|
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
messages.value.pop();
|
|
}
|
|
|
|
// 发送消息方法
|
|
async function handleSendMessage(input, onComplete) {
|
|
console.log("发送内容:", input);
|
|
// 标记为用户主动搜索
|
|
isUserInitiated.value = true;
|
|
// 重置历史记录模式状态,确保正常对话时显示conversation-area
|
|
isHistoryMode.value = false;
|
|
|
|
// 检查用户输入内容是否为空
|
|
if (!input || !input.trim()) {
|
|
ElMessage.warning("输入内容不能为空");
|
|
// 调用完成回调,重新启用输入框
|
|
if (onComplete && typeof onComplete === "function") {
|
|
onComplete();
|
|
// 清除保存的回调函数
|
|
currentOnCompleteCallback.value = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 用户输入不为空,立即清空页面内容并触发图片旋转逻辑
|
|
isPageLoaded.value = false; // 立即隐藏页面内容
|
|
isRotating.value = true;
|
|
const previousMessages = [...messages.value]; // 保存历史消息
|
|
messages.value = []; // 清空历史数据
|
|
|
|
// 添加用户消息(只添加一次)
|
|
const userMessage = reactive({ sender: "user", text: input });
|
|
messages.value.push(userMessage);
|
|
|
|
// 将用户消息添加到emotion store中
|
|
emotionStore.addConversation({
|
|
sender: "user",
|
|
text: input,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
// 检查用户剩余次数
|
|
await chatStore.getUserCount(); // 获取最新的用户次数
|
|
if (chatStore.UserCount <= 0) {
|
|
const aiMessage = reactive({
|
|
sender: "ai",
|
|
text: "您的剩余次数为0,无法使用情绪大模型,请联系客服或购买服务包。",
|
|
});
|
|
messages.value.push(aiMessage);
|
|
|
|
// 将AI消息添加到emotion store中
|
|
emotionStore.addConversation({
|
|
sender: "ai",
|
|
text: "您的剩余次数为0,无法使用情绪大模型,请联系客服或购买服务包。",
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
// 停止图片旋转,恢复历史数据
|
|
isRotating.value = false;
|
|
messages.value = [...previousMessages, ...messages.value];
|
|
// 调用完成回调,重新启用输入框
|
|
if (onComplete && typeof onComplete === "function") {
|
|
onComplete();
|
|
// 清除保存的回调函数
|
|
currentOnCompleteCallback.value = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 开始思考过程(不带股票名称)
|
|
const thinkingMessageRef = await showThinkingProcess();
|
|
let thinkingMessage3Ref = null;
|
|
|
|
try {
|
|
// 第一步:调用第一个接口验证用户输入内容是否合法
|
|
// const params = {
|
|
// content: userMessage.text,
|
|
// userData: {
|
|
// token: localStorage.getItem('localToken'),
|
|
// language: "cn",
|
|
// brainPrivilegeState: '1',
|
|
// swordPrivilegeState: '1',
|
|
// stockForecastPrivilegeState: '1',
|
|
// spaceForecastPrivilegeState: '1',
|
|
// aibullPrivilegeState: '1',
|
|
// aigoldBullPrivilegeState: '1',
|
|
// airadarPrivilegeState: '1',
|
|
// marketList: "hk,cn,usa,my,sg,vi,in,gb",
|
|
// },
|
|
// };
|
|
|
|
const result = await getReplyAPI({
|
|
token: localStorage.getItem("localToken"),
|
|
language: "cn",
|
|
marketList: "hk,cn,usa,my,sg,vi,in,gb",
|
|
content: userMessage.text,
|
|
});
|
|
const response = result;
|
|
const parsedData = response.data;
|
|
console.log("第一个接口返回的完整数据:", parsedData);
|
|
// 检查用户输入是否合法
|
|
if (!parsedData || !parsedData.market || !parsedData.code) {
|
|
// 输入不合法,先清理思考过程消息
|
|
if (thinkingMessageRef) {
|
|
const index = messages.value.indexOf(thinkingMessageRef);
|
|
if (index > -1) {
|
|
messages.value.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
// 关闭加载状态和等待提示,返回refuse信息,停止图片旋转,恢复历史数据
|
|
// isLoading.value = false;
|
|
isPageLoaded.value = false;
|
|
const refuseMessage = response && response.msg ? response.msg : "接口返回数据异常,请重试";
|
|
const aiMessage = reactive({
|
|
sender: "ai",
|
|
text: processRefuseMessage(refuseMessage),
|
|
});
|
|
messages.value.push(aiMessage);
|
|
|
|
// 将AI消息添加到emotion store中
|
|
emotionStore.addConversation({
|
|
sender: "ai",
|
|
text: processRefuseMessage(refuseMessage),
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
isRotating.value = false;
|
|
messages.value = [...previousMessages, ...messages.value];
|
|
// 调用完成回调,重新启用输入框
|
|
if (onComplete && typeof onComplete === "function") {
|
|
onComplete();
|
|
// 清除保存的回调函数
|
|
currentOnCompleteCallback.value = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 输入合法,继续执行后续处理
|
|
// 获取到股票名称后,继续思考过程
|
|
if (thinkingMessageRef && parsedData.name) {
|
|
thinkingMessage3Ref = await continueThinkingProcess(
|
|
thinkingMessageRef,
|
|
parsedData.name
|
|
);
|
|
}
|
|
// 设置加载状态,隐藏图表页面
|
|
// isLoading.value = true;
|
|
|
|
isPageLoaded.value = false;
|
|
|
|
// 调用第二个工作流接口
|
|
const conclusionParams = {
|
|
recordId: parsedData.recordId,
|
|
parentId: parsedData.parentId,
|
|
stockId: parsedData.stockId,
|
|
token: localStorage.getItem("localToken"),
|
|
language: "cn",
|
|
};
|
|
|
|
console.log("第二个接口参数:", conclusionParams);
|
|
// 同时调用第二个数据流接口和fetchData方法
|
|
const [conclusionResult, fetchDataResult] = await Promise.all([
|
|
getConclusionAPI(conclusionParams),
|
|
fetchData(
|
|
parsedData.code,
|
|
parsedData.market,
|
|
parsedData.name || "未知股票",
|
|
input.trim(),
|
|
parsedData.stockId
|
|
),
|
|
]);
|
|
|
|
// 处理结论接口返回的数据
|
|
const conclusionResponse = conclusionResult;
|
|
// 检查所有数据是否都加载成功
|
|
if (conclusionResponse && conclusionResponse.data && fetchDataResult) {
|
|
// 第二个工作流接口成功,完成思考过程
|
|
if (thinkingMessage3Ref) {
|
|
await finishThinkingProcess(thinkingMessage3Ref);
|
|
}
|
|
|
|
// 将结论数据存储到响应式变量和store中
|
|
conclusionData.value = conclusionResponse.data;
|
|
console.log("第二个接口返回的完整数据结构:", conclusionResponse.data);
|
|
// 将结论数据存储到store中的当前激活股票
|
|
emotionStore.updateActiveStockConclusion(conclusionResponse.data);
|
|
|
|
// 所有数据加载完成,关闭加载状态,显示页面
|
|
// isLoading.value = false;
|
|
isPageLoaded.value = true;
|
|
|
|
// 使用nextTick确保DOM更新后清空对话显示并启动高度监听器
|
|
nextTick(() => {
|
|
messages.value = [];
|
|
// 启动页面高度监听器,实时监听内容变化并自动滚动
|
|
startHeightObserver();
|
|
// 立即滚动到底部
|
|
scrollToBottom();
|
|
});
|
|
|
|
// 数据获取成功后,重新获取用户次数以实现实时更新
|
|
try {
|
|
await chatStore.getUserCount();
|
|
console.log("数据获取成功后,用户次数已更新");
|
|
} catch (error) {
|
|
console.error("更新用户次数失败:", error);
|
|
}
|
|
|
|
// 重新调用获取历史记录的接口
|
|
try {
|
|
chatStore.searchRecord = true;
|
|
console.log("getConclusionAPI成功后,已触发历史记录更新");
|
|
} catch (error) {
|
|
console.error("触发历史记录更新失败:", error);
|
|
}
|
|
|
|
// 确保页面状态更新后触发图表渲染和音频文本
|
|
nextTick(() => {
|
|
if (currentStock.value && currentStock.value.apiData) {
|
|
renderCharts(currentStock.value.apiData);
|
|
|
|
// 只有在用户主动搜索时才自动触发音频和文本
|
|
if (
|
|
isUserInitiated.value &&
|
|
parsedConclusion.value &&
|
|
audioUrl.value
|
|
) {
|
|
const stockCode =
|
|
currentStock.value.stockInfo?.code ||
|
|
currentStock.value.stockInfo?.symbol;
|
|
if (stockCode && !stockTypewriterShown.value.has(stockCode)) {
|
|
if (!stockAudioPlayed.value.has(stockCode)) {
|
|
stockAudioPlayed.value.set(stockCode, true);
|
|
playAudioQueue(parsedConclusion.value, true, onComplete);
|
|
} else {
|
|
// 如果音频已播放过,只启动打字机效果
|
|
const stockCode = currentStock.value?.stockInfo?.code || currentStock.value?.stockInfo?.symbol;
|
|
startTypewriterEffect(parsedConclusion.value, stockCode, onComplete);
|
|
}
|
|
stockTypewriterShown.value.set(stockCode, true);
|
|
} else {
|
|
// 如果不需要打字机效果,直接调用完成回调
|
|
if (onComplete && typeof onComplete === "function") {
|
|
onComplete();
|
|
// 清除保存的回调函数
|
|
currentOnCompleteCallback.value = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 重置用户主动搜索标志
|
|
isUserInitiated.value = false;
|
|
}
|
|
});
|
|
} else {
|
|
// 数据加载失败,清理第三步思考过程消息
|
|
if (thinkingMessage3Ref) {
|
|
const index = messages.value.indexOf(thinkingMessage3Ref);
|
|
if (index > -1) {
|
|
messages.value.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
// 数据加载失败,停止图片旋转,恢复历史数据
|
|
// isLoading.value = false;
|
|
// 如果 fetchDataResult 为 false,说明数据不完整的错误信息已经在 fetchData 中添加到 messages
|
|
// 只有在 conclusionResponse 有问题时才添加通用错误信息
|
|
if (!conclusionResponse || !conclusionResponse.data) {
|
|
const aiMessage = reactive({
|
|
sender: "ai",
|
|
text: "网络加载失败,请重试",
|
|
});
|
|
messages.value.push(aiMessage);
|
|
}
|
|
isRotating.value = false;
|
|
messages.value = [...previousMessages, ...messages.value];
|
|
|
|
// 如果有之前的股票数据且页面已加载,重新渲染图表
|
|
if (
|
|
isPageLoaded.value &&
|
|
emotionStore.activeStock &&
|
|
emotionStore.activeStock.apiData
|
|
) {
|
|
nextTick(() => {
|
|
renderCharts(emotionStore.activeStock.apiData);
|
|
console.log(
|
|
"搜索失败,恢复显示之前股票的图表:",
|
|
emotionStore.activeStock.stockInfo.name
|
|
);
|
|
});
|
|
}
|
|
|
|
// 调用完成回调,重新启用输入框
|
|
if (onComplete && typeof onComplete === "function") {
|
|
onComplete();
|
|
// 清除保存的回调函数
|
|
currentOnCompleteCallback.value = null;
|
|
}
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
// 请求失败,清理第三步思考过程消息
|
|
if (thinkingMessage3Ref) {
|
|
const index = messages.value.indexOf(thinkingMessage3Ref);
|
|
if (index > -1) {
|
|
messages.value.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
// 请求失败时关闭加载状态
|
|
// isLoading.value = false;
|
|
|
|
// 如果有之前的股票数据,恢复显示状态;否则设置为false
|
|
if (emotionStore.stockList.length > 0 && emotionStore.activeStock) {
|
|
isPageLoaded.value = true;
|
|
// 使用nextTick确保DOM更新后清空对话显示并启动高度监听器
|
|
nextTick(() => {
|
|
messages.value = [];
|
|
// 启动页面高度监听器,实时监听内容变化并自动滚动
|
|
startHeightObserver();
|
|
// 立即滚动到底部
|
|
scrollToBottom();
|
|
});
|
|
console.log("请求工作流接口失败,但恢复显示之前的股票数据");
|
|
// 立即渲染之前股票的图表,提升用户体验
|
|
nextTick(() => {
|
|
renderCharts(emotionStore.activeStock.apiData);
|
|
console.log(
|
|
"立即恢复显示之前股票的图表:",
|
|
emotionStore.activeStock.stockInfo.name
|
|
);
|
|
});
|
|
} else {
|
|
isPageLoaded.value = false;
|
|
}
|
|
|
|
// 请求失败时停止图片旋转,恢复历史数据
|
|
isRotating.value = false;
|
|
messages.value = [...previousMessages, ...messages.value];
|
|
|
|
// 如果有之前的股票数据且页面已加载,重新渲染图表
|
|
if (
|
|
isPageLoaded.value &&
|
|
emotionStore.activeStock &&
|
|
emotionStore.activeStock.apiData
|
|
) {
|
|
nextTick(() => {
|
|
renderCharts(emotionStore.activeStock.apiData);
|
|
console.log(
|
|
"请求失败,恢复显示之前股票的图表:",
|
|
emotionStore.activeStock.stockInfo.name
|
|
);
|
|
});
|
|
}
|
|
|
|
// 调用完成回调,重新启用输入框
|
|
if (onComplete && typeof onComplete === "function") {
|
|
onComplete();
|
|
// 清除保存的回调函数
|
|
currentOnCompleteCallback.value = null;
|
|
}
|
|
return;
|
|
} finally {
|
|
// 停止图片旋转(只有在设置了旋转状态时才需要停止)
|
|
if (isRotating.value) {
|
|
isRotating.value = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 请求数据接口
|
|
async function fetchData(code, market, stockName, queryText, stockId) {
|
|
try {
|
|
const stockDataParams = {
|
|
stockId: stockId,
|
|
};
|
|
const stockDataResult = await axios.post(
|
|
// "http://39.101.133.168:8828/link/api/aiEmotion/client/getAiEmotionData",
|
|
`${APIurl}/api/workflow/getStockData`,
|
|
stockDataParams,
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
}
|
|
);
|
|
|
|
const stockDataResponse = stockDataResult.data; // 获取返回所有的数据
|
|
|
|
if (stockDataResponse.code === 200 && stockDataResponse.data) {
|
|
// 检查关键数据字段是否完整
|
|
const validation = validateRequiredFields(stockDataResponse.data);
|
|
|
|
// 如果有关键数据缺失,返回失败,不添加到StockTabs
|
|
if (!validation.isValid) {
|
|
console.log("API返回数据不完整,缺失字段:", validation.missingFields);
|
|
// 关闭加载状态
|
|
// isLoading.value = false;
|
|
|
|
// 如果有之前的股票数据,恢复显示状态;否则设置为false
|
|
if (emotionStore.stockList.length > 0 && emotionStore.activeStock) {
|
|
isPageLoaded.value = true;
|
|
// 使用nextTick确保DOM更新后清空对话显示并启动高度监听器
|
|
nextTick(() => {
|
|
messages.value = [];
|
|
// 启动页面高度监听器,实时监听内容变化并自动滚动
|
|
startHeightObserver();
|
|
// 立即滚动到底部
|
|
scrollToBottom();
|
|
});
|
|
console.log("数据验证失败,但恢复显示之前的股票数据");
|
|
// 立即渲染之前股票的图表,提升用户体验
|
|
nextTick(() => {
|
|
renderCharts(emotionStore.activeStock.apiData);
|
|
console.log(
|
|
"立即恢复显示之前股票的图表:",
|
|
emotionStore.activeStock.stockInfo.name
|
|
);
|
|
});
|
|
} else {
|
|
isPageLoaded.value = false;
|
|
}
|
|
|
|
const aiMessage = reactive({
|
|
sender: "ai",
|
|
text: `数据丢失了,请稍后重试。`,
|
|
});
|
|
messages.value.push(aiMessage);
|
|
|
|
// 将AI消息添加到emotion store中
|
|
emotionStore.addConversation({
|
|
sender: "ai",
|
|
text: "数据丢失了,请稍后重试。",
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
return false; // 返回失败标识,不添加股票到标签
|
|
}
|
|
|
|
// 只有数据完整时才创建股票数据对象并添加到store
|
|
const stockData = {
|
|
queryText: queryText,
|
|
stockInfo: {
|
|
name: stockName,
|
|
code: code,
|
|
market: market,
|
|
},
|
|
apiData: stockDataResponse.data,
|
|
conclusionData: conclusionData.value, // 包含结论数据
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
// 将股票数据添加到store中,显示在StockTabs中
|
|
emotionStore.addStock(stockData);
|
|
return true; // 返回成功标识
|
|
} else {
|
|
// 关闭加载状态
|
|
// isLoading.value = false;
|
|
|
|
// 如果有之前的股票数据,恢复显示状态;否则设置为false
|
|
if (emotionStore.stockList.length > 0 && emotionStore.activeStock) {
|
|
isPageLoaded.value = true;
|
|
// 使用nextTick确保DOM更新后清空对话显示并启动高度监听器
|
|
nextTick(() => {
|
|
messages.value = [];
|
|
// 启动页面高度监听器,实时监听内容变化并自动滚动
|
|
startHeightObserver();
|
|
// 立即滚动到底部
|
|
scrollToBottom();
|
|
});
|
|
console.log("API请求失败,但恢复显示之前的股票数据");
|
|
// 立即渲染之前股票的图表,提升用户体验
|
|
nextTick(() => {
|
|
renderCharts(emotionStore.activeStock.apiData);
|
|
console.log(
|
|
"立即恢复显示之前股票的图表:",
|
|
emotionStore.activeStock.stockInfo.name
|
|
);
|
|
});
|
|
} else {
|
|
isPageLoaded.value = false;
|
|
}
|
|
|
|
const aiMessage = reactive({
|
|
sender: "ai",
|
|
text: "图表数据请求失败,请检查网络连接",
|
|
});
|
|
messages.value.push(aiMessage);
|
|
|
|
// 将AI消息添加到emotion store中
|
|
emotionStore.addConversation({
|
|
sender: "ai",
|
|
text: "图表数据请求失败,请检查网络连接",
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
return false; // 返回失败标识
|
|
}
|
|
} catch (error) {
|
|
// 关闭加载状态
|
|
// isLoading.value = false;
|
|
|
|
// 如果有之前的股票数据,恢复显示状态;否则设置为false
|
|
if (emotionStore.stockList.length > 0 && emotionStore.activeStock) {
|
|
isPageLoaded.value = true;
|
|
// 使用nextTick确保DOM更新后清空对话显示并启动高度监听器
|
|
nextTick(() => {
|
|
messages.value = [];
|
|
// 启动页面高度监听器,实时监听内容变化并自动滚动
|
|
startHeightObserver();
|
|
// 立即滚动到底部
|
|
scrollToBottom();
|
|
});
|
|
console.log("网络异常,但恢复显示之前的股票数据");
|
|
// 立即渲染之前股票的图表,提升用户体验
|
|
nextTick(() => {
|
|
renderCharts(emotionStore.activeStock.apiData);
|
|
console.log(
|
|
"立即恢复显示之前股票的图表:",
|
|
emotionStore.activeStock.stockInfo.name
|
|
);
|
|
});
|
|
} else {
|
|
isPageLoaded.value = false;
|
|
}
|
|
|
|
const aiMessage = reactive({
|
|
sender: "ai",
|
|
text: "图表数据请求失败,请检查网络连接",
|
|
});
|
|
messages.value.push(aiMessage);
|
|
|
|
// 将AI消息添加到emotion store中
|
|
emotionStore.addConversation({
|
|
sender: "ai",
|
|
text: "图表数据请求失败,请检查网络连接",
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
return false; // 返回失败标识
|
|
}
|
|
}
|
|
|
|
// 检查关键数据字段是否完整的函数
|
|
function validateRequiredFields(data) {
|
|
const requiredFields = ["GSWDJ", "KLine20", "QXJMQ", "QXTDLD", "WDRL"];
|
|
const missingFields = [];
|
|
|
|
for (const field of requiredFields) {
|
|
if (
|
|
!data[field] ||
|
|
(Array.isArray(data[field]) && data[field].length === 0) ||
|
|
(typeof data[field] === "object" && !hasValidData(data[field]))
|
|
) {
|
|
missingFields.push(field);
|
|
}
|
|
}
|
|
|
|
return {
|
|
isValid: missingFields.length === 0,
|
|
missingFields: missingFields,
|
|
};
|
|
}
|
|
|
|
// 检查对象是否包含有效数据的辅助函数
|
|
function hasValidData(obj) {
|
|
if (!obj || typeof obj !== "object") {
|
|
return false;
|
|
}
|
|
|
|
// 定义可以为空的数组字段
|
|
const allowedEmptyArrays = ["lowxh", "qixh", "topxh"];
|
|
|
|
// 检查对象的所有属性值
|
|
for (const key in obj) {
|
|
if (obj.hasOwnProperty(key)) {
|
|
const value = obj[key];
|
|
|
|
// 如果是字符串字段
|
|
if (typeof value === "string") {
|
|
// 字符串字段必须有内容,为空则表示异常
|
|
if (value.trim() !== "") {
|
|
return true;
|
|
}
|
|
}
|
|
// 如果是数组字段
|
|
else if (Array.isArray(value)) {
|
|
// 数组字段可以为空,但如果有内容则表示有效
|
|
if (value.length > 0) {
|
|
return true;
|
|
}
|
|
}
|
|
// 如果是数字且不为0
|
|
else if (typeof value === "number" && value !== 0) {
|
|
return true;
|
|
}
|
|
// 如果是布尔值且为true
|
|
else if (typeof value === "boolean" && value === true) {
|
|
return true;
|
|
}
|
|
// 如果是对象且包含有效数据(递归检查)
|
|
else if (typeof value === "object" && value !== null) {
|
|
if (hasValidData(value)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// 依次渲染图表的方法 - 支持多个股票
|
|
async function renderChartsSequentially(clonedData, stockIndex = 0) {
|
|
console.log(`开始渲染第${stockIndex}个股票的图表`);
|
|
|
|
// 定义图表渲染顺序和配置
|
|
const chartConfigs = [
|
|
{
|
|
name: "股市温度计",
|
|
ref: marketTemperatureRef.value[stockIndex],
|
|
visibility: chartVisibility.value.marketTemperature,
|
|
method: "initChart",
|
|
params: [clonedData.GSWDJ, clonedData.KLine20, clonedData.WDRL],
|
|
},
|
|
{
|
|
name: "情绪解码器",
|
|
ref: emotionDecodRef.value[stockIndex],
|
|
visibility: chartVisibility.value.emotionDecod,
|
|
method: "initQXNLZHEcharts",
|
|
params: [clonedData.KLine20, clonedData.QXJMQ],
|
|
},
|
|
{
|
|
name: "情绪探底雷达",
|
|
ref: emotionalBottomRadarRef.value[stockIndex],
|
|
visibility: chartVisibility.value.emotionalBottomRadar,
|
|
method: "initEmotionalBottomRadar",
|
|
params: [clonedData.KLine20, clonedData.QXTDLD],
|
|
},
|
|
{
|
|
name: "情绪能量转化器",
|
|
ref: emoEnergyConverterRef.value[stockIndex],
|
|
visibility: chartVisibility.value.emoEnergyConverter,
|
|
method: "initQXNLZHEcharts",
|
|
params: [clonedData.KLine20, clonedData.QXNLZHQ],
|
|
},
|
|
];
|
|
|
|
// 依次渲染每个图表
|
|
for (const config of chartConfigs) {
|
|
if (config.ref && config.visibility) {
|
|
console.log(`开始渲染第${stockIndex}个股票的${config.name}图表`);
|
|
console.log(`${config.name}Ref方法:`, typeof config.ref[config.method]);
|
|
|
|
if (typeof config.ref[config.method] === "function") {
|
|
try {
|
|
// 等待DOM元素完全渲染
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
config.ref[config.method](...config.params);
|
|
console.log(`第${stockIndex}个股票的${config.name}图表渲染成功`);
|
|
|
|
// 每个图表渲染完成后等待一段时间再渲染下一个
|
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
} catch (error) {
|
|
console.error(
|
|
`第${stockIndex}个股票的${config.name}图表渲染失败:`,
|
|
error
|
|
);
|
|
}
|
|
} else {
|
|
console.error(
|
|
`第${stockIndex}个股票的${config.name}Ref.${config.method} 方法不存在`
|
|
);
|
|
}
|
|
} else {
|
|
console.log(
|
|
`第${stockIndex}个股票的${config.name}图表未渲染,ref存在:`,
|
|
!!config.ref,
|
|
"数据存在:",
|
|
config.visibility
|
|
);
|
|
}
|
|
}
|
|
|
|
console.log(`第${stockIndex}个股票的所有图表依次渲染完成`);
|
|
}
|
|
|
|
// 渲染组件图表的方法
|
|
function renderCharts(data) {
|
|
console.log("开始渲染图表,数据:", data);
|
|
|
|
// 深拷贝数据避免污染原始数据
|
|
const clonedData = JSON.parse(JSON.stringify(data));
|
|
|
|
// 检查关键数据字段是否完整
|
|
const validation = validateRequiredFields(clonedData);
|
|
|
|
// 如果有任何关键数据缺失,不渲染页面并返回提示
|
|
if (!validation.isValid) {
|
|
console.log("关键数据缺失:", validation.missingFields);
|
|
const aiMessage = reactive({
|
|
sender: "ai",
|
|
text: `数据不完整,缺少以下关键数据:${validation.missingFields.join(
|
|
"、"
|
|
)}。请稍后重试或联系客服。`,
|
|
});
|
|
messages.value.push(aiMessage);
|
|
|
|
// 将AI消息添加到emotion store中
|
|
emotionStore.addConversation({
|
|
sender: "ai",
|
|
text: `数据不完整,缺少以下关键数据:${validation.missingFields.join(
|
|
"、"
|
|
)}。请稍后重试或联系客服。`,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
// 隐藏页面内容
|
|
isPageLoaded.value = false;
|
|
// isLoading.value = false;
|
|
|
|
return; // 直接返回,不进行后续渲染
|
|
}
|
|
|
|
// 先设置图表组件显示状态
|
|
chartVisibility.value = {
|
|
marketTemperature: !!(clonedData.GSWDJ && clonedData.GSWDJ.length > 0),
|
|
emotionDecod: !!(clonedData.QXJMQ && clonedData.QXJMQ.length > 0),
|
|
emotionalBottomRadar: !!(clonedData.QXTDLD && clonedData.QXTDLD.length > 0),
|
|
emoEnergyConverter: !!(
|
|
clonedData.QXNLZHQ &&
|
|
(Array.isArray(clonedData.QXNLZHQ)
|
|
? clonedData.QXNLZHQ.length > 0
|
|
: hasValidData(clonedData.QXNLZHQ))
|
|
),
|
|
};
|
|
|
|
console.log("图表显示状态:", chartVisibility.value);
|
|
console.log("数据检查:", {
|
|
GSWDJ: !!(clonedData.GSWDJ && clonedData.GSWDJ.length > 0),
|
|
QXJMQ: !!(clonedData.QXJMQ && clonedData.QXJMQ.length > 0),
|
|
QXTDLD: !!(clonedData.QXTDLD && clonedData.QXTDLD.length > 0),
|
|
QXNLZHQ: !!(
|
|
clonedData.QXNLZHQ &&
|
|
(Array.isArray(clonedData.QXNLZHQ)
|
|
? clonedData.QXNLZHQ.length > 0
|
|
: hasValidData(clonedData.QXNLZHQ))
|
|
),
|
|
});
|
|
console.log("QXNLZHQ数据详情:", clonedData.QXNLZHQ);
|
|
|
|
nextTick(() => {
|
|
// 增加延迟确保DOM完全更新和组件完全挂载
|
|
setTimeout(() => {
|
|
try {
|
|
console.log("图表组件ref状态:", {
|
|
marketTemperatureRef: !!marketTemperatureRef.value,
|
|
emotionDecodRef: !!emotionDecodRef.value,
|
|
emotionalBottomRadarRef: !!emotionalBottomRadarRef.value,
|
|
emoEnergyConverterRef: !!emoEnergyConverterRef.value,
|
|
});
|
|
|
|
// 检查DOM元素是否存在
|
|
console.log("DOM元素检查:", {
|
|
marketTemperatureDOM: !!document.querySelector(
|
|
".market-temperature-section"
|
|
),
|
|
emotionDecodDOM: !!document.querySelector(".emotion-decoder-section"),
|
|
emotionalBottomRadarDOM: !!document.querySelector(
|
|
".bottom-radar-section"
|
|
),
|
|
emoEnergyConverterDOM: !!document.querySelector(
|
|
".energy-converter-section"
|
|
),
|
|
});
|
|
|
|
// 检查具体的组件元素
|
|
const emoEnergyElement = document.querySelector("emo-energy-converter");
|
|
console.log("emoEnergyConverter元素:", emoEnergyElement);
|
|
|
|
// 等待更长时间再次检查ref
|
|
setTimeout(() => {
|
|
console.log(
|
|
"延迟检查emoEnergyConverterRef:",
|
|
!!emoEnergyConverterRef.value
|
|
);
|
|
if (emoEnergyConverterRef.value) {
|
|
console.log(
|
|
"emoEnergyConverter方法:",
|
|
typeof emoEnergyConverterRef.value.initQXNLZHEcharts
|
|
);
|
|
}
|
|
}, 1000);
|
|
|
|
// 开始依次渲染图表 - 为每个股票渲染
|
|
emotionStore.stockList.forEach((stock, index) => {
|
|
if (stock.apiData) {
|
|
renderChartsSequentially(stock.apiData, index);
|
|
}
|
|
});
|
|
|
|
console.log("图表渲染完成");
|
|
} catch (error) {
|
|
console.error("图表渲染错误:", error);
|
|
const aiMessage = reactive({
|
|
sender: "ai",
|
|
text: "图表渲染失败,请重试",
|
|
});
|
|
messages.value.push(aiMessage);
|
|
|
|
// 将AI消息添加到emotion store中
|
|
emotionStore.addConversation({
|
|
sender: "ai",
|
|
text: "图表渲染失败,请重试",
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
}, 500); // 增加延迟到500ms确保DOM和组件完全稳定
|
|
});
|
|
}
|
|
|
|
// scrollToBottom函数已移除
|
|
|
|
// 处理用户滚动事件(用于其他滚动相关功能)
|
|
const handleUserScroll = () => {
|
|
// 用户滚动事件处理逻辑已简化,因为自动滚动功能已移除
|
|
};
|
|
|
|
// 处理滚轮事件
|
|
const handleWheel = (event) => {
|
|
handleUserScroll();
|
|
};
|
|
|
|
// 处理触摸滚动事件
|
|
const handleTouchMove = (event) => {
|
|
handleUserScroll();
|
|
};
|
|
|
|
// 检查数据是否已加载完成
|
|
function isDataLoaded() {
|
|
// 检查页面是否已加载
|
|
if (!isPageLoaded.value) {
|
|
console.log("页面数据尚未加载完成");
|
|
return false;
|
|
}
|
|
|
|
// 检查当前股票数据是否存在
|
|
if (!currentStock.value || !currentStock.value.apiData) {
|
|
console.log("股票数据尚未加载完成");
|
|
return false;
|
|
}
|
|
|
|
// 检查图表组件是否已渲染 - 检查所有股票的组件
|
|
const stockCount = emotionStore.stockList.length;
|
|
if (stockCount === 0) {
|
|
console.log("没有股票数据");
|
|
return false;
|
|
}
|
|
|
|
// 检查每个股票的图表组件是否都已加载
|
|
for (let i = 0; i < stockCount; i++) {
|
|
const requiredRefs = [
|
|
marketTemperatureRef.value[i],
|
|
emotionDecodRef.value[i],
|
|
emotionalBottomRadarRef.value[i],
|
|
emoEnergyConverterRef.value[i],
|
|
];
|
|
|
|
const allRefsLoaded = requiredRefs.every((ref) => ref !== null);
|
|
if (!allRefsLoaded) {
|
|
console.log(`第${i}个股票的图表组件尚未完全加载`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
console.log("所有数据和组件已加载完成");
|
|
return true;
|
|
}
|
|
|
|
// 自动滚动函数已移除
|
|
|
|
// 设置Intersection Observer监听场景应用部分
|
|
function setupIntersectionObserver() {
|
|
// 检查 scenarioApplicationRef.value 是否存在且是有效的 DOM 元素
|
|
if (
|
|
!scenarioApplicationRef.value ||
|
|
!(scenarioApplicationRef.value instanceof Element)
|
|
) {
|
|
console.warn(
|
|
"scenarioApplicationRef.value 不是有效的 DOM 元素,跳过 IntersectionObserver 设置"
|
|
);
|
|
return;
|
|
}
|
|
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
console.log("场景应用部分进入视口");
|
|
|
|
// 获取当前股票代码
|
|
const stockCode =
|
|
currentStock.value?.stockInfo?.code ||
|
|
currentStock.value?.stockInfo?.symbol;
|
|
|
|
if (parsedConclusion.value && stockCode) {
|
|
// 检查该股票是否是第一次触发
|
|
if (!stockTypewriterShown.value.has(stockCode)) {
|
|
// 如果是用户主动搜索,启动打字机效果和音频播放
|
|
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, stockCode);
|
|
}
|
|
|
|
stockTypewriterShown.value.set(stockCode, true);
|
|
} else {
|
|
// 非用户主动搜索(如历史记录恢复),直接显示完整内容
|
|
console.log(
|
|
"非用户主动搜索,该股票第一次进入场景应用,直接显示完整内容"
|
|
);
|
|
|
|
// 结论内容现在直接通过parsedConclusion计算属性显示
|
|
|
|
// 记录该股票已显示过
|
|
stockTypewriterShown.value.set(stockCode, true);
|
|
stockAudioPlayed.value.set(stockCode, true);
|
|
}
|
|
} else {
|
|
// 非第一次或已经触发过:直接显示完整内容,不播放音频和打字机效果
|
|
console.log("非第一次进入场景应用或已触发过,直接显示完整内容");
|
|
|
|
// 结论内容现在直接通过parsedConclusion计算属性显示
|
|
}
|
|
}
|
|
}
|
|
});
|
|
},
|
|
{
|
|
threshold: 0.3, // 当30%的元素进入视口时触发
|
|
rootMargin: "0px 0px -100px 0px", // 提前100px触发
|
|
}
|
|
);
|
|
|
|
observer.observe(scenarioApplicationRef.value);
|
|
intersectionObserver.value = observer;
|
|
}
|
|
|
|
// 手动触发自动滚动函数已移除
|
|
|
|
// 返回顶部功能
|
|
const scrollToTop = () => {
|
|
const topAnchor = document.getElementById("top-anchor");
|
|
if (topAnchor) {
|
|
topAnchor.scrollIntoView({
|
|
behavior: "smooth",
|
|
block: "start",
|
|
inline: "nearest",
|
|
});
|
|
} else {
|
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
}
|
|
|
|
// 备用方案:直接滚动到页面顶部
|
|
setTimeout(() => {
|
|
const currentScrollTop =
|
|
window.pageYOffset || document.documentElement.scrollTop;
|
|
if (currentScrollTop > 50) {
|
|
document.documentElement.scrollTop = 0;
|
|
document.body.scrollTop = 0;
|
|
}
|
|
}, 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 scrollTop =
|
|
window.pageYOffset ||
|
|
document.documentElement.scrollTop ||
|
|
document.body.scrollTop;
|
|
showBackToTop.value = scrollTop > 200;
|
|
};
|
|
|
|
// 监听容器滚动(备用方案)
|
|
const handleContainerScroll = () => {
|
|
const container = userInputDisplayRef.value;
|
|
if (container) {
|
|
const scrollTop = container.scrollTop;
|
|
if (scrollTop > 200) {
|
|
showBackToTop.value = true;
|
|
}
|
|
}
|
|
};
|
|
|
|
// 页面挂载完成后触发图片旋转和设置滚动监听
|
|
onMounted(async () => {
|
|
// 恢复对话记录
|
|
loadConversationsFromStore();
|
|
// 从stockList加载对话记录
|
|
loadConversationsFromStockList();
|
|
|
|
// 确保获取用户次数
|
|
// try {
|
|
// await chatStore.getUserCount();
|
|
// console.log('情绪大模型页面:用户次数获取成功');
|
|
// } catch (error) {
|
|
// console.error('情绪大模型页面:获取用户次数失败', error);
|
|
// }
|
|
|
|
// 添加全局resize监听器,确保所有图表和容器响应页面宽度变化
|
|
const globalResizeHandler = debounce(() => {
|
|
console.log("AiEmotion页面:窗口大小变化,触发容器和图表resize");
|
|
|
|
// 强制重新计算容器布局
|
|
const mainContainer = document.querySelector(".main");
|
|
if (mainContainer) {
|
|
// 触发重排,确保容器尺寸正确更新
|
|
mainContainer.style.display = "none";
|
|
mainContainer.offsetHeight; // 强制重排
|
|
mainContainer.style.display = "";
|
|
}
|
|
|
|
// 触发所有图表组件的resize
|
|
const resizeHandlers = [
|
|
window.emoEnergyConverterResizeHandler,
|
|
window.marketTempResizeHandler,
|
|
window.emotionalBottomRadarResizeHandler,
|
|
window.emotionDecodResizeHandler,
|
|
];
|
|
|
|
resizeHandlers.forEach((handler) => {
|
|
if (typeof handler === "function") {
|
|
try {
|
|
handler();
|
|
} catch (error) {
|
|
console.error("AiEmotion页面:图表resize失败", error);
|
|
}
|
|
}
|
|
});
|
|
|
|
// 延迟再次触发图表resize,确保容器尺寸稳定后图表能正确适配
|
|
setTimeout(() => {
|
|
resizeHandlers.forEach((handler) => {
|
|
if (typeof handler === "function") {
|
|
try {
|
|
handler();
|
|
} catch (error) {
|
|
console.error("AiEmotion页面:延迟图表resize失败", error);
|
|
}
|
|
}
|
|
});
|
|
}, 100);
|
|
}, 150); // 150ms防抖延迟
|
|
|
|
// 移除之前的监听器(如果存在)
|
|
if (window.aiEmotionGlobalResizeHandler) {
|
|
window.removeEventListener("resize", window.aiEmotionGlobalResizeHandler);
|
|
}
|
|
|
|
// 添加新的监听器
|
|
window.addEventListener("resize", globalResizeHandler);
|
|
window.aiEmotionGlobalResizeHandler = globalResizeHandler;
|
|
|
|
// 添加滚动事件监听器
|
|
const container = userInputDisplayRef.value;
|
|
if (container) {
|
|
container.addEventListener("wheel", handleWheel, { passive: true });
|
|
container.addEventListener("touchmove", handleTouchMove, { passive: true });
|
|
container.addEventListener("scroll", handleUserScroll, { passive: true });
|
|
// 添加容器滚动监听器用于返回顶部按钮
|
|
container.addEventListener("scroll", handleContainerScroll, {
|
|
passive: true,
|
|
});
|
|
}
|
|
|
|
// 添加页面滚动监听器,控制返回顶部按钮显示
|
|
window.addEventListener("scroll", handlePageScroll, { passive: true });
|
|
|
|
// 添加document滚动监听器(备用方案)
|
|
document.addEventListener("scroll", handlePageScroll, { passive: true });
|
|
|
|
// 防抖函数定义
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
startImageRotation();
|
|
|
|
// 检查是否有已保存的股票数据需要恢复
|
|
if (emotionStore.stockList.length > 0 && emotionStore.activeStock) {
|
|
console.log("检测到已保存的股票数据,开始恢复页面状态(不自动播放音频)");
|
|
|
|
// 恢复页面加载状态
|
|
isPageLoaded.value = true;
|
|
// 使用nextTick确保DOM更新后清空对话显示并启动高度监听器
|
|
nextTick(() => {
|
|
messages.value = [];
|
|
// 启动页面高度监听器,实时监听内容变化并自动滚动
|
|
startHeightObserver();
|
|
// 立即滚动到底部
|
|
scrollToBottom();
|
|
});
|
|
|
|
// 等待DOM渲染后恢复图表和数据
|
|
nextTick(() => {
|
|
const currentStockData = emotionStore.activeStock;
|
|
if (currentStockData && currentStockData.apiData) {
|
|
console.log("恢复图表数据:", currentStockData.stockInfo.name);
|
|
renderCharts(currentStockData.apiData);
|
|
|
|
// 恢复结论数据并显示内容
|
|
if (currentStockData.conclusionData) {
|
|
conclusionData.value = currentStockData.conclusionData;
|
|
|
|
// 结论内容现在直接通过parsedConclusion计算属性显示
|
|
conclusionData.value = currentStockData.conclusionData;
|
|
|
|
// 标记该股票的打字机效果和音频已经显示过,避免后续自动触发
|
|
const stockCode =
|
|
currentStockData.stockInfo?.code ||
|
|
currentStockData.stockInfo?.symbol;
|
|
if (stockCode) {
|
|
stockTypewriterShown.value.set(stockCode, true);
|
|
stockAudioPlayed.value.set(stockCode, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
setupIntersectionObserver();
|
|
});
|
|
} else {
|
|
// 没有保存的数据,正常设置监听器
|
|
nextTick(() => {
|
|
setupIntersectionObserver();
|
|
});
|
|
}
|
|
});
|
|
|
|
// 组件卸载时清理定时器、音频和observer
|
|
onUnmounted(() => {
|
|
clearTypewriterTimers();
|
|
|
|
// 如果有未完成的回调函数,调用它来重新启用输入框
|
|
if (
|
|
currentOnCompleteCallback.value &&
|
|
typeof currentOnCompleteCallback.value === "function"
|
|
) {
|
|
currentOnCompleteCallback.value();
|
|
currentOnCompleteCallback.value = null;
|
|
}
|
|
|
|
stopAudio();
|
|
|
|
// 重置触发状态
|
|
hasTriggeredAudio.value = false;
|
|
hasTriggeredTypewriter.value = false;
|
|
|
|
// 清理页面高度监听器
|
|
stopHeightObserver();
|
|
|
|
// 清理Intersection Observer
|
|
if (intersectionObserver.value) {
|
|
intersectionObserver.value.disconnect();
|
|
intersectionObserver.value = null;
|
|
}
|
|
|
|
// 清理全局resize监听器
|
|
if (window.aiEmotionGlobalResizeHandler) {
|
|
window.removeEventListener("resize", window.aiEmotionGlobalResizeHandler);
|
|
window.aiEmotionGlobalResizeHandler = null;
|
|
}
|
|
|
|
// 清理滚动事件监听器
|
|
const container = userInputDisplayRef.value;
|
|
if (container) {
|
|
container.removeEventListener("wheel", handleWheel);
|
|
container.removeEventListener("touchmove", handleTouchMove);
|
|
container.removeEventListener("scroll", handleUserScroll);
|
|
container.removeEventListener("scroll", handleContainerScroll);
|
|
}
|
|
|
|
// 清理页面滚动监听器
|
|
window.removeEventListener("scroll", handlePageScroll);
|
|
document.removeEventListener("scroll", handlePageScroll);
|
|
|
|
// 滚动相关清理已简化
|
|
});
|
|
|
|
// 声明组件可以触发的事件
|
|
const emit = defineEmits(["updateMessage", "sendMessage", "ensureAIchat", "enableInput"]);
|
|
|
|
// 导出方法供外部使用(已在上方定义)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.disclaimer-text {
|
|
font-size: 24px;
|
|
color: #EEEEEE;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
}
|
|
/* 股票统计概览样式 */
|
|
.stock-statistics-overview {
|
|
background: rgba(255, 255, 255, 0.95);
|
|
border-radius: 15px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
}
|
|
|
|
.statistics-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 2px solid #f0f0f0;
|
|
}
|
|
|
|
.statistics-header h3 {
|
|
margin: 0;
|
|
color: #333;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.stock-count {
|
|
background: linear-gradient(45deg, #667eea, #764ba2);
|
|
color: white;
|
|
padding: 5px 12px;
|
|
border-radius: 20px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.statistics-content {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 15px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.stat-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 10px 15px;
|
|
background: rgba(102, 126, 234, 0.1);
|
|
border-radius: 8px;
|
|
border-left: 4px solid #667eea;
|
|
}
|
|
|
|
.stat-label {
|
|
color: #666;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.stat-value {
|
|
color: #333;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.stat-value.hot-temp {
|
|
color: #ff4757;
|
|
}
|
|
|
|
.stat-value.cold-temp {
|
|
color: #3742fa;
|
|
}
|
|
|
|
.temperature-distribution {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
}
|
|
|
|
.distribution-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 12px;
|
|
background: rgba(255, 255, 255, 0.8);
|
|
border-radius: 20px;
|
|
border: 1px solid #e0e0e0;
|
|
}
|
|
|
|
.temp-level {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
color: white;
|
|
}
|
|
|
|
.temp-level.hot {
|
|
background: #ff4757;
|
|
}
|
|
|
|
.temp-level.warm {
|
|
background: #ff6b35;
|
|
}
|
|
|
|
.temp-level.normal {
|
|
background: #26de81;
|
|
}
|
|
|
|
.temp-level.cool {
|
|
background: #45aaf2;
|
|
}
|
|
|
|
.temp-level.cold {
|
|
background: #3742fa;
|
|
}
|
|
|
|
.temp-count {
|
|
color: #666;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.matrix-header {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.disclaimer-item p {
|
|
color: #ffffff !important;
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.temperature-content {
|
|
padding-top: 8%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10rem;
|
|
}
|
|
|
|
.temperature-content .content1 {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
|
|
.temperature-content .content1 img {
|
|
scale: 0.5;
|
|
}
|
|
|
|
.temperature-display {
|
|
display: flex;
|
|
flex-direction: column;
|
|
/* 竖向排列元素 */
|
|
/* margin-left: 15%; */
|
|
gap: 30px;
|
|
/* margin-top: -12%; */
|
|
/* width: 100%; */
|
|
/* height: auto; */
|
|
}
|
|
|
|
.temperature-display::after {
|
|
content: "";
|
|
display: table;
|
|
clear: both;
|
|
}
|
|
|
|
.temperature-content .temperature-hot {
|
|
background-image: url("@/assets/img/AiEmotion/redBorder.png");
|
|
background-repeat: no-repeat;
|
|
background-size: 100% 100%;
|
|
/* width: 50%; */
|
|
width: 22vw;
|
|
max-width: 400px;
|
|
min-width: 200px;
|
|
text-align: center;
|
|
font-size: 24px;
|
|
color: white;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.temperature-content .temperature-cold {
|
|
background-image: url("@/assets/img/AiEmotion/blueBorder.png");
|
|
background-repeat: no-repeat;
|
|
background-size: 100% 100%;
|
|
/* width: 35%; */
|
|
width: 22vw;
|
|
max-width: 400px;
|
|
min-width: 200px;
|
|
text-align: center;
|
|
font-size: 24px;
|
|
color: white;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.golden-wheel {
|
|
width: 100%;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.golden-wheel-img {
|
|
width: 60%;
|
|
max-width: 500px;
|
|
height: auto;
|
|
}
|
|
|
|
/* 定义旋转动画 */
|
|
@keyframes rotate {
|
|
from {
|
|
transform: rotate(0deg);
|
|
}
|
|
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
/* 应用动画到图片 */
|
|
.rotating-image {
|
|
animation: rotate 5s linear;
|
|
/* 5 秒完成一次旋转,线性速度*/
|
|
will-change: transform;
|
|
/* 优化动画性能 */
|
|
}
|
|
|
|
.bk-image {
|
|
background-image: url("@/assets/img/AiEmotion/bk00000.png");
|
|
background-size: 100% 100%;
|
|
background-repeat: no-repeat;
|
|
width: 50vw;
|
|
height: auto;
|
|
margin: 0 auto;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.bk-image .conclusion-container {
|
|
padding: 20px;
|
|
border-radius: 15px;
|
|
margin: 20px;
|
|
}
|
|
|
|
.bk-image .conclusion-container .conclusion-item {
|
|
border-radius: 12px;
|
|
transition: all 0.3s ease;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.bk-image .conclusion-container .conclusion-item:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.bk-image .conclusion-container .conclusion-item .conclusion-title {
|
|
color: #ffd700;
|
|
font-size: 22px;
|
|
font-weight: bold;
|
|
margin: 0 0 15px 0;
|
|
text-align: center;
|
|
letter-spacing: 2px;
|
|
margin-top: 22px;
|
|
}
|
|
|
|
.bk-image .conclusion-container .conclusion-item .conclusion-text {
|
|
color: #ffffff;
|
|
font-size: 20px;
|
|
line-height: 1.8;
|
|
margin: 0 0 12px 0;
|
|
text-align: center;
|
|
word-wrap: break-word;
|
|
position: relative;
|
|
}
|
|
|
|
.bk-image .conclusion-container .conclusion-item .conclusion-text:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.bk-image .conclusion-placeholder {
|
|
padding: 30px;
|
|
text-align: center;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.bk-image .conclusion-placeholder p {
|
|
color: #999999;
|
|
font-size: 16px;
|
|
margin: 0;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* 最后文字的颜色 */
|
|
.text-container {
|
|
position: relative;
|
|
color: white;
|
|
text-align: left;
|
|
padding: 20px;
|
|
border-radius: 15px;
|
|
}
|
|
|
|
/* .text-container p {
|
|
margin: 0 auto;
|
|
font-size: 40px;
|
|
margin-left: 0%;
|
|
border-radius: 12px;
|
|
transition: all 0.3s ease;
|
|
overflow: hidden;
|
|
letter-spacing: 2px;
|
|
} */
|
|
|
|
.text-container .title {
|
|
display: block;
|
|
color: #ffd700;
|
|
font-weight: bold;
|
|
margin-top: 0px;
|
|
margin-bottom: 20px;
|
|
font-size: 22px;
|
|
text-align: center;
|
|
}
|
|
|
|
.text-container .content {
|
|
display: block;
|
|
color: white;
|
|
text-align: center;
|
|
font-size: 22px;
|
|
}
|
|
|
|
.core-logic-section {
|
|
background-image: url("@/assets/img/AiEmotion/bk00000.png");
|
|
background-size: cover;
|
|
background-repeat: no-repeat;
|
|
width: 95%;
|
|
height: auto;
|
|
min-height: 70rem;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.core-logic-header {
|
|
margin: 0 auto;
|
|
width: fit-content;
|
|
margin-top: 2%;
|
|
margin-bottom: 1%;
|
|
}
|
|
|
|
.core-highlights-header {
|
|
margin: 0 auto;
|
|
/* width: fit-content; */
|
|
margin-top: 2%;
|
|
margin-bottom: 1%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.emotion-decoder-content {
|
|
/* width: 80vw; */
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.energy-converter-content {
|
|
min-width: 100%;
|
|
margin-top: 3%;
|
|
}
|
|
|
|
.bottom-radar-header {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.bottom-radar-header img {
|
|
scale: 0.5;
|
|
}
|
|
|
|
.energy-converter-header {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.energy-converter-header img {
|
|
scale: 0.5;
|
|
}
|
|
|
|
.bottom-radar-icon {
|
|
width: fit-content;
|
|
height: auto;
|
|
margin: 0 auto;
|
|
margin-top: 2%;
|
|
margin-bottom: 1%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.energy-converter-icon {
|
|
width: fit-content;
|
|
height: auto;
|
|
margin: 0 auto;
|
|
margin-top: 2%;
|
|
margin-bottom: 1%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.core-logic-content {
|
|
margin: 0 auto;
|
|
width: fit-content;
|
|
}
|
|
|
|
.market-temperature-icon {
|
|
width: fit-content;
|
|
height: auto;
|
|
margin: 0 auto;
|
|
margin-top: 2%;
|
|
margin-bottom: 1%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.bottom-radar-title {
|
|
margin: 0 auto;
|
|
width: fit-content;
|
|
margin-top: 2%;
|
|
margin-bottom: 1%;
|
|
}
|
|
|
|
.emotion-decoder-title {
|
|
margin: 0 auto;
|
|
width: fit-content;
|
|
margin-top: 2%;
|
|
margin-bottom: 1%;
|
|
}
|
|
|
|
.temperature-title {
|
|
margin: 0 auto;
|
|
width: fit-content;
|
|
margin-top: 2%;
|
|
margin-bottom: 1%;
|
|
}
|
|
|
|
.matrix-title {
|
|
margin: 0 auto;
|
|
width: fit-content;
|
|
margin-bottom: 1%;
|
|
}
|
|
|
|
.emotion-decoder-header {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.emotion-decoder-header img {
|
|
scale: 0.5;
|
|
}
|
|
|
|
.emotion-decoder-icon {
|
|
width: fit-content;
|
|
height: auto;
|
|
margin: 0 auto;
|
|
margin-top: 2%;
|
|
margin-bottom: 1%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.emotion-decoder-text {
|
|
color: white;
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
/* margin-left: 45%; */
|
|
}
|
|
|
|
.bottom-radar-text {
|
|
color: white;
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
/* margin-left: 44.6%; */
|
|
}
|
|
|
|
.energy-converter-text {
|
|
color: white;
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
/* margin-left: 44%; */
|
|
}
|
|
|
|
.scenario-application-section {
|
|
text-align: center;
|
|
margin-top: 2%;
|
|
margin-bottom: 1%;
|
|
}
|
|
|
|
/* 为需要放大的图片添加样式 */
|
|
.scaled-img {
|
|
background-image: url("@/assets/img/AiEmotion/tree00000.jpg");
|
|
background-size: 100% 100%;
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
width: 80%;
|
|
height: 350px;
|
|
margin: 0 auto;
|
|
margin-top: 2%;
|
|
margin-bottom: 3%;
|
|
}
|
|
|
|
.lz-img {
|
|
text-align: center;
|
|
padding-top: 30px;
|
|
}
|
|
|
|
.decision-tree-section {
|
|
background-image: url("@/assets/img/AiEmotion/bk00000.png");
|
|
background-size: 100% 100%;
|
|
background-repeat: no-repeat;
|
|
width: 50vw;
|
|
height: auto;
|
|
min-height: 30rem;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.energy-converter-section {
|
|
background-image: url("@/assets/img/AiEmotion/bk00000.png");
|
|
background-size: 100% 100%;
|
|
background-repeat: no-repeat;
|
|
width: 50vw;
|
|
height: auto;
|
|
margin: 0 auto;
|
|
box-sizing: border-box;
|
|
transition: all 0.3s ease;
|
|
min-height: 85vh;
|
|
}
|
|
|
|
.bottom-radar-section {
|
|
background-image: url("@/assets/img/AiEmotion/bk00000.png");
|
|
background-size: 100% 100%;
|
|
background-repeat: no-repeat;
|
|
width: 50vw;
|
|
max-width: 100%;
|
|
height: auto;
|
|
margin: 0 auto;
|
|
box-sizing: border-box;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.emotion-decoder-section {
|
|
background-image: url("@/assets/img/AiEmotion/bk00000.png");
|
|
background-size: 100% 100%;
|
|
background-repeat: no-repeat;
|
|
width: 50vw;
|
|
max-width: 100%;
|
|
height: auto;
|
|
margin: 0 auto;
|
|
box-sizing: border-box;
|
|
transition: all 0.3s ease;
|
|
padding-bottom: 1rem;
|
|
}
|
|
|
|
.market-temperature-section {
|
|
background-image: url("@/assets/img/AiEmotion/bk00000.png");
|
|
background-size: 100% 100%;
|
|
background-repeat: no-repeat;
|
|
width: 50vw;
|
|
max-width: 100%;
|
|
height: auto;
|
|
min-height: 70rem;
|
|
margin: 0 auto;
|
|
box-sizing: border-box;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.main-content-wrapper {
|
|
background-size: 100% 100%;
|
|
background-repeat: no-repeat;
|
|
width: 77vw;
|
|
max-width: 100%;
|
|
height: auto;
|
|
margin: 0 auto;
|
|
box-sizing: border-box;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.matrix-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
/* 竖向排列元素 */
|
|
/* gap: 1rem; */
|
|
/* margin-left: 10%; */
|
|
}
|
|
|
|
.matrix-main-title {
|
|
color: white;
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
margin-left: 0%;
|
|
}
|
|
|
|
.market-temperature-value {
|
|
font-size: 1.5rem;
|
|
font-weight: bold;
|
|
color: white;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.market-temperature-label {
|
|
background-image: url("@/assets/img/AiEmotion/bk01.png");
|
|
background-size: 100% 100%;
|
|
background-repeat: no-repeat;
|
|
padding: 10px;
|
|
color: #fff;
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
margin-left: 0;
|
|
width: 30%;
|
|
min-width: 200px;
|
|
max-width: 50%;
|
|
height: auto;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.main {
|
|
width: 83%;
|
|
max-width: 1400px;
|
|
min-width: 320px;
|
|
min-height: 100px;
|
|
height: auto;
|
|
padding: 1rem;
|
|
box-sizing: border-box;
|
|
background-color: #2b378d;
|
|
margin: 0 auto;
|
|
transition: width 0.3s ease;
|
|
margin-bottom: 10rem;
|
|
}
|
|
|
|
.ai-emotion-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
position: relative;
|
|
}
|
|
|
|
.user-input-display {
|
|
margin-top: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
}
|
|
|
|
.message-container {
|
|
display: flex;
|
|
margin-bottom: 30px;
|
|
width: 100%;
|
|
}
|
|
|
|
/* 用户消息容器样式 */
|
|
.user-message-container {
|
|
align-items: center;
|
|
margin-left: auto;
|
|
gap: 10px;
|
|
margin-right: 5px;
|
|
}
|
|
|
|
.user-content {
|
|
display: flex;
|
|
height: 100%;
|
|
align-items: center;
|
|
margin-right: 5px;
|
|
}
|
|
|
|
.user-message-speaker {
|
|
width: 32px;
|
|
height: 32px;
|
|
object-fit: contain;
|
|
margin-right: 5px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.user-sendTime {
|
|
width: 100%;
|
|
text-align: center;
|
|
color: rgba(255, 255, 255, 0.6);
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.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);
|
|
}
|
|
}
|
|
|
|
.user-message {
|
|
color: #6d22f8;
|
|
background: white;
|
|
font-weight: bold;
|
|
padding: 15px 20px;
|
|
border-radius: 15px;
|
|
max-width: 60%;
|
|
text-align: left;
|
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
margin: 0;
|
|
/* 将用户消息推到右边 */
|
|
}
|
|
|
|
/* AI消息容器样式 */
|
|
.ai-message-container {
|
|
background-color: #f1f1f1;
|
|
border-radius: 15px;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
margin-right: auto;
|
|
/* max-width: 80%; */
|
|
white-space: normal;
|
|
width: fit-content;
|
|
overflow: visible;
|
|
align-items: center;
|
|
display: flex;
|
|
}
|
|
|
|
/* 思考过程动图样式 */
|
|
.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);
|
|
}
|
|
}
|
|
|
|
/* 动态加载点样式 */
|
|
.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;
|
|
}
|
|
}
|
|
|
|
.ai-message {
|
|
/* background-color: #f1f1f1; */
|
|
color: #000000;
|
|
font-weight: bold;
|
|
padding: 20px 30px;
|
|
border-radius: 15px;
|
|
text-align: left;
|
|
margin-right: auto;
|
|
/* 将AI消息保持在左边 */
|
|
/* white-space: nowrap; */
|
|
width: fit-content;
|
|
overflow: visible;
|
|
align-items: center;
|
|
display: flex;
|
|
}
|
|
|
|
.input-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.fixed-bottom {
|
|
position: fixed;
|
|
bottom: 100px;
|
|
left: 0;
|
|
width: 100%;
|
|
background-color: #f8f9fa;
|
|
padding: 10px 20px;
|
|
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.input-box {
|
|
padding: 10px;
|
|
font-size: 16px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 5px;
|
|
width: calc(100% - 120px);
|
|
}
|
|
|
|
.send-button {
|
|
padding: 10px 20px;
|
|
font-size: 16px;
|
|
color: #fff;
|
|
background-color: #007bff;
|
|
border: none;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.send-button:hover {
|
|
background-color: #0056b3;
|
|
}
|
|
|
|
/* 响应式布局媒体查询 */
|
|
@media only screen and (max-width: 1200px) {
|
|
.main {
|
|
width: 95%;
|
|
padding: 0.8rem;
|
|
}
|
|
|
|
.market-temperature-label {
|
|
width: 40%;
|
|
min-width: 170px;
|
|
max-width: 60%;
|
|
font-size: 1.3rem;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.market-temperature-value {
|
|
font-size: 1.3rem;
|
|
}
|
|
|
|
/* 调整图表容器高度 */
|
|
/* .class00 {
|
|
min-height: 45rem;
|
|
} */
|
|
|
|
.market-temperature-section {
|
|
min-height: 60rem;
|
|
}
|
|
|
|
.decision-tree-section {
|
|
min-height: 42rem;
|
|
}
|
|
|
|
.scaled-img {
|
|
height: 350px;
|
|
min-height: 30rem;
|
|
}
|
|
}
|
|
|
|
@media only screen and (max-width: 992px) {
|
|
.main {
|
|
width: 98%;
|
|
padding: 0.6rem;
|
|
}
|
|
|
|
/* 调整图表容器高度 */
|
|
/* .class00 {
|
|
min-height: 40rem;
|
|
} */
|
|
|
|
.market-temperature-section {
|
|
min-height: 55rem;
|
|
}
|
|
|
|
.decision-tree-section {
|
|
min-height: 35rem;
|
|
}
|
|
|
|
.scaled-img {
|
|
height: 300px;
|
|
min-height: 25rem;
|
|
background-size: contain;
|
|
}
|
|
}
|
|
|
|
/* 手机端适配样式 */
|
|
@media only screen and (max-width: 768px) {
|
|
.disclaimer-text {
|
|
font-size: 18px;
|
|
color: #EEEEEE;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
}
|
|
|
|
|
|
.text-container .content {
|
|
display: block;
|
|
color: white;
|
|
text-align: center;
|
|
font-size: 20px;
|
|
}
|
|
|
|
.market-temperature-icon {
|
|
width: auto;
|
|
height: auto;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
scale: 0.5;
|
|
}
|
|
|
|
.matrix-header {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.main {
|
|
width: 90%;
|
|
padding: 0.5rem;
|
|
margin-bottom: 5rem;
|
|
background-color: #2b378d;
|
|
border-radius: 0px 0px 10px 10px;
|
|
}
|
|
|
|
.energy-converter-text {
|
|
color: white;
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
margin-left: 28%;
|
|
}
|
|
|
|
.energy-converter-content {
|
|
min-width: 100%;
|
|
/* margin-top: 25%; */
|
|
}
|
|
|
|
.scaled-img {
|
|
background-image: url("@/assets/img/AiEmotion/tree00000.jpg");
|
|
background-size: 100% 100%;
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
background-size: contain;
|
|
text-align: center;
|
|
width: 90%;
|
|
margin-top: 4%;
|
|
height: 200px;
|
|
min-height: 200px;
|
|
}
|
|
|
|
.bottom-radar-text {
|
|
color: white;
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
margin-left: 30%;
|
|
}
|
|
|
|
.emotion-decoder-text {
|
|
color: white;
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
margin-left: 30%;
|
|
}
|
|
|
|
/* 图片样式 */
|
|
.golden-wheel img {
|
|
width: 50%;
|
|
}
|
|
|
|
.matrix-title img {
|
|
width: 100%;
|
|
}
|
|
|
|
.temperature-title img {
|
|
width: 100%;
|
|
margin: 10px 10px;
|
|
}
|
|
|
|
.emotion-decoder-title img {
|
|
width: 100%;
|
|
margin: 10px 10px;
|
|
}
|
|
|
|
.bottom-radar-title img {
|
|
width: 100%;
|
|
margin: 10px 10px;
|
|
}
|
|
|
|
.core-highlights-header img {
|
|
scale: 0.5;
|
|
}
|
|
|
|
.core-logic-header img {
|
|
width: 100%;
|
|
margin: 10px 10px;
|
|
}
|
|
|
|
.scaled-img img {
|
|
width: 30%;
|
|
height: auto;
|
|
}
|
|
|
|
.scenario-application-section img {
|
|
width: 100%;
|
|
margin: 10px 10px;
|
|
}
|
|
|
|
.matrix-main-title {
|
|
font-size: 20px;
|
|
margin-left: 5%;
|
|
}
|
|
|
|
/* .span01 {
|
|
width: 50%;
|
|
min-width: 150px;
|
|
max-width: 70%;
|
|
font-size: 16px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
padding: 8px;
|
|
} */
|
|
|
|
.matrix-header .market-temperature-value {
|
|
font-size: 14px;
|
|
color: white;
|
|
float: right;
|
|
}
|
|
|
|
.market-temperature-section {
|
|
background-image: url("@/assets/img/AiEmotion/bk00000.png");
|
|
background-size: 100% 100%;
|
|
background-repeat: no-repeat;
|
|
width: 100%;
|
|
/* margin-left: -45px; */
|
|
height: auto;
|
|
}
|
|
|
|
/* .main {
|
|
min-height: 100px;
|
|
height: auto;
|
|
box-sizing: border-box;
|
|
background-color: #02107d;
|
|
margin-bottom: 10rem;
|
|
} */
|
|
|
|
.emotion-decoder-section {
|
|
width: 80vw;
|
|
height: auto;
|
|
margin: 0 auto;
|
|
/* min-height: 38rem; */
|
|
/* min-height: 51rem; */
|
|
/* margin-left: -39px; */
|
|
/* min-height: 38rem; */
|
|
}
|
|
|
|
/* 调整其他图表容器高度 */
|
|
/* .class00 {
|
|
min-height: 35rem;
|
|
} */
|
|
|
|
.market-temperature-section {
|
|
min-height: 45rem;
|
|
}
|
|
|
|
.matrix-header img {
|
|
width: 68%;
|
|
height: auto;
|
|
/* margin-top: 5%; */
|
|
margin-left: 0%;
|
|
}
|
|
|
|
.emotion-decoder-icon {
|
|
width: 25%;
|
|
height: auto;
|
|
scale: 0.5;
|
|
}
|
|
|
|
.bottom-radar-icon,
|
|
.energy-converter-icon {
|
|
width: 25%;
|
|
height: auto;
|
|
scale: 0.5;
|
|
}
|
|
|
|
.lz-img {
|
|
margin-bottom: 0;
|
|
padding-top: 0;
|
|
|
|
img {
|
|
width: 30%;
|
|
height: auto;
|
|
margin-top: 5%;
|
|
}
|
|
}
|
|
|
|
.decision-tree-section {
|
|
background-size: 100% 100%;
|
|
background-repeat: no-repeat;
|
|
width: 80vw;
|
|
height: auto;
|
|
min-height: 20rem;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.bk-image {
|
|
.conclusion-container {
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
margin: 8px;
|
|
|
|
.conclusion-item {
|
|
&:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.conclusion-title {
|
|
color: #ffd700;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
margin: 0 0 8px 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.conclusion-text {
|
|
color: #ffffff;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
margin: 0 0 6px 0;
|
|
text-align: left;
|
|
word-wrap: break-word;
|
|
|
|
&:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.conclusion-placeholder {
|
|
padding: 15px;
|
|
text-align: center;
|
|
|
|
p {
|
|
color: #999999;
|
|
font-size: 12px;
|
|
margin: 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
.bk-image {
|
|
background-size: 100% 100%;
|
|
background-repeat: no-repeat;
|
|
width: 80vw;
|
|
height: auto;
|
|
margin: 0 auto;
|
|
|
|
.conclusion-container {
|
|
padding-top: 20px;
|
|
border-radius: 15px;
|
|
margin: 20px;
|
|
|
|
.conclusion-item {
|
|
border-radius: 12px;
|
|
transition: all 0.3s ease;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.conclusion-item:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.conclusion-item .conclusion-title {
|
|
color: #ffd700;
|
|
font-size: 22px;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
letter-spacing: 2px;
|
|
margin-top: 22px;
|
|
}
|
|
|
|
.conclusion-item .conclusion-text {
|
|
color: #ffffff;
|
|
font-size: 20px;
|
|
line-height: 1.8;
|
|
margin: 0 0 12px 0;
|
|
text-align: center;
|
|
word-wrap: break-word;
|
|
position: relative;
|
|
}
|
|
|
|
.conclusion-item .conclusion-text:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
}
|
|
|
|
.conclusion-placeholder {
|
|
padding: 30px;
|
|
text-align: center;
|
|
border-radius: 12px;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px dashed rgba(153, 153, 153, 0.3);
|
|
}
|
|
|
|
.conclusion-placeholder p {
|
|
color: #999999;
|
|
font-size: 16px;
|
|
margin: 0;
|
|
font-style: italic;
|
|
}
|
|
|
|
.disclaimer-item {
|
|
/* margin-top: 30px; */
|
|
padding-bottom: 15%;
|
|
text-align: center;
|
|
}
|
|
|
|
.disclaimer-item p {
|
|
color: #ffffff !important;
|
|
font-size: 18px;
|
|
margin: 0;
|
|
letter-spacing: 1px;
|
|
}
|
|
}
|
|
|
|
.bottom-radar-section {
|
|
background-size: 100% 100%;
|
|
background-repeat: no-repeat;
|
|
width: 80vw;
|
|
height: auto;
|
|
/* min-height: 48rem; */
|
|
}
|
|
|
|
.energy-converter-section {
|
|
background-size: 100% 100%;
|
|
background-repeat: no-repeat;
|
|
width: 80vw;
|
|
height: auto;
|
|
margin: 0 auto;
|
|
min-height: 55vh;
|
|
}
|
|
|
|
.temperature-display {
|
|
display: flex;
|
|
flex-direction: column;
|
|
/* margin-left: 5rem; */
|
|
gap: 0;
|
|
/* margin-top: -6rem; */
|
|
/* width: 100%; */
|
|
height: auto;
|
|
}
|
|
|
|
.temperature-content .temperature-cold,
|
|
.temperature-content .temperature-hot {
|
|
min-width: 200px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.temperature-content {
|
|
padding-top: 8%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0rem;
|
|
}
|
|
|
|
.market-temperature-label {
|
|
background-image: url("@/assets/img/AiEmotion/bk01.png");
|
|
background-size: 100% 100%;
|
|
background-repeat: no-repeat;
|
|
display: inline-block;
|
|
padding: 10px;
|
|
color: #fff;
|
|
font-size: 14px;
|
|
text-align: center;
|
|
width: 50%;
|
|
}
|
|
|
|
.temperature-display {
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
}
|
|
|
|
/* .span01 {
|
|
width: 60%;
|
|
font-size: 1.2rem;
|
|
padding: 8px;
|
|
} */
|
|
|
|
.market-temperature-value {
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.matrix-main-title,
|
|
.emotion-decoder-text,
|
|
.bottom-radar-text,
|
|
.energy-converter-text {
|
|
font-size: 18px;
|
|
margin-left: 0;
|
|
}
|
|
}
|
|
|
|
/* 超小屏幕设备 */
|
|
@media only screen and (max-width: 480px) {
|
|
.main {
|
|
width: 90%;
|
|
padding: 0.3rem;
|
|
margin-bottom: 3rem;
|
|
}
|
|
|
|
.temperature-display {
|
|
flex-direction: column;
|
|
gap: 0.8rem;
|
|
align-items: center;
|
|
margin-right: 30px;
|
|
}
|
|
|
|
.temperature-content .temperature-cold,
|
|
.temperature-content .temperature-hot {
|
|
/* width: 90%; */
|
|
width: 10vw;
|
|
min-width: 100px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
/* .span01 {
|
|
width: 50%;
|
|
font-size: 1rem;
|
|
padding: 6px;
|
|
} */
|
|
|
|
.market-temperature-value {
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.golden-wheel-img {
|
|
width: 80%;
|
|
}
|
|
|
|
/* 调整图表容器高度适配超小屏幕 */
|
|
/* .class00 {
|
|
min-height: 25rem;
|
|
} */
|
|
|
|
.market-temperature-section {
|
|
min-height: 35rem;
|
|
}
|
|
|
|
.decision-tree-section {
|
|
min-height: 15rem;
|
|
}
|
|
|
|
.scaled-img {
|
|
height: 150px;
|
|
min-height: 150px;
|
|
}
|
|
}
|
|
|
|
/* 加载提示样式 */
|
|
/* .loading-container {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
min-height: 20vh;
|
|
padding: 20px 10px;
|
|
} */
|
|
|
|
/* .loading-content {
|
|
text-align: center;
|
|
background: linear-gradient(135deg, rgba(0, 212, 255, 0.15) 0%, rgba(0, 100, 200, 0.15) 100%);
|
|
border: 2px solid rgba(0, 212, 255, 0.4);
|
|
border-radius: 20px;
|
|
padding: 40px;
|
|
box-shadow: 0 8px 25px rgba(0, 212, 255, 0.3);
|
|
} */
|
|
|
|
/* .loading-spinner {
|
|
width: 60px;
|
|
height: 60px;
|
|
border: 4px solid rgba(0, 212, 255, 0.3);
|
|
border-top: 4px solid #00d4ff;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto 20px;
|
|
} */
|
|
|
|
@keyframes spin {
|
|
0% {
|
|
transform: rotate(0deg);
|
|
}
|
|
|
|
100% {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
/* .loading-text {
|
|
color: #00d4ff;
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
text-shadow: 0 2px 8px rgba(0, 212, 255, 0.5);
|
|
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 {
|
|
position: relative;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 1px;
|
|
display: block;
|
|
visibility: visible;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* 返回顶部按钮样式 */
|
|
.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);
|
|
}
|
|
|
|
/* 页面主容器样式 */
|
|
.page-container {
|
|
position: relative;
|
|
width: 100%;
|
|
}
|
|
|
|
.master:last-child {
|
|
border-bottom: none;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
/* class01容器样式 */
|
|
.main {
|
|
position: relative;
|
|
}
|
|
|
|
/* 移动端适配 */
|
|
@media only screen and (max-width: 768px) {
|
|
.back-to-top {
|
|
left: calc(100% - 65px) !important;
|
|
width: 45px !important;
|
|
height: 45px !important;
|
|
}
|
|
}
|
|
|
|
@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>
|