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.

980 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 { onShow, onHide } from "@dcloudio/uni-app";
  48. import util from "../../common/util.js";
  49. import IndexCard from "../../components/IndexCard.vue";
  50. import { useMarketSituationStore } from "../../stores/modules/marketSituation.js";
  51. const marketSituationStore = useMarketSituationStore();
  52. import { getGlobalIndexAPI } from "../../api/marketSituation/marketSituation.js";
  53. const iSMT = ref(0);
  54. const searchValue = ref("");
  55. const contentHeight = ref(0);
  56. const headerHeight = ref(0); // 动态计算的header高度
  57. const isWarnTextOverflow = ref(false); // warn文字是否溢出
  58. const pageIndex = ref(0);
  59. const scrollToView = ref("");
  60. // 计算属性:精准计算content区域的top值
  61. const contentTopPosition = computed(() => {
  62. const statusBarHeight = iSMT.value || 0;
  63. const currentHeaderHeight = headerHeight.value > 0 ? headerHeight.value : 140;
  64. return statusBarHeight + currentHeaderHeight;
  65. });
  66. // warn文字的class计算属性
  67. const warnTextClass = computed(() => {
  68. return isWarnTextOverflow.value ? "warn_text scroll-active" : "warn_text";
  69. });
  70. const globalIndexArray = ref([]);
  71. // 搜索输入事件
  72. const onSearchInput = (e) => {
  73. searchValue.value = e.detail.value;
  74. };
  75. // 搜索确认事件
  76. const onSearchConfirm = (e) => {
  77. console.log("搜索内容:", e.detail.value);
  78. // 这里可以添加搜索逻辑
  79. performSearch(e.detail.value);
  80. };
  81. // 搜索图标点击事件
  82. const onSearchClick = () => {
  83. if (searchValue.value.trim()) {
  84. performSearch(searchValue.value);
  85. }
  86. };
  87. // 执行搜索
  88. const performSearch = (keyword) => {
  89. if (!keyword.trim()) {
  90. uni.showToast({
  91. title: "请输入搜索内容",
  92. icon: "none",
  93. });
  94. return;
  95. }
  96. uni.showToast({
  97. title: `搜索: ${keyword}`,
  98. icon: "none",
  99. });
  100. // 这里添加实际的搜索逻辑
  101. };
  102. // 检测warn文字是否溢出
  103. const checkWarnTextOverflow = () => {
  104. nextTick(() => {
  105. setTimeout(() => {
  106. const query = uni.createSelectorQuery();
  107. // 同时查询容器和文字元素
  108. query.select(".warn_text_container").boundingClientRect();
  109. query.select(".warn_text").boundingClientRect();
  110. query.exec((res) => {
  111. const containerRect = res[0];
  112. const textRect = res[1];
  113. if (!containerRect || !textRect) {
  114. return;
  115. }
  116. // 判断文字是否超出容器(留一些余量)
  117. const isOverflow = textRect.width > containerRect.width - 10;
  118. isWarnTextOverflow.value = isOverflow;
  119. });
  120. }, 500);
  121. });
  122. };
  123. // 方法:查看指数详情
  124. const viewIndexDetail = (item, index) => {
  125. console.log("查看指数详情:", item.stockName);
  126. // uni.showToast({
  127. // title: `查看 ${item.stockName} 详情`,
  128. // icon: 'none',
  129. // duration: 2000
  130. // })
  131. // 这里可以跳转到具体的指数详情页面
  132. uni.navigateTo({
  133. url: `/pages/marketSituation/marketCondition?stockInformation=${encodeURIComponent(JSON.stringify(item))}&index=${index}&from=marketOverview`,
  134. });
  135. };
  136. // 跳转到全球指数页面
  137. const goToGlobalIndex = () => {
  138. uni.navigateTo({
  139. url: "/pages/marketSituation/globalIndex",
  140. });
  141. };
  142. const getGlobalIndex = async () => {
  143. try {
  144. const result = await getGlobalIndexAPI();
  145. globalIndexArray.value = result.data;
  146. } catch (e) {
  147. console.log("获取全球指数失败", e);
  148. }
  149. };
  150. // TCP相关响应式变量
  151. import tcpConnection from "@/api/tcpConnection.js";
  152. const tcpConnected = ref(false);
  153. const connectionListener = ref(null);
  154. const messageListener = ref(null);
  155. // 初始化TCP监听器
  156. const initTcpListeners = () => {
  157. // 创建连接状态监听器并保存引用
  158. connectionListener.value = (status, result) => {
  159. tcpConnected.value = status === "connected";
  160. console.log("TCP连接状态变化:", status, tcpConnected.value);
  161. // 如果连接,发送获取批量数据
  162. if (status === "connected") {
  163. sendTcpMessage("batch_real_time");
  164. }
  165. };
  166. // 创建消息监听器并保存引用
  167. messageListener.value = (type, message, parsedArray) => {
  168. const messageObj = {
  169. type: type,
  170. content: message,
  171. parsedArray: parsedArray,
  172. timestamp: new Date().toLocaleTimeString(),
  173. direction: "received",
  174. };
  175. // 解析股票数据
  176. parseStockData(message);
  177. };
  178. // 注册监听器
  179. tcpConnection.onConnectionChange(connectionListener.value);
  180. tcpConnection.onMessage(messageListener.value);
  181. };
  182. // 连接TCP服务器
  183. const connectTcp = () => {
  184. console.log("开始连接TCP服务器...");
  185. tcpConnection.connect();
  186. };
  187. // 断开TCP连接
  188. const disconnectTcp = () => {
  189. console.log("断开TCP连接...");
  190. tcpConnection.disconnect();
  191. tcpConnected.value = false;
  192. };
  193. // 发送TCP消息
  194. const sendTcpMessage = (command) => {
  195. let messageData;
  196. let messageDataArray = [];
  197. if (command == "batch_real_time") {
  198. messageDataArray = globalIndexArray.value.map((item) => item.stockCode);
  199. }
  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. console.log("初始化完成,,,,,,,,,,,,,,,");
  399. connectTcp();
  400. } catch (error) {
  401. console.error("建立连接并设置监听出错:", error);
  402. }
  403. };
  404. onShow(async () => {
  405. console.log("显示页面");
  406. await getGlobalIndex();
  407. initTcpListeners();
  408. await nextTick();
  409. // 开始连接
  410. startTcp();
  411. });
  412. onHide(() => {
  413. console.log("隐藏页面");
  414. sendTcpMessage("stop_real_time");
  415. removeTcpListeners();
  416. disconnectTcp();
  417. });
  418. onUnmounted(() => {
  419. console.log("卸载页面");
  420. sendTcpMessage("stop_real_time");
  421. removeTcpListeners();
  422. disconnectTcp();
  423. });
  424. onMounted(async () => {
  425. console.log("挂载页面");
  426. // 状态栏高度
  427. iSMT.value = uni.getSystemInfoSync().statusBarHeight;
  428. // 确保DOM渲染完成后再查询高度
  429. nextTick(() => {
  430. // 动态计算header实际高度
  431. uni
  432. .createSelectorQuery()
  433. .select(".header_fixed")
  434. .boundingClientRect((rect) => {
  435. if (rect) {
  436. headerHeight.value = rect.height;
  437. console.log("Header实际高度:", headerHeight.value, "px");
  438. }
  439. })
  440. .exec();
  441. // 检测warn文字是否溢出
  442. checkWarnTextOverflow();
  443. });
  444. });
  445. // 监听headerHeight变化,重新计算contentHeight
  446. watch(headerHeight, (newHeight) => {
  447. if (newHeight > 0) {
  448. const systemInfo = uni.getSystemInfoSync();
  449. const windowHeight = systemInfo.windowHeight;
  450. const statusBarHeight = systemInfo.statusBarHeight || 0;
  451. const footerHeight = 100;
  452. contentHeight.value = windowHeight - statusBarHeight - newHeight - footerHeight;
  453. console.log("重新计算contentHeight:", contentHeight.value);
  454. }
  455. });
  456. </script>
  457. <style scoped>
  458. /* 状态栏占位 */
  459. .top {
  460. position: fixed;
  461. top: 0;
  462. left: 0;
  463. right: 0;
  464. z-index: 1001;
  465. background-color: #ffffff;
  466. }
  467. /* 固定头部样式 */
  468. .header_fixed {
  469. position: fixed;
  470. left: 0;
  471. right: 0;
  472. z-index: 1000;
  473. background-color: #ffffff;
  474. padding: 20rpx 0 0 0;
  475. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  476. }
  477. /* 可滚动内容区域 */
  478. .content_scroll {
  479. position: fixed;
  480. left: 0;
  481. right: 0;
  482. bottom: 100rpx;
  483. /* 底部导航栏高度 */
  484. overflow-y: auto;
  485. }
  486. .header_content {
  487. display: flex;
  488. align-items: center;
  489. justify-content: space-between;
  490. height: 80rpx;
  491. padding: 0 20rpx;
  492. margin-bottom: 10rpx;
  493. }
  494. .header_input_wrapper {
  495. display: flex;
  496. align-items: center;
  497. width: 100%;
  498. margin: 0 20rpx 0 0;
  499. height: 70rpx;
  500. border-radius: 35rpx;
  501. background-color: #ffffff;
  502. border: 1rpx solid #e9ecef;
  503. padding: 0 80rpx 0 30rpx;
  504. font-size: 28rpx;
  505. color: #5c5c5c;
  506. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  507. }
  508. .search_icon {
  509. width: 40rpx;
  510. height: 40rpx;
  511. opacity: 0.6;
  512. }
  513. .header_input {
  514. margin-left: 10rpx;
  515. }
  516. .header_icons {
  517. display: flex;
  518. align-items: center;
  519. gap: 15rpx;
  520. }
  521. .header_icon {
  522. width: 40rpx;
  523. height: 40rpx;
  524. display: flex;
  525. align-items: center;
  526. justify-content: center;
  527. }
  528. .header_icon image {
  529. width: 40rpx;
  530. height: 40rpx;
  531. }
  532. /* Tab 栏样式 */
  533. .channel_li {
  534. display: flex;
  535. align-items: center;
  536. height: 80rpx;
  537. background-color: #ffffff;
  538. border-bottom: 1rpx solid #f0f0f0;
  539. }
  540. .channel_wrap {
  541. width: calc(100% - 60rpx);
  542. height: 100%;
  543. overflow: hidden;
  544. flex-shrink: 0;
  545. }
  546. .channel_innerWrap {
  547. display: flex;
  548. align-items: center;
  549. height: 100%;
  550. padding: 0 20rpx;
  551. white-space: nowrap;
  552. }
  553. .channel_item {
  554. position: relative;
  555. display: flex;
  556. flex-direction: column;
  557. align-items: center;
  558. justify-content: center;
  559. height: 60rpx;
  560. padding: 0 20rpx;
  561. border-radius: 30rpx;
  562. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  563. cursor: pointer;
  564. white-space: nowrap;
  565. flex-shrink: 0;
  566. }
  567. .channel_item:active {
  568. transform: scale(0.98);
  569. }
  570. .channel_item.active {
  571. color: #333;
  572. font-weight: bold;
  573. }
  574. .channel_text {
  575. font-size: 28rpx;
  576. font-weight: 500;
  577. color: #666666;
  578. transition: color 0.3s ease;
  579. white-space: nowrap;
  580. }
  581. .channel_item.active .channel_text {
  582. color: #333333;
  583. font-weight: 400;
  584. z-index: 2;
  585. }
  586. .active_indicator {
  587. position: absolute;
  588. left: 50%;
  589. top: 60%;
  590. transform: translateX(-45%);
  591. width: calc(100% - 20rpx);
  592. min-width: 40rpx;
  593. max-width: 120rpx;
  594. height: 8rpx;
  595. background-image: url("/static/marketSituation-image/bg.png");
  596. background-size: cover;
  597. background-position: center;
  598. background-repeat: no-repeat;
  599. animation: slideIn 0.1s ease;
  600. border-radius: 8rpx;
  601. z-index: 1;
  602. }
  603. @keyframes slideIn {
  604. from {
  605. width: 0;
  606. opacity: 0;
  607. }
  608. to {
  609. width: 40rpx;
  610. opacity: 1;
  611. }
  612. }
  613. .scroll_indicator {
  614. border-left: 1rpx solid #b6b6b6;
  615. display: flex;
  616. align-items: center;
  617. justify-content: center;
  618. width: 60rpx;
  619. height: 30rpx;
  620. background-color: #ffffff;
  621. flex-shrink: 0;
  622. }
  623. .scroll_indicator image {
  624. width: 20rpx;
  625. height: 20rpx;
  626. opacity: 0.5;
  627. }
  628. .content {
  629. margin-top: 20rpx;
  630. background-color: white;
  631. }
  632. .map {
  633. width: calc(100% - 60rpx);
  634. margin: 0 30rpx;
  635. display: flex;
  636. align-items: center;
  637. justify-content: center;
  638. background-color: #f3f3f3;
  639. border-radius: 30rpx;
  640. border: 1rpx solid #e0e0e0;
  641. padding: 30rpx 20rpx;
  642. box-sizing: border-box;
  643. /* 设置最小高度保护,但允许内容撑开 */
  644. min-height: 200rpx;
  645. }
  646. .map image {
  647. width: 100%;
  648. height: auto;
  649. max-width: 100%;
  650. display: block;
  651. /* widthFix模式下,高度会自动按比例调整 */
  652. /* 设置最大高度避免图片过大 */
  653. max-height: 60vh;
  654. /* 添加平滑过渡效果 */
  655. transition: all 0.3s ease;
  656. max-height: 60vh;
  657. }
  658. /* 响应式优化 */
  659. @media screen and (max-width: 750rpx) {
  660. .map {
  661. margin: 0 20rpx;
  662. width: calc(100% - 40rpx);
  663. padding: 20rpx 15rpx;
  664. }
  665. }
  666. @media screen and (max-width: 480rpx) {
  667. .map {
  668. margin: 0 15rpx;
  669. width: calc(100% - 30rpx);
  670. padding: 15rpx 10rpx;
  671. }
  672. }
  673. .static-footer {
  674. position: fixed;
  675. bottom: 0;
  676. }
  677. /* 弹窗样式 */
  678. .modal_overlay {
  679. position: fixed;
  680. top: 0;
  681. left: 0;
  682. right: 0;
  683. bottom: 0;
  684. background-color: rgba(0, 0, 0, 0.5);
  685. display: flex;
  686. align-items: flex-end;
  687. z-index: 1000;
  688. }
  689. .modal_content {
  690. width: 100%;
  691. background-color: #fff;
  692. border-radius: 20rpx 20rpx 0 0;
  693. max-height: 80vh;
  694. overflow: hidden;
  695. }
  696. .modal_header {
  697. position: relative;
  698. display: flex;
  699. justify-content: center;
  700. align-items: center;
  701. padding: 30rpx 40rpx;
  702. border-bottom: 1rpx solid #f0f0f0;
  703. }
  704. .modal_title {
  705. font-size: 32rpx;
  706. font-weight: bold;
  707. color: #333333;
  708. text-align: center;
  709. }
  710. .modal_close {
  711. position: absolute;
  712. right: 40rpx;
  713. top: 50%;
  714. transform: translateY(-50%);
  715. width: 60rpx;
  716. height: 60rpx;
  717. display: flex;
  718. align-items: center;
  719. justify-content: center;
  720. font-size: 40rpx;
  721. color: #999;
  722. }
  723. .modal_body {
  724. padding: 40rpx;
  725. }
  726. .country_grid {
  727. display: grid;
  728. grid-template-columns: 1fr 1fr;
  729. gap: 20rpx;
  730. }
  731. .country_item {
  732. padding: 24rpx 30rpx;
  733. border-radius: 12rpx;
  734. background-color: #f8f8f8;
  735. display: flex;
  736. align-items: center;
  737. justify-content: center;
  738. transition: all 0.3s ease;
  739. }
  740. .country_item.selected {
  741. background-color: #ff4444;
  742. color: #fff;
  743. }
  744. .country_text {
  745. font-size: 28rpx;
  746. color: #333;
  747. }
  748. .country_item.selected .country_text {
  749. color: #fff;
  750. }
  751. .global_index {
  752. margin: 30rpx 20rpx 0 20rpx;
  753. display: flex;
  754. justify-content: space-between;
  755. }
  756. .global_index_title {
  757. margin-left: 20rpx;
  758. font-size: 40rpx;
  759. font-weight: 100;
  760. color: #333333;
  761. align-items: center;
  762. }
  763. .global_index_more {
  764. display: flex;
  765. gap: 10rpx;
  766. font-size: 28rpx;
  767. color: #333333;
  768. align-items: center;
  769. }
  770. .global_index_more image {
  771. width: 40rpx;
  772. height: 40rpx;
  773. align-items: center;
  774. }
  775. /* 卡片网格样式 */
  776. .cards_grid {
  777. display: grid;
  778. grid-template-columns: repeat(3, 1fr);
  779. margin: 0;
  780. box-sizing: border-box;
  781. width: 100%;
  782. padding: 30rpx 0;
  783. }
  784. .card_item {
  785. width: 100%;
  786. box-sizing: border-box;
  787. min-width: 0;
  788. /* 防止内容溢出 */
  789. }
  790. /* 响应式布局 - 小屏幕时改为两列 */
  791. @media (max-width: 600rpx) {
  792. .cards_grid {
  793. grid-template-columns: repeat(2, 1fr);
  794. padding: 30rpx 0;
  795. }
  796. }
  797. /* 超小屏幕时改为单列 */
  798. @media (max-width: 400rpx) {
  799. .cards_grid {
  800. grid-template-columns: 1fr;
  801. padding: 30rpx 0;
  802. }
  803. }
  804. .warn {
  805. display: flex;
  806. align-items: center;
  807. justify-content: flex-start;
  808. gap: 10rpx;
  809. font-size: 28rpx;
  810. color: #666666;
  811. padding: 20rpx;
  812. max-width: 100%;
  813. overflow: hidden;
  814. position: relative;
  815. }
  816. .warn image {
  817. width: 40rpx;
  818. height: 40rpx;
  819. flex-shrink: 0;
  820. /* 防止图片被压缩 */
  821. position: relative;
  822. z-index: 2;
  823. /* 确保图片在最上层 */
  824. }
  825. .warn_text_container {
  826. flex: 1;
  827. overflow: hidden;
  828. position: relative;
  829. min-width: 0;
  830. /* 允许容器收缩 */
  831. }
  832. .warn_text {
  833. display: block;
  834. white-space: nowrap;
  835. will-change: transform;
  836. /* 优化动画性能 */
  837. }
  838. /* 文字滚动动画 */
  839. @keyframes scrollText {
  840. 0% {
  841. transform: translateX(0);
  842. }
  843. 20% {
  844. transform: translateX(0);
  845. }
  846. 80% {
  847. transform: translateX(-85%);
  848. }
  849. 100% {
  850. transform: translateX(-85%);
  851. }
  852. }
  853. /* 当文字超长时启用滚动动画 */
  854. .warn_text.scroll-active {
  855. animation: scrollText 12s linear infinite;
  856. animation-delay: 2s;
  857. /* 延迟2秒开始滚动,让用户先看到开头 */
  858. }
  859. /* 底部安全区域 */
  860. .bottom_safe_area {
  861. height: 40rpx;
  862. background-color: transparent;
  863. }
  864. /* 主容器样式调整 */
  865. .main {
  866. position: relative;
  867. height: 100vh;
  868. overflow: hidden;
  869. background-color: white;
  870. }
  871. </style>