atmcompanymap.vue 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. <template>
  2. <div class="map-wrapper">
  3. <div ref="mapContainer" class="map-container"></div>
  4. </div>
  5. </template>
  6. <script setup>
  7. import { ref, onMounted } from 'vue';
  8. import L from 'leaflet';
  9. import 'leaflet/dist/leaflet.css';
  10. const mapContainer = ref(null);
  11. // 定义蓝色三角形标记(保持不变)
  12. const blueTriangle = L.divIcon({
  13. className: 'custom-div-icon',
  14. html: `<svg width="24" height="24" viewBox="0 0 24 24">
  15. <path d="M12 2L2 22h20L12 2z" fill="#0066CC" stroke="#003366" stroke-width="2"/>
  16. </svg>`,
  17. iconSize: [24, 24],
  18. iconAnchor: [12, 24]
  19. });
  20. onMounted(() => {
  21. if (!mapContainer.value) {
  22. console.error('❌ 地图容器未找到!');
  23. return;
  24. }
  25. const map = L.map(mapContainer.value, {
  26. center: [24.7, 114], // 韶关大致中心
  27. zoom: 8.5,
  28. minZoom: 8.3,
  29. });
  30. // 区县颜色映射(保持不变)
  31. const districtColorMap = {
  32. "武江区": "#FF6B6B",
  33. "浈江区": "#4ECDC4",
  34. "曲江区": "#FFD166",
  35. "始兴县": "#A0DAA9",
  36. "仁化县": "#6A0572",
  37. "翁源县": "#1A535C",
  38. "乳源瑶族自治县": "#FF9F1C",
  39. "新丰县": "#87CEEB",
  40. "乐昌市": "#118AB2",
  41. "南雄市": "#06D6A0",
  42. "韶关市": "#cccccc",
  43. };
  44. // 区县颜色匹配函数(保持不变)
  45. function getDistrictColor(name) {
  46. if (districtColorMap[name]) return districtColorMap[name];
  47. const normalizedName = name.replace(/市|县|区|自治县/g, '');
  48. for (const key in districtColorMap) {
  49. if (key.includes(normalizedName) || normalizedName.includes(key.replace(/市|县|区|自治县/g, ''))) {
  50. return districtColorMap[key];
  51. }
  52. }
  53. return '#cccccc';
  54. }
  55. // 加载区县边界(保持不变)
  56. fetch('/data/韶关市各区县边界图.geojson')
  57. .then(res => {
  58. if (!res.ok) throw new Error(`区县边界加载失败:${res.status}`);
  59. return res.json();
  60. })
  61. .then(geojson => {
  62. L.geoJSON(geojson, {
  63. style: (feature) => ({
  64. fillColor: getDistrictColor(feature.properties.name || ''),
  65. fillOpacity: 0.7,
  66. color: '#333333',
  67. weight: 2,
  68. }),
  69. }).addTo(map);
  70. // 从接口加载大气污染源数据(核心修改:适配新接口)
  71. fetch('http://localhost:3000/table/Atmosphere_company_data')
  72. .then(res => {
  73. if (!res.ok) throw new Error(`接口请求失败:${res.status}`);
  74. return res.json();
  75. })
  76. .then(apiData => {
  77. console.log('✅ 接口数据加载完成,共', apiData.length, '条记录');
  78. let markerCount = 0;
  79. apiData.forEach((item, idx) => {
  80. try {
  81. // 解析经纬度(注意:接口里经纬度是字符串,需转数字)
  82. const lng = parseFloat(item.经度);
  83. const lat = parseFloat(item.纬度);
  84. if (isNaN(lat) || isNaN(lng) || lat < 22.7 || lat > 25.5 || lng < 112.7 || lng > 115.3) {
  85. console.warn(`❌ 无效坐标(第${idx}条):`, lat, lng);
  86. return;
  87. }
  88. // 解析大气颗粒物排放量
  89. const emission = parseFloat(item["大气颗粒物排放(t/a)"]);
  90. const formattedEmission = isNaN(emission)
  91. ? '未知'
  92. : `${emission.toFixed(3)} t/a`;
  93. // 创建标记(保持不变)
  94. const marker = L.marker([lat, lng], {
  95. icon: blueTriangle,
  96. zIndexOffset: 1000,
  97. }).addTo(map);
  98. // 弹窗内容修改:适配新字段
  99. marker.bindPopup(`
  100. <div class="popup-container">
  101. <h3 class="popup-title">${item.公司 || '未知'}</h3>
  102. <div class="popup-divider"></div>
  103. <p><strong>污染源序号:</strong> ${item["污染源序号"] || '未知'}</p>
  104. <p><strong>企业类型:</strong> ${item.类型 || '未知'}</p>
  105. <p><strong>所属区县:</strong> ${item["所属区县"] || '未知'}</p>
  106. <p><strong>大气颗粒物排放:</strong> ${formattedEmission}</p>
  107. </div>
  108. `);
  109. markerCount++;
  110. } catch (err) {
  111. console.error(`❌ 处理第${idx}条数据失败:`, err);
  112. }
  113. });
  114. console.log(`✅ 成功创建 ${markerCount} 个有效标记`);
  115. })
  116. .catch(err => {
  117. console.error('❌ 接口数据加载失败:', err);
  118. alert('数据加载错误:' + err.message);
  119. });
  120. })
  121. .catch(err => {
  122. console.error('❌ 区县边界加载失败:', err);
  123. alert('区县边界加载错误:' + err.message);
  124. });
  125. map.on('load', () => {
  126. // 延迟执行,确保 DOM 已完全渲染
  127. setTimeout(() => {
  128. map.invalidateSize(); // 强制 Leaflet 重新计算地图尺寸
  129. console.log('✅ 地图尺寸已重新计算');
  130. }, 300);
  131. });
  132. // 监听窗口大小变化,确保响应式布局中地图尺寸正确
  133. window.addEventListener('resize', () => {
  134. map.invalidateSize();
  135. });
  136. });
  137. </script>
  138. <style scoped>
  139. .map-wrapper {
  140. width: 100%;
  141. height: 100%;
  142. position: relative;
  143. }
  144. .map-container {
  145. width: 100% !important;
  146. height: 100% !important;
  147. }
  148. /* 弹窗样式 */
  149. ::v-deep .popup-title {
  150. text-align: center;
  151. font-size: 18px;
  152. font-weight: 700;
  153. color: #0066CC;
  154. margin: 0 0 6px;
  155. border-bottom: none;
  156. padding-bottom: 8px;
  157. }
  158. ::v-deep .popup-divider {
  159. height: 1px;
  160. background: #0066CC;
  161. margin: 8px 0;
  162. }
  163. ::v-deep .popup-container {
  164. min-width: 240px;
  165. max-width: 300px;
  166. padding: 16px;
  167. font-family: "Microsoft YaHei", sans-serif;
  168. }
  169. ::v-deep .popup-container p {
  170. margin: 6px 0;
  171. font-size: 15px;
  172. color: #666;
  173. line-height: 1.6;
  174. }
  175. ::v-deep .popup-container strong {
  176. color: #0066CC;
  177. font-weight: 600;
  178. }
  179. ::v-deep .exceeding {
  180. color: #FF3333;
  181. font-weight: bold;
  182. }
  183. /* 美化弹窗 */
  184. ::v-deep .leaflet-popup-content-wrapper {
  185. padding: 0 !important;
  186. border-radius: 12px !important;
  187. box-shadow: 0 6px 16px rgba(0,0,0,0.2) !important;
  188. }
  189. ::v-deep .leaflet-popup-content {
  190. margin: 0 !important;
  191. width: auto !important;
  192. }
  193. ::v-deep .leaflet-popup-tip {
  194. display: none;
  195. }
  196. /* 图例样式 */
  197. ::v-deep .info {
  198. padding: 6px 8px;
  199. background: white;
  200. background: rgba(255,255,255,0.9);
  201. box-shadow: 0 0 15px rgba(0,0,0,0.2);
  202. border-radius: 5px;
  203. }
  204. /* 自定义标记样式 */
  205. ::v-deep .custom-div-icon svg {
  206. transition: transform 0.2s;
  207. display: block;
  208. }
  209. </style>