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.

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