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.

1217 lines
30 KiB

4 weeks 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">
  7. </image>
  8. </view>
  9. <view class="header-center">
  10. <text class="title">DeepMate</text>
  11. </view>
  12. <view class="header-right">
  13. <image src="https://d31zlh4on95l9h.cloudfront.net/images/d7c4e74201213a25dd9574e908233928.svg" class="icon">
  14. </image>
  15. <image style="margin-left: 10px;" src="https://d31zlh4on95l9h.cloudfront.net/images/099903c4aabf5713488b5cb60815e3f7.svg" class="icon"
  16. @click="openHistoryDrawer"></image>
  17. <!-- 新增新会话按钮
  18. <button class="new-chat-button" @click="newChat">
  19. <text class="new-chat-text">新会话</text>
  20. </button> -->
  21. </view>
  22. </view>
  23. <!-- 主要内容区域 -->
  24. <view class="main-content">
  25. <view class="banner-panel" v-if="messages.length === 0">
  26. <image src="https://d31zlh4on95l9h.cloudfront.net/images/42e18bd7fe97d4f4f37aa70439a0990b.svg"
  27. class="pray-banner"></image>
  28. <view class="contain">
  29. <!-- 机器人头像和欢迎语 -->
  30. <view class="robot-container" v-if="messages.length === 0">
  31. <image src="https://d31zlh4on95l9h.cloudfront.net/images/61fa384381c88ad80be28f41827fe0e5.svg"
  32. class="robot-avatar"></image>
  33. <view class="welcome-message">
  34. <text class="greeting">Hi, 我是您的股市随身顾问~</text>
  35. <text class="description">个股诊断市场情绪解读都可以找我</text>
  36. </view>
  37. </view>
  38. <!-- 功能标签栏 -->
  39. <!-- <view class="function-tabs" v-if="messages.length === 0">
  40. <view ref="tabsTrack" class="tabs-track" :style="{ transform: 'translate3d(-' + marqueeOffset + 'px,0,0)' }">
  41. <view class="tab-item" v-for="(tab, idx) in marqueeList" :key="idx">{{ tab }}</view>
  42. </view>
  43. </view> -->
  44. <!-- 特斯拉推荐卡片 -->
  45. <view class="recommend-card" v-if="messages.length === 0">
  46. <view class="arrow" v-if="messages.length === 0"></view>
  47. <view class="card-content">
  48. <image src="../../static/images/tesla-logo.png" class="logo"></image>
  49. <view class="card-text">
  50. <text class="main-question">当前特斯拉该如何布局</text>
  51. <text class="stock-code">TSLA</text>
  52. </view>
  53. <image src="https://d31zlh4on95l9h.cloudfront.net/images/40d94054644f6e3f1c366751f07f0010.svg"
  54. class="arrow-icon" @click="goBlank"></image>
  55. </view>
  56. </view>
  57. </view>
  58. </view>
  59. <!-- 可能感兴趣的话题 -->
  60. <!-- <view v-if="messages.length === 0" class="interest-section">
  61. <text class="section-title">- 您可能感兴趣 -</text>
  62. <view class="topics-list">
  63. <view class="topic-item" v-for="topic in hotTopics" :key="topic.id">
  64. <image :src="topic.icon" class="tag-icon"></image>
  65. <text class="topic-text" @click="sendMessageList(topic.text)">{{
  66. topic.text
  67. }}</text>
  68. </view>
  69. </view>
  70. </view> -->
  71. <!-- 聊天区域 -->
  72. <!-- 顶部粘性欢迎块始终保留在聊天上方 -->
  73. <view class="chat-header" v-if="messages.length > 0">
  74. <view class="robot-container">
  75. <image src="https://d31zlh4on95l9h.cloudfront.net/images/61fa384381c88ad80be28f41827fe0e5.svg"
  76. class="robot-avatar"></image>
  77. <view class="welcome-message"> <text class="greeting">Hi, 我是您的股市随身顾问~</text> <text
  78. class="description">个股诊断市场情绪解读都可以找我</text>
  79. </view>
  80. </view>
  81. </view>
  82. <scroll-view class="chat-container" scroll-y="true" :scroll-top="chatScrollTop" @scroll="onChatScroll"
  83. v-if="messages.length > 0">
  84. <view class="message-list" id="messageList">
  85. <view v-for="(message, index) in messages" :key="index" :class="message.isUser ? 'message user-message' : 'message bot-message'
  86. ">
  87. <!-- 会话图标 -->
  88. <text :class="message.isUser
  89. ? 'fa-solid fa-user message-icon'
  90. : 'fa-solid fa-robot message-icon'
  91. "></text>
  92. <!-- 会话内容 -->
  93. <view class="message-content">
  94. <!-- <text class="message-text">{{ message.content }}</text> -->
  95. <!-- loading -->
  96. <view
  97. class="loading-dots"
  98. v-if="message.isThinking || !message.isUser"
  99. >
  100. <view class="thinking-process">
  101. <view class="thinking-header">
  102. <view class="thinking-icon"></view>
  103. <view class="thinking-title">{{
  104. message.isTyping ? "正在思考" : "思考完成"
  105. }}</view>
  106. <view class="thinking-count">
  107. </view>
  108. <view
  109. class="thinking-toggle"
  110. @click="message.isThinking = !message.isThinking"
  111. >
  112. <span v-if="message.isThinking"></span>
  113. <span v-else></span>
  114. </view>
  115. </view>
  116. <view v-show="message.isThinking" class="thinking-content">
  117. <view class="thinking-item">
  118. <view class="item-status">
  119. <span class="checkmark"></span>
  120. </view>
  121. <view class="item-text">问题分析完成</view>
  122. </view>
  123. <view class="thinking-item">
  124. <view class="item-status">
  125. <span class="checkmark"></span>
  126. </view>
  127. <view class="item-text">收集相关信息</view>
  128. </view>
  129. </view>
  130. </view>
  131. </view>
  132. <!-- 使用 rich-text 渲染 Markdown 内容 -->
  133. <rich-text v-if="!message.isUser" class="message-text"
  134. :nodes="renderMarkdown(message.content)"></rich-text>
  135. <text v-else class="message-text">{{ message.content }}</text>
  136. </view>
  137. </view>
  138. </view>
  139. </scroll-view>
  140. </view>
  141. <!-- 输入框区域 -->
  142. <view class="input-area">
  143. <view class="input-wrapper">
  144. <input type="text" placeholder="请输入股票代码/名称,获取AI洞察" placeholder-style="color:#fff;opacity:1" class="input-field"
  145. v-model="inputMessage" @confirm="sendMessage" />
  146. <image class="send-button" @click="sendMessage" :disabled="isSending">
  147. <!-- <image
  148. src="https://d31zlh4on95l9h.cloudfront.net/images/95f1ea2262e9157db13c93c0dc1c5d96.svg"
  149. class="send-icon"
  150. ></image> -->
  151. </image>
  152. </view>
  153. <text class="disclaimer">以上数据由AI生成不作为最终投资建议决策需独立</text>
  154. </view>
  155. <image class="back-to-top" src="https://d31zlh4on95l9h.cloudfront.net/images/ba357635d2bb480241952bb1cabacd73.svg"
  156. :style="{ transform: 'translate3d(' + backTopX + 'px,' + backTopY + 'px,0)' }" @touchstart="onBackTopTouchStart"
  157. @touchmove="onBackTopTouchMove" @touchend="onBackTopTouchEnd" @click="onBackTopClick"></image>
  158. <!-- 搜索历史侧拉框 -->
  159. <view class="drawer-overlay" v-show="showHistoryDrawer" @click="closeHistoryDrawer"></view>
  160. <view class="drawer-panel" v-show="showHistoryDrawer" @click.stop>
  161. <view class="drawer-header">
  162. <text class="drawer-title">搜索历史</text>
  163. <text class="drawer-close" @click="closeHistoryDrawer">×</text>
  164. </view>
  165. <scroll-view scroll-y="true" class="drawer-content">
  166. <view v-if="searchHistory.length === 0" class="empty-history">
  167. <text>暂无搜索历史</text>
  168. </view>
  169. <view v-for="(item, idx) in searchHistory" :key="idx" class="history-card">
  170. <text class="history-query">{{ item.query }}</text>
  171. <text class="history-time">{{ formatTime(item.time) }}</text>
  172. </view>
  173. </scroll-view>
  174. </view>
  175. <footerBar class="static-footer" :type="type"></footerBar>
  176. </view>
  177. </template>
  178. <script setup>
  179. const { safeAreaInsets } = uni.getSystemInfoSync();
  180. import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue";
  181. import footerBar from '../../components/footerBar-cn'
  182. import marked from "marked"; // 引入 marked 库
  183. import { onPageScroll } from '@dcloudio/uni-app'
  184. // 设置 marked 选项
  185. marked.setOptions({
  186. renderer: new marked.Renderer(),
  187. highlight: null, // 如果需要代码高亮,可以设置适当的函数
  188. langPrefix: "language-",
  189. pedantic: false,
  190. gfm: true,
  191. breaks: false,
  192. sanitize: false,
  193. smartLists: true,
  194. smartypants: false,
  195. xhtml: false,
  196. });
  197. // 创建一个用于渲染 Markdown 的函数
  198. const renderMarkdown = (content) => {
  199. if (!content) return "";
  200. return marked.parse(content);
  201. };
  202. const type = ref('member')
  203. const inputMessage = ref("");
  204. const showThinking = ref(true);
  205. const isSending = ref(false);
  206. const chatScrollTop = ref(0);
  207. const chatContainerHeight = ref(0);
  208. const uuid = ref("");
  209. const messages = ref([]);
  210. const showHistoryDrawer = ref(false);
  211. const searchHistory = ref([]);
  212. const hotTopics = ref([
  213. {
  214. id: 1,
  215. text: "英伟达(NVDA)股票情绪温度?",
  216. icon: "https://d31zlh4on95l9h.cloudfront.net/images/7ed58be0f4b81aeb398d9ba2534a624b.svg",
  217. },
  218. {
  219. id: 2,
  220. text: "博通(AVGO)明天还能涨吗?",
  221. icon: "https://d31zlh4on95l9h.cloudfront.net/images/7ed58be0f4b81aeb398d9ba2534a624b.svg",
  222. },
  223. {
  224. id: 3,
  225. text: "为什么Fluence Energy(FLNC)会暴涨?",
  226. icon: "https://d31zlh4on95l9h.cloudfront.net/images/7ed58be0f4b81aeb398d9ba2534a624b.svg",
  227. },
  228. {
  229. id: 4,
  230. text: "为什么Fluence Energy(FLNC)会暴涨?",
  231. icon: "https://d31zlh4on95l9h.cloudfront.net/images/7ed58be0f4b81aeb398d9ba2534a624b.svg",
  232. },
  233. ]);
  234. // 统一 raf/caf(小程序端可能没有 rAF)
  235. // const hasRAF = typeof requestAnimationFrame === 'function';
  236. // const startFrame = (fn) => hasRAF ? requestAnimationFrame(fn) : setTimeout(fn, 16);
  237. // const stopFrame = (id) => hasRAF ? cancelAnimationFrame(id) : clearTimeout(id);
  238. // 初始化
  239. onMounted(() => {
  240. const sys = uni.getSystemInfoSync();
  241. const iconSize = uni.upx2px(100); // 和样式保持一致
  242. const reserveBottom = uni.upx2px(260); // 预留底部输入区域空间
  243. const initX = Math.max(0, sys.windowWidth - iconSize - uni.upx2px(30));
  244. const initY = Math.max(0, Math.floor(sys.windowHeight * 0.65) - iconSize);
  245. backTopTargetX.value = initX;
  246. backTopTargetY.value = initY;
  247. backTopX.value = initX;
  248. backTopY.value = initY;
  249. initUUID();
  250. if (messages.value.length === 0) {
  251. // nextTick(startTabsMarquee);
  252. }
  253. if (messages.value.length > 0) {
  254. nextTick(() => { measureChatContainer(); scrollToBottom(); });
  255. }
  256. // 载入历史
  257. const hist = uni.getStorageSync("search_history") || [];
  258. searchHistory.value = Array.isArray(hist) ? hist : [];
  259. });
  260. // 初始化 UUID
  261. const initUUID = () => {
  262. let storedUUID = uni.getStorageSync("user_uuid");
  263. if (!storedUUID) {
  264. storedUUID = generateUUID();
  265. uni.setStorageSync("user_uuid", storedUUID);
  266. }
  267. uuid.value = storedUUID;
  268. };
  269. // 生成简单UUID
  270. const generateUUID = () => {
  271. return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
  272. var r = (Math.random() * 16) | 0,
  273. v = c == "x" ? r : (r & 0x3) | 0x8;
  274. return v.toString(16);
  275. });
  276. };
  277. // 计算聊天容器可视高度
  278. const measureChatContainer = () => {
  279. const q = uni.createSelectorQuery();
  280. q.select('.chat-container').boundingClientRect();
  281. q.exec((res) => {
  282. chatContainerHeight.value = res[0]?.height || 0;
  283. });
  284. };
  285. // 新会话
  286. const newChat = () => {
  287. messages.value = [];
  288. uni.removeStorageSync("user_uuid");
  289. initUUID();
  290. };
  291. // 跳转到空白页
  292. const goBlank = () => {
  293. uni.navigateTo({
  294. url: "/pages/blank/blank",
  295. });
  296. };
  297. // 历史抽屉控制
  298. const openHistoryDrawer = () => { showHistoryDrawer.value = true; };
  299. const closeHistoryDrawer = () => { showHistoryDrawer.value = false; };
  300. // 时间格式化:YYYY-MM-DD HH:mm
  301. const pad2 = (n) => (n < 10 ? '0' + n : '' + n);
  302. const formatTime = (t) => {
  303. const d = new Date(t);
  304. const y = d.getFullYear();
  305. const m = pad2(d.getMonth() + 1);
  306. const day = pad2(d.getDate());
  307. const hh = pad2(d.getHours());
  308. const mm = pad2(d.getMinutes());
  309. return `${y}-${m}-${day} ${hh}:${mm}`;
  310. };
  311. // 发送消息
  312. const sendMessage = () => {
  313. if (inputMessage.value.trim() === "" || isSending.value) return;
  314. const userMessage = {
  315. content: inputMessage.value,
  316. isUser: true,
  317. isThinking: false,
  318. isTyping: false,
  319. };
  320. messages.value.push(userMessage);
  321. inputMessage.value = "";
  322. // 记录搜索历史
  323. const entry = { query: userMessage.content, time: Date.now() };
  324. searchHistory.value.unshift(entry);
  325. uni.setStorageSync('search_history', searchHistory.value);
  326. // 发送后强制恢复并滚到底部
  327. shouldAutoScroll.value = true;
  328. nextTick(() => { scrollToBottom(); });
  329. // 模拟机器人回复
  330. simulateBotResponse(userMessage.content);
  331. };
  332. // 模拟机器人回复
  333. const simulateBotResponse = async(userMessage) => {
  334. isSending.value = true;
  335. // 添加机器人加载消息
  336. const botMsg = {
  337. content: "",
  338. isUser: false,
  339. isTyping: true,
  340. isThinking: true,
  341. };
  342. messages.value.push(botMsg);
  343. // 添加请求延迟
  344. await new Promise((resolve) => setTimeout(resolve, 2000));
  345. const toDataInfo = await getDataInfo();
  346. console.log(toDataInfo);
  347. // dataInfo.value = toDataInfo.data;
  348. // console.log(dataInfo.value);
  349. messages.value[messages.value.length - 1].isThinking = false;
  350. // 滚动到底部
  351. nextTick(() => {
  352. scrollToBottom();
  353. });
  354. // 模拟流式响应
  355. let responseText = `我已经收到您的消息: "${userMessage}"。+"${toDataInfo.data}" `;
  356. let index = 0;
  357. const botIndex = messages.value.length - 1;
  358. const baseDelay = 5; // 普通字符基础延迟(毫秒)
  359. const slowPunct = /[。!?!?;;]/; // 句号、感叹号、分号等较长停顿
  360. const midPunct = /[,、,::]/; // 逗号、顿号、冒号等中等停顿
  361. const typeWriter = () => {
  362. if (index < responseText.length) {
  363. const ch = responseText.charAt(index);
  364. const current = messages.value[botIndex];
  365. // 通过数组替换触发渲染,避免部分平台对子项属性变更不响应
  366. messages.value.splice(
  367. botIndex,
  368. 1,
  369. { ...current, content: current.content + ch, isTyping: true }
  370. );
  371. index++;
  372. scrollToBottom();
  373. const delay = slowPunct.test(ch) ? 220 : midPunct.test(ch) ? 120 : baseDelay;
  374. setTimeout(typeWriter, delay);
  375. } else {
  376. const current = messages.value[botIndex];
  377. messages.value.splice(
  378. botIndex,
  379. 1,
  380. { ...current, isTyping: false }
  381. );
  382. isSending.value = false;
  383. nextTick(() => { scrollToBottom(); });
  384. }
  385. };
  386. // 启动前稍作停顿,避免过快开始
  387. setTimeout(typeWriter, 500);
  388. };
  389. // 当消息出现或变化时,测量容器并滚到底部
  390. watch(messages, (arr) => {
  391. if (arr.length > 0) {
  392. nextTick(() => {
  393. measureChatContainer();
  394. scrollToBottom();
  395. });
  396. }
  397. });
  398. // 滚动到底部(仅聊天区域滚动)
  399. const scrollToBottom = () => {
  400. if (!shouldAutoScroll.value) return;
  401. const query = uni.createSelectorQuery();
  402. query.select('#messageList').boundingClientRect();
  403. query.exec((res) => {
  404. if (res[0]) {
  405. latestContentHeight.value = res[0].height;
  406. chatScrollTop.value = res[0].height; // scroll-view 会自动夹紧到最大位置
  407. }
  408. });
  409. };
  410. const scrollToTop = () => {
  411. chatScrollTop.value = 0;
  412. };
  413. // 自动滚动控制:用户向上滚动时暂停自动滚到底部
  414. const shouldAutoScroll = ref(true);
  415. const latestContentHeight = ref(0);
  416. const lastScrollTop = ref(0);
  417. const windowHeight = uni.getSystemInfoSync().windowHeight;
  418. const AUTO_SCROLL_REENABLE_THRESHOLD = 40000; // px,接近底部时恢复自动滚动
  419. const onChatScroll = (e) => {
  420. const st = e.detail?.scrollTop || 0;
  421. const delta = st - lastScrollTop.value;
  422. lastScrollTop.value = st;
  423. if (delta < 0) {
  424. shouldAutoScroll.value = false;
  425. return;
  426. }
  427. const distanceToBottom = latestContentHeight.value - st - chatContainerHeight.value;
  428. if (distanceToBottom <= AUTO_SCROLL_REENABLE_THRESHOLD) {
  429. shouldAutoScroll.value = true;
  430. }
  431. };
  432. // 回到顶部图标拖拽状态
  433. const backTopX = ref(0);
  434. const backTopY = ref(0);
  435. const backTopDragging = ref(false);
  436. const backTopDragOffset = ref({ x: 0, y: 0 });
  437. const backTopTargetX = ref(0);
  438. const backTopTargetY = ref(0);
  439. let backTopRAF = 0;
  440. const backTopSmoothing = 0.15; // 越大越跟手,越小越顺滑
  441. const backTopEpsilon = 0.5; // 收敛阈值
  442. const raf = typeof requestAnimationFrame === 'function' ? requestAnimationFrame : (fn) => setTimeout(fn, 16);
  443. const caf = typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : (id) => clearTimeout(id);
  444. function stepBackTop() {
  445. const dx = backTopTargetX.value - backTopX.value;
  446. const dy = backTopTargetY.value - backTopY.value;
  447. // 插值缓动,避免每帧重排
  448. backTopX.value += dx * backTopSmoothing;
  449. backTopY.value += dy * backTopSmoothing;
  450. if (Math.abs(dx) > backTopEpsilon || Math.abs(dy) > backTopEpsilon) {
  451. backTopRAF = raf(stepBackTop);
  452. } else {
  453. backTopX.value = backTopTargetX.value;
  454. backTopY.value = backTopTargetY.value;
  455. backTopRAF = 0;
  456. }
  457. }
  458. function ensureBackTopRAF() {
  459. if (!backTopRAF) {
  460. backTopRAF = raf(stepBackTop);
  461. }
  462. }
  463. const clamp = (val, min, max) => Math.max(min, Math.min(val, max));
  464. const onBackTopTouchStart = (e) => {
  465. const t = e.touches && e.touches[0];
  466. if (!t) return;
  467. backTopDragging.value = false;
  468. backTopDragOffset.value = {
  469. x: t.pageX - backTopX.value,
  470. y: t.pageY - backTopY.value,
  471. };
  472. };
  473. const onBackTopTouchMove = (e) => {
  474. const t = e.touches && e.touches[0];
  475. if (!t) return;
  476. const sys = uni.getSystemInfoSync();
  477. const iconSize = uni.upx2px(100);
  478. const reserveBottom = uni.upx2px(260);
  479. const nx = t.pageX - backTopDragOffset.value.x;
  480. const ny = t.pageY - backTopDragOffset.value.y;
  481. const clampedX = clamp(nx, 0, sys.windowWidth - iconSize);
  482. const clampedY = clamp(ny, 0, sys.windowHeight - reserveBottom - iconSize);
  483. if (Math.abs(clampedX - backTopX.value) + Math.abs(clampedY - backTopY.value) > 3) {
  484. backTopDragging.value = true;
  485. }
  486. backTopTargetX.value = clampedX;
  487. backTopTargetY.value = clampedY;
  488. ensureBackTopRAF();
  489. };
  490. const onBackTopTouchEnd = () => {
  491. // 结束拖拽即可
  492. };
  493. const onBackTopClick = () => {
  494. if (backTopDragging.value) return; // 拖拽时不触发点击回到顶部
  495. scrollToTop();
  496. };
  497. function getDataInfo() {
  498. return new Promise((resolve, reject) => {
  499. uni.request({
  500. url: "http://localhost:8888/ka",
  501. data: {},
  502. header: {
  503. Accept: "application/json",
  504. "Content-Type": "application/json",
  505. "X-Requested-With": "XMLHttpRequest",
  506. },
  507. method: "GET",
  508. sslVerify: true,
  509. success: (res) => {
  510. resolve(res.data);
  511. },
  512. fail: (error) => {
  513. reject(error);
  514. },
  515. });
  516. });
  517. }
  518. </script>
  519. <style scoped>
  520. .deepMate-page {
  521. display: flex;
  522. flex-direction: column;
  523. position: fixed;
  524. /* 充满视口,彻底禁用页面滚动 */
  525. top: 0;
  526. left: 0;
  527. right: 0;
  528. bottom: 0;
  529. height: 100vh;
  530. overflow: hidden;
  531. /* 锁定页面滚动 */
  532. background-color: #ffffff;
  533. padding: 20rpx 0rpx;
  534. }
  535. .header {
  536. display: flex;
  537. justify-content: space-between;
  538. align-items: center;
  539. padding: 20rpx 30rpx;
  540. background-color: #ffffff;
  541. box-shadow: 0 2rpx rgba(0, 0, 0, 0.1);
  542. }
  543. .header-left,
  544. .header-right {
  545. display: flex;
  546. align-items: center;
  547. }
  548. .header-left .icon,
  549. .header-right .icon {
  550. width: 40rpx;
  551. height: 40rpx;
  552. /* margin-right: 20rpx; */
  553. }
  554. .header-center .title {
  555. position: fixed;
  556. top: 10rpx;
  557. left: 50%;
  558. transform: translateX(-50%);
  559. font-size: 36rpx;
  560. font-weight: bold;
  561. color: #333333;
  562. }
  563. .new-chat-button {
  564. background-color: #ff6600;
  565. border: none;
  566. border-radius: 8rpx;
  567. padding: 10rpx 20rpx;
  568. }
  569. .new-chat-text {
  570. color: white;
  571. font-size: 24rpx;
  572. }
  573. .main-content {
  574. background-color: #fff;
  575. flex: 1;
  576. display: flex;
  577. flex-direction: column;
  578. overflow: hidden;
  579. /* 内部滚动交给聊天容器 */
  580. padding: 20rpx;
  581. margin-top: 1rpx;
  582. margin-bottom: 280rpx;
  583. }
  584. /* 聊天顶部粘性欢迎块样式 */
  585. .chat-header {
  586. position: sticky;
  587. top: 0;
  588. /* 在页面滚动时始终贴顶 */
  589. z-index: 50;
  590. background-color: #ffffff;
  591. padding: 3rpx 20rpx;
  592. }
  593. .robot-container {
  594. display: flex;
  595. align-items: center;
  596. margin-top: 30rpx;
  597. }
  598. .robot-avatar {
  599. width: 130rpx;
  600. height: 130rpx;
  601. border-radius: 50%;
  602. margin-left: 20rpx;
  603. }
  604. .welcome-message {
  605. flex: 1;
  606. }
  607. .greeting {
  608. font-size: 32rpx;
  609. margin-left: 50rpx;
  610. top: 40rpx;
  611. font-weight: bold;
  612. color: #333333;
  613. line-height: 48rpx;
  614. }
  615. .description {
  616. display: block;
  617. font-size: 24rpx;
  618. color: #666666;
  619. line-height: 36rpx;
  620. margin-top: 10rpx;
  621. margin-left: 45rpx;
  622. }
  623. .function-tabs {
  624. display: block;
  625. overflow: hidden;
  626. margin-bottom: 30rpx;
  627. }
  628. .tabs-track {
  629. display: inline-flex;
  630. white-space: nowrap;
  631. will-change: transform;
  632. }
  633. .tab-item {
  634. padding: 5rpx 20rpx;
  635. border-radius: 20rpx;
  636. font-size: 20rpx;
  637. font-weight: 700;
  638. color: #666666;
  639. background-color: #fffefe;
  640. margin-right: 20rpx;
  641. transition: all 0.3s;
  642. }
  643. .tab-item.active {
  644. color: #ff6600;
  645. background-color: #fff;
  646. border: 1rpx solid #ff6600;
  647. }
  648. .recommend-card {
  649. background: url("https://d31zlh4on95l9h.cloudfront.net/images/4da1d629a55c307c3605ca15bf15189a.svg");
  650. background-repeat: no-repeat;
  651. background-position: center bottom;
  652. background-size: contain;
  653. /* min-height: 20rpx; */
  654. /* border-radius: 20rpx; */
  655. padding: 40rpx;
  656. margin-top: 20rpx;
  657. margin-bottom: 10rpx;
  658. /* box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); */
  659. }
  660. .card-content {
  661. display: flex;
  662. align-items: center;
  663. justify-content: space-between;
  664. margin-left: 40rpx;
  665. }
  666. .logo {
  667. width: 80rpx;
  668. height: 80rpx;
  669. background-color: #ff0000;
  670. border-radius: 10rpx;
  671. display: flex;
  672. align-items: center;
  673. justify-content: center;
  674. margin-right: 20rpx;
  675. }
  676. .card-text {
  677. flex: 1;
  678. margin-left: 20rpx;
  679. }
  680. .main-question {
  681. font-size: 32rpx;
  682. color: #333333;
  683. line-height: 48rpx;
  684. }
  685. .stock-code {
  686. display: block;
  687. font-size: 24rpx;
  688. color: #ff3b30;
  689. background-color: #ffffff;
  690. padding: 2rpx 15rpx;
  691. border-radius: 12rpx;
  692. margin-top: 8rpx;
  693. width: fit-content;
  694. border: 1rpx solid #ff3b30;
  695. }
  696. .arrow-icon {
  697. background: url("https://d31zlh4on95l9h.cloudfront.net/images/40d94054644f6e3f1c366751f07f0010.svg");
  698. background-repeat: no-repeat;
  699. left: 0.5rem;
  700. top: 1.8rem;
  701. background-size: 100% 100%;
  702. width: 60rpx;
  703. height: 60rpx;
  704. }
  705. .interest-section {
  706. margin-bottom: 30rpx;
  707. }
  708. .section-title {
  709. display: block;
  710. text-align: center;
  711. font-size: 26rpx;
  712. color: #666666;
  713. margin-bottom: 20rpx;
  714. }
  715. .topics-list {
  716. display: flex;
  717. flex-direction: column;
  718. gap: 15rpx;
  719. }
  720. .topic-item {
  721. display: flex;
  722. align-items: center;
  723. padding: 15rpx 20rpx;
  724. background-color: #f0f0f0;
  725. border-radius: 15rpx;
  726. width: fit-content;
  727. }
  728. .tag-icon {
  729. width: 24rpx;
  730. height: 24rpx;
  731. margin-right: 10rpx;
  732. }
  733. .topic-text {
  734. font-size: 28rpx;
  735. color: #333333;
  736. flex: 1;
  737. }
  738. /* 聊天区域样式 */
  739. .chat-container {
  740. margin-top: 30rpx;
  741. border-radius: 10rpx;
  742. height: calc(100vh - 50rpx);
  743. /* 缩短滚动区域,避免被输入框覆盖 */
  744. overflow-y: auto;
  745. -webkit-overflow-scrolling: touch;
  746. }
  747. .message-list {
  748. background-color: #fff;
  749. margin-bottom: 400rpx;
  750. /* padding: 20rpx; */
  751. }
  752. .message {
  753. display: flex;
  754. align-items: flex-start;
  755. margin-bottom: 30rpx;
  756. }
  757. /* .user-message {
  758. flex-direction: row-reverse;
  759. } */
  760. .message-icon {
  761. font-size: 24rpx;
  762. margin: 0 10rpx;
  763. padding: 10rpx;
  764. border-radius: 50%;
  765. background-color: #ddd;
  766. width: 40rpx;
  767. height: 40rpx;
  768. display: flex;
  769. align-items: center;
  770. justify-content: center;
  771. }
  772. .user-message .message-icon {
  773. background-color: transparent;
  774. border-radius: 0;
  775. /* padding: 0;
  776. width: auto;
  777. height: auto; */
  778. color: #333;
  779. }
  780. .bot-message .message-icon {
  781. background: url('https://d31zlh4on95l9h.cloudfront.net/images/61fa384381c88ad80be28f41827fe0e5.svg');
  782. color: white;
  783. }
  784. .message-content {
  785. max-width: 70%;
  786. position: relative;
  787. }
  788. .user-message .message-content {
  789. background-color: #007aff;
  790. border-radius: 10rpx;
  791. padding: 15rpx;
  792. }
  793. .bot-message .message-content {
  794. background-color: #f0f0f0;
  795. border-radius: 10rpx;
  796. padding: 15rpx;
  797. }
  798. .message-text {
  799. font-size: 28rpx;
  800. line-height: 40rpx;
  801. }
  802. .user-message .message-text {
  803. color: white;
  804. }
  805. .bot-message .message-text {
  806. color: #333;
  807. }
  808. .loading-dots {
  809. display: flex;
  810. align-items: center;
  811. padding-top: 10rpx;
  812. }
  813. .dot {
  814. width: 10rpx;
  815. height: 10rpx;
  816. background-color: #666;
  817. border-radius: 50%;
  818. margin: 0 4rpx;
  819. animation: loading 1.4s infinite ease-in-out both;
  820. }
  821. .user-message .dot {
  822. background-color: white;
  823. }
  824. .dot:nth-child(1) {
  825. animation-delay: -0.32s;
  826. }
  827. .dot:nth-child(2) {
  828. animation-delay: -0.16s;
  829. }
  830. @keyframes loading {
  831. 0%,
  832. 80%,
  833. 100% {
  834. transform: scale(0);
  835. }
  836. 40% {
  837. transform: scale(1);
  838. }
  839. }
  840. .input-area {
  841. position: fixed;
  842. margin-top: 20rpx;
  843. bottom: 70rpx;
  844. left: 0;
  845. right: 0;
  846. padding: 0 40rpx 80rpx 40rpx;
  847. background-color: #ffffff;
  848. }
  849. .input-wrapper {
  850. position: relative;
  851. display: flex;
  852. align-items: center;
  853. padding: 15rpx 20rpx;
  854. background-color: rgb(220, 31, 29);
  855. border-radius: 100rpx;
  856. display: flex;
  857. align-items: center;
  858. justify-content: center;
  859. height: 50rpx;
  860. }
  861. .mic-icon {
  862. width: 36rpx;
  863. height: 36rpx;
  864. margin-right: 20rpx;
  865. }
  866. .input-field {
  867. flex: 1;
  868. font-size: 28rpx;
  869. color: #fff;
  870. display: flex;
  871. align-items: center;
  872. justify-content: center;
  873. margin-left: 60rpx;
  874. background: none;
  875. border: none;
  876. outline: none;
  877. }
  878. .input-field::placeholder {
  879. color: #ffffff !important;
  880. opacity: 1;
  881. }
  882. /* .uni-scroll-view{
  883. height: 92%;
  884. } */
  885. .send-button {
  886. background: url("https://d31zlh4on95l9h.cloudfront.net/images/95f1ea2262e9157db13c93c0dc1c5d96.svg");
  887. background-repeat: no-repeat;
  888. background-size: 100% 100%;
  889. height: 50rpx;
  890. width: 50rpx;
  891. padding: 0;
  892. border: 1rpx solid transparent;
  893. margin-left: 20rpx;
  894. }
  895. .send-icon {
  896. width: 36rpx;
  897. height: 36rpx;
  898. }
  899. .disclaimer {
  900. font-size: 15rpx;
  901. color: #4d4c4c;
  902. display: flex;
  903. align-items: center;
  904. justify-content: center;
  905. margin-top: 15rpx;
  906. }
  907. .banner-panel {
  908. position: relative;
  909. height: 480rpx;
  910. /* 拉长容器,灰色背景跟随变高 */
  911. overflow: hidden;
  912. /* 让圆角和内部层剪裁一致 */
  913. border-radius: 15rpx;
  914. }
  915. .panelShow {
  916. height: 12%;
  917. }
  918. .pray-banner {
  919. position: absolute;
  920. /* background-size: 100% 100%; */
  921. inset: 0;
  922. /* 顶部、底部、左、右都贴合容器 */
  923. width: 100%;
  924. height: 81%;
  925. border-radius: 15rpx;
  926. z-index: 1;
  927. /* 在灰底之上、内容之下 */
  928. }
  929. .contain {
  930. margin: 0 20rpx;
  931. gap: 5rpx;
  932. }
  933. .banner-panel .robot-container,
  934. .banner-panel .function-tabs,
  935. .banner-panel .recommend-card {
  936. position: relative;
  937. z-index: 2;
  938. }
  939. .back-to-top {
  940. position: fixed;
  941. left: 0;
  942. top: 0;
  943. width: 100rpx;
  944. height: 100rpx;
  945. z-index: 1000;
  946. }
  947. .back-to-top:active {
  948. transform: scale(0.96);
  949. }
  950. .static-footer {
  951. position: fixed;
  952. bottom: 0;
  953. }
  954. /* 搜索历史侧拉框样式 */
  955. .drawer-overlay {
  956. position: fixed;
  957. left: 0;
  958. top: 0;
  959. right: 0;
  960. bottom: 0;
  961. background-color: rgba(0, 0, 0, 0.35);
  962. z-index: 900;
  963. }
  964. .drawer-panel {
  965. position: fixed;
  966. top: 0;
  967. right: 0;
  968. bottom: 0;
  969. width: 600rpx;
  970. max-width: 75%;
  971. background: #ffffff;
  972. box-shadow: -8rpx 0 20rpx rgba(0, 0, 0, 0.08);
  973. z-index: 901;
  974. display: flex;
  975. flex-direction: column;
  976. }
  977. .drawer-header {
  978. display: flex;
  979. align-items: center;
  980. justify-content: space-between;
  981. padding: 24rpx 28rpx;
  982. border-bottom: 2rpx solid #f0f0f0;
  983. }
  984. .drawer-title {
  985. font-size: 32rpx;
  986. font-weight: 600;
  987. color: #333333;
  988. }
  989. .drawer-close {
  990. font-size: 42rpx;
  991. color: #999999;
  992. }
  993. .drawer-content {
  994. flex: 1;
  995. padding: 20rpx 24rpx;
  996. }
  997. .history-card {
  998. background-color: #fff;
  999. border: 2rpx solid #f2f2f2;
  1000. border-left: 8rpx solid rgb(220, 31, 29);
  1001. border-radius: 12rpx;
  1002. padding: 18rpx 20rpx;
  1003. margin-bottom: 16rpx;
  1004. box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.03);
  1005. }
  1006. .history-query {
  1007. font-size: 28rpx;
  1008. color: #333333;
  1009. font-weight: 500; }
  1010. .history-time { margin-top: 8rpx; font-size: 22rpx; color: #888888; }
  1011. .empty-history { padding: 40rpx; color: #999999; text-align: center; }
  1012. .thinking-process {
  1013. margin: 20rpx 0;
  1014. border: 2rpx solid #e5e5e5;
  1015. border-radius: 2rpx;
  1016. background-color: #f9f9f9;
  1017. }
  1018. .thinking-header {
  1019. display: flex;
  1020. align-items: center;
  1021. padding: 20rpx 30rpx;
  1022. cursor: pointer;
  1023. background-color: #fff;
  1024. border-bottom: 2rpx solid #e5e5e5;
  1025. }
  1026. .thinking-icon {
  1027. font-size: 32rpx;
  1028. margin-right: 16rpx;
  1029. color: #d47c45;
  1030. }
  1031. .thinking-title {
  1032. font-size: 28rpx;
  1033. font-weight: 500;
  1034. color: #d47c45;
  1035. margin-right: 16rpx;
  1036. }
  1037. .thinking-count {
  1038. font-size: 24rpx;
  1039. color: #666;
  1040. margin-right: 16rpx;
  1041. }
  1042. .thinking-toggle {
  1043. font-size: 24rpx;
  1044. color: #999;
  1045. }
  1046. .thinking-content {
  1047. padding: 20rpx 30rpx;
  1048. }
  1049. .thinking-item {
  1050. display: flex;
  1051. align-items: center;
  1052. margin-bottom: 16rpx;
  1053. padding: 8rpx 0;
  1054. }
  1055. .item-status {
  1056. width: 32rpx;
  1057. height: 32rpx;
  1058. border-radius: 50%;
  1059. background-color: #f0f0f0;
  1060. display: flex;
  1061. justify-content: center;
  1062. align-items: center;
  1063. margin-right: 16rpx;
  1064. }
  1065. .checkmark {
  1066. font-size: 20rpx;
  1067. color: #ff0000;
  1068. }
  1069. .item-text {
  1070. font-size: 24rpx;
  1071. color: #333;
  1072. }
  1073. </style>