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.

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