|
|
<script setup> // 导入
import { ref, computed, onMounted, watch, nextTick, onUnmounted } from "vue"; import { setHeight } from "../utils/setHeight"; import { getUserCountAPI } from "../api/AIxiaocaishen"; import { ElMessage } from 'element-plus' import AIchat from "./AIchat.vue"; import AIfind from "./AIfind.vue"; import Feedback from "./Feedback.vue"; import { useAppBridge } from '../assets/js/useAppBridge.js' import { useDataStore } from '@/store/dataList.js' import { useChatStore } from '../store/chat' import { useAudioStore } from '../store/audio' import _ from "lodash";
import logo from "../assets/img/homePage/logo.png"; import madeInHL from "../assets/img/homePage/madeInHL.png"; import getCountAll from "../assets/img/homePage/get-count-all.png"; import announcementBtn from "../assets/img/homePage/announcement.png"; import thinkActive from "../assets/img/homePage/tail/think-active.png"; import thinkNoActive from "../assets/img/homePage/tail/think-no-active.png"; import languageBtn from "../assets/img/homePage/tail/language.png"; import voice from "../assets/img/homePage/tail/voice.png"; import voiceNoActive from "../assets/img/homePage/tail/voice-no-active.png"; import sendBtn from "../assets/img/homePage/tail/send.png"; import msgBtn from "../assets/img/homePage/tail/msg.png"; import feedbackBtn from "../assets/img/Feedback/feedbackBtn.png";
// import VConsole from 'vconsole';
// const vConsole = new VConsole();
// import { useUserStore } from "../store/userPessionCode.js";
const { getQueryVariable, setActiveTabIndex } = useDataStore() const dataStore = useDataStore() const chatStore = useChatStore() // 变量
// 音频管理
const audioStore = useAudioStore() const isVoice = computed(() => audioStore.isVoiceEnabled) const toggleVoice = () => { audioStore.toggleVoice() } // 将默认值改为从 sessionStorage 中获取,如果没有则使用默认值 'aifindCow'为第一个默认tab
const activeTab = ref(sessionStorage.getItem("activeTabAI") || "AIchat"); const activeIndex = ref( parseInt(sessionStorage.getItem("activeIndexAI") || "0") );
const tabs = computed(() => [ { name: "AIchat", label: "AI对话", }, { name: "AIfind", label: "发现", }, ]);
// 修改 setActiveTab 方法,添加一个可选参数 forceAIchat
const setActiveTab = (tab, index, forceAIchat = false) => { isScrolling.value = false; //回复滚动到底部方法
isAnnouncementVisible.value = false; if (forceAIchat && activeTab.value !== "AIchat") { activeTab.value = "AIchat"; activeIndex.value = 0; sessionStorage.setItem("activeTabAI", "AIchat"); sessionStorage.setItem("activeIndexAI", "0"); } else { activeTab.value = tab; activeIndex.value = index; sessionStorage.setItem("activeTabAI", tab); sessionStorage.setItem("activeIndexAI", index.toString()); } setActiveTabIndex(index) console.log(tab, index, "tab, index"); setHeight(document.getElementById("testId")); // 给父组件发送窗口高度
};
// 修改 activeComponent 的计算逻辑
const activeComponent = computed(() => { if (isAnnouncementVisible.value) { return Announcement; } return activeTab.value === "AIchat" ? AIchat : AIfind; });
// 新增一个方法,调用时先判断是否处于 AIchat,若不在则跳转到 AIchat
const ensureAIchat = () => { setActiveTab("AIchat", 0, true); };
// 获取次数
const UserCount = computed(() => chatStore.UserCount)
const getCount = () => { console.log('点击了获取次数的按钮') }
// 深度思考
const isThinking = ref(true); const toggleThink = () => { isThinking.value = !isThinking.value; };
// 发送消息
const message = ref(""); // 传输对象
const messages = ref([]); // 信息加载状态
const isLoading = computed(() => { chatStore.isLoading });
// 添加用户消息
const updateMessage = (title) => { message.value = title; // console.log("updateMessage 的值:", title);
}; const sendMessage = async () => { if (localStorage.getItem('localToken') == null || localStorage.getItem('localToken') == '') { ElMessage.error('请先登录'); return; } isScrolling.value = false; // 调用 ensureAIchat 确保跳转到 AIchat 页面
ensureAIchat();
console.log(chatStore.isLoading, 'isLoading.value1111'); if (!message.value) return; if (chatStore.isLoading) return; chatStore.setLoading(true); console.log(chatStore.isLoading, 'isLoading.value2222');
const messageContent = message.value; // 重置消息输入框
message.value = "";
setTimeout(() => { console.log("延时后添加消息", messageContent); // 发送消息时,设置 isLoading 为 true
messages.value = [ ...messages.value, { sender: "user", content: messageContent, timestamp: new Date().toISOString(), } ]; console.log(messages.value, 'messages.value'); }, 200);
};
// 公告
// 引入公告组件
import Announcement from "./Announcement.vue";
// 新增一个变量来控制是否显示公告页面
const isAnnouncementVisible = ref(false);
const showAnnouncement = async () => { console.log("打开公告"); dataStore.isFeedback = false; // 显示用户反馈页面
isScrolling.value = false; //回复滚动到底部方法
setActiveTab('', -1); // 清空当前选中状态
isAnnouncementVisible.value = true; // 显示公告页面
};
// 跳转用户反馈
const showFeedback = () => { console.log("打开用户反馈"); dataStore.isFeedback = true; // 显示用户反馈页面
}
// 点击剩余次数会弹出的弹窗
// 新增一个 ref 来控制弹窗的显示与隐藏
const dialogVisible = ref(false); // 获取次数
const showCount = () => { console.log("显示剩余次数"); // 显示弹窗
dialogVisible.value = true; console.log("dialogVisible 的值:", dialogVisible.value); // 添加日志确认
};
// 保证发送消息时,滚动屏在底部
const tabContent = ref(null); const isScrolling = ref(false); //判断用户是否在滚动
const smoothScrollToBottom = async () => { // console.log('调用滚动到底部的方法')
// await nextTick();
const container = tabContent.value; // console.log(container, 'container')
// console.log(isScrolling.value, 'isScrolling.value')
if (!container) return;
await nextTick(); // 确保在DOM更新后执行
if (!isScrolling.value) { container.scrollTop = container.scrollHeight - container.offsetHeight; // container.scrollTop = container.scrollHeight;
// container.scrollTop = container.offsetHeight;
// container.scrollTop = container.scrollHeight + container.offsetHeight;
// console.log(container.scrollHeight, container.offsetHeight, container.scrollHeight - container.offsetHeight, container.scrollTop, "总长度", "可视长度", "位置")
}
}
const throttledSmoothScrollToBottom = _.throttle(smoothScrollToBottom, 500, { trailing: false });
watch( () => chatStore.messages, () => { // console.log('messages变化了')
throttledSmoothScrollToBottom(); // setTimeout(throttledSmoothScrollToBottom, 100);
}, { deep: true, immediate: true } );
watch( activeTab, async () => { console.log('activeTab变化了', activeTab.value) if (activeTab.value === 'AIchat') { isScrolling.value = false; //回复滚动到底部方法
setTimeout(() => { throttledSmoothScrollToBottom(); }, 100) } // setTimeout(throttledSmoothScrollToBottom, 100);
}, { deep: true, immediate: true } );
// 获取token的核心函数
const fnGetToken = () => { // console.log('进入fnGetToken')
window.JWready = (ress) => { // console.log('进入JWready')
try { ress = JSON.parse(ress) // console.log(ress, 'ress')
} catch (error) { console.log(error, 'fnGetToken error') } //platform为5是app端
// platform.value = ress.data.platform
// 处理平台判断
console.log(ress.data.platform, 'ress.data.platform') if (!ress.data.platform) { // 非App环境通过URL参数获取
localStorage.setItem('localToken', decodeURIComponent(String(getQueryVariable('token')))) // localStorage.setItem('localToken', "+SsksARQgUHIbIG3rRnnbZi0+fEeMx8pywnIlrmTxo5EOPR/wjWDV7w7+ZUseiBtf9kFa/atmNx6QfSpv5w")
} else { // App环境通过桥接获取
useAppBridge().packageFun( 'JWgetStorage', (response) => { const res = JSON.parse(response) // 解析返回的结果
localStorage.setItem('localToken', res.data) // localStorage.setItem('localToken', "+SsksARQgUHIbIG3rRnnbZi0+fEeMx8pywnIlrmTxo5EOPR/wjWDV7w7+ZUseiBtf9kFa/atmNx6QfSpv5w")
}, 5, { key: 'token' } ) } } // console.log('出来了')
// 触发App桥接
useAppBridge().packageFun('JWwebReady', () => { }, 5, {}) }
// 在setTimeout中延迟执行
setTimeout(() => { fnGetToken() }, 800)
const heightListener = () => { const tabContainer = tabContent.value; let befortop = 0;
const scrollHandler = () => { const aftertop = tabContainer.scrollTop;
// 新增底部判断逻辑
const isBottom = aftertop + tabContainer.offsetHeight + 50 >= tabContainer.scrollHeight;
if (activeTab.value === 'AIchat') { if (aftertop - befortop > 0) { // console.log('向下滚动');
isScrolling.value = true; } else { // console.log('向上滚动');
isScrolling.value = true; }
// 添加底部状态检测
if (isBottom) { // console.log('滚动到底部');
isScrolling.value = false; } } befortop = aftertop; };
// console.log(isScrolling.value, 'isScrolling.value')
tabContainer.addEventListener('scroll', scrollHandler); };
const throttledHeightListener = _.throttle(heightListener, 500, { trailing: false });
const goToRecharge = () => { console.log('点击充值') // http://39.101.133.168:8919/payment/recharge/index?
// url=http%3A%2F%2Flocalhost%3A8080%2FLiveActivity%2Fpck
// &platform=1
// &token=+S4h5QEE1hTIb4CxphrnbZi0+fEeMx8pywnIlrmTmo4QO6IolWnVWu5r+J4rKXMwK41UPfKqyIp+RvWmtM8
const userAgent = navigator.userAgent.toLowerCase(); const mobileKeywords = ['mobile', 'android', 'iphone', 'ipad', 'ipod']; const isMobile = mobileKeywords.some(keyword => userAgent.includes(keyword));
console.log(isMobile ? '手机' : '电脑')
const url = encodeURI("http://39.101.133.168:8857/aixiaocaishen/homePage") console.log(url, 'url') const platform = isMobile ? 2 : 1 const token = encodeURIComponent(localStorage.getItem('localToken')) console.log(token, 'token') const rechargeUrl = 'http://39.101.133.168:8919/payment/recharge/index?' + 'url=' + url + '&platform=' + platform + '&token=' + token console.log(rechargeUrl, 'rechargeUrl') window.location.href = rechargeUrl // window.open(rechargeUrl)
}
const adjustFooterPosition = (height) => { console.log('调整底部位置', height) const footer = document.querySelector('.el-footer'); const main = document.querySelector('.el-main'); const homePage = document.querySelector('.homepage'); const app = document.getElementById('app'); // Footer 的默认高度(假设为 60px) // 动态推高 Footer
// footer.style.bottom = `${keyboardHeight}px`;
// 给 Main 区域留出 Footer + 键盘的空间
homePage.style.height = `${height}px`; // app.style.height = `${height}px`;
void homePage.offsetHeight;
const html = document.querySelector('html'); const body = document.querySelector('body');
html.style.height = `${height}px`; body.style.height = `${height}px`;
html.scrollTop = 0;
setTimeout(() => { // 隐藏滚动条
html.style.overflow = 'hidden'; body.style.overflow = 'hidden'; }, 200)
// console.log(html.offsetHeight, 'html')
// console.log(html.clientHeight, 'html')
// console.log(html.scrollHeight, 'htmlScrollHeight')
// console.log(body.clientHeight, 'body')
// console.log(body.scrollHeight, 'bodyScrollHeight')
// console.log(homePage.offsetHeight, 'homePage')
// console.log(homePage.clientHeight, 'homePageClientHeight')
// console.log(homePage.scrollHeight, 'homePageScrollHeight')
// console.log(window.innerHeight, 'window.innerHeight')
// console.log(window.visualViewport.height, 'window.visualViewport.height')
// console.log(main.offsetHeight, 'main')
// console.log(main.clientHeight, 'mainClientHeight')
// console.log(main.scrollHeight, 'mainScrollHeight')
};
const onFocus = function () { const visualViewport = window.visualViewport // 获取可视区域高度
setTimeout(() => { console.log('输入框聚焦')
console.log(visualViewport.height, 'visualViewport.height') const keyboardHeight = window.innerHeight - visualViewport.height console.log(window.innerHeight, 'window.innerHeight') console.log(keyboardHeight, 'keyboardHeight')
adjustFooterPosition(visualViewport.height) }, 200) }
const onBlur = function () { const visualViewport = window.visualViewport setTimeout(() => { console.log('输入框失焦')
const keyboardHeight = window.innerHeight - visualViewport.height console.log(window.innerHeight, 'window.innerHeight') console.log(visualViewport.height, 'visualViewport.height') console.log(keyboardHeight, 'keyboardHeight') adjustFooterPosition(visualViewport.height) }, 200) }
window.addEventListener('resize', () => { // 检测是否为iOS设备
const isIOS = /iPhone|iPad|iPod|ios/i.test(navigator.userAgent); console.log('是否为iOS设备:', isIOS); if (!isIOS) { console.log('窗口大小变化') const homePage = document.querySelector('.homepage'); homePage.style.height = `${window.innerHeight}px`; } });
// 禁用全局触摸滚动
document.addEventListener('touchmove', (e) => { if (!dataStore.isFeedback) { // 判断触摸目标是否在可滚动区域内
const isScrollableArea = e.target.closest('.tab-content');
// 如果不在可滚动区域,则阻止滚动
if (!isScrollableArea) { e.preventDefault(); } } }, { passive: false });
onMounted(async () => { const isPhone = /phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone/i.test( navigator.userAgent ) !isPhone && localStorage.setItem('localToken', decodeURIComponent(String(getQueryVariable('token')))) setHeight(document.getElementById("testId")); // 给父组件发送窗口高度
// 获取次数
await chatStore.getUserCount(); // 滚动到底部
throttledSmoothScrollToBottom(); // 监听页面高度
throttledHeightListener();
// 添加输入框焦点处理
// handleInputFocus();
// 初始化视口高度变量
// updateAppHeight();
}) </script>
<template> <div class="homepage" id="testId"> <el-container v-if="!dataStore.isFeedback"> <!-- AI小财神头部: logo 次数 公告 --> <el-header class="homepage-head"> <!-- logo --> <div class="homepage-logo"> <img :src="logo" alt="图片加载失败" class="logo1" /> <img :src="madeInHL" alt="图片加载失败" class="logo2" /> </div>
<div class="homepage-right-group"> <div class="count-badge" @click="showCount"> <img :src="getCountAll" class="action-btn" /> <div class="count-number">{{ UserCount }}次</div> </div> <img :src="announcementBtn" class="announcement-btn action-btn" @click="showAnnouncement" /> <img :src="feedbackBtn" class="announcement-btn action-btn" @click="showFeedback" /> </div> </el-header>
<!-- 主体部分:小人 问题轮询图 对话内容 --> <el-main class="homepage-body">
<div class="main-wrapper"> <section class="tab-section"> <div class="tab-container"> <div v-for="(tab, index) in tabs" :key="tab.name" @click="setActiveTab(tab.name, index)" :class="['tab-item', { active: activeIndex === index && !isAnnouncementVisible }]"> <span>{{ tab.label }}</span> </div> </div> </section> <div class="tab-content" ref="tabContent"> <component :is="activeComponent" :messages="messages" @updateMessage="updateMessage" @sendMessage="sendMessage" @ensureAIchat="ensureAIchat" /> </div> </div>
</el-main> <!-- 尾部: 问题输入框 深度思考 多语言 语音播报 --> <el-footer class="homepage-footer" id="input"> <!-- 第一行按钮 --> <div class="footer-first-line"> <div class="left-group"> <img v-if="isThinking" :src="thinkActive" @click="toggleThink" class="action-btn" /> <img v-else :src="thinkNoActive" @click="toggleThink" class="action-btn" /> <img :src="languageBtn" @click="changeLanguage" class="action-btn" /> <img v-if="isVoice" :src="voice" @click="toggleVoice" class="action-btn" /> <img v-else :src="voiceNoActive" @click="toggleVoice" class="action-btn" /> </div> <img v-if="!chatStore.isLoading" :src="sendBtn" @click="sendMessage" class="action-btn send-btn" /> <div v-else> <el-icon class="is-loading"> <Loading /> </el-icon> </div> </div>
<!-- 第二行输入框 --> <div class="footer-second-line"> <img :src="msgBtn" class="msg-icon" /> <el-input type="textarea" v-model="message" @focus="onFocus" @blur="onBlur" :autosize="{ minRows: 1, maxRows: 4 }" placeholder="给AI小财神发消息..." class="msg-input" @keydown.enter.exact.prevent="isLoading ? null : sendMessage()" resize="none"> </el-input> </div> </el-footer> </el-container> <el-container v-else> <el-header class="homepage-head"> <!-- logo --> <div class="homepage-logo"> <img :src="logo" alt="图片加载失败" class="logo1" /> <img :src="madeInHL" alt="图片加载失败" class="logo2" /> </div>
<div class="homepage-right-group"> <div class="count-badge" @click="showCount"> <img :src="getCountAll" class="action-btn" /> <div class="count-number">{{ UserCount }}次</div> </div> <img :src="announcementBtn" class="announcement-btn action-btn" @click="showAnnouncement" /> <img :src="feedbackBtn" class="announcement-btn action-btn" @click="showFeedback" /> </div> </el-header>
<!-- 主体部分:小人 问题轮询图 对话内容 --> <el-main class="homepage-body"> <feedback :is="Feedback" /> </el-main> </el-container> <!-- 弹窗 --> <!-- 新增弹窗组件 --> <el-dialog v-model="dialogVisible" max-width="65%"> <!-- 自定义标题插槽,实现居中显示 --> <template #header> <div style="text-align: center"> <span>活动规则</span> </div> </template> <!-- 中间内容部分 --> <div class="ruleContent"> <p> 试运行期间,AI小财神可以检索全市场数据 </p> <p> (每个市场20支股票,股票详情参见【公告】页面), </p> <p> 弘历会员每人每日拥有10次检索机会! </p> </div> <template #footer> <!-- 添加一个div来包裹按钮,并设置样式使其居中 --> <div style="text-align: center"> <el-button style="background-color: orange; color: white; border: none" @click="goToRecharge"> 去充值 </el-button> </div> </template> </el-dialog> </div> </template>
<style scoped> /* 标签栏 */ .tab-container { display: flex; gap: 30px; margin-bottom: 10px; padding: 0 20px; justify-content: flex-end; /* 新增右对齐 */ }
.tab-item { cursor: pointer; padding: 8px 12px; font-size: clamp(18px, 3vw, 20px); color: #999; transition: all 0.3s; border-bottom: 2px solid transparent; font-weight: bold; }
.tab-item.active { color: #000; border-color: #000; }
.tab-item:not(.active):hover { color: #666; }
.tab-content { overflow-y: auto; overflow-x: hidden; scroll-behavior: smooth; height: 100%; /* 添加平滑滚动效果 */ }
@media (max-width: 768px) { .tab-container { gap: 15px; padding: 0 10px; }
.tab-item { font-size: clamp(14px, 3vw, 16px); padding: 6px 10px; } } </style>
<style scoped> html { height: 100dvh; overflow: hidden !important; position: fixed; margin: 0; padding: 0; -webkit-overflow-scrolling: auto; /* 禁用 iOS 弹性滚动 */ }
body { height: 100dvh; overflow: clip; margin: 0; padding: 0; -webkit-overflow-scrolling: auto; /* 禁用 iOS 弹性滚动 */ position: fixed; }
#app { overflow: hidden; height: 100%; margin: 0; padding: 0; }
.homepage { /* height: var(--app-height, 100vh); */ height: var(--app-height, 100vh); margin: 0 auto; background-image: url(/src/assets/img/homePage/bk.png); background-size: 100% 100%; background-repeat: no-repeat; background-position: center; display: flex; overflow: hidden; position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; /* -webkit-overflow-scrolling: touch; */ }
.homepage .el-container { height: 100%; flex-direction: column; display: flex; width: 100%; overflow: hidden; /* 防止容器滚动 */ }
.el-container .el-header { flex-shrink: 0; /* 防止头部压缩 */ height: auto; min-height: 60px; padding: 5px 0; position: sticky; top: 0; z-index: 10; /* background-color: rgba(255, 255, 255, 0.9); */ }
.el-container .el-main { flex: 1; /* 自动占据剩余空间 */ overflow: hidden; /* 主容器不滚动 */ display: flex; flex-direction: column; min-height: 0; /* 允许内容区域缩小 */ position: relative; height: auto; }
.el-container .el-footer { flex-shrink: 0; height: auto; min-height: 70px; position: sticky; bottom: 0; z-index: 20; background-color: rgba(211, 24, 24, 0); box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); -webkit-transform: translateZ(0); transform: translateZ(0); padding-bottom: env(safe-area-inset-bottom, 0); /* 适配iPhone X及以上的底部安全区域 */ }
.homepage-head { padding: 0px; display: flex; position: relative; justify-content: space-between; width: 100%; }
.homepage-right-group { display: flex; gap: 8px; align-items: center; margin-left: auto; margin-right: 20px; }
.homepage-right-group .action-btn { height: 40px; }
.homepage-right-group .count-badge { position: relative; cursor: pointer; }
.homepage-right-group .count-badge .count-number { position: absolute; top: 6px; right: 29px; color: #573dfc; font-size: 12px; font-weight: bold; }
.homepage-right-group .announcement-btn { cursor: pointer; transition: transform 0.3s; }
.homepage-right-group .announcement-btn:hover { transform: scale(1.3); }
.homepage-body { padding: 0px; display: flex; flex-direction: column; flex: 1; min-height: 0; /* 允许内容区域缩小 */ overflow: hidden; }
.main-wrapper { height: 100%; display: flex; flex-direction: column; flex: 1; min-height: 0; /* 允许内容区域缩小 */ }
.tab-section { flex-shrink: 0; /* 禁止伸缩 */ }
.tab-content { flex: 1; overflow-y: auto; min-height: 0; /* 关键:允许内容收缩 */ }
.homepage-logo { height: 100%; width: fit-content; display: flex; flex-direction: column; align-items: center; justify-content: center; margin-left: 20px; margin-right: auto; position: relative; }
@media (max-width: 768px) { .homepage-logo { margin-left: 10px; left: 0; } }
.logo1 { width: 120px; height: auto; margin-bottom: 8px; }
.logo2 { width: 80px; height: auto; }
/* 尾部 */ .homepage-footer { display: flex; flex-direction: column; gap: 5px; flex-shrink: 0; width: 100%; background-color: #fff; }
.footer-first-line { display: flex; justify-content: space-between; align-items: center; padding: 5px 15px; flex-shrink: 0; }
.left-group { display: flex; gap: 15px; }
.action-btn { cursor: pointer; transition: transform 0.2s; height: 28px; }
.action-btn:hover { transform: scale(1.05); }
.send-btn { margin-left: auto; margin-right: 5px; }
.footer-second-line { position: relative; display: flex; align-items: center; padding: 5px 15px 10px; flex-shrink: 0; }
.msg-icon { position: absolute; left: 25px; top: 50%; transform: translateY(-50%); width: 24px; z-index: 999; }
.msg-input:deep(.el-textarea__inner) { border: none !important; box-shadow: none !important; overflow-y: auto !important; transition: all 0.2s ease-out; padding: 8px 20px 8px 45px !important; resize: none !important; line-height: 1.5 !important; max-height: 100px !important; }
.msg-input { min-height: 34px; width: 100%; border-radius: 20px; font-size: 16px; transition: all 0.3s ease-out; overflow-y: hidden; box-shadow: 0 4px 12px rgba(89, 24, 241, 0.3); background: #fff; z-index: 5; /* 添加iOS设备特殊处理 */ -webkit-appearance: none; appearance: none; }
.msg-input:focus { outline: none; }
@media (max-width: 768px) { .action-btn { height: 28px; }
.footer-second-line { padding: 5px 10px 10px; }
.msg-input { font-size: 16px; } }
.ruleContent{ text-align: center; }
</style>
|