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.

825 lines
18 KiB

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