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.

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