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.

799 lines
18 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 || message.isTyping"
  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. botMsg.content += responseText.charAt(index);
  291. index++;
  292. // 滚动到底部
  293. scrollToBottom();
  294. setTimeout(typeWriter, 30);
  295. } else {
  296. botMsg.isTyping = false;
  297. isSending.value = false;
  298. }
  299. };
  300. setTimeout(typeWriter, 500);
  301. };
  302. // 滚动到底部
  303. const scrollToBottom = () => {
  304. const query = uni.createSelectorQuery();
  305. query.select("#messageList").boundingClientRect();
  306. query.selectViewport().scrollOffset();
  307. query.exec((res) => {
  308. if (res[0] && res[1]) {
  309. uni.pageScrollTo({
  310. scrollTop: res[0].height,
  311. duration: 100,
  312. });
  313. }
  314. });
  315. };
  316. const scrollToTop = () => {
  317. uni.pageScrollTo({ scrollTop: 0, duration: 200 });
  318. };
  319. </script>
  320. <style scoped>
  321. .deepMate-page {
  322. display: flex;
  323. flex-direction: column;
  324. height: 100vh;
  325. background-color: #ffffff;
  326. padding: 20rpx 0rpx;
  327. }
  328. .header {
  329. display: flex;
  330. justify-content: space-between;
  331. align-items: center;
  332. padding: 20rpx 30rpx;
  333. background-color: #ffffff;
  334. }
  335. .header-left,
  336. .header-right {
  337. display: flex;
  338. align-items: center;
  339. }
  340. .header-left .icon,
  341. .header-right .icon {
  342. width: 40rpx;
  343. height: 40rpx;
  344. margin-right: 20rpx;
  345. }
  346. .header-center .title {
  347. font-size: 36rpx;
  348. font-weight: bold;
  349. color: #333333;
  350. }
  351. .new-chat-button {
  352. background-color: #ff6600;
  353. border: none;
  354. border-radius: 8rpx;
  355. padding: 10rpx 20rpx;
  356. }
  357. .new-chat-text {
  358. color: white;
  359. font-size: 24rpx;
  360. }
  361. .main-content {
  362. flex: 1;
  363. padding: 20rpx;
  364. overflow-y: auto;
  365. margin-top: 20rpx;
  366. margin-bottom: 120rpx;
  367. }
  368. .robot-container {
  369. display: flex;
  370. align-items: center;
  371. margin-bottom: 30rpx;
  372. }
  373. .robot-avatar {
  374. width: 130rpx;
  375. height: 130rpx;
  376. border-radius: 50%;
  377. margin-right: 10rpx;
  378. }
  379. .welcome-message {
  380. flex: 1;
  381. }
  382. .greeting {
  383. font-size: 32rpx;
  384. margin-left: 50rpx;
  385. top: 40rpx;
  386. font-weight: bold;
  387. color: #333333;
  388. line-height: 48rpx;
  389. }
  390. .description {
  391. display: block;
  392. font-size: 24rpx;
  393. color: #666666;
  394. line-height: 36rpx;
  395. margin-top: 10rpx;
  396. margin-left: 45rpx;
  397. }
  398. .function-tabs {
  399. display: flex;
  400. margin-bottom: 30rpx;
  401. }
  402. .tab-item {
  403. padding: 5rpx 20rpx;
  404. border-radius: 20rpx;
  405. font-size: 20rpx;
  406. font-weight: 700;
  407. color: #666666;
  408. background-color: #fffefe;
  409. margin-right: 20rpx;
  410. transition: all 0.3s;
  411. }
  412. .tab-item.active {
  413. color: #ff6600;
  414. background-color: #fff;
  415. border: 1rpx solid #ff6600;
  416. }
  417. .recommend-card {
  418. background: url("https://d31zlh4on95l9h.cloudfront.net/images/4da1d629a55c307c3605ca15bf15189a.svg");
  419. background-repeat: no-repeat;
  420. /* border-radius: 20rpx; */
  421. padding: 40rpx;
  422. margin-bottom: 30rpx;
  423. /* box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); */
  424. }
  425. .card-content {
  426. display: flex;
  427. align-items: center;
  428. justify-content: space-between;
  429. }
  430. .logo {
  431. width: 80rpx;
  432. height: 80rpx;
  433. background-color: #ff0000;
  434. border-radius: 10rpx;
  435. display: flex;
  436. align-items: center;
  437. justify-content: center;
  438. margin-right: 20rpx;
  439. }
  440. .card-text {
  441. flex: 1;
  442. margin-left: 20rpx;
  443. }
  444. .main-question {
  445. font-size: 32rpx;
  446. color: #333333;
  447. line-height: 48rpx;
  448. }
  449. .stock-code {
  450. display: block;
  451. font-size: 24rpx;
  452. color: #ff3b30;
  453. background-color: #ffffff;
  454. padding: 2rpx 15rpx;
  455. border-radius: 12rpx;
  456. margin-top: 8rpx;
  457. width: fit-content;
  458. border: 1rpx solid #ff3b30;
  459. }
  460. .arrow-icon {
  461. background: url("https://d31zlh4on95l9h.cloudfront.net/images/40d94054644f6e3f1c366751f07f0010.svg");
  462. background-repeat: no-repeat;
  463. left: 0.5rem;
  464. top: 1.8rem;
  465. background-size: 100% 100%;
  466. width: 60rpx;
  467. height: 60rpx;
  468. }
  469. .interest-section {
  470. margin-bottom: 30rpx;
  471. }
  472. .section-title {
  473. display: block;
  474. text-align: center;
  475. font-size: 26rpx;
  476. color: #666666;
  477. margin-bottom: 20rpx;
  478. }
  479. .topics-list {
  480. display: flex;
  481. flex-direction: column;
  482. gap: 15rpx;
  483. }
  484. .topic-item {
  485. display: flex;
  486. align-items: center;
  487. padding: 15rpx 20rpx;
  488. background-color: #f0f0f0;
  489. border-radius: 15rpx;
  490. width: fit-content;
  491. }
  492. .tag-icon {
  493. width: 24rpx;
  494. height: 24rpx;
  495. margin-right: 10rpx;
  496. }
  497. .topic-text {
  498. font-size: 28rpx;
  499. color: #333333;
  500. flex: 1;
  501. }
  502. /* 聊天区域样式 */
  503. .chat-container {
  504. margin-top: 30rpx;
  505. border-radius: 10rpx;
  506. height: fit-content;
  507. /* overflow-y: auto; */
  508. }
  509. .message-list {
  510. /* padding: 20rpx; */
  511. }
  512. .message {
  513. display: flex;
  514. align-items: flex-start;
  515. margin-bottom: 30rpx;
  516. }
  517. .user-message {
  518. flex-direction: row-reverse;
  519. }
  520. .message-icon {
  521. font-size: 24rpx;
  522. margin: 0 10rpx;
  523. padding: 10rpx;
  524. border-radius: 50%;
  525. background-color: #ddd;
  526. width: 40rpx;
  527. height: 40rpx;
  528. display: flex;
  529. align-items: center;
  530. justify-content: center;
  531. }
  532. .user-message .message-icon {
  533. background-color: #007aff;
  534. color: white;
  535. }
  536. .bot-message .message-icon {
  537. background-color: #34c759;
  538. color: white;
  539. }
  540. .message-content {
  541. max-width: 70%;
  542. position: relative;
  543. }
  544. .user-message .message-content {
  545. background-color: #007aff;
  546. border-radius: 10rpx;
  547. padding: 15rpx;
  548. }
  549. .bot-message .message-content {
  550. background-color: #f0f0f0;
  551. border-radius: 10rpx;
  552. padding: 15rpx;
  553. }
  554. .message-text {
  555. font-size: 28rpx;
  556. line-height: 40rpx;
  557. }
  558. .user-message .message-text {
  559. color: white;
  560. }
  561. .bot-message .message-text {
  562. color: #333;
  563. }
  564. .loading-dots {
  565. display: flex;
  566. align-items: center;
  567. padding-top: 10rpx;
  568. }
  569. .dot {
  570. width: 10rpx;
  571. height: 10rpx;
  572. background-color: #666;
  573. border-radius: 50%;
  574. margin: 0 4rpx;
  575. animation: loading 1.4s infinite ease-in-out both;
  576. }
  577. .user-message .dot {
  578. background-color: white;
  579. }
  580. .dot:nth-child(1) {
  581. animation-delay: -0.32s;
  582. }
  583. .dot:nth-child(2) {
  584. animation-delay: -0.16s;
  585. }
  586. @keyframes loading {
  587. 0%,
  588. 80%,
  589. 100% {
  590. transform: scale(0);
  591. }
  592. 40% {
  593. transform: scale(1);
  594. }
  595. }
  596. .input-area {
  597. position: fixed;
  598. bottom: 0;
  599. left: 0;
  600. right: 0;
  601. padding: 0 40rpx 80rpx 40rpx;
  602. background-color: #ffffff;
  603. }
  604. .input-wrapper {
  605. position: relative;
  606. display: flex;
  607. align-items: center;
  608. padding: 15rpx 20rpx;
  609. background-color: rgb(220, 31, 29);
  610. border-radius: 100rpx;
  611. display: flex;
  612. align-items: center;
  613. justify-content: center;
  614. height: 50rpx;
  615. }
  616. .mic-icon {
  617. width: 36rpx;
  618. height: 36rpx;
  619. margin-right: 20rpx;
  620. }
  621. .input-field {
  622. flex: 1;
  623. font-size: 28rpx;
  624. color: #fff;
  625. display: flex;
  626. align-items: center;
  627. justify-content: center;
  628. margin-left: 60rpx;
  629. background: none;
  630. border: none;
  631. outline: none;
  632. }
  633. .input-field::placeholder {
  634. color: #ffffff !important;
  635. opacity: 1;
  636. }
  637. .send-button {
  638. background: url("https://d31zlh4on95l9h.cloudfront.net/images/95f1ea2262e9157db13c93c0dc1c5d96.svg");
  639. background-repeat: no-repeat;
  640. background-size: 100% 100%;
  641. height: 50rpx;
  642. width: 50rpx;
  643. padding: 0;
  644. border: 1rpx solid transparent;
  645. margin-left: 20rpx;
  646. }
  647. .send-icon {
  648. width: 36rpx;
  649. height: 36rpx;
  650. }
  651. .disclaimer {
  652. font-size: 15rpx;
  653. color: #4d4c4c;
  654. display: flex;
  655. align-items: center;
  656. justify-content: center;
  657. margin-top: 15rpx;
  658. }
  659. .banner-panel {
  660. position: relative;
  661. height: 480rpx; /* 拉长容器,灰色背景跟随变高 */
  662. overflow: hidden; /* 让圆角和内部层剪裁一致 */
  663. border-radius: 15rpx;
  664. }
  665. .panelShow{
  666. height: 12%;
  667. }
  668. .pray-banner {
  669. position: absolute;
  670. /* background-size: 100% 100%; */
  671. inset: 0; /* 顶部、底部、左、右都贴合容器 */
  672. width: 100%;
  673. height: 88%;
  674. border-radius: 15rpx;
  675. z-index: 1; /* 在灰底之上、内容之下 */
  676. }
  677. .contain {
  678. margin: 0 20rpx;
  679. gap: 5rpx;
  680. }
  681. .banner-panel .robot-container,
  682. .banner-panel .function-tabs,
  683. .banner-panel .recommend-card {
  684. position: relative;
  685. z-index: 2;
  686. }
  687. .back-to-top {
  688. position: fixed;
  689. right: 30rpx;
  690. bottom: 35%;
  691. width: 100rpx;
  692. height: 100rpx;
  693. z-index: 1000;
  694. }
  695. .back-to-top:active {
  696. transform: scale(0.96);
  697. }
  698. .static-footer {
  699. position: fixed;
  700. bottom: 0;
  701. }
  702. </style>