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.

755 lines
16 KiB

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