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.

1134 lines
29 KiB

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