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.

855 lines
19 KiB

  1. <template>
  2. <view class="deepMate-page">
  3. <!-- 顶部导航栏 - 固定定位 -->
  4. <view class="header" :style="{ paddingTop: safeAreaInsets?.top + 'px' }">
  5. <view class="header-left">
  6. <image
  7. src="https://d31zlh4on95l9h.cloudfront.net/images/f91e09b5987802185e7679055dafd272.svg"
  8. class="icon"
  9. ></image>
  10. </view>
  11. <view class="header-center">
  12. <text class="title">DeepMate</text>
  13. </view>
  14. <view class="header-right">
  15. <image
  16. src="https://d31zlh4on95l9h.cloudfront.net/images/d7c4e74201213a25dd9574e908233928.svg"
  17. class="icon"
  18. ></image>
  19. <image
  20. src="https://d31zlh4on95l9h.cloudfront.net/images/099903c4aabf5713488b5cb60815e3f7.svg"
  21. class="icon"
  22. ></image>
  23. <!-- 新增新会话按钮
  24. <button class="new-chat-button" @click="newChat">
  25. <text class="new-chat-text">新会话</text>
  26. </button> -->
  27. </view>
  28. </view>
  29. <!-- 主要内容区域 -->
  30. <view class="main-content">
  31. <!-- 顶部固定区域占位符 -->
  32. <view class="banner-placeholder"></view>
  33. <view class="banner-panel"
  34. :class="messages.length === 0 ? '' : 'panelShow'"
  35. >
  36. <image
  37. src="https://d31zlh4on95l9h.cloudfront.net/images/42e18bd7fe97d4f4f37aa70439a0990b.svg"
  38. class="pray-banner"
  39. :class="messages.length === 0 ? '' : 'show'"
  40. mode="aspectFill"
  41. ></image>
  42. <view class="contain">
  43. <!-- 机器人头像和欢迎语 -->
  44. <view class="robot-container">
  45. <image
  46. src="https://d31zlh4on95l9h.cloudfront.net/images/61fa384381c88ad80be28f41827fe0e5.svg"
  47. class="robot-avatar"
  48. ></image>
  49. <view class="welcome-message">
  50. <text class="greeting">Hi, 我是您的股市随身顾问~</text>
  51. <text class="description"
  52. >个股诊断市场情绪解读都可以找我</text
  53. >
  54. </view>
  55. </view>
  56. <!-- 功能标签栏 -->
  57. <view
  58. class="function-tabs"
  59. v-if="messages.length === 0"
  60. scroll-x="true"
  61. show-scrollbar="false"
  62. >
  63. <view class="tab-item">个股诊断</view>
  64. <view class="tab-item">市场情绪温度计</view>
  65. <view class="tab-item">买卖时机提示</view>
  66. <view class="tab-item">个股</view>
  67. </view>
  68. <!-- 特斯拉推荐卡片 -->
  69. <view class="recommend-card" v-if="messages.length === 0">
  70. <view class="arrow" v-if="messages.length === 0"></view>
  71. <view class="card-content">
  72. <image
  73. src="../../static/images/tesla-logo.png"
  74. class="logo"
  75. ></image>
  76. <view class="card-text">
  77. <text class="main-question">当前特斯拉该如何布局</text>
  78. <text class="stock-code">TSLA</text>
  79. </view>
  80. <image
  81. src="https://d31zlh4on95l9h.cloudfront.net/images/40d94054644f6e3f1c366751f07f0010.svg"
  82. class="arrow-icon"
  83. @click="goBlank"
  84. ></image>
  85. </view>
  86. </view>
  87. </view>
  88. </view>
  89. <!-- 可能感兴趣的话题 -->
  90. <view v-if="messages.length === 0" class="interest-section">
  91. <text class="section-title">- 您可能感兴趣 -</text>
  92. <view class="topics-list">
  93. <view class="topic-item" v-for="topic in hotTopics" :key="topic.id">
  94. <image :src="topic.icon" class="tag-icon"></image>
  95. <text class="topic-text" @click="sendMessageList(topic.text)">{{
  96. topic.text
  97. }}</text>
  98. </view>
  99. </view>
  100. </view>
  101. <!-- 聊天区域 -->
  102. <view class="chat-container" v-if="messages.length > 0">
  103. <!-- 给聊天容器添加滚动引用 -->
  104. <scroll-view class="chat-scroll-view" scroll-y="true" :scroll-top="scrollTop">
  105. <view class="message-list" id="messageList">
  106. <view
  107. v-for="(message, index) in messages"
  108. :key="index"
  109. :class="
  110. message.isUser ? 'message user-message' : 'message bot-message'
  111. "
  112. >
  113. <!-- 会话图标 -->
  114. <text
  115. :class="
  116. message.isUser
  117. ? 'fa-solid fa-user message-icon'
  118. : 'fa-solid fa-robot message-icon'
  119. "
  120. ></text>
  121. <!-- 会话内容 -->
  122. <view class="message-content">
  123. <text class="message-text">{{ message.content }}</text>
  124. <!-- loading -->
  125. <view
  126. class="loading-dots"
  127. v-if="message.isThinking "
  128. >
  129. <text class="dot"></text>
  130. <text class="dot"></text>
  131. <text class="dot"></text>
  132. </view>
  133. </view>
  134. </view>
  135. </view>
  136. </scroll-view>
  137. </view>
  138. </view>
  139. <!-- 输入框区域 -->
  140. <view class="input-area">
  141. <view class="input-wrapper">
  142. <input
  143. type="text"
  144. placeholder="请输入股票代码/名称,获取AI洞察"
  145. placeholder-style="color:#fff;opacity:1"
  146. class="input-field"
  147. v-model="inputMessage"
  148. @confirm="sendMessage"
  149. />
  150. <image class="send-button" @click="sendMessage" :disabled="isSending">
  151. <!-- <image
  152. src="https://d31zlh4on95l9h.cloudfront.net/images/95f1ea2262e9157db13c93c0dc1c5d96.svg"
  153. class="send-icon"
  154. ></image> -->
  155. </image>
  156. </view>
  157. <text class="disclaimer"
  158. >以上数据由AI生成不作为最终投资建议决策需独立</text
  159. >
  160. </view>
  161. <image
  162. class="back-to-top"
  163. src="https://d31zlh4on95l9h.cloudfront.net/images/ba357635d2bb480241952bb1cabacd73.svg"
  164. @click="scrollToTop"
  165. ></image>
  166. <footerBar class="static-footer" :type="type"></footerBar>
  167. </view>
  168. </template>
  169. <script setup>
  170. const { safeAreaInsets } = uni.getSystemInfoSync();
  171. import { ref, onMounted, nextTick, watch } from "vue";
  172. import footerBar from "../../components/footerBar-cn.vue";
  173. const type = ref('member')
  174. const inputMessage = ref("");
  175. const isSending = ref(false);
  176. const uuid = ref("");
  177. const messages = ref([]);
  178. const scrollTop = ref(0); // 用于控制scroll-view的滚动位置
  179. const hotTopics = ref([
  180. {
  181. id: 1,
  182. text: "英伟达(NVDA)股票情绪温度?",
  183. icon: "https://d31zlh4on95l9h.cloudfront.net/images/7ed58be0f4b81aeb398d9ba2534a624b.svg",
  184. },
  185. {
  186. id: 2,
  187. text: "博通(AVGO)明天还能涨吗?",
  188. icon: "https://d31zlh4on95l9h.cloudfront.net/images/7ed58be0f4b81aeb398d9ba2534a624b.svg",
  189. },
  190. {
  191. id: 3,
  192. text: "为什么Fluence Energy(FLNC)会暴涨?",
  193. icon: "https://d31zlh4on95l9h.cloudfront.net/images/7ed58be0f4b81aeb398d9ba2534a624b.svg",
  194. },
  195. {
  196. id: 4,
  197. text: "为什么Fluence Energy(FLNC)会暴涨?",
  198. icon: "https://d31zlh4on95l9h.cloudfront.net/images/7ed58be0f4b81aeb398d9ba2534a624b.svg",
  199. },
  200. ]);
  201. // 初始化
  202. onMounted(() => {
  203. initUUID();
  204. // 如果有历史消息,滚动到底部
  205. if (messages.value.length > 0) {
  206. setTimeout(() => {
  207. scrollToBottom();
  208. }, 200);
  209. }
  210. });
  211. // 监听消息变化,当有新消息时自动滚动到最新消息
  212. watch(messages, (newMessages) => {
  213. // 延迟执行滚动,确保DOM更新完成
  214. setTimeout(() => {
  215. scrollToBottom();
  216. }, 100);
  217. }, { deep: true });
  218. // 初始化 UUID
  219. const initUUID = () => {
  220. let storedUUID = uni.getStorageSync("user_uuid");
  221. if (!storedUUID) {
  222. storedUUID = generateUUID();
  223. uni.setStorageSync("user_uuid", storedUUID);
  224. }
  225. uuid.value = storedUUID;
  226. };
  227. // 生成简单UUID
  228. const generateUUID = () => {
  229. return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
  230. var r = (Math.random() * 16) | 0,
  231. v = c == "x" ? r : (r & 0x3) | 0x8;
  232. return v.toString(16);
  233. });
  234. };
  235. // 新会话
  236. const newChat = () => {
  237. messages.value = [];
  238. uni.removeStorageSync("user_uuid");
  239. initUUID();
  240. };
  241. // 跳转到空白页
  242. const goBlank = () => {
  243. uni.navigateTo({
  244. url: "/pages/blank/blank",
  245. });
  246. };
  247. // 发送消息
  248. const sendMessage = () => {
  249. if (inputMessage.value.trim() === "" || isSending.value) return;
  250. const userMessage = {
  251. content: inputMessage.value,
  252. isUser: true,
  253. isThinking: false,
  254. isTyping: false,
  255. };
  256. messages.value.push(userMessage);
  257. inputMessage.value = "";
  258. // 滚动到底部
  259. setTimeout(() => {
  260. scrollToBottom();
  261. }, 100);
  262. // 模拟机器人回复
  263. simulateBotResponse(userMessage.content);
  264. };
  265. // 发送消息
  266. const sendMessageList = (listMessage) => {
  267. console.log(listMessage);
  268. const userMessage = {
  269. content: listMessage,
  270. isUser: true,
  271. isThinking: false,
  272. isTyping: false,
  273. };
  274. messages.value.push(userMessage);
  275. inputMessage.value = "";
  276. // 滚动到底部
  277. setTimeout(() => {
  278. scrollToBottom();
  279. }, 100);
  280. // 模拟机器人回复
  281. simulateBotResponse(userMessage.content);
  282. };
  283. // 模拟机器人回复
  284. const simulateBotResponse = (userMessage) => {
  285. isSending.value = true;
  286. // 添加机器人加载消息
  287. const botMsg = {
  288. content: "",
  289. isUser: false,
  290. isTyping: true,
  291. isThinking: false,
  292. };
  293. messages.value.push(botMsg);
  294. // 滚动到底部
  295. setTimeout(() => {
  296. scrollToBottom();
  297. }, 100);
  298. // 模拟流式响应
  299. let responseText = `我已经收到您的消息: "${userMessage}"。作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?`;
  300. let index = 0;
  301. const typeWriter = () => {
  302. if (index < responseText.length) {
  303. // 使用 Vue 的响应式更新机制
  304. messages.value[messages.value.length - 1].content = responseText.substring(0, index + 1);
  305. index++;
  306. // 滚动到底部,更频繁地触发滚动以适应文本增长
  307. setTimeout(() => {
  308. scrollToBottom();
  309. }, 20);
  310. setTimeout(typeWriter, 30);
  311. } else {
  312. messages.value[messages.value.length - 1].isTyping = false;
  313. isSending.value = false;
  314. // 最后确保滚动到底部
  315. setTimeout(() => {
  316. scrollToBottom();
  317. }, 100);
  318. }
  319. };
  320. setTimeout(typeWriter, 500);
  321. };
  322. // 滚动到底部
  323. const scrollToBottom = () => {
  324. // 使用scroll-view的scrollTop属性来控制滚动
  325. const query = uni.createSelectorQuery();
  326. query.select("#messageList").boundingClientRect();
  327. query.exec((res) => {
  328. if (res[0]) {
  329. // 设置scrollTop为消息列表的高度,实现滚动到底部
  330. scrollTop.value = res[0].height;
  331. }
  332. });
  333. };
  334. const scrollToTop = () => {
  335. // 滚动到顶部
  336. scrollTop.value = 0;
  337. };
  338. </script>
  339. <style scoped>
  340. .deepMate-page {
  341. display: flex;
  342. flex-direction: column;
  343. height: 100vh;
  344. background-color: #ffffff;
  345. padding: 20rpx 0rpx;
  346. position: relative;
  347. overflow: hidden; /* 禁止页面整体滚动 */
  348. }
  349. /* 顶部导航栏 - 固定定位 */
  350. .header {
  351. display: flex;
  352. justify-content: space-between;
  353. align-items: center;
  354. padding: 20rpx 30rpx;
  355. background-color: #ffffff;
  356. position: fixed;
  357. top: 0;
  358. left: 0;
  359. right: 0;
  360. z-index: 999;
  361. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
  362. }
  363. /* 顶部固定区域占位符 */
  364. .header-placeholder {
  365. height: 120rpx; /* 与header高度一致 */
  366. }
  367. /* 顶部固定区域占位符 */
  368. .banner-placeholder {
  369. height: 80rpx; /* 与header高度一致,防止内容被固定头部遮挡 */
  370. }
  371. .header-left,
  372. .header-right {
  373. display: flex;
  374. align-items: center;
  375. }
  376. .header-left .icon,
  377. .header-right .icon {
  378. width: 40rpx;
  379. height: 40rpx;
  380. margin-right: 20rpx;
  381. }
  382. .header-center .title {
  383. font-size: 36rpx;
  384. font-weight: bold;
  385. color: #333333;
  386. }
  387. .new-chat-button {
  388. background-color: #ff6600;
  389. border: none;
  390. border-radius: 8rpx;
  391. padding: 10rpx 20rpx;
  392. }
  393. .new-chat-text {
  394. color: white;
  395. font-size: 24rpx;
  396. }
  397. .main-content {
  398. flex: 1;
  399. padding: 20rpx;
  400. overflow-y: hidden; /* 禁止主内容区域滚动 */
  401. margin-top: 20rpx;
  402. margin-bottom: 250rpx;
  403. }
  404. .robot-container {
  405. display: flex;
  406. align-items: center;
  407. margin-bottom: 30rpx;
  408. }
  409. .robot-avatar {
  410. width: 130rpx;
  411. height: 130rpx;
  412. border-radius: 50%;
  413. margin-right: 10rpx;
  414. }
  415. .welcome-message {
  416. flex: 1;
  417. }
  418. .greeting {
  419. font-size: 32rpx;
  420. margin-left: 50rpx;
  421. top: 40rpx;
  422. font-weight: bold;
  423. color: #333333;
  424. line-height: 48rpx;
  425. }
  426. .description {
  427. display: block;
  428. font-size: 24rpx;
  429. color: #666666;
  430. line-height: 36rpx;
  431. margin-top: 10rpx;
  432. margin-left: 45rpx;
  433. }
  434. .function-tabs {
  435. display: flex;
  436. margin-bottom: 30rpx;
  437. }
  438. .tab-item {
  439. padding: 5rpx 20rpx;
  440. border-radius: 20rpx;
  441. font-size: 20rpx;
  442. font-weight: 700;
  443. color: #666666;
  444. background-color: #fffefe;
  445. margin-right: 20rpx;
  446. transition: all 0.3s;
  447. }
  448. .tab-item.active {
  449. color: #ff6600;
  450. background-color: #fff;
  451. border: 1rpx solid #ff6600;
  452. }
  453. .recommend-card {
  454. background: url("https://d31zlh4on95l9h.cloudfront.net/images/4da1d629a55c307c3605ca15bf15189a.svg");
  455. background-repeat: no-repeat;
  456. /* border-radius: 20rpx; */
  457. padding: 40rpx;
  458. margin-bottom: 30rpx;
  459. /* box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); */
  460. }
  461. .card-content {
  462. display: flex;
  463. align-items: center;
  464. justify-content: space-between;
  465. }
  466. .logo {
  467. width: 80rpx;
  468. height: 80rpx;
  469. background-color: #ff0000;
  470. border-radius: 10rpx;
  471. display: flex;
  472. align-items: center;
  473. justify-content: center;
  474. margin-right: 20rpx;
  475. }
  476. .card-text {
  477. flex: 1;
  478. margin-left: 20rpx;
  479. }
  480. .main-question {
  481. font-size: 32rpx;
  482. color: #333333;
  483. line-height: 48rpx;
  484. }
  485. .stock-code {
  486. display: block;
  487. font-size: 24rpx;
  488. color: #ff3b30;
  489. background-color: #ffffff;
  490. padding: 2rpx 15rpx;
  491. border-radius: 12rpx;
  492. margin-top: 8rpx;
  493. width: fit-content;
  494. border: 1rpx solid #ff3b30;
  495. }
  496. .arrow-icon {
  497. background: url("https://d31zlh4on95l9h.cloudfront.net/images/40d94054644f6e3f1c366751f07f0010.svg");
  498. background-repeat: no-repeat;
  499. left: 0.5rem;
  500. top: 1.8rem;
  501. background-size: 100% 100%;
  502. width: 60rpx;
  503. height: 60rpx;
  504. }
  505. .interest-section {
  506. margin-bottom: 30rpx;
  507. }
  508. .section-title {
  509. display: block;
  510. text-align: center;
  511. font-size: 26rpx;
  512. color: #666666;
  513. margin-bottom: 20rpx;
  514. }
  515. .topics-list {
  516. display: flex;
  517. flex-direction: column;
  518. gap: 15rpx;
  519. }
  520. .topic-item {
  521. display: flex;
  522. align-items: center;
  523. padding: 15rpx 20rpx;
  524. background-color: #f0f0f0;
  525. border-radius: 15rpx;
  526. width: fit-content;
  527. }
  528. .tag-icon {
  529. width: 24rpx;
  530. height: 24rpx;
  531. margin-right: 10rpx;
  532. }
  533. .topic-text {
  534. font-size: 28rpx;
  535. color: #333333;
  536. flex: 1;
  537. }
  538. /* 聊天区域样式 */
  539. .chat-container {
  540. margin-top: 30rpx;
  541. border-radius: 10rpx;
  542. height: fit-content;
  543. /* overflow-y: auto; */
  544. }
  545. .chat-scroll-view {
  546. height: calc(80vh - 250rpx); /* 根据需要调整高度 */
  547. margin-top: 180rpx;
  548. }
  549. .message-list {
  550. /* padding: 20rpx; */
  551. /* margin-top: 200rpx; */
  552. }
  553. .message {
  554. display: flex;
  555. align-items: flex-start;
  556. margin-bottom: 30rpx;
  557. }
  558. .user-message {
  559. flex-direction: row-reverse;
  560. }
  561. .message-icon {
  562. font-size: 24rpx;
  563. margin: 0 10rpx;
  564. padding: 10rpx;
  565. border-radius: 50%;
  566. background-color: #ddd;
  567. width: 40rpx;
  568. height: 40rpx;
  569. display: flex;
  570. align-items: center;
  571. justify-content: center;
  572. }
  573. .user-message .message-icon {
  574. background-color: #007aff;
  575. color: white;
  576. }
  577. .bot-message .message-icon {
  578. background-color: #34c759;
  579. color: white;
  580. }
  581. .message-content {
  582. max-width: 70%;
  583. position: relative;
  584. }
  585. .user-message .message-content {
  586. background-color: #007aff;
  587. border-radius: 10rpx;
  588. padding: 15rpx;
  589. }
  590. .bot-message .message-content {
  591. background-color: #f0f0f0;
  592. border-radius: 10rpx;
  593. padding: 15rpx;
  594. }
  595. .message-text {
  596. font-size: 28rpx;
  597. line-height: 40rpx;
  598. }
  599. .user-message .message-text {
  600. color: white;
  601. }
  602. .bot-message .message-text {
  603. color: #333;
  604. }
  605. .loading-dots {
  606. display: flex;
  607. align-items: center;
  608. padding-top: 10rpx;
  609. }
  610. .dot {
  611. width: 10rpx;
  612. height: 10rpx;
  613. background-color: #666;
  614. border-radius: 50%;
  615. margin: 0 4rpx;
  616. animation: loading 1.4s infinite ease-in-out both;
  617. }
  618. .user-message .dot {
  619. background-color: white;
  620. }
  621. .dot:nth-child(1) {
  622. animation-delay: -0.32s;
  623. }
  624. .dot:nth-child(2) {
  625. animation-delay: -0.16s;
  626. }
  627. @keyframes loading {
  628. 0%,
  629. 80%,
  630. 100% {
  631. transform: scale(0);
  632. }
  633. 40% {
  634. transform: scale(1);
  635. }
  636. }
  637. .input-area {
  638. position: fixed;
  639. bottom: 70rpx;
  640. left: 0;
  641. right: 0;
  642. padding: 0 40rpx 80rpx 40rpx;
  643. background-color: #ffffff;
  644. z-index: 999;
  645. }
  646. .input-wrapper {
  647. position: relative;
  648. display: flex;
  649. align-items: center;
  650. padding: 15rpx 20rpx;
  651. background-color: rgb(220, 31, 29);
  652. border-radius: 100rpx;
  653. display: flex;
  654. align-items: center;
  655. justify-content: center;
  656. height: 50rpx;
  657. }
  658. .mic-icon {
  659. width: 36rpx;
  660. height: 36rpx;
  661. margin-right: 20rpx;
  662. }
  663. .input-field {
  664. flex: 1;
  665. font-size: 28rpx;
  666. color: #fff;
  667. display: flex;
  668. align-items: center;
  669. justify-content: center;
  670. margin-left: 60rpx;
  671. background: none;
  672. border: none;
  673. outline: none;
  674. }
  675. .input-field::placeholder {
  676. color: #ffffff !important;
  677. opacity: 1;
  678. }
  679. .send-button {
  680. background: url("https://d31zlh4on95l9h.cloudfront.net/images/95f1ea2262e9157db13c93c0dc1c5d96.svg");
  681. background-repeat: no-repeat;
  682. background-size: 100% 100%;
  683. height: 50rpx;
  684. width: 50rpx;
  685. padding: 0;
  686. border: 1rpx solid transparent;
  687. margin-left: 20rpx;
  688. }
  689. .send-icon {
  690. width: 36rpx;
  691. height: 36rpx;
  692. }
  693. .disclaimer {
  694. font-size: 15rpx;
  695. color: #4d4c4c;
  696. display: flex;
  697. align-items: center;
  698. justify-content: center;
  699. margin-top: 15rpx;
  700. }
  701. .banner-panel {
  702. position: relative;
  703. height: 480rpx; /* 拉长容器,灰色背景跟随变高 */
  704. overflow: hidden; /* 让圆角和内部层剪裁一致 */
  705. border-radius: 15rpx;
  706. }
  707. .panelShow{
  708. height: 12%;
  709. position: fixed;
  710. top: 70rpx;
  711. z-index: 999;
  712. width: 95%;
  713. }
  714. .pray-banner {
  715. position: absolute;
  716. /* background-size: 100% 100%; */
  717. inset: 0; /* 顶部、底部、左、右都贴合容器 */
  718. width: 100%;
  719. height: 88%;
  720. border-radius: 15rpx;
  721. z-index: 1; /* 在灰底之上、内容之下 */
  722. }
  723. .contain {
  724. margin: 0 20rpx;
  725. gap: 5rpx;
  726. }
  727. .banner-panel .robot-container,
  728. .banner-panel .function-tabs,
  729. .banner-panel .recommend-card {
  730. position: relative;
  731. z-index: 2;
  732. }
  733. .back-to-top {
  734. position: fixed;
  735. right: 30rpx;
  736. bottom: 35%;
  737. width: 100rpx;
  738. height: 100rpx;
  739. z-index: 1000;
  740. }
  741. .back-to-top:active {
  742. transform: scale(0.96);
  743. }
  744. .static-footer {
  745. position: fixed;
  746. bottom: 0;
  747. z-index: 999;
  748. }
  749. /* 顶部固定区域占位符 */
  750. .banner-placeholder {
  751. height: 60rpx; /* 与header高度一致,防止内容被固定头部遮挡 */
  752. }
  753. </style>