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.

1136 lines
30 KiB

4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
4 months ago
4 months ago
2 weeks ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
3 months ago
3 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
  1. <script setup>
  2. // 导入
  3. import { ref, computed, onMounted, watch, nextTick, onUnmounted } from "vue";
  4. import { setHeight } from "../utils/setHeight";
  5. import { getUserCountAPI } from "../api/AIxiaocaishen";
  6. import { ElMessage } from "element-plus";
  7. import AIchat from "./AIchat.vue";
  8. import AIfind from "./AIfind.vue";
  9. import Feedback from "./Feedback.vue";
  10. import { useAppBridge } from "../assets/js/useAppBridge.js";
  11. import { useDataStore } from "@/store/dataList.js";
  12. import { useChatStore } from "../store/chat";
  13. import { useEmotionAudioStore } from "../store/emotionAudio";
  14. import { useAudioStore } from "../store/audio";
  15. import _ from "lodash";
  16. import logo from "../assets/img/homePage/logo.png";
  17. import madeInHL from "../assets/img/homePage/madeInHL.png";
  18. import getCountAll from "../assets/img/homePage/get-count-all.png";
  19. import announcementBtn from "../assets/img/homePage/announcement.png";
  20. import thinkActive from "../assets/img/homePage/tail/think-active.png";
  21. import thinkNoActive from "../assets/img/homePage/tail/think-no-active.png";
  22. import languageBtn from "../assets/img/homePage/tail/language.png";
  23. import dbqbButton01 from "../assets/img/AiEmotion/dbqb-button01.png";
  24. import dbqbButton02 from "../assets/img/AiEmotion/dbqb-button02.png";
  25. import emotionButton01 from "../assets/img/AiEmotion/emotion-button01.png";
  26. import emotionButton02 from "../assets/img/AiEmotion/emotion-button02.png";
  27. import voice from "../assets/img/homePage/tail/voice.png";
  28. import voiceNoActive from "../assets/img/homePage/tail/voice-no-active.png";
  29. import sendBtn from "../assets/img/homePage/tail/send.png";
  30. import msgBtn from "../assets/img/homePage/tail/msg.png";
  31. import feedbackBtn from "../assets/img/Feedback/feedbackBtn.png";
  32. import AiEmotion from "./AiEmotion.vue";
  33. import VConsole from "vconsole";
  34. const vConsole = new VConsole();
  35. // 获取 AiEmotion 组件的 ref
  36. const aiEmotionRef = ref(null);
  37. // import { useUserStore } from "../store/userPessionCode.js";
  38. const { getQueryVariable, setActiveTabIndex } = useDataStore();
  39. const dataStore = useDataStore();
  40. const chatStore = useChatStore();
  41. // 变量
  42. // 音频管理
  43. const emotionAudioStore = useEmotionAudioStore();
  44. const audioStore = useAudioStore();
  45. // 根据当前页面类型获取对应的音频store
  46. const getCurrentAudioStore = () => {
  47. return activeTab.value === "AiEmotion" ? emotionAudioStore : audioStore;
  48. };
  49. const isVoice = computed(() => {
  50. const currentStore = getCurrentAudioStore();
  51. return currentStore.isVoiceEnabled;
  52. });
  53. const toggleVoice = () => {
  54. const currentStore = getCurrentAudioStore();
  55. if (!currentStore.isVoiceEnabled) {
  56. // 如果语音功能关闭,先开启语音功能
  57. currentStore.toggleVoice();
  58. } else {
  59. // 如果语音功能开启,则切换播放/暂停状态
  60. if (currentStore.currentAudioUrl || currentStore.ttsUrl) {
  61. // 有音频时切换播放/暂停
  62. currentStore.togglePlayPause();
  63. } else {
  64. // 没有音频时关闭语音功能
  65. currentStore.toggleVoice();
  66. }
  67. }
  68. };
  69. // 将默认值改为从 sessionStorage 中获取,如果没有则使用默认值 'aifindCow'为第一个默认tab
  70. const activeTab = ref(sessionStorage.getItem("activeTabAI") || "AIchat");
  71. const activeIndex = ref(
  72. parseInt(sessionStorage.getItem("activeIndexAI") || "0")
  73. );
  74. const tabs = computed(() => [
  75. {
  76. name: "AIchat",
  77. label: "夺宝奇兵大模型",
  78. },
  79. // {
  80. // name: "AIfind",
  81. // label: "发现",
  82. // },
  83. {
  84. name: "AiEmotion",
  85. label: "AI情绪大模型",
  86. },
  87. ]);
  88. // 修改 setActiveTab 方法,添加一个可选参数 forceAIchat
  89. const setActiveTab = (tab, index, forceAIchat = false) => {
  90. isScrolling.value = false; //回复滚动到底部方法
  91. isAnnouncementVisible.value = false;
  92. if (forceAIchat && activeTab.value !== "AIchat") {
  93. activeTab.value = "AIchat";
  94. activeIndex.value = 0;
  95. sessionStorage.setItem("activeTabAI", "AIchat");
  96. sessionStorage.setItem("activeIndexAI", "0");
  97. } else {
  98. activeTab.value = tab;
  99. activeIndex.value = index;
  100. sessionStorage.setItem("activeTabAI", tab);
  101. sessionStorage.setItem("activeIndexAI", index.toString());
  102. }
  103. setActiveTabIndex(index);
  104. console.log(tab, index, "tab, index");
  105. setHeight(document.getElementById("testId")); // 给父组件发送窗口高度
  106. };
  107. // 修改 activeComponent 的计算逻辑
  108. const activeComponent = computed(() => {
  109. if (isAnnouncementVisible.value) {
  110. return Announcement;
  111. }
  112. if (activeTab.value === "AIchat") {
  113. return AIchat;
  114. } else if (activeTab.value === "AIfind") {
  115. return AIfind;
  116. } else if (activeTab.value === "AiEmotion") {
  117. return AiEmotion; // 新增逻辑
  118. }
  119. });
  120. // 新增一个方法,调用时先判断是否处于 AIchat,若不在则跳转到 AIchat
  121. const ensureAIchat = () => {
  122. setActiveTab("AIchat", 0, true);
  123. };
  124. // 获取次数
  125. const UserCount = computed(() => chatStore.UserCount);
  126. const getCount = () => {
  127. console.log("点击了获取次数的按钮");
  128. };
  129. // 深度思考
  130. const isThinking = ref(true);
  131. const toggleThink = () => {
  132. isThinking.value = !isThinking.value;
  133. };
  134. // 发送消息
  135. const message = ref("");
  136. // 传输对象
  137. const messages = ref([]);
  138. // 信息加载状态
  139. const isLoading = computed(() => {
  140. chatStore.isLoading;
  141. });
  142. // 添加用户消息
  143. const updateMessage = (title) => {
  144. message.value = title;
  145. // console.log("updateMessage 的值:", title);
  146. };
  147. const sendMessage = async () => {
  148. if (
  149. localStorage.getItem("localToken") == null ||
  150. localStorage.getItem("localToken") == ""
  151. ) {
  152. ElMessage.error("请先登录");
  153. return;
  154. }
  155. // 检查输入内容是否为空
  156. if (!message.value || !message.value.trim()) {
  157. ElMessage.warning("输入内容不能为空");
  158. return;
  159. }
  160. isScrolling.value = false;
  161. // 判断当前是否为 AiEmotion 组件
  162. if (activeTab.value === "AiEmotion") {
  163. // 调用 AiEmotion 组件的 handleSendMessage 方法
  164. aiEmotionRef.value?.handleSendMessage(message.value);
  165. message.value = ""; // 清空输入框
  166. return;
  167. }
  168. // 调用 ensureAIchat 确保跳转到 AIchat 页面
  169. ensureAIchat();
  170. console.log(chatStore.isLoading, "isLoading.value1111");
  171. if (chatStore.isLoading) return;
  172. chatStore.setLoading(true);
  173. console.log(chatStore.isLoading, "isLoading.value2222");
  174. const messageContent = message.value;
  175. // 重置消息输入框
  176. message.value = "";
  177. setTimeout(() => {
  178. console.log("延时后添加消息", messageContent);
  179. // 发送消息时,设置 isLoading 为 true
  180. messages.value = [
  181. ...messages.value,
  182. {
  183. sender: "user",
  184. content: messageContent,
  185. timestamp: new Date().toISOString(),
  186. },
  187. ];
  188. console.log(messages.value, "messages.value");
  189. }, 200);
  190. };
  191. // 公告
  192. // 引入公告组件
  193. import Announcement from "./Announcement.vue";
  194. // 新增一个变量来控制是否显示公告页面
  195. const isAnnouncementVisible = ref(false);
  196. const showAnnouncement = async () => {
  197. console.log("打开公告");
  198. dataStore.isFeedback = false; // 显示用户反馈页面
  199. isScrolling.value = false; //回复滚动到底部方法
  200. setActiveTab("", -1); // 清空当前选中状态
  201. isAnnouncementVisible.value = true; // 显示公告页面
  202. };
  203. // 跳转用户反馈
  204. const showFeedback = () => {
  205. console.log("打开用户反馈");
  206. dataStore.isFeedback = true; // 显示用户反馈页面
  207. };
  208. // 点击剩余次数会弹出的弹窗
  209. // 新增一个 ref 来控制弹窗的显示与隐藏
  210. const dialogVisible = ref(false);
  211. // 获取次数
  212. const showCount = () => {
  213. console.log("显示剩余次数");
  214. // 显示弹窗
  215. dialogVisible.value = true;
  216. console.log("dialogVisible 的值:", dialogVisible.value); // 添加日志确认
  217. };
  218. // 保证发送消息时,滚动屏在底部
  219. const tabContent = ref(null);
  220. const isScrolling = ref(false); //判断用户是否在滚动
  221. const smoothScrollToBottom = async () => {
  222. console.log("调用滚动到底部的方法");
  223. // await nextTick();
  224. const container = tabContent.value;
  225. // console.log(container, 'container')
  226. // console.log(isScrolling.value, 'isScrolling.value')
  227. if (!container) return;
  228. await nextTick(); // 确保在DOM更新后执行
  229. if (!isScrolling.value) {
  230. container.scrollTop = container.scrollHeight - container.offsetHeight;
  231. // container.scrollTop = container.scrollHeight;
  232. // container.scrollTop = container.offsetHeight;
  233. // container.scrollTop = container.scrollHeight + container.offsetHeight;
  234. // console.log(container.scrollHeight, container.offsetHeight, container.scrollHeight - container.offsetHeight, container.scrollTop, "总长度", "可视长度", "位置")
  235. }
  236. };
  237. const throttledSmoothScrollToBottom = _.throttle(smoothScrollToBottom, 300, {
  238. trailing: false,
  239. });
  240. watch(
  241. () => chatStore.messages,
  242. () => {
  243. // console.log('messages变化了')
  244. throttledSmoothScrollToBottom();
  245. // setTimeout(throttledSmoothScrollToBottom, 100);
  246. },
  247. { deep: true, immediate: true }
  248. );
  249. watch(
  250. activeTab,
  251. async () => {
  252. console.log("activeTab变化了", activeTab.value);
  253. if (activeTab.value === "AIchat") {
  254. isScrolling.value = false; //回复滚动到底部方法
  255. setTimeout(() => {
  256. throttledSmoothScrollToBottom();
  257. }, 100);
  258. }
  259. // setTimeout(throttledSmoothScrollToBottom, 100);
  260. },
  261. { deep: true, immediate: true }
  262. );
  263. // 获取token的核心函数
  264. const fnGetToken = () => {
  265. // console.log('进入fnGetToken')
  266. window.JWready = (ress) => {
  267. // console.log('进入JWready')
  268. try {
  269. ress = JSON.parse(ress);
  270. // console.log(ress, 'ress')
  271. } catch (error) {
  272. console.log(error, "fnGetToken error");
  273. } //platform为5是app端
  274. // platform.value = ress.data.platform
  275. // 处理平台判断
  276. console.log(ress.data.platform, "ress.data.platform");
  277. if (!ress.data.platform) {
  278. // 非App环境通过URL参数获取
  279. localStorage.setItem(
  280. "localToken",
  281. decodeURIComponent(String(getQueryVariable("token")))
  282. );
  283. // localStorage.setItem('localToken', "+SsksARQgUHIbIG3rRnnbZi0+fEeMx8pywnIlrmTxo5EOPR/wjWDV7w7+ZUseiBtf9kFa/atmNx6QfSpv5w")
  284. } else {
  285. // App环境通过桥接获取
  286. useAppBridge().packageFun(
  287. "JWgetStorage",
  288. (response) => {
  289. const res = JSON.parse(response); // 解析返回的结果
  290. localStorage.setItem("localToken", res.data);
  291. // localStorage.setItem('localToken', "+SsksARQgUHIbIG3rRnnbZi0+fEeMx8pywnIlrmTxo5EOPR/wjWDV7w7+ZUseiBtf9kFa/atmNx6QfSpv5w")
  292. },
  293. 5,
  294. {
  295. key: "token",
  296. }
  297. );
  298. }
  299. };
  300. // console.log('出来了')
  301. // 触发App桥接
  302. useAppBridge().packageFun("JWwebReady", () => {}, 5, {});
  303. };
  304. // 在setTimeout中延迟执行
  305. setTimeout(() => {
  306. fnGetToken();
  307. }, 800);
  308. const heightListener = () => {
  309. const tabContainer = tabContent.value;
  310. let befortop = 0;
  311. const scrollHandler = () => {
  312. const aftertop = tabContainer.scrollTop;
  313. // 新增底部判断逻辑
  314. const isBottom =
  315. aftertop + tabContainer.offsetHeight + 70 >= tabContainer.scrollHeight;
  316. if (activeTab.value === "AIchat") {
  317. if (aftertop - befortop > 0) {
  318. // console.log('向下滚动');
  319. isScrolling.value = true;
  320. } else {
  321. // console.log('向上滚动');
  322. isScrolling.value = true;
  323. }
  324. // 添加底部状态检测
  325. if (isBottom) {
  326. // console.log('滚动到底部');
  327. isScrolling.value = false;
  328. }
  329. }
  330. befortop = aftertop;
  331. };
  332. // console.log(isScrolling.value, 'isScrolling.value')
  333. tabContainer.addEventListener("scroll", scrollHandler);
  334. };
  335. const throttledHeightListener = _.throttle(heightListener, 500, {
  336. trailing: false,
  337. });
  338. const goToRecharge = () => {
  339. console.log("点击充值");
  340. // http://39.101.133.168:8919/payment/recharge/index?
  341. // url=http%3A%2F%2Flocalhost%3A8080%2FLiveActivity%2Fpck
  342. // &platform=1
  343. // &token=+S4h5QEE1hTIb4CxphrnbZi0+fEeMx8pywnIlrmTmo4QO6IolWnVWu5r+J4rKXMwK41UPfKqyIp+RvWmtM8
  344. const userAgent = navigator.userAgent.toLowerCase();
  345. const mobileKeywords = ["mobile", "android", "iphone", "ipad", "ipod"];
  346. const isMobile = mobileKeywords.some((keyword) =>
  347. userAgent.includes(keyword)
  348. );
  349. console.log(isMobile ? "手机" : "电脑");
  350. const url = encodeURI("http://39.101.133.168:8857/aixiaocaishen/homePage");
  351. console.log(url, "url");
  352. const platform = isMobile ? 2 : 1;
  353. const token = encodeURIComponent(localStorage.getItem("localToken"));
  354. console.log(token, "token");
  355. const rechargeUrl =
  356. "http://39.101.133.168:8919/payment/recharge/index?" +
  357. "url=" +
  358. url +
  359. "&platform=" +
  360. platform +
  361. "&token=" +
  362. token;
  363. console.log(rechargeUrl, "rechargeUrl");
  364. window.location.href = rechargeUrl;
  365. // window.open(rechargeUrl)
  366. };
  367. const adjustFooterPosition = (height) => {
  368. console.log("调整底部位置", height);
  369. const footer = document.querySelector(".el-footer");
  370. const main = document.querySelector(".el-main");
  371. const homePage = document.querySelector(".homepage");
  372. const app = document.getElementById("app");
  373. // Footer 的默认高度(假设为 60px) // 动态推高 Footer
  374. // footer.style.bottom = `${keyboardHeight}px`;
  375. // 给 Main 区域留出 Footer + 键盘的空间
  376. homePage.style.height = `${height}px`;
  377. // app.style.height = `${height}px`;
  378. void homePage.offsetHeight;
  379. const html = document.querySelector("html");
  380. const body = document.querySelector("body");
  381. html.style.height = `${height}px`;
  382. body.style.height = `${height}px`;
  383. html.scrollTop = 0;
  384. setTimeout(() => {
  385. // 隐藏滚动条
  386. html.style.overflow = "hidden";
  387. body.style.overflow = "hidden";
  388. }, 200);
  389. // console.log(html.offsetHeight, 'html')
  390. // console.log(html.clientHeight, 'html')
  391. // console.log(html.scrollHeight, 'htmlScrollHeight')
  392. // console.log(body.clientHeight, 'body')
  393. // console.log(body.scrollHeight, 'bodyScrollHeight')
  394. // console.log(homePage.offsetHeight, 'homePage')
  395. // console.log(homePage.clientHeight, 'homePageClientHeight')
  396. // console.log(homePage.scrollHeight, 'homePageScrollHeight')
  397. // console.log(window.innerHeight, 'window.innerHeight')
  398. // console.log(window.visualViewport.height, 'window.visualViewport.height')
  399. // console.log(main.offsetHeight, 'main')
  400. // console.log(main.clientHeight, 'mainClientHeight')
  401. // console.log(main.scrollHeight, 'mainScrollHeight')
  402. };
  403. const onFocus = function () {
  404. const visualViewport = window.visualViewport;
  405. // 获取可视区域高度
  406. setTimeout(() => {
  407. console.log("输入框聚焦");
  408. console.log(visualViewport.height, "visualViewport.height");
  409. const keyboardHeight = window.innerHeight - visualViewport.height;
  410. console.log(window.innerHeight, "window.innerHeight");
  411. console.log(keyboardHeight, "keyboardHeight");
  412. adjustFooterPosition(visualViewport.height);
  413. }, 200);
  414. };
  415. const onBlur = function () {
  416. const visualViewport = window.visualViewport;
  417. setTimeout(() => {
  418. console.log("输入框失焦");
  419. const keyboardHeight = window.innerHeight - visualViewport.height;
  420. console.log(window.innerHeight, "window.innerHeight");
  421. console.log(visualViewport.height, "visualViewport.height");
  422. console.log(keyboardHeight, "keyboardHeight");
  423. adjustFooterPosition(visualViewport.height);
  424. }, 200);
  425. };
  426. window.addEventListener("resize", () => {
  427. // 检测是否为iOS设备
  428. const isIOS = /iPhone|iPad|iPod|ios/i.test(navigator.userAgent);
  429. console.log("是否为iOS设备:", isIOS);
  430. if (!isIOS) {
  431. console.log("窗口大小变化");
  432. const homePage = document.querySelector(".homepage");
  433. homePage.style.height = `${window.innerHeight}px`;
  434. }
  435. });
  436. // 禁用全局触摸滚动
  437. document.addEventListener(
  438. "touchmove",
  439. (e) => {
  440. if (!dataStore.isFeedback) {
  441. // 判断触摸目标是否在可滚动区域内
  442. const isScrollableArea = e.target.closest(".tab-content");
  443. // 如果不在可滚动区域,则阻止滚动
  444. if (!isScrollableArea) {
  445. e.preventDefault();
  446. }
  447. }
  448. },
  449. { passive: false }
  450. );
  451. onMounted(async () => {
  452. // const isPhone =
  453. // /phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone/i.test(
  454. // navigator.userAgent
  455. // );
  456. // !isPhone &&
  457. // localStorage.setItem(
  458. // "localToken",
  459. // decodeURIComponent(String(getQueryVariable("token")))
  460. // );
  461. setHeight(document.getElementById("testId")); // 给父组件发送窗口高度
  462. // 获取次数
  463. await chatStore.getUserCount();
  464. // 滚动到底部
  465. throttledSmoothScrollToBottom();
  466. // 监听页面高度
  467. throttledHeightListener();
  468. // 添加输入框焦点处理
  469. // handleInputFocus();
  470. // 初始化视口高度变量
  471. // updateAppHeight();
  472. });
  473. </script>
  474. <template>
  475. <div class="homepage" id="testId">
  476. <el-container v-if="!dataStore.isFeedback">
  477. <!-- AI小财神头部 logo 次数 公告 -->
  478. <el-header class="homepage-head">
  479. <!-- logo -->
  480. <div class="homepage-logo">
  481. <img :src="logo" alt="图片加载失败" class="logo1" />
  482. <!-- <img :src="madeInHL" alt="图片加载失败" class="logo2" /> -->
  483. </div>
  484. <div class="homepage-right-group">
  485. <div class="count-badge" @click="showCount">
  486. <img :src="getCountAll" class="action-btn" />
  487. <div class="count-number">{{ UserCount }}</div>
  488. </div>
  489. <img
  490. :src="announcementBtn"
  491. class="announcement-btn action-btn"
  492. @click="showAnnouncement"
  493. />
  494. <img
  495. :src="feedbackBtn"
  496. class="announcement-btn action-btn"
  497. @click="showFeedback"
  498. />
  499. </div>
  500. </el-header>
  501. <!-- 主体部分小人 问题轮询图 对话内容 -->
  502. <el-main class="homepage-body">
  503. <div class="main-wrapper">
  504. <section class="tab-section">
  505. <div class="tab-container">
  506. <div
  507. v-for="(tab, index) in tabs"
  508. :key="tab.name"
  509. @click="setActiveTab(tab.name, index)"
  510. :class="[
  511. 'tab-item',
  512. { active: activeIndex === index && !isAnnouncementVisible },
  513. ]"
  514. >
  515. <span>{{ tab.label }}</span>
  516. </div>
  517. </div>
  518. </section>
  519. <div class="tab-content" ref="tabContent">
  520. <component
  521. :is="activeComponent"
  522. :messages="messages"
  523. @updateMessage="updateMessage"
  524. @sendMessage="sendMessage"
  525. @ensureAIchat="ensureAIchat"
  526. ref="aiEmotionRef"
  527. />
  528. </div>
  529. </div>
  530. </el-main>
  531. <!-- 尾部 问题输入框 深度思考 多语言 语音播报 -->
  532. <el-footer class="homepage-footer" id="input">
  533. <!-- 第一行按钮 -->
  534. <div class="footer-first-line">
  535. <div class="left-group">
  536. <!-- <img v-if="isThinking" :src="thinkActive" @click="toggleThink" class="action-btn" />
  537. <img v-else :src="thinkNoActive" @click="toggleThink" class="action-btn" />
  538. <img :src="languageBtn" @click="changeLanguage" class="action-btn" /> -->
  539. <!-- 夺宝奇兵大模型按钮 -->
  540. <img
  541. :src="activeTab === 'AIchat' ? dbqbButton01 : dbqbButton02"
  542. @click="setActiveTab('AIchat', 0)"
  543. class="action-btn model-btn"
  544. alt="夺宝奇兵大模型"
  545. />
  546. <!-- AI情绪大模型按钮 -->
  547. <img
  548. :src="
  549. activeTab === 'AiEmotion' ? emotionButton01 : emotionButton02
  550. "
  551. @click="setActiveTab('AiEmotion', 1)"
  552. class="action-btn model-btn"
  553. alt="AI情绪大模型"
  554. />
  555. <img
  556. v-if="
  557. getCurrentAudioStore().isVoiceEnabled &&
  558. !getCurrentAudioStore().isPlaying
  559. "
  560. :src="voice"
  561. @click="toggleVoice"
  562. class="action-btn"
  563. />
  564. <img
  565. v-else-if="
  566. getCurrentAudioStore().isVoiceEnabled &&
  567. getCurrentAudioStore().isPlaying
  568. "
  569. :src="voice"
  570. @click="toggleVoice"
  571. class="action-btn"
  572. style="opacity: 0.7; animation: pulse 1.5s infinite"
  573. />
  574. <img
  575. v-else
  576. :src="voiceNoActive"
  577. @click="toggleVoice"
  578. class="action-btn"
  579. />
  580. </div>
  581. </div>
  582. <!-- 第二行输入框 -->
  583. <div class="footer-second-line">
  584. <img :src="msgBtn" class="msg-icon" />
  585. <el-input
  586. type="textarea"
  587. v-model="message"
  588. @focus="onFocus"
  589. @blur="onBlur"
  590. :autosize="{ minRows: 1, maxRows: 4 }"
  591. placeholder="请输入股票名称或股票代码..."
  592. class="msg-input"
  593. @keydown.enter.exact.prevent="isLoading ? null : sendMessage()"
  594. resize="none"
  595. >
  596. </el-input>
  597. <img
  598. v-if="!chatStore.isLoading"
  599. :src="sendBtn"
  600. @click="sendMessage"
  601. class="action-btn send-btn"
  602. />
  603. <!-- <div v-else @click="chatStore.setLoading(false)"> -->
  604. <div v-else>
  605. <el-icon class="is-loading">
  606. <Loading />
  607. </el-icon>
  608. </div>
  609. </div>
  610. </el-footer>
  611. </el-container>
  612. <el-container v-else>
  613. <el-header class="homepage-head">
  614. <!-- logo -->
  615. <div class="homepage-logo">
  616. <img :src="logo" alt="图片加载失败" class="logo1" />
  617. <img :src="madeInHL" alt="图片加载失败" class="logo2" />
  618. </div>
  619. <div class="homepage-right-group">
  620. <div class="count-badge" @click="showCount">
  621. <img :src="getCountAll" class="action-btn" />
  622. <div class="count-number">{{ UserCount }}</div>
  623. </div>
  624. <img
  625. :src="announcementBtn"
  626. class="announcement-btn action-btn"
  627. @click="showAnnouncement"
  628. />
  629. <img
  630. :src="feedbackBtn"
  631. class="announcement-btn action-btn"
  632. @click="showFeedback"
  633. />
  634. </div>
  635. </el-header>
  636. <!-- 主体部分小人 问题轮询图 对话内容 -->
  637. <el-main class="homepage-body">
  638. <feedback :is="Feedback" />
  639. </el-main>
  640. </el-container>
  641. <!-- 弹窗 -->
  642. <!-- 新增弹窗组件 -->
  643. <el-dialog v-model="dialogVisible" max-width="65%">
  644. <!-- 自定义标题插槽实现居中显示 -->
  645. <template #header>
  646. <div style="text-align: center">
  647. <span>活动规则</span>
  648. </div>
  649. </template>
  650. <!-- 中间内容部分 -->
  651. <div class="ruleContent">
  652. <p>试运行期间AI小财神可以检索全市场数据</p>
  653. <p>每个市场20支股票股票详情参见公告页面</p>
  654. <!-- <p>弘历会员每人每日拥有10次检索机会</p> -->
  655. </div>
  656. <!-- <template #footer> -->
  657. <!-- 添加一个div来包裹按钮并设置样式使其居中 -->
  658. <!-- <div style="text-align: center"> -->
  659. <!-- <el-button style="background-color: orange; color: white; border: none" @click="goToRecharge"> -->
  660. <!-- 去充值 -->
  661. <!-- </el-button> -->
  662. <!-- </div> -->
  663. <!-- </template> -->
  664. </el-dialog>
  665. </div>
  666. </template>
  667. <style scoped>
  668. /* 标签栏 */
  669. .tab-container {
  670. display: flex;
  671. gap: 30px;
  672. margin-right: 40px;
  673. margin-left: 40px;
  674. margin-bottom: 10px;
  675. padding: 0 20px;
  676. justify-content: space-between;
  677. height: 100%;
  678. /* 新增右对齐 */
  679. }
  680. .tab-item {
  681. cursor: pointer;
  682. padding: 8px 12px;
  683. font-size: clamp(18px, 3vw, 20px);
  684. /* color: #999; */
  685. color: #fff;
  686. transition: all 0.3s;
  687. border-bottom: 2px solid transparent;
  688. font-weight: bold;
  689. }
  690. .tab-item.active {
  691. /* color: #000;
  692. border-color: #000; */
  693. background: linear-gradient(0deg, #ffffff, #fec13e);
  694. -webkit-background-clip: text;
  695. background-clip: text;
  696. -webkit-text-fill-color: transparent;
  697. color: transparent;
  698. border-color: #fec13e;
  699. }
  700. .tab-item:not(.active):hover {
  701. color: #999999;
  702. }
  703. .tab-content {
  704. overflow-y: auto;
  705. overflow-x: hidden;
  706. scroll-behavior: smooth;
  707. height: 100%;
  708. /* 添加平滑滚动效果 */
  709. }
  710. @media (max-width: 768px) {
  711. .tab-container {
  712. gap: 15px;
  713. padding: 0 10px;
  714. }
  715. .tab-item {
  716. font-size: clamp(14px, 3vw, 16px);
  717. padding: 6px 10px;
  718. }
  719. }
  720. </style>
  721. <style scoped>
  722. html {
  723. height: 100dvh;
  724. overflow: hidden !important;
  725. position: fixed;
  726. margin: 0;
  727. padding: 0;
  728. -webkit-overflow-scrolling: auto;
  729. /* 禁用 iOS 弹性滚动 */
  730. }
  731. body {
  732. height: 100dvh;
  733. overflow: clip;
  734. margin: 0;
  735. padding: 0;
  736. -webkit-overflow-scrolling: auto;
  737. /* 禁用 iOS 弹性滚动 */
  738. position: fixed;
  739. }
  740. #app {
  741. overflow: hidden;
  742. height: 100%;
  743. margin: 0;
  744. padding: 0;
  745. }
  746. .homepage {
  747. /* height: var(--app-height, 100vh); */
  748. height: var(--app-height, 100vh);
  749. margin: 0 auto;
  750. background-image: url(/src/assets/img/homePage/bk01.jpg);
  751. background-size: 100% 100%;
  752. background-repeat: no-repeat;
  753. background-position: center;
  754. display: flex;
  755. overflow: hidden;
  756. position: fixed;
  757. top: 0;
  758. left: 0;
  759. right: 0;
  760. bottom: 0;
  761. width: 100%;
  762. /* -webkit-overflow-scrolling: touch; */
  763. }
  764. .homepage .el-container {
  765. height: 100%;
  766. flex-direction: column;
  767. display: flex;
  768. width: 100%;
  769. overflow: hidden;
  770. /* 防止容器滚动 */
  771. }
  772. .el-container .el-header {
  773. flex-shrink: 0;
  774. /* 防止头部压缩 */
  775. height: auto;
  776. min-height: 60px;
  777. padding: 5px 0;
  778. position: sticky;
  779. top: 0;
  780. z-index: 10;
  781. /* background-color: rgba(255, 255, 255, 0.9); */
  782. }
  783. .el-container .el-main {
  784. flex: 1;
  785. /* 自动占据剩余空间 */
  786. overflow: hidden;
  787. /* 主容器不滚动 */
  788. display: flex;
  789. flex-direction: column;
  790. min-height: 0;
  791. /* 允许内容区域缩小 */
  792. position: relative;
  793. height: auto;
  794. }
  795. .el-container .el-footer {
  796. flex-shrink: 0;
  797. height: auto;
  798. min-height: 70px;
  799. position: sticky;
  800. bottom: 0;
  801. z-index: 20;
  802. background-color: rgba(211, 24, 24, 0);
  803. box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
  804. -webkit-transform: translateZ(0);
  805. transform: translateZ(0);
  806. padding-bottom: env(safe-area-inset-bottom, 0);
  807. /* 适配iPhone X及以上的底部安全区域 */
  808. }
  809. .homepage-head {
  810. padding: 0px;
  811. display: flex;
  812. position: relative;
  813. justify-content: space-between;
  814. width: 100%;
  815. }
  816. .homepage-right-group {
  817. display: flex;
  818. gap: 8px;
  819. align-items: center;
  820. margin-left: auto;
  821. margin-right: 20px;
  822. }
  823. .homepage-right-group .action-btn {
  824. height: 40px;
  825. }
  826. .homepage-right-group .count-badge {
  827. position: relative;
  828. cursor: pointer;
  829. }
  830. .homepage-right-group .count-badge .count-number {
  831. position: absolute;
  832. top: 6px;
  833. right: 20px;
  834. color: #573dfc;
  835. font-size: 12px;
  836. font-weight: bold;
  837. }
  838. .homepage-right-group .announcement-btn {
  839. cursor: pointer;
  840. transition: transform 0.3s;
  841. }
  842. .homepage-right-group .announcement-btn:hover {
  843. transform: scale(1.3);
  844. }
  845. .homepage-body {
  846. padding: 0px;
  847. display: flex;
  848. flex-direction: column;
  849. flex: 1;
  850. min-height: 0;
  851. /* 允许内容区域缩小 */
  852. overflow: hidden;
  853. }
  854. .main-wrapper {
  855. height: 100%;
  856. display: flex;
  857. flex-direction: column;
  858. flex: 1;
  859. min-height: 0;
  860. /* 允许内容区域缩小 */
  861. }
  862. .tab-section {
  863. flex-shrink: 0;
  864. /* 禁止伸缩 */
  865. }
  866. .tab-content {
  867. flex: 1;
  868. overflow-y: auto;
  869. min-height: 0;
  870. /* 关键:允许内容收缩 */
  871. }
  872. .homepage-logo {
  873. height: 100%;
  874. width: fit-content;
  875. display: flex;
  876. flex-direction: column;
  877. align-items: center;
  878. justify-content: center;
  879. margin-left: 20px;
  880. margin-right: auto;
  881. position: relative;
  882. }
  883. @media (max-width: 768px) {
  884. .homepage-logo {
  885. margin-left: 10px;
  886. left: 0;
  887. }
  888. }
  889. .logo1 {
  890. width: 120px;
  891. height: auto;
  892. margin-bottom: 8px;
  893. }
  894. .logo2 {
  895. width: 80px;
  896. height: auto;
  897. }
  898. /* 尾部 */
  899. .homepage-footer {
  900. display: flex;
  901. flex-direction: column;
  902. gap: 5px;
  903. flex-shrink: 0;
  904. width: 100%;
  905. background-color: #fff;
  906. }
  907. .footer-first-line {
  908. display: flex;
  909. justify-content: space-between;
  910. align-items: center;
  911. padding: 5px 15px;
  912. flex-shrink: 0;
  913. }
  914. .left-group {
  915. display: flex;
  916. gap: 15px;
  917. }
  918. .action-btn {
  919. cursor: pointer;
  920. transition: transform 0.2s;
  921. height: 28px;
  922. }
  923. .action-btn:hover {
  924. transform: scale(1.05);
  925. }
  926. .model-btn {
  927. height: 32px;
  928. transition: all 0.3s ease;
  929. }
  930. .model-btn:hover {
  931. transform: scale(1.1);
  932. }
  933. .send-btn {
  934. margin-left: 10px;
  935. height: 33px !important;
  936. width: auto;
  937. /* margin-right: 5px; */
  938. }
  939. /* 音频播放动画 */
  940. @keyframes pulse {
  941. 0% {
  942. transform: scale(1);
  943. }
  944. 50% {
  945. transform: scale(1.1);
  946. }
  947. 100% {
  948. transform: scale(1);
  949. }
  950. }
  951. .footer-second-line {
  952. position: relative;
  953. display: flex;
  954. align-items: center;
  955. padding: 5px 15px 10px;
  956. flex-shrink: 0;
  957. }
  958. .msg-icon {
  959. position: absolute;
  960. left: 25px;
  961. top: 50%;
  962. transform: translateY(-50%);
  963. width: 24px;
  964. z-index: 999;
  965. }
  966. .msg-input:deep(.el-textarea__inner) {
  967. border: none !important;
  968. box-shadow: none !important;
  969. overflow-y: auto !important;
  970. transition: all 0.2s ease-out;
  971. padding: 8px 20px 8px 45px !important;
  972. resize: none !important;
  973. line-height: 1.5 !important;
  974. max-height: 100px !important;
  975. }
  976. .msg-input:deep(.el-textarea__inner::placeholder) {
  977. white-space: nowrap !important;
  978. overflow: hidden !important;
  979. text-overflow: ellipsis !important;
  980. }
  981. .msg-input {
  982. min-height: 34px;
  983. width: 100%;
  984. border-radius: 20px;
  985. font-size: 16px;
  986. transition: all 0.3s ease-out;
  987. overflow-y: hidden;
  988. box-shadow: 0 4px 12px rgba(89, 24, 241, 0.3);
  989. background: #fff;
  990. z-index: 5;
  991. /* 添加iOS设备特殊处理 */
  992. -webkit-appearance: none;
  993. appearance: none;
  994. }
  995. .msg-input:focus {
  996. outline: none;
  997. }
  998. @media (max-width: 768px) {
  999. .action-btn {
  1000. height: 21px;
  1001. }
  1002. .footer-second-line {
  1003. padding: 5px 10px 10px;
  1004. }
  1005. .msg-input {
  1006. font-size: 16px;
  1007. }
  1008. }
  1009. .ruleContent {
  1010. text-align: center;
  1011. }
  1012. </style>