airSampleTencentMap.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. <template>
  2. <div class="map-page">
  3. <div ref="mapContainer" class="map-container"></div>
  4. <!-- 错误提示 -->
  5. <div v-if="error" class="error-message">{{ error }}</div>
  6. </div>
  7. </template>
  8. <script setup>
  9. import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
  10. import axios from 'axios'
  11. const isMapReady = ref(false)
  12. const mapContainer = ref(null)
  13. const error = ref(null)
  14. const TMap = ref(null);
  15. let activeTempMarker = ref(null)
  16. let infoWindow = ref(null)
  17. let map = null
  18. let markersLayer = null
  19. let soilTypeVectorLayer = null; // 土壤类型多边形图层
  20. let overlay = null
  21. const state = reactive({
  22. showOverlay: false,
  23. showSoilTypes: true,
  24. showSurveyData: true,
  25. shoeWaterSystem: true,
  26. excelData: [], // 用于存储从接口获取的数据
  27. lastTapTime: 0
  28. })
  29. const tMapConfig = reactive({
  30. key: import.meta.env.VITE_TMAP_KEY, // 请替换为你的开发者密钥
  31. geocoderURL: 'https://apis.map.qq.com/ws/geocoder/v1/'
  32. })
  33. // 加载SDK的代码保持不变...
  34. const loadSDK = () => {
  35. return new Promise((resolve, reject) => {
  36. if (window.TMap?.service?.Geocoder) {
  37. console.log('SDK已缓存,直接使用');
  38. TMap.value = window.TMap
  39. return resolve(window.TMap)
  40. }
  41. const script = document.createElement('script')
  42. script.src = `https://map.qq.com/api/gljs?v=2.exp&libraries=basic,service,vector&key=${tMapConfig.key}&callback=initTMap`
  43. window.initTMap = () => {
  44. if (!window.TMap?.service?.Geocoder) {
  45. console.error('SDK加载后仍无效');
  46. reject(new Error('地图SDK加载失败'))
  47. return
  48. }
  49. console.log('SDK动态加载完毕');
  50. TMap.value = window.TMap
  51. resolve(window.TMap)
  52. }
  53. script.onerror = (err) => {
  54. console.error('SDK加载报错', err);
  55. reject(`地图资源加载失败: ${err.message}`)
  56. document.head.removeChild(script)
  57. }
  58. document.head.appendChild(script)
  59. })
  60. }
  61. // 初始化地图 - 保持大部分不变,增加数据加载
  62. const initMap = async () => {
  63. try {
  64. await loadSDK()
  65. console.log('开始创建地图实例');
  66. map = new TMap.value.Map(mapContainer.value, {
  67. center: new TMap.value.LatLng(24.9, 113.9),//前大往下,后大往左
  68. zoom: 10,
  69. minZoom: 9.25,
  70. maxZoom: 11,
  71. renderOptions: {
  72. antialias: true
  73. },
  74. })
  75. console.log('地图实例创建成功');
  76. // 创建标记点向量图层
  77. markersLayer = new TMap.value.MultiMarker({
  78. map: map,
  79. zIndex: 1000,
  80. collision:false,
  81. styles: {
  82. default: new TMap.value.MarkerStyle({
  83. width: 15, // 图标宽度
  84. height: 15, // 图标高度
  85. anchor: { x: 12.5, y: 12.5 }, // 居中定位
  86. src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMCIgaGVpZ2h0PSIzMCI+PGNpcmNsZSBjeD0iMTUiIGN5PSIxNSIgcj0iMTAiIGZpbGw9InJlZCIvPjwvc3ZnPg=='
  87. })
  88. }
  89. });
  90. // 绑定标记点击事件
  91. markersLayer.on('click', handleMarkerClick);
  92. // 创建土壤类型多边形图层
  93. soilTypeVectorLayer = new TMap.value.MultiPolygon({
  94. map: map,
  95. styles: {
  96. default: new TMap.value.PolygonStyle({
  97. fillColor: '#cccccc',
  98. fillOpacity: 0.4,
  99. strokeColor: '#333',
  100. strokeWidth: 1
  101. })
  102. }
  103. });
  104. // 先加载数据,再更新标记
  105. await fetchData(); // 新增:获取数据
  106. if(state.excelData.length > 0) {
  107. const first = state.excelData[0];
  108. console.log('第一条数据坐标:',
  109. first.纬度 || first.latitude,
  110. first.经度 || first.longitude
  111. );
  112. }
  113. updateMarkers(); // 更新标记点
  114. // 标记地图就绪
  115. isMapReady.value = true;
  116. console.log('地图初始化完成');
  117. } catch (err) {
  118. isMapReady.value = true;
  119. console.error('initMap执行异常:', err);
  120. error.value = err.message
  121. }
  122. }
  123. // 新增:从接口获取数据
  124. const fetchData = async () => {
  125. try {
  126. const response = await axios.get('http://localhost:3000/table/Atmosphere_summary_data', {
  127. timeout: 100000
  128. });
  129. state.excelData = response.data.filter(item => {
  130. // 检查数据完整性
  131. if(!item['样品编码'] || !item.纬度 || !item.经度) {
  132. console.warn(`数据不完整,已跳过: ${item.样品编码 || '未知编码'}`);
  133. return false;
  134. }
  135. const lat = Number(item.纬度);
  136. const lng = Number(item.经度);
  137. // 验证数值范围
  138. const isValid = !isNaN(lat) && !isNaN(lng) &&
  139. lat >= -90 && lat <= 90 &&
  140. lng >= -180 && lng <= 180;
  141. if(!isValid) {
  142. console.error(`无效经纬度: ${item.样品编码} (${item.纬度}, ${item.经度})`);
  143. }
  144. return isValid;
  145. });
  146. console.log('有效数据记录:', state.excelData.length);
  147. } catch (err) {
  148. console.error('数据请求失败详情:', err.response?.data || err.message);
  149. error.value = `数据加载失败: ${err.message}`;
  150. }
  151. }
  152. // 更新标记点 - 保持不变
  153. const updateMarkers = () => {
  154. const coordCount = new Map();
  155. const geometries = state.excelData.map(item => {
  156. console.log(`ID: ${item.样品编码}, 坐标: (${item.纬度}, ${item.经度})`); // 替换字段名
  157. if (!item.样品编码 || !item.纬度 || !item.经度) {
  158. console.error(`无效数据项: ${JSON.stringify(item)}`);
  159. return null;
  160. }
  161. const lat = Number(item.纬度);
  162. const lng = Number(item.经度);
  163. if (isNaN(lat) || isNaN(lng)) {
  164. console.error(`坐标值非数字: ${item.样品编码} (${item.纬度}, ${item.经度})`);
  165. return null;
  166. }
  167. const coordKey = `${lat}_${lng}`;
  168. const count = coordCount.get(coordKey) || 0;
  169. coordCount.set(coordKey, count + 1);
  170. let finalLat = lat;
  171. let finalLng = lng;
  172. // 重复坐标添加偏移
  173. if (count > 0) {
  174. const latOffset = count * 0.01; // 南北方向偏移(约11米)
  175. const lngOffset = count * 0.02;
  176. finalLat = lat + latOffset;
  177. finalLng = lng + lngOffset;
  178. console.log(`偏移点 ${item.样品编码}: ${lat},${lng} → ${finalLat},${finalLng}`);
  179. }
  180. const position = new TMap.value.LatLng(finalLat, finalLng);
  181. return {
  182. id: item.样品名称,
  183. styleId: 'default',
  184. position:position, // 替换字段名
  185. properties: {
  186. title: item.采样 || `采样点 ${item.样品名称}`,
  187. sampler_id: item.样品编码,
  188. originalPosition: { lat, lng }
  189. }
  190. };
  191. })
  192. markersLayer.setGeometries(geometries);
  193. console.log('成功添加标记点数量:', geometries.length);
  194. };
  195. // Marker点击事件处理 - 保持不变
  196. const handleMarkerClick = async (e) => {
  197. console.log('点击标记点');
  198. const marker = e.geometry;
  199. if (!marker) {
  200. console.error('未获取到标记点对象');
  201. return;
  202. }
  203. // 关闭之前的信息窗口
  204. if (infoWindow.value) {
  205. infoWindow.value.close();
  206. infoWindow.value = null;
  207. }
  208. // 显示加载中
  209. infoWindow.value = new TMap.value.InfoWindow({
  210. map: map,
  211. position: marker.position,
  212. content: '<div style="padding:12px;text-align:center">加载数据中...</div>',
  213. //offset: { x: 0, y: -32 }
  214. });
  215. infoWindow.value.open();
  216. try {
  217. const markerId = marker.id.trim();
  218. console.log('点击标记点样品名称:', markerId);
  219. // 直接从本地数据查找,无需二次请求
  220. const matchedData = state.excelData.find(item =>
  221. item.样品名称.trim() === markerId
  222. );
  223. if (!matchedData) {
  224. console.error("无法匹配的数据列表:", state.excelData.map(i => i.样品名称));
  225. throw new Error(`未找到样品名称为 ${markerId} 的监测数据`);
  226. }
  227. // 创建信息窗口内容
  228. const content = `
  229. <div class="water-info-window">
  230. <h3 class="info-title">${matchedData.采样}</h3>
  231. <div class="info-row">
  232. <span class="info-label">采样点ID:</span>
  233. <span class="info-value">${matchedData.样品名称}</span>
  234. </div>
  235. <div class="info-row">
  236. <span class="info-label">样品编号:</span>
  237. <span class="info-value">${matchedData.样品编号}</span>
  238. </div>
  239. <div class="contaminant-grid" style="grid-template-columns: repeat(2, 1fr); gap: 8px;">
  240. <div class="contaminant-item">
  241. <span class="contaminant-name">Cr mg/kg:</span>
  242. <span class="contaminant-value">${matchedData['Cr mg/kg']}</span>
  243. </div>
  244. <div class="contaminant-item">
  245. <span class="contaminant-name">Cr ug/m3:</span>
  246. <span class="contaminant-value">${matchedData['Cr ug/m3']}</span>
  247. </div>
  248. <div class="contaminant-item">
  249. <span class="contaminant-name">As mg/kg:</span>
  250. <span class="contaminant-value">${matchedData['As mg/kg']}</span>
  251. </div>
  252. <div class="contaminant-item">
  253. <span class="contaminant-name">As ug/m3:</span>
  254. <span class="contaminant-value">${matchedData['As ug/m3']}</span>
  255. </div>
  256. <div class="contaminant-item">
  257. <span class="contaminant-name">Cd mg/kg:</span>
  258. <span class="contaminant-value">${matchedData['Cd mg/kg']}</span>
  259. </div>
  260. <div class="contaminant-item">
  261. <span class="contaminant-name">Cd ug/m3:</span>
  262. <span class="contaminant-value">${matchedData['Cd ug/m3']}</span>
  263. </div>
  264. <div class="contaminant-item">
  265. <span class="contaminant-name">Hg mg/kg:</span>
  266. <span class="contaminant-value">${matchedData['Hg mg/kg']}</span>
  267. </div>
  268. <div class="contaminant-item">
  269. <span class="contaminant-name">Hg ug/m3:</span>
  270. <span class="contaminant-value">${matchedData['Hg ug/m3']}</span>
  271. </div>
  272. <div class="contaminant-item">
  273. <span class="contaminant-name">Pb mg/kg:</span>
  274. <span class="contaminant-value">${matchedData['Pb mg/kg']}</span>
  275. </div>
  276. <div class="contaminant-item">
  277. <span class="contaminant-name">Pb ug/m3:</span>
  278. <span class="contaminant-value">${matchedData['Pb ug/m3']}</span>
  279. </div>
  280. <div class="contaminant-item">
  281. <span class="contaminant-name">颗粒物的重量 mg:</span>
  282. <span class="contaminant-value">${matchedData['颗粒物的重量 mg']}</span>
  283. </div>
  284. <div class="contaminant-item">
  285. <span class="contaminant-name">标准体积 m3:</span>
  286. <span class="contaminant-value">${matchedData['标准体积 m3']}</span>
  287. </div>
  288. <div class="contaminant-item">
  289. <span class="contaminant-name">颗粒物浓度ug/m3:</span>
  290. <span class="contaminant-value">${matchedData['颗粒物浓度ug/m3']}</span>
  291. </div>
  292. </div>
  293. </div>
  294. `;
  295. // 更新信息窗口
  296. infoWindow.value.setContent(content);
  297. } catch (error) {
  298. console.error('API请求失败:', error);
  299. // 显示错误信息
  300. const errorContent = `
  301. <div style="padding:12px;color:red">
  302. <h3>${marker.properties.title}</h3>
  303. <p>获取数据失败: ${error.message}</p>
  304. </div>
  305. `;
  306. infoWindow.value.setContent(errorContent);
  307. }
  308. }
  309. // 其余函数保持不变...
  310. const manageTempMarker = {
  311. add: (lat, lng, phValue) => {
  312. if (activeTempMarker.value) {
  313. markersLayer.remove("-999")
  314. }
  315. // 确保已添加临时样式
  316. if (!markersLayer.getStyles().temp) {
  317. markersLayer.setStyles({
  318. temp: new TMap.value.MarkerStyle({
  319. width: 30,
  320. height: 30,
  321. anchor: { x: 12.5, y: 12.5 },
  322. src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTIgMkg2Yy0xLjEgMC0yIC45LTIgMnYxNmMwIDEuMS45IDIgMiAyaDEyYzEuMSAwIDIgLS45IDItMnYtNGMwLTEuMS0uOS0yLTItMmgtMnY0aC00di00SDEyVjJ6bTAgMTZINnYtOEgxOFYxOHoiIGZpbGw9IiNGRjAwMDAiLz48L3N2Zz4='
  323. })
  324. });
  325. }
  326. const tempMarker = markersLayer.add({
  327. id: "-999",
  328. position: new TMap.value.LatLng(lat, lng),
  329. styleId: 'temp',
  330. properties: {
  331. title: '克里金插值',
  332. phValue: parseFloat(phValue).toFixed(2),
  333. isTemp: true
  334. }
  335. })
  336. activeTempMarker.value = tempMarker
  337. },
  338. remove: () => {
  339. if (activeTempMarker.value) {
  340. markersLayer.remove("-999")
  341. activeTempMarker.value = null
  342. }
  343. }
  344. }
  345. onMounted(async () => {
  346. console.log('开始执行 onMounted');
  347. try {
  348. await initMap()
  349. console.log('地图初始化完成');
  350. } catch (err) {
  351. console.error('onMounted执行异常', err);
  352. error.value = err.message
  353. }
  354. })
  355. onBeforeUnmount(() => {
  356. if (activeTempMarker.value) {
  357. manageTempMarker.remove()
  358. }
  359. if (markersLayer) markersLayer.setMap(null)
  360. if (overlay) overlay.setMap(null)
  361. if (infoWindow.value) {
  362. infoWindow.value.close()
  363. infoWindow.value = null
  364. }
  365. if (soilTypeVectorLayer) soilTypeVectorLayer.setMap(null)
  366. })
  367. </script>
  368. <style>
  369. /* 原有样式保持不变,修改以下部分 */
  370. .error-message {
  371. position: fixed;
  372. top: 20px;
  373. left: 50%;
  374. transform: translateX(-50%);
  375. padding: 12px 20px;
  376. background-color: #ff4444;
  377. color: white;
  378. border-radius: 4px;
  379. z-index: 9999;
  380. box-shadow: 0 2px 8px rgba(0,0,0,0.2);
  381. animation: fadein 0.5s, fadeout 0.5s 4.5s;
  382. }
  383. @keyframes fadein {
  384. from { top: 0; opacity: 0; }
  385. to { top: 20px; opacity: 1; }
  386. }
  387. @keyframes fadeout {
  388. from { top: 20px; opacity: 1; }
  389. to { top: 0; opacity: 0; }
  390. }
  391. .map-page {
  392. position: relative;
  393. width: 100vw;
  394. height: 100vh;
  395. }
  396. .map-container {
  397. width: 100%;
  398. height: 100vh ;
  399. min-height: 600px;
  400. pointer-events: all;
  401. }
  402. .contaminants {
  403. display: grid;
  404. grid-template-columns: repeat(3, 1fr);
  405. gap: 2px;
  406. }
  407. /* 窗口容器:精准控制尺寸 */
  408. .water-info-window {
  409. max-width: 340px !important;
  410. width: 100%;
  411. height: auto;
  412. padding: 8px;
  413. box-sizing: border-box;
  414. background: #FFFFFF;
  415. border-radius: 8px;
  416. box-shadow: 0 3px 12px rgba(0, 32, 71, 0.1);
  417. border: 1px solid #e5e7eb;
  418. overflow: hidden !important;
  419. }
  420. /* 标题区样式 */
  421. .info-title {
  422. font-size: 0.9rem;
  423. padding: 6px 8px;
  424. letter-spacing: 0.2px;
  425. border-bottom: 1px solid #f1f2f6;
  426. margin: 0 0 8px 0;
  427. }
  428. /* 基础数据行 */
  429. .info-row {
  430. display: flex;
  431. align-items: center;
  432. margin: 4px 0;
  433. padding-left: 8px;
  434. }
  435. .info-label {
  436. font-size: 0.8rem;
  437. padding-right: 6px;
  438. flex: 0 0 80px;
  439. }
  440. .info-value {
  441. font-size: 0.8rem;
  442. padding: 2px 6px;
  443. border-left: 2px solid #e5e7eb;
  444. }
  445. /* 污染物网格:优化布局使名称和数值在同一行并放大字体 */
  446. .contaminant-grid {
  447. display: grid;
  448. grid-template-columns: repeat(2, 1fr);
  449. gap: 4px; /* 减小间距以补偿字体增大 */
  450. margin: 4px 0;
  451. }
  452. /* 污染物项:确保名称和数值在同一行 */
  453. .contaminant-item {
  454. display: flex;
  455. align-items: center;
  456. justify-content: space-between;
  457. padding: 3px 5px; /* 减小内边距 */
  458. background: #f9fafb;
  459. border-radius: 4px;
  460. }
  461. /* 增大字体大小,保持在同一行 */
  462. .contaminant-name {
  463. font-size: 0.8rem; /* 增大字体 */
  464. color: #6b7280;
  465. white-space: nowrap;
  466. overflow: hidden;
  467. text-overflow: ellipsis;
  468. flex: 1;
  469. margin-right: 5px;
  470. }
  471. .contaminant-value {
  472. font-size: 0.8rem; /* 增大字体 */
  473. background: #e5e7eb;
  474. padding: 2px 6px;
  475. border-radius: 3px;
  476. min-width: 40px;
  477. text-align: center;
  478. flex-shrink: 0;
  479. }
  480. </style>