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.

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