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.

859 lines
23 KiB

4 weeks ago
  1. <!-- @format -->
  2. <template>
  3. <view class="main">
  4. <!-- 固定头部 -->
  5. <view class="header_fixed" :style="{ top: iSMT + 'px' }">
  6. <view class="header_content">
  7. <view class="header_back" @click="goBack">
  8. <image src="/static/marketSituation-image/back.png" mode=""></image>
  9. </view>
  10. <view class="header_input_wrapper">
  11. <image class="search_icon" src="/static/marketSituation-image/search.png" mode="" @click="onSearchClick"></image>
  12. <input class="header_input" type="text" placeholder="搜索" placeholder-style="color: #A6A6A6; font-size: 22rpx;" v-model="searchValue" @input="onSearchInput" @confirm="onSearchConfirm" />
  13. </view>
  14. <view class="header_icons">
  15. <view class="header_icon" @click="selected">
  16. <image src="/static/marketSituation-image/mySeclected.png" mode=""></image>
  17. </view>
  18. <view class="header_icon" @click="history">
  19. <image src="/static/marketSituation-image/history.png" mode=""></image>
  20. </view>
  21. </view>
  22. </view>
  23. <view class="warn">
  24. <image src="/static/marketSituation-image/warn.png" mode="aspectFit"></image>
  25. <view class="warn_text_container">
  26. <text :class="warnTextClass">{{ $t("marketSituation.warn") }}</text>
  27. </view>
  28. </view>
  29. </view>
  30. <!-- 内容区域 -->
  31. <scroll-view class="content" :style="{ top: contentTopPosition + 'px' }" scroll-y="true">
  32. <!-- 亚太-中华 -->
  33. <view class="market-section" v-for="(item, parentIndex) in marketSituationStore.gloablCardData" :key="item">
  34. <view class="market-header">
  35. <text class="market-title">{{ item.ac }}</text>
  36. <view class="market-more" @click="viewMore(item.ac)">
  37. <text class="more-text">查看更多</text>
  38. <text class="more-arrow">></text>
  39. </view>
  40. </view>
  41. <view class="cards-grid-three">
  42. <view v-for="(iitem, index) in item.list" :key="iitem" class="card-item">
  43. <IndexCard
  44. :market="iitem.market"
  45. :stockName="iitem.name"
  46. :currentPrice="iitem.currentPrice"
  47. :changeAmount="iitem.changeAmount"
  48. :changePercent="iitem.changePercent"
  49. :isRising="iitem.isRising"
  50. @click="viewIndexDetail(iitem, parentIndex, index)"
  51. />
  52. </view>
  53. </view>
  54. </view>
  55. <!-- 底部安全区域 -->
  56. <view class="bottom-safe-area"></view>
  57. </scroll-view>
  58. </view>
  59. <!-- 底部导航栏 -->
  60. <footerBar class="static-footer" :type="'marketSituation'"></footerBar>
  61. </template>
  62. <script setup>
  63. import { ref, onMounted, onUnmounted, computed, nextTick, watch } from "vue";
  64. import footerBar from "../../components/footerBar.vue";
  65. import IndexCard from "../../components/IndexCard.vue";
  66. import { getRegionalGroupAPI } from "../../api/marketSituation/marketSituation.js";
  67. import { useMarketSituationStore } from "../../stores/modules/marketSituation.js";
  68. const marketSituationStore = useMarketSituationStore();
  69. // 响应式数据
  70. const iSMT = ref(0); // 状态栏高度
  71. const contentHeight = ref(0);
  72. const headerHeight = ref(0); // 头部高度
  73. const searchValue = ref(""); // 搜索值
  74. const isWarnTextOverflow = ref(false); // warn文字是否溢出
  75. // warn文字的class计算属性
  76. const warnTextClass = computed(() => {
  77. return isWarnTextOverflow.value ? "warn_text scroll-active" : "warn_text";
  78. });
  79. // 检测warn文字是否溢出
  80. const checkWarnTextOverflow = () => {
  81. nextTick(() => {
  82. setTimeout(() => {
  83. const query = uni.createSelectorQuery();
  84. // 同时查询容器和文字元素
  85. query.select(".warn_text_container").boundingClientRect();
  86. query.select(".warn_text").boundingClientRect();
  87. query.exec((res) => {
  88. const containerRect = res[0];
  89. const textRect = res[1];
  90. if (!containerRect || !textRect) {
  91. return;
  92. }
  93. // 判断文字是否超出容器(留一些余量)
  94. const isOverflow = textRect.width > containerRect.width - 10;
  95. isWarnTextOverflow.value = isOverflow;
  96. });
  97. }, 500);
  98. });
  99. };
  100. const globalIndexArray = ref([]);
  101. // 亚太-中华指数数据
  102. const asiachinaIndexes = ref([
  103. {
  104. flagIcon: "/static/c1.png",
  105. stockName: "上证指数",
  106. stockCode: "noCode",
  107. currentPrice: "3933.96",
  108. changeAmount: "+24.32",
  109. changePercent: "+0.62%",
  110. isRising: true,
  111. },
  112. {
  113. flagIcon: "/static/c2.png",
  114. stockName: "深证成指",
  115. stockCode: "noCode",
  116. currentPrice: "45757.90",
  117. changeAmount: "-123.45",
  118. changePercent: "-0.27%",
  119. isRising: false,
  120. },
  121. {
  122. flagIcon: "/static/c3.png",
  123. stockName: "创业板指",
  124. stockCode: "noCode",
  125. currentPrice: "6606.08",
  126. changeAmount: "+89.76",
  127. changePercent: "+1.38%",
  128. isRising: true,
  129. },
  130. {
  131. flagIcon: "/static/c4.png",
  132. stockName: "HSI50",
  133. stockCode: "noCode",
  134. currentPrice: "22333.96",
  135. changeAmount: "+156.78",
  136. changePercent: "+0.71%",
  137. isRising: true,
  138. },
  139. {
  140. flagIcon: "/static/c5.png",
  141. stockName: "沪深300",
  142. stockCode: "noCode",
  143. currentPrice: "45757.90",
  144. changeAmount: "-89.12",
  145. changePercent: "-0.19%",
  146. isRising: false,
  147. },
  148. {
  149. flagIcon: "/static/c6.png",
  150. stockName: "上证50",
  151. stockCode: "noCode",
  152. currentPrice: "45757.90",
  153. changeAmount: "+234.56",
  154. changePercent: "+0.52%",
  155. isRising: true,
  156. },
  157. ]);
  158. // 亚太指数数据
  159. const asiaIndexes = ref([
  160. {
  161. flagIcon: "/static/c7.png",
  162. stockName: "日经225",
  163. stockCode: "noCode",
  164. currentPrice: "28456.78",
  165. changeAmount: "+234.56",
  166. changePercent: "+0.83%",
  167. isRising: true,
  168. },
  169. {
  170. flagIcon: "/static/c8.png",
  171. stockName: "韩国KOSPI",
  172. stockCode: "noCode",
  173. currentPrice: "2567.89",
  174. changeAmount: "-12.34",
  175. changePercent: "-0.48%",
  176. isRising: false,
  177. },
  178. {
  179. flagIcon: "/static/c9.png",
  180. stockName: "印度孟买",
  181. stockCode: "noCode",
  182. currentPrice: "65432.10",
  183. changeAmount: "+456.78",
  184. changePercent: "+0.70%",
  185. isRising: true,
  186. },
  187. ]);
  188. // 美洲指数数据
  189. const americaIndexes = ref([
  190. {
  191. flagIcon: "/static/c7.png",
  192. stockName: "道琼斯指数",
  193. stockCode: "noCode",
  194. currentPrice: "34567.89",
  195. changeAmount: "+123.45",
  196. changePercent: "+0.36%",
  197. isRising: true,
  198. },
  199. {
  200. flagIcon: "/static/c8.png",
  201. stockName: "纳斯达克",
  202. stockCode: "noCode",
  203. currentPrice: "13456.78",
  204. changeAmount: "-67.89",
  205. changePercent: "-0.50%",
  206. isRising: false,
  207. },
  208. {
  209. flagIcon: "/static/c9.png",
  210. stockName: "标普500",
  211. stockCode: "noCode",
  212. currentPrice: "4234.56",
  213. changeAmount: "+23.45",
  214. changePercent: "+0.56%",
  215. isRising: true,
  216. },
  217. ]);
  218. // 计算属性:内容区域顶部位置
  219. const contentTopPosition = computed(() => {
  220. const statusBarHeight = iSMT.value || 0;
  221. const currentHeaderHeight = headerHeight.value > 0 ? headerHeight.value : 100;
  222. return statusBarHeight + currentHeaderHeight;
  223. });
  224. // 方法:返回上一页
  225. const goBack = () => {
  226. uni.navigateBack();
  227. };
  228. // 方法:搜索输入
  229. const onSearchInput = (e) => {
  230. searchValue.value = e.detail.value;
  231. };
  232. // 方法:清除搜索
  233. const clearSearch = () => {
  234. searchValue.value = "";
  235. };
  236. // 方法:查看更多
  237. const viewMore = (market) => {
  238. console.log("查看更多:", market);
  239. uni.navigateTo({
  240. url: `/pages/marketSituation/marketDetail?market=${market}`,
  241. });
  242. };
  243. // 方法:查看指数详情
  244. const viewIndexDetail = (item, parentIndex, index) => {
  245. console.log("查看指数详情:", item.stockName);
  246. // uni.showToast({
  247. // title: `查看 ${item.stockName} 详情`,
  248. // icon: 'none',
  249. // duration: 2000
  250. // })
  251. // 这里可以跳转到具体的指数详情页面
  252. uni.navigateTo({
  253. url: `/pages/marketSituation/marketCondition?stockInformation=${encodeURIComponent(JSON.stringify(item))}&parentIndex=${parentIndex}&index=${index}&from=globalIndex`,
  254. });
  255. };
  256. const getRegionalGroup = async () => {
  257. try {
  258. const result = await getRegionalGroupAPI();
  259. globalIndexArray.value = result.data;
  260. marketSituationStore.gloablCardData = result.data;
  261. } catch (e) {
  262. console.log("获取区域指数失败", e);
  263. }
  264. };
  265. // TCP相关响应式变量
  266. import tcpConnection, { TCPConnection, TCP_CONFIG } from "@/api/tcpConnection.js";
  267. const tcpConnected = ref(false);
  268. const connectionListener = ref(null);
  269. const messageListener = ref(null);
  270. // 初始化TCP监听器
  271. const initTcpListeners = () => {
  272. // 创建连接状态监听器并保存引用
  273. connectionListener.value = (status, result) => {
  274. tcpConnected.value = status === "connected";
  275. console.log("TCP连接状态变化:", status, tcpConnected.value);
  276. // 显示连接状态提示
  277. // 如果连接,发送获取批量数据
  278. if (status === "connected") {
  279. sendTcpMessage("batch_real_time");
  280. }
  281. };
  282. // 创建消息监听器并保存引用
  283. messageListener.value = (type, message, parsedArray) => {
  284. const messageObj = {
  285. type: type,
  286. content: message,
  287. parsedArray: parsedArray,
  288. timestamp: new Date().toLocaleTimeString(),
  289. direction: "received",
  290. };
  291. // 解析股票数据
  292. parseStockData(message);
  293. };
  294. // 注册监听器
  295. tcpConnection.onConnectionChange(connectionListener.value);
  296. tcpConnection.onMessage(messageListener.value);
  297. };
  298. // 连接TCP服务器
  299. const connectTcp = () => {
  300. console.log("开始连接TCP服务器...");
  301. tcpConnection.connect();
  302. };
  303. // 断开TCP连接
  304. const disconnectTcp = () => {
  305. console.log("断开TCP连接...");
  306. tcpConnection.disconnect();
  307. tcpConnected.value = false;
  308. };
  309. // 发送TCP消息
  310. const sendTcpMessage = (command) => {
  311. let messageData;
  312. let messageDataArray = [];
  313. if (command == "batch_real_time") {
  314. for (let i = 0; i < globalIndexArray.value.length; ++i) {
  315. for (let j = 0; j < globalIndexArray.value[i].list.length; ++j) {
  316. messageDataArray.push(globalIndexArray.value[i].list[j].code);
  317. }
  318. }
  319. }
  320. console.log(messageDataArray);
  321. switch (command) {
  322. // 实时行情推送
  323. case "real_time":
  324. messageData = {
  325. command: "real_time",
  326. stock_code: "SH.000001",
  327. };
  328. break;
  329. // 初始化获取行情历史数据
  330. case "init_real_time":
  331. messageData = {
  332. command: "init_real_time",
  333. stock_code: "SH.000001",
  334. };
  335. break;
  336. case "stop_real_time":
  337. messageData = {
  338. command: "stop_real_time",
  339. };
  340. break;
  341. // 股票列表
  342. case "stock_list":
  343. messageData = {
  344. command: "stock_list",
  345. };
  346. break;
  347. case "batch_real_time":
  348. messageData = {
  349. command: "batch_real_time",
  350. stock_codes: messageDataArray,
  351. };
  352. break;
  353. case "help":
  354. messageData = {
  355. command: "help",
  356. };
  357. break;
  358. }
  359. if (!messageData) {
  360. return;
  361. } else {
  362. try {
  363. // 发送消息
  364. const success = tcpConnection.send(messageData);
  365. if (success) {
  366. console.log("home发送TCP消息:", messageData);
  367. }
  368. } catch (error) {
  369. console.error("发送TCP消息时出错:", error);
  370. }
  371. }
  372. };
  373. // 获取TCP连接状态
  374. const getTcpStatus = () => {
  375. const status = tcpConnection.getConnectionStatus();
  376. uni.showModal({
  377. title: "TCP连接状态",
  378. content: `当前状态: ${status ? "已连接" : "未连接"}`,
  379. showCancel: false,
  380. });
  381. };
  382. let isMorePacket = {
  383. init_batch_real_time: false,
  384. batch_real_time: false,
  385. };
  386. let receivedMessage;
  387. // 解析TCP股票数据
  388. const parseStockData = (message) => {
  389. try {
  390. console.log("进入parseStockData, message类型:", typeof message);
  391. let parsedMessage;
  392. // 如果isMorePacket是true,说明正在接受分包数据,无条件接收
  393. // 如果message是字符串且以{开头,说明是JSON字符串,需要解析
  394. // 如果不属于以上两种情况,说明是普通字符串,不预解析
  395. if (message.includes("欢迎连接到股票数据服务器")) {
  396. console.log("服务器命令列表,不予处理");
  397. return;
  398. }
  399. if ((typeof message === "string" && message.includes("batch_data_start")) || isMorePacket.init_batch_real_time) {
  400. if (typeof message === "string" && message.includes("batch_data_start")) {
  401. console.log("开始接受分包数据");
  402. receivedMessage = "";
  403. } else {
  404. console.log("接收分包数据过程中");
  405. }
  406. isMorePacket.init_batch_real_time = true;
  407. receivedMessage += message;
  408. // 如果当前消息包含},说明收到JSON字符串结尾,结束接收,开始解析
  409. if (receivedMessage.includes("batch_data_complete")) {
  410. console.log("接受分包数据结束");
  411. isMorePacket.init_batch_real_time = false;
  412. console.log("展示数据", receivedMessage);
  413. let startIndex = 0;
  414. let startCount = 0;
  415. let endIndex = receivedMessage.indexOf("batch_data_complete");
  416. for (let i = 0; i < receivedMessage.length; ++i) {
  417. if (receivedMessage[i] == "{") {
  418. startCount++;
  419. if (startCount == 2) {
  420. startIndex = i;
  421. break;
  422. }
  423. }
  424. }
  425. for (let i = receivedMessage.indexOf("batch_data_complete"); i >= 0; --i) {
  426. if (receivedMessage[i] == "}" || startIndex == endIndex) {
  427. endIndex = i;
  428. break;
  429. }
  430. }
  431. if (startIndex >= endIndex) {
  432. throw new Error("JSON字符串格式错误");
  433. }
  434. console.log("message", startIndex, endIndex, receivedMessage[endIndex], receivedMessage[startIndex]);
  435. parsedMessage = JSON.parse(receivedMessage.substring(startIndex, endIndex + 1));
  436. console.log("JSON解析成功,解析后类型:", typeof parsedMessage, parsedMessage);
  437. const stockDataArray = parsedMessage.data;
  438. for (let i = 0; i < globalIndexArray.value.length; ++i) {
  439. for (let j = 0; j < globalIndexArray.value[i].list.length; ++j) {
  440. const stockCode = globalIndexArray.value[i].list[j].code;
  441. marketSituationStore.gloablCardData[i].list[j].currentPrice = stockDataArray[stockCode][0].current_price.toFixed(2);
  442. marketSituationStore.gloablCardData[i].list[j].changeAmount = (stockDataArray[stockCode][0].current_price - stockDataArray[stockCode][0].pre_close).toFixed(2);
  443. marketSituationStore.gloablCardData[i].list[j].changePercent = ((100 * (stockDataArray[stockCode][0].current_price - stockDataArray[stockCode][0].pre_close)) / stockDataArray[stockCode][0].pre_close).toFixed(2) + "%";
  444. marketSituationStore.gloablCardData[i].list[j].isRising = stockDataArray[stockCode][0].current_price - stockDataArray[stockCode][0].pre_close >= 0;
  445. }
  446. }
  447. }
  448. } else if ((typeof message === "string" && message.includes('{"count')) || isMorePacket.batch_real_time) {
  449. if (typeof message === "string" && message.includes('{"count')) {
  450. console.log("开始接受分包数据");
  451. receivedMessage = "";
  452. } else {
  453. console.log("接收分包数据过程中");
  454. }
  455. isMorePacket.batch_real_time = true;
  456. receivedMessage += message;
  457. // 如果当前消息包含},说明收到JSON字符串结尾,结束接收,开始解析
  458. if (receivedMessage.includes("batch_realtime_data")) {
  459. console.log("接受分包数据结束");
  460. isMorePacket.batch_real_time = false;
  461. console.log("展示数据", receivedMessage);
  462. let startIndex = 0;
  463. let endIndex = receivedMessage.length - 1;
  464. for (let i = 0; i < receivedMessage.length; ++i) {
  465. if (receivedMessage[i] == "{") {
  466. startIndex = i;
  467. break;
  468. }
  469. }
  470. for (let i = receivedMessage.length - 1; i >= 0; --i) {
  471. if (receivedMessage[i] == "}" || startIndex == endIndex) {
  472. endIndex = i;
  473. break;
  474. }
  475. }
  476. if (startIndex >= endIndex) {
  477. throw new Error("JSON字符串格式错误");
  478. }
  479. parsedMessage = JSON.parse(receivedMessage.substring(startIndex, endIndex + 1));
  480. console.log("JSON解析成功,解析后类型:", typeof parsedMessage, parsedMessage);
  481. const stockDataArray = parsedMessage.data;
  482. for (let i = 0; i < globalIndexArray.value.length; ++i) {
  483. for (let j = 0; j < globalIndexArray.value[i].list.length; ++j) {
  484. const stockCode = globalIndexArray.value[i].list[j].code;
  485. marketSituationStore.gloablCardData[i].list[j].currentPrice = stockDataArray[stockCode][0].current_price.toFixed(2);
  486. marketSituationStore.gloablCardData[i].list[j].changeAmount = (stockDataArray[stockCode][0].current_price - stockDataArray[stockCode][0].pre_close).toFixed(2);
  487. marketSituationStore.gloablCardData[i].list[j].changePercent = ((100 * (stockDataArray[stockCode][0].current_price - stockDataArray[stockCode][0].pre_close)) / stockDataArray[stockCode][0].pre_close).toFixed(2) + "%";
  488. marketSituationStore.gloablCardData[i].list[j].isRising = stockDataArray[stockCode][0].current_price - stockDataArray[stockCode][0].pre_close >= 0;
  489. }
  490. }
  491. }
  492. } else {
  493. // 没有通过JSON解析判断,说明不是需要的数据
  494. console.log("不是需要的数据,不做处理");
  495. }
  496. } catch (error) {
  497. console.error("解析TCP股票数据失败:", error.message);
  498. console.error("错误详情:", error);
  499. }
  500. };
  501. // 移除TCP监听器
  502. const removeTcpListeners = () => {
  503. if (connectionListener.value) {
  504. tcpConnection.removeConnectionListener(connectionListener.value);
  505. connectionListener.value = null;
  506. console.log("已移除TCP连接状态监听器");
  507. }
  508. if (messageListener.value) {
  509. tcpConnection.removeMessageListener(messageListener.value);
  510. messageListener.value = null;
  511. console.log("已移除TCP消息监听器");
  512. }
  513. };
  514. const startTcp = () => {
  515. try {
  516. removeTcpListeners();
  517. disconnectTcp();
  518. initTcpListeners();
  519. connectTcp();
  520. } catch (error) {
  521. console.error("建立连接并设置监听出错:", error);
  522. }
  523. };
  524. onUnmounted(() => {
  525. sendTcpMessage("stop_real_time");
  526. removeTcpListeners();
  527. disconnectTcp();
  528. });
  529. // 生命周期:页面挂载
  530. onMounted(async () => {
  531. await getRegionalGroup();
  532. initTcpListeners();
  533. await nextTick();
  534. // 开始连接
  535. startTcp();
  536. // 获取系统信息
  537. const systemInfo = uni.getSystemInfoSync();
  538. iSMT.value = systemInfo.statusBarHeight || 0;
  539. console.log("全球指数页面加载完成");
  540. // 动态计算header实际高度
  541. uni
  542. .createSelectorQuery()
  543. .select(".header_fixed")
  544. .boundingClientRect((rect) => {
  545. if (rect) {
  546. headerHeight.value = rect.height;
  547. console.log("Header实际高度:", headerHeight.value, "px");
  548. }
  549. })
  550. .exec();
  551. // 检测warn文字是否溢出
  552. checkWarnTextOverflow();
  553. });
  554. // 监听headerHeight变化,重新计算contentHeight
  555. watch(headerHeight, (newHeight) => {
  556. if (newHeight > 0) {
  557. const systemInfo = uni.getSystemInfoSync();
  558. const windowHeight = systemInfo.windowHeight;
  559. const statusBarHeight = systemInfo.statusBarHeight || 0;
  560. const footerHeight = 100;
  561. contentHeight.value = windowHeight - statusBarHeight - newHeight - footerHeight;
  562. console.log("重新计算contentHeight:", contentHeight.value);
  563. }
  564. });
  565. </script>
  566. <style lang="scss" scoped>
  567. .main {
  568. position: relative;
  569. height: 100vh;
  570. overflow: hidden;
  571. background-color: #f5f5f5;
  572. }
  573. /* 状态栏占位 */
  574. .top {
  575. position: fixed;
  576. top: 0;
  577. left: 0;
  578. right: 0;
  579. z-index: 1001;
  580. background-color: #ffffff;
  581. }
  582. /* 固定头部样式 */
  583. .header_fixed {
  584. position: fixed;
  585. left: 0;
  586. right: 0;
  587. z-index: 1000;
  588. background-color: #ffffff;
  589. padding: 20rpx 0 0 0;
  590. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  591. }
  592. .header_content {
  593. display: flex;
  594. align-items: center;
  595. justify-content: space-between;
  596. height: 80rpx;
  597. padding: 0 20rpx;
  598. margin-bottom: 10rpx;
  599. }
  600. .header_back {
  601. margin-right: 20rpx;
  602. width: 25rpx;
  603. height: 30rpx;
  604. }
  605. .header_back image {
  606. width: 25rpx;
  607. height: 30rpx;
  608. }
  609. .header_input_wrapper {
  610. display: flex;
  611. align-items: center;
  612. width: 100%;
  613. margin: 0 20rpx 0 0;
  614. height: 70rpx;
  615. border-radius: 35rpx;
  616. background-color: #ffffff;
  617. border: 1rpx solid #e9ecef;
  618. padding: 0 80rpx 0 30rpx;
  619. font-size: 28rpx;
  620. color: #5c5c5c;
  621. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  622. }
  623. .search_icon {
  624. width: 40rpx;
  625. height: 40rpx;
  626. opacity: 0.6;
  627. }
  628. .header_input {
  629. margin-left: 10rpx;
  630. }
  631. .header_icons {
  632. display: flex;
  633. align-items: center;
  634. gap: 15rpx;
  635. }
  636. .header_icon {
  637. width: 40rpx;
  638. height: 40rpx;
  639. display: flex;
  640. align-items: center;
  641. justify-content: center;
  642. }
  643. .header_icon image {
  644. width: 40rpx;
  645. height: 40rpx;
  646. }
  647. .warn {
  648. display: flex;
  649. align-items: center;
  650. justify-content: flex-start;
  651. gap: 10rpx;
  652. font-size: 28rpx;
  653. color: #666666;
  654. padding: 20rpx;
  655. max-width: 100%;
  656. overflow: hidden;
  657. position: relative;
  658. }
  659. .warn image {
  660. width: 40rpx;
  661. height: 40rpx;
  662. flex-shrink: 0;
  663. /* 防止图片被压缩 */
  664. position: relative;
  665. z-index: 2;
  666. /* 确保图片在最上层 */
  667. }
  668. .warn_text_container {
  669. flex: 1;
  670. overflow: hidden;
  671. position: relative;
  672. min-width: 0;
  673. /* 允许容器收缩 */
  674. }
  675. .warn_text {
  676. display: block;
  677. white-space: nowrap;
  678. will-change: transform;
  679. /* 优化动画性能 */
  680. }
  681. /* 文字滚动动画 */
  682. @keyframes scrollText {
  683. 0% {
  684. transform: translateX(0);
  685. }
  686. 20% {
  687. transform: translateX(0);
  688. }
  689. 80% {
  690. transform: translateX(-85%);
  691. }
  692. 100% {
  693. transform: translateX(-85%);
  694. }
  695. }
  696. /* 当文字超长时启用滚动动画 */
  697. .warn_text.scroll-active {
  698. animation: scrollText 12s linear infinite;
  699. animation-delay: 2s;
  700. /* 延迟2秒开始滚动,让用户先看到开头 */
  701. }
  702. /* 内容区域 */
  703. .content {
  704. position: fixed;
  705. left: 0;
  706. right: 0;
  707. bottom: 120rpx;
  708. background-color: #f5f5f5;
  709. padding: 0;
  710. }
  711. /* 市场分组 */
  712. .market-section {
  713. background-color: white;
  714. border-radius: 20rpx;
  715. }
  716. .market-header {
  717. margin: 20rpx 20rpx 0 20rpx;
  718. display: flex;
  719. align-items: center;
  720. justify-content: space-between;
  721. margin-bottom: 10rpx;
  722. padding-bottom: 10rpx;
  723. border-bottom: 2rpx solid #f0f0f0;
  724. }
  725. .market-title {
  726. font-size: 32rpx;
  727. font-weight: 600;
  728. color: #333;
  729. }
  730. .market-more {
  731. display: flex;
  732. align-items: center;
  733. gap: 8rpx;
  734. }
  735. .more-text {
  736. font-size: 24rpx;
  737. color: #666;
  738. }
  739. .more-arrow {
  740. font-size: 20rpx;
  741. color: #666;
  742. font-weight: bold;
  743. }
  744. /* 三列卡片网格 */
  745. .cards-grid-three {
  746. display: grid;
  747. grid-template-columns: repeat(3, 1fr);
  748. }
  749. .card-item {
  750. background-color: white;
  751. border-radius: 16rpx;
  752. overflow: hidden;
  753. transition: transform 0.2s ease, box-shadow 0.2s ease;
  754. }
  755. .card-item:active {
  756. transform: scale(0.98);
  757. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.12);
  758. }
  759. /* 底部安全区域 */
  760. .bottom-safe-area {
  761. height: 40rpx;
  762. background-color: transparent;
  763. }
  764. /* 底部导航栏 */
  765. .static-footer {
  766. position: fixed;
  767. bottom: 0;
  768. left: 0;
  769. right: 0;
  770. z-index: 1000;
  771. }
  772. /* 响应式设计 */
  773. @media (max-width: 400rpx) {
  774. .cards-grid-three {
  775. grid-template-columns: repeat(2, 1fr);
  776. }
  777. }
  778. </style>