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.

947 lines
20 KiB

4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
  1. <!-- @format -->
  2. <template>
  3. <view class="main">
  4. <!-- 可滚动内容区域 -->
  5. <scroll-view class="content_scroll" scroll-y="true" :style="{ top: contentTopPosition + 'px' }">
  6. <view class="content">
  7. <view class="map">
  8. <image src="/static/marketSituation-image/map.png" mode="widthFix"></image>
  9. </view>
  10. <view class="global_index">
  11. <view class="global_index_title">
  12. {{ $t("marketSituation.globalIndex") }}
  13. </view>
  14. <view class="global_index_more" @click="goToGlobalIndex">
  15. <text>{{ $t("marketSituation.globalIndexMore") }}</text>
  16. <image src="/static/marketSituation-image/more.png" mode="aspectFit"></image>
  17. </view>
  18. </view>
  19. <!-- 卡片网格 -->
  20. <view class="cards_grid">
  21. <view v-for="(card, index) in marketSituationStore.cardData" :key="index" class="card_item">
  22. <IndexCard
  23. :market="card.market"
  24. :stockName="card.stockName"
  25. :currentPrice="card.currentPrice"
  26. :changeAmount="card.changeAmount"
  27. :changePercent="card.changePercent"
  28. :isRising="card.isRising"
  29. @click="viewIndexDetail(card, index)"
  30. />
  31. </view>
  32. </view>
  33. <view class="warn">
  34. <image src="/static/marketSituation-image/warn.png" mode="aspectFit"></image>
  35. <view class="warn_text_container">
  36. <text :class="warnTextClass">{{ $t("marketSituation.warn") }}</text>
  37. </view>
  38. </view>
  39. <!-- 底部安全区域防止被导航栏遮挡 -->
  40. <view class="bottom_safe_area"></view>
  41. </view>
  42. </scroll-view>
  43. </view>
  44. </template>
  45. <script setup>
  46. import { ref, onMounted, onUnmounted, watch, nextTick, computed } from "vue";
  47. import util from "../../common/util.js";
  48. import IndexCard from "../../components/IndexCard.vue";
  49. import { useMarketSituationStore } from "../../stores/modules/marketSituation.js";
  50. const marketSituationStore = useMarketSituationStore();
  51. import { getGlobalIndexAPI } from "../../api/marketSituation/marketSituation.js";
  52. const iSMT = ref(0);
  53. const searchValue = ref("");
  54. const contentHeight = ref(0);
  55. const headerHeight = ref(0); // 动态计算的header高度
  56. const isWarnTextOverflow = ref(false); // warn文字是否溢出
  57. const pageIndex = ref(0);
  58. const scrollToView = ref("");
  59. // 计算属性:精准计算content区域的top值
  60. const contentTopPosition = computed(() => {
  61. const statusBarHeight = iSMT.value || 0;
  62. const currentHeaderHeight = headerHeight.value > 0 ? headerHeight.value : 140;
  63. return statusBarHeight + currentHeaderHeight;
  64. });
  65. // warn文字的class计算属性
  66. const warnTextClass = computed(() => {
  67. return isWarnTextOverflow.value ? "warn_text scroll-active" : "warn_text";
  68. });
  69. const globalIndexArray = ref([]);
  70. // 搜索输入事件
  71. const onSearchInput = (e) => {
  72. searchValue.value = e.detail.value;
  73. };
  74. // 搜索确认事件
  75. const onSearchConfirm = (e) => {
  76. console.log("搜索内容:", e.detail.value);
  77. // 这里可以添加搜索逻辑
  78. performSearch(e.detail.value);
  79. };
  80. // 搜索图标点击事件
  81. const onSearchClick = () => {
  82. if (searchValue.value.trim()) {
  83. performSearch(searchValue.value);
  84. }
  85. };
  86. // 执行搜索
  87. const performSearch = (keyword) => {
  88. if (!keyword.trim()) {
  89. uni.showToast({
  90. title: "请输入搜索内容",
  91. icon: "none",
  92. });
  93. return;
  94. }
  95. uni.showToast({
  96. title: `搜索: ${keyword}`,
  97. icon: "none",
  98. });
  99. // 这里添加实际的搜索逻辑
  100. };
  101. // 检测warn文字是否溢出
  102. const checkWarnTextOverflow = () => {
  103. nextTick(() => {
  104. setTimeout(() => {
  105. const query = uni.createSelectorQuery();
  106. // 同时查询容器和文字元素
  107. query.select(".warn_text_container").boundingClientRect();
  108. query.select(".warn_text").boundingClientRect();
  109. query.exec((res) => {
  110. const containerRect = res[0];
  111. const textRect = res[1];
  112. if (!containerRect || !textRect) {
  113. return;
  114. }
  115. // 判断文字是否超出容器(留一些余量)
  116. const isOverflow = textRect.width > containerRect.width - 10;
  117. isWarnTextOverflow.value = isOverflow;
  118. });
  119. }, 500);
  120. });
  121. };
  122. // 方法:查看指数详情
  123. const viewIndexDetail = (item, index) => {
  124. console.log("查看指数详情:", item.stockName);
  125. // uni.showToast({
  126. // title: `查看 ${item.stockName} 详情`,
  127. // icon: 'none',
  128. // duration: 2000
  129. // })
  130. // 这里可以跳转到具体的指数详情页面
  131. uni.navigateTo({
  132. url: `/pages/marketSituation/marketCondition?stockInformation=${encodeURIComponent(JSON.stringify(item))}&index=${index}&from=marketOverview`,
  133. });
  134. };
  135. // 跳转到全球指数页面
  136. const goToGlobalIndex = () => {
  137. uni.navigateTo({
  138. url: "/pages/marketSituation/globalIndex",
  139. });
  140. };
  141. const getGlobalIndex = async () => {
  142. try {
  143. const result = await getGlobalIndexAPI();
  144. globalIndexArray.value = result.data;
  145. } catch (e) {
  146. console.log("获取全球指数失败", e);
  147. }
  148. };
  149. // TCP相关响应式变量
  150. import tcpConnection, { TCPConnection, TCP_CONFIG } from "@/api/tcpConnection.js";
  151. const tcpConnected = ref(false);
  152. const connectionListener = ref(null);
  153. const messageListener = ref(null);
  154. // 初始化TCP监听器
  155. const initTcpListeners = () => {
  156. // 创建连接状态监听器并保存引用
  157. connectionListener.value = (status, result) => {
  158. tcpConnected.value = status === "connected";
  159. console.log("TCP连接状态变化:", status, tcpConnected.value);
  160. // 显示连接状态提示
  161. uni.showToast({
  162. title: status === "connected" ? "TCP连接成功" : "TCP连接断开",
  163. icon: status === "connected" ? "success" : "none",
  164. duration: 2000,
  165. });
  166. // 如果连接,发送获取批量数据
  167. if (status === "connected") {
  168. if (klineTab.value == 1) {
  169. sendTcpMessage("batch_real_time");
  170. }
  171. }
  172. };
  173. // 创建消息监听器并保存引用
  174. messageListener.value = (type, message, parsedArray) => {
  175. const messageObj = {
  176. type: type,
  177. content: message,
  178. parsedArray: parsedArray,
  179. timestamp: new Date().toLocaleTimeString(),
  180. direction: "received",
  181. };
  182. // 解析股票数据
  183. parseStockData(message);
  184. };
  185. // 注册监听器
  186. tcpConnection.onConnectionChange(connectionListener.value);
  187. tcpConnection.onMessage(messageListener.value);
  188. };
  189. // 连接TCP服务器
  190. const connectTcp = () => {
  191. console.log("开始连接TCP服务器...");
  192. tcpConnection.connect();
  193. };
  194. // 断开TCP连接
  195. const disconnectTcp = () => {
  196. console.log("断开TCP连接...");
  197. tcpConnection.disconnect();
  198. tcpConnected.value = false;
  199. };
  200. // 发送TCP消息
  201. const sendTcpMessage = (command) => {
  202. let messageData;
  203. let messageDataArray = [];
  204. if (command == "batch_real_time") {
  205. messageDataArray = globalIndexArray.value.map((item) => item.stockCode);
  206. }
  207. switch (command) {
  208. // 实时行情推送
  209. case "real_time":
  210. messageData = {
  211. command: "real_time",
  212. stock_code: "SH.000001",
  213. };
  214. break;
  215. // 初始化获取行情历史数据
  216. case "init_real_time":
  217. messageData = {
  218. command: "init_real_time",
  219. stock_code: "SH.000001",
  220. };
  221. break;
  222. case "stop_real_time":
  223. messageData = {
  224. command: "stop_real_time",
  225. };
  226. break;
  227. // 股票列表
  228. case "stock_list":
  229. messageData = {
  230. command: "stock_list",
  231. };
  232. break;
  233. case "batch_real_time":
  234. messageData = {
  235. command: "batch_real_time",
  236. stock_codes: ["SH.000001", "SH.000002"],
  237. };
  238. break;
  239. case "help":
  240. messageData = {
  241. command: "help",
  242. };
  243. break;
  244. }
  245. if (!messageData) {
  246. uni.showToast({
  247. title: "命令不存在",
  248. icon: "none",
  249. duration: 1000,
  250. });
  251. return;
  252. } else {
  253. try {
  254. // 发送消息
  255. const success = tcpConnection.send(messageData);
  256. if (success) {
  257. console.log("home发送TCP消息:", messageData);
  258. uni.showToast({
  259. title: "消息发送成功",
  260. icon: "success",
  261. duration: 1000,
  262. });
  263. }
  264. } catch (error) {
  265. console.error("发送TCP消息时出错:", error);
  266. uni.showToast({
  267. title: "消息发送失败",
  268. icon: "none",
  269. duration: 1000,
  270. });
  271. }
  272. }
  273. };
  274. // 获取TCP连接状态
  275. const getTcpStatus = () => {
  276. const status = tcpConnection.getConnectionStatus();
  277. uni.showModal({
  278. title: "TCP连接状态",
  279. content: `当前状态: ${status ? "已连接" : "未连接"}`,
  280. showCancel: false,
  281. });
  282. };
  283. let isMorePacket = {
  284. batch_real_time: false,
  285. };
  286. let receivedMessage;
  287. const findJsonPacket = (message, command) => {
  288. let jsonStartIndex = 0;
  289. let jsonEndIndex = message.indexOf(command);
  290. let jsonStartCount = 0;
  291. let jsonEndCount = 0;
  292. for (let i = 0; i < message.length - 1; ++i) {
  293. if (message[i] == "{") {
  294. jsonStartCount++;
  295. if (jsonStartCount == 2) {
  296. jsonStartIndex = i;
  297. break;
  298. }
  299. }
  300. }
  301. for (let i = message.indexOf(command); i >= 0; --i) {
  302. if (message[i] == "}" || i == jsonStartIndex) {
  303. jsonEndCount++;
  304. if (jsonEndCount == 1) {
  305. jsonEndIndex = i;
  306. break;
  307. }
  308. }
  309. }
  310. // 检查JSON字符串是否有效
  311. if (jsonStartIndex >= jsonEndIndex) {
  312. return { error: true };
  313. }
  314. return { json: JSON.parse(message.substring(jsonStartIndex, jsonEndIndex + 1)) };
  315. };
  316. // 解析TCP股票数据
  317. const parseStockData = (message) => {
  318. try {
  319. console.log("进入parseStockData, message类型:", typeof message);
  320. let parsedMessage;
  321. // 如果isMorePacket是true,说明正在接受分包数据,无条件接收
  322. // 如果message是字符串且以{开头,说明是JSON字符串,需要解析
  323. // 如果不属于以上两种情况,说明是普通字符串,不预解析
  324. if (message.includes("欢迎连接到股票数据服务器")) {
  325. console.log("服务器命令列表,不予处理");
  326. return;
  327. }
  328. if (message.includes("real_time")) {
  329. let startIndex = 0;
  330. let endIndex = message.length;
  331. for (let i = 0; i < message.length - 1; ++i) {
  332. if (message[i] == "{") {
  333. startIndex = i;
  334. break;
  335. }
  336. }
  337. for (let i = message.length - 1; i >= 0; --i) {
  338. if (message[i] == "}") {
  339. endIndex = i;
  340. break;
  341. }
  342. }
  343. parsedMessage = JSON.parse(message.substring(startIndex, endIndex + 1));
  344. console.log("实时数据解析", parsedMessage);
  345. return;
  346. }else {
  347. // 没有通过JSON解析判断,说明不是需要的数据
  348. console.log("不是需要的数据,不做处理");
  349. }
  350. } catch (error) {
  351. console.error("解析TCP股票数据失败:", error.message);
  352. console.error("错误详情:", error);
  353. }
  354. };
  355. // 移除TCP监听器
  356. const removeTcpListeners = () => {
  357. if (connectionListener.value) {
  358. tcpConnection.removeConnectionListener(connectionListener.value);
  359. connectionListener.value = null;
  360. console.log("已移除TCP连接状态监听器");
  361. }
  362. if (messageListener.value) {
  363. tcpConnection.removeMessageListener(messageListener.value);
  364. messageListener.value = null;
  365. console.log("已移除TCP消息监听器");
  366. }
  367. };
  368. const startTcp = () => {
  369. try {
  370. removeTcpListeners();
  371. disconnectTcp();
  372. initTcpListeners();
  373. connectTcp();
  374. } catch (error) {
  375. console.error("建立连接并设置监听出错:", error);
  376. uni.showToast({
  377. title: "建立连接并设置监听",
  378. icon: "none",
  379. duration: 1500,
  380. });
  381. }
  382. };
  383. onUnmounted(() => {
  384. sendTcpMessage("stop_real_time");
  385. removeTcpListeners();
  386. disconnectTcp();
  387. });
  388. onMounted(async () => {
  389. await getGlobalIndex();
  390. initTcpListeners();
  391. await nextTick();
  392. // 开始连接
  393. startTcp();
  394. // 状态栏高度
  395. iSMT.value = uni.getSystemInfoSync().statusBarHeight;
  396. // 确保DOM渲染完成后再查询高度
  397. nextTick(() => {
  398. // 动态计算header实际高度
  399. uni
  400. .createSelectorQuery()
  401. .select(".header_fixed")
  402. .boundingClientRect((rect) => {
  403. if (rect) {
  404. headerHeight.value = rect.height;
  405. console.log("Header实际高度:", headerHeight.value, "px");
  406. }
  407. })
  408. .exec();
  409. // 检测warn文字是否溢出
  410. checkWarnTextOverflow();
  411. });
  412. });
  413. // 监听headerHeight变化,重新计算contentHeight
  414. watch(headerHeight, (newHeight) => {
  415. if (newHeight > 0) {
  416. const systemInfo = uni.getSystemInfoSync();
  417. const windowHeight = systemInfo.windowHeight;
  418. const statusBarHeight = systemInfo.statusBarHeight || 0;
  419. const footerHeight = 100;
  420. contentHeight.value = windowHeight - statusBarHeight - newHeight - footerHeight;
  421. console.log("重新计算contentHeight:", contentHeight.value);
  422. }
  423. });
  424. </script>
  425. <style scoped>
  426. /* 状态栏占位 */
  427. .top {
  428. position: fixed;
  429. top: 0;
  430. left: 0;
  431. right: 0;
  432. z-index: 1001;
  433. background-color: #ffffff;
  434. }
  435. /* 固定头部样式 */
  436. .header_fixed {
  437. position: fixed;
  438. left: 0;
  439. right: 0;
  440. z-index: 1000;
  441. background-color: #ffffff;
  442. padding: 20rpx 0 0 0;
  443. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  444. }
  445. /* 可滚动内容区域 */
  446. .content_scroll {
  447. position: fixed;
  448. left: 0;
  449. right: 0;
  450. bottom: 100rpx;
  451. /* 底部导航栏高度 */
  452. overflow-y: auto;
  453. }
  454. .header_content {
  455. display: flex;
  456. align-items: center;
  457. justify-content: space-between;
  458. height: 80rpx;
  459. padding: 0 20rpx;
  460. margin-bottom: 10rpx;
  461. }
  462. .header_input_wrapper {
  463. display: flex;
  464. align-items: center;
  465. width: 100%;
  466. margin: 0 20rpx 0 0;
  467. height: 70rpx;
  468. border-radius: 35rpx;
  469. background-color: #ffffff;
  470. border: 1rpx solid #e9ecef;
  471. padding: 0 80rpx 0 30rpx;
  472. font-size: 28rpx;
  473. color: #5c5c5c;
  474. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  475. }
  476. .search_icon {
  477. width: 40rpx;
  478. height: 40rpx;
  479. opacity: 0.6;
  480. }
  481. .header_input {
  482. margin-left: 10rpx;
  483. }
  484. .header_icons {
  485. display: flex;
  486. align-items: center;
  487. gap: 15rpx;
  488. }
  489. .header_icon {
  490. width: 40rpx;
  491. height: 40rpx;
  492. display: flex;
  493. align-items: center;
  494. justify-content: center;
  495. }
  496. .header_icon image {
  497. width: 40rpx;
  498. height: 40rpx;
  499. }
  500. /* Tab 栏样式 */
  501. .channel_li {
  502. display: flex;
  503. align-items: center;
  504. height: 80rpx;
  505. background-color: #ffffff;
  506. border-bottom: 1rpx solid #f0f0f0;
  507. }
  508. .channel_wrap {
  509. width: calc(100% - 60rpx);
  510. height: 100%;
  511. overflow: hidden;
  512. flex-shrink: 0;
  513. }
  514. .channel_innerWrap {
  515. display: flex;
  516. align-items: center;
  517. height: 100%;
  518. padding: 0 20rpx;
  519. white-space: nowrap;
  520. }
  521. .channel_item {
  522. position: relative;
  523. display: flex;
  524. flex-direction: column;
  525. align-items: center;
  526. justify-content: center;
  527. height: 60rpx;
  528. padding: 0 20rpx;
  529. border-radius: 30rpx;
  530. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  531. cursor: pointer;
  532. white-space: nowrap;
  533. flex-shrink: 0;
  534. }
  535. .channel_item:active {
  536. transform: scale(0.98);
  537. }
  538. .channel_item.active {
  539. color: #333;
  540. font-weight: bold;
  541. }
  542. .channel_text {
  543. font-size: 28rpx;
  544. font-weight: 500;
  545. color: #666666;
  546. transition: color 0.3s ease;
  547. white-space: nowrap;
  548. }
  549. .channel_item.active .channel_text {
  550. color: #333333;
  551. font-weight: 400;
  552. z-index: 2;
  553. }
  554. .active_indicator {
  555. position: absolute;
  556. left: 50%;
  557. top: 60%;
  558. transform: translateX(-45%);
  559. width: calc(100% - 20rpx);
  560. min-width: 40rpx;
  561. max-width: 120rpx;
  562. height: 8rpx;
  563. background-image: url("/static/marketSituation-image/bg.png");
  564. background-size: cover;
  565. background-position: center;
  566. background-repeat: no-repeat;
  567. animation: slideIn 0.1s ease;
  568. border-radius: 8rpx;
  569. z-index: 1;
  570. }
  571. @keyframes slideIn {
  572. from {
  573. width: 0;
  574. opacity: 0;
  575. }
  576. to {
  577. width: 40rpx;
  578. opacity: 1;
  579. }
  580. }
  581. .scroll_indicator {
  582. border-left: 1rpx solid #b6b6b6;
  583. display: flex;
  584. align-items: center;
  585. justify-content: center;
  586. width: 60rpx;
  587. height: 30rpx;
  588. background-color: #ffffff;
  589. flex-shrink: 0;
  590. }
  591. .scroll_indicator image {
  592. width: 20rpx;
  593. height: 20rpx;
  594. opacity: 0.5;
  595. }
  596. .content {
  597. margin-top: 20rpx;
  598. background-color: white;
  599. }
  600. .map {
  601. width: calc(100% - 60rpx);
  602. margin: 0 30rpx;
  603. display: flex;
  604. align-items: center;
  605. justify-content: center;
  606. background-color: #f3f3f3;
  607. border-radius: 30rpx;
  608. border: 1rpx solid #e0e0e0;
  609. padding: 30rpx 20rpx;
  610. box-sizing: border-box;
  611. /* 设置最小高度保护,但允许内容撑开 */
  612. min-height: 200rpx;
  613. }
  614. .map image {
  615. width: 100%;
  616. height: auto;
  617. max-width: 100%;
  618. display: block;
  619. /* widthFix模式下,高度会自动按比例调整 */
  620. /* 设置最大高度避免图片过大 */
  621. max-height: 60vh;
  622. /* 添加平滑过渡效果 */
  623. transition: all 0.3s ease;
  624. max-height: 60vh;
  625. }
  626. /* 响应式优化 */
  627. @media screen and (max-width: 750rpx) {
  628. .map {
  629. margin: 0 20rpx;
  630. width: calc(100% - 40rpx);
  631. padding: 20rpx 15rpx;
  632. }
  633. }
  634. @media screen and (max-width: 480rpx) {
  635. .map {
  636. margin: 0 15rpx;
  637. width: calc(100% - 30rpx);
  638. padding: 15rpx 10rpx;
  639. }
  640. }
  641. .static-footer {
  642. position: fixed;
  643. bottom: 0;
  644. }
  645. /* 弹窗样式 */
  646. .modal_overlay {
  647. position: fixed;
  648. top: 0;
  649. left: 0;
  650. right: 0;
  651. bottom: 0;
  652. background-color: rgba(0, 0, 0, 0.5);
  653. display: flex;
  654. align-items: flex-end;
  655. z-index: 1000;
  656. }
  657. .modal_content {
  658. width: 100%;
  659. background-color: #fff;
  660. border-radius: 20rpx 20rpx 0 0;
  661. max-height: 80vh;
  662. overflow: hidden;
  663. }
  664. .modal_header {
  665. position: relative;
  666. display: flex;
  667. justify-content: center;
  668. align-items: center;
  669. padding: 30rpx 40rpx;
  670. border-bottom: 1rpx solid #f0f0f0;
  671. }
  672. .modal_title {
  673. font-size: 32rpx;
  674. font-weight: bold;
  675. color: #333333;
  676. text-align: center;
  677. }
  678. .modal_close {
  679. position: absolute;
  680. right: 40rpx;
  681. top: 50%;
  682. transform: translateY(-50%);
  683. width: 60rpx;
  684. height: 60rpx;
  685. display: flex;
  686. align-items: center;
  687. justify-content: center;
  688. font-size: 40rpx;
  689. color: #999;
  690. }
  691. .modal_body {
  692. padding: 40rpx;
  693. }
  694. .country_grid {
  695. display: grid;
  696. grid-template-columns: 1fr 1fr;
  697. gap: 20rpx;
  698. }
  699. .country_item {
  700. padding: 24rpx 30rpx;
  701. border-radius: 12rpx;
  702. background-color: #f8f8f8;
  703. display: flex;
  704. align-items: center;
  705. justify-content: center;
  706. transition: all 0.3s ease;
  707. }
  708. .country_item.selected {
  709. background-color: #ff4444;
  710. color: #fff;
  711. }
  712. .country_text {
  713. font-size: 28rpx;
  714. color: #333;
  715. }
  716. .country_item.selected .country_text {
  717. color: #fff;
  718. }
  719. .global_index {
  720. margin: 30rpx 20rpx 0 20rpx;
  721. display: flex;
  722. justify-content: space-between;
  723. }
  724. .global_index_title {
  725. margin-left: 20rpx;
  726. font-size: 40rpx;
  727. font-weight: 100;
  728. color: #333333;
  729. align-items: center;
  730. }
  731. .global_index_more {
  732. display: flex;
  733. gap: 10rpx;
  734. font-size: 28rpx;
  735. color: #333333;
  736. align-items: center;
  737. }
  738. .global_index_more image {
  739. width: 40rpx;
  740. height: 40rpx;
  741. align-items: center;
  742. }
  743. /* 卡片网格样式 */
  744. .cards_grid {
  745. display: grid;
  746. grid-template-columns: repeat(3, 1fr);
  747. margin: 0;
  748. box-sizing: border-box;
  749. width: 100%;
  750. padding: 30rpx 0;
  751. }
  752. .card_item {
  753. width: 100%;
  754. box-sizing: border-box;
  755. min-width: 0;
  756. /* 防止内容溢出 */
  757. }
  758. /* 响应式布局 - 小屏幕时改为两列 */
  759. @media (max-width: 600rpx) {
  760. .cards_grid {
  761. grid-template-columns: repeat(2, 1fr);
  762. padding: 30rpx 0;
  763. }
  764. }
  765. /* 超小屏幕时改为单列 */
  766. @media (max-width: 400rpx) {
  767. .cards_grid {
  768. grid-template-columns: 1fr;
  769. padding: 30rpx 0;
  770. }
  771. }
  772. .warn {
  773. display: flex;
  774. align-items: center;
  775. justify-content: flex-start;
  776. gap: 10rpx;
  777. font-size: 28rpx;
  778. color: #666666;
  779. padding: 20rpx;
  780. max-width: 100%;
  781. overflow: hidden;
  782. position: relative;
  783. }
  784. .warn image {
  785. width: 40rpx;
  786. height: 40rpx;
  787. flex-shrink: 0;
  788. /* 防止图片被压缩 */
  789. position: relative;
  790. z-index: 2;
  791. /* 确保图片在最上层 */
  792. }
  793. .warn_text_container {
  794. flex: 1;
  795. overflow: hidden;
  796. position: relative;
  797. min-width: 0;
  798. /* 允许容器收缩 */
  799. }
  800. .warn_text {
  801. display: block;
  802. white-space: nowrap;
  803. will-change: transform;
  804. /* 优化动画性能 */
  805. }
  806. /* 文字滚动动画 */
  807. @keyframes scrollText {
  808. 0% {
  809. transform: translateX(0);
  810. }
  811. 20% {
  812. transform: translateX(0);
  813. }
  814. 80% {
  815. transform: translateX(-85%);
  816. }
  817. 100% {
  818. transform: translateX(-85%);
  819. }
  820. }
  821. /* 当文字超长时启用滚动动画 */
  822. .warn_text.scroll-active {
  823. animation: scrollText 12s linear infinite;
  824. animation-delay: 2s;
  825. /* 延迟2秒开始滚动,让用户先看到开头 */
  826. }
  827. /* 底部安全区域 */
  828. .bottom_safe_area {
  829. height: 40rpx;
  830. background-color: transparent;
  831. }
  832. /* 主容器样式调整 */
  833. .main {
  834. position: relative;
  835. height: 100vh;
  836. overflow: hidden;
  837. background-color: white;
  838. }
  839. </style>