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.

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