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.

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