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.

1379 lines
36 KiB

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