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.

1038 lines
25 KiB

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