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.

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