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.

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