irrwatermap.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. <template>
  2. <div class="map-wrapper" @click.stop>
  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. onMounted(() => {
  12. // 初始化地图(强制确保容器可用)
  13. if (!mapContainer.value) {
  14. console.error('❌ 地图容器未找到!');
  15. return;
  16. }
  17. // 定义位置格式化函数(处理"广东省韶关市"前缀)
  18. const formatLocation = (fullLocation) => {
  19. if (!fullLocation) return '未知位置'; // 处理空值
  20. // 移除前缀并清理空格
  21. const processed = fullLocation.replace(/^(广东省)?韶关市/, '').trim();
  22. // 处理移除后为空的情况
  23. return processed || '未知位置';
  24. };
  25. const map = L.map(mapContainer.value, {
  26. center: [25, 114], // 韶关大致中心 前大往下,后大往左
  27. zoom: 8.5,
  28. minZoom: 8.3,
  29. });
  30. // 区县颜色映射(与GeoJSON的properties.name严格匹配)
  31. const districtColorMap = {
  32. "武江区": "#FF6B6B",
  33. "浈江区": "#4ECDC4",
  34. "曲江区": "#FFD166",
  35. "始兴县": "#A0DAA9",
  36. "仁化县": "#6A0572",
  37. "翁源县": "#1A535C",
  38. "乳源瑶族自治县": "#FF9F1C",
  39. "新丰县": "#87CEEB",
  40. "乐昌市": "#118AB2",
  41. "南雄市": "#06D6A0",
  42. };
  43. // 加载区县边界(带完整错误处理)
  44. fetch('/data/韶关市各区县边界图.geojson')
  45. .then(res => {
  46. if (!res.ok) throw new Error(`区县边界加载失败:${res.status}`);
  47. return res.json();
  48. })
  49. .then(geojson => {
  50. console.log('✅ 区县边界数据加载完成,要素数:', geojson.features.length);
  51. L.geoJSON(geojson, {
  52. style: (feature) => {
  53. const districtName = feature.properties.name; // 匹配GeoJSON的name字段
  54. const color = districtColorMap[districtName] || '#cccccc';
  55. return {
  56. fillColor: color,
  57. fillOpacity: 0.7,
  58. color: '#333333', // 边界颜色
  59. weight: 2, // 边界宽度
  60. };
  61. },
  62. }).addTo(map);
  63. // 加载水系图(新增,带样式和错误处理)
  64. fetch('/data/韶关市河流水系图.geojson')
  65. .then(res => {
  66. if (!res.ok) throw new Error(`水系图加载失败:${res.status}`);
  67. return res.json();
  68. })
  69. .then(waterGeojson => {
  70. console.log('✅ 水系图数据加载完成,要素数:', waterGeojson.features.length);
  71. L.geoJSON(waterGeojson, {
  72. style: {
  73. color: '#0066CC', // 水系颜色
  74. weight: 2, // 线条宽度
  75. opacity: 0.8, // 透明度
  76. },
  77. }).addTo(map);
  78. // ========================
  79. // 修复核心:加载采样+检测数据(单接口)
  80. // ========================
  81. fetch('http://localhost:8000/api/vector/export/all?table_name=water_sampling_data')
  82. .then(res => {
  83. if (!res.ok) throw new Error(`数据加载失败:HTTP ${res.status}`);
  84. return res.json();
  85. })
  86. .then(geoJSONData => { // geoJSONData 是 FeatureCollection 结构
  87. console.log('✅ 采样数据加载完成,要素数:', geoJSONData.features.length);
  88. let markerCount = 0;
  89. geoJSONData.features.forEach((feature, idx) => {
  90. const props = feature.properties; // 单个要素的完整属性(采样+检测)
  91. try {
  92. // 智能提取经纬度字段(支持多种可能的字段名)
  93. const latField = ['latitude', 'lat', 'Latitude', 'Lat'].find(key => props[key] !== undefined);
  94. const lngField = ['longitude', 'lng', 'Longitude', 'Lng'].find(key => props[key] !== undefined);
  95. if (!latField || !lngField) {
  96. console.error(`❌ 未找到经纬度字段(第${idx}条):`, props);
  97. return;
  98. }
  99. // 清理并转换经纬度(处理特殊字符和逗号)
  100. const cleanLat = String(props[latField]).replace(/[^\d.-]/g, '');
  101. const cleanLng = String(props[lngField]).replace(/[^\d.-]/g, '');
  102. // 强制四舍五入到6位小数(避免精度问题)
  103. const lat = parseFloat(parseFloat(cleanLat).toFixed(6));
  104. const lng = parseFloat(parseFloat(cleanLng).toFixed(6));
  105. // 范围校验(扩大范围10%,兼容边界值)
  106. if (isNaN(lat) || isNaN(lng) || lat < 22.7 || lat > 25.5 || lng < 112.7 || lng > 115.3) {
  107. console.warn(`❌ 坐标超出合理范围(第${idx}条):`, lat, lng, props);
  108. return;
  109. }
  110. // 创建标记点(使用 L.circleMarker 而非 L.marker)
  111. const marker = L.circleMarker([lat, lng], {
  112. radius: 4, // 增大圆点半径,确保可见
  113. color: '#FF3333', // 边框颜色(红色)
  114. fillColor: '#FF3333', // 填充颜色(红色)
  115. fillOpacity: 0.9, // 填充透明度(接近不透明)
  116. weight: 2, // 边框宽度(加粗)
  117. zIndexOffset: 1000, // 提高层级,确保在所有图层之上
  118. }).addTo(map);
  119. // 弹窗内容:直接从 props 提取检测数据(匹配实际字段名)
  120. marker.bindPopup(`
  121. <div class="popup-container">
  122. <h3 class="popup-title">${formatLocation(props.sampling_location)}</h3>
  123. <div class="popup-divider"></div> <!-- 分隔线 -->
  124. <table class="popup-table">
  125. <thead>
  126. <tr>
  127. <th>检测项</th>
  128. <th>数值</th>
  129. </tr>
  130. </thead>
  131. <tbody>
  132. <tr><td>Ph</td><td>${props.ph_value || '未知'}</td></tr>
  133. <tr><td>铬(Cr)(ug/L)</td><td>${props.cr_concentration.toFixed(6)|| '未知'}</td></tr>
  134. <tr><td>砷(As)(ug/L)</td><td>${props.as_concentration.toFixed(6) || '未知'}</td></tr>
  135. <tr><td>镉(Cd)(ug/L)</td><td>${props.cd_concentration.toFixed(6) || '未知'}</td></tr>
  136. <tr><td>汞(Hg)(ug/L)</td><td>${props.hg_concentration.toFixed(6) || '未知'}</td></tr>
  137. <tr><td>铅(Pb)(ug/L)</td><td>${props.pb_concentration.toFixed(6) || '未知'}</td></tr>
  138. </tbody>
  139. </table>
  140. </div>
  141. `);
  142. markerCount++;
  143. } catch (err) {
  144. console.error(`❌ 处理采样点失败(第${idx}条):`, err);
  145. }
  146. });
  147. console.log(`✅ 成功创建 ${markerCount} 个标记点`);
  148. })
  149. .catch(err => {
  150. console.error('❌ 采样/检测数据加载失败:', err);
  151. alert('数据接口错误:' + err.message);
  152. });
  153. // ========================
  154. // 水系图加载完成后的逻辑结束
  155. // ========================
  156. })
  157. .catch(err => {
  158. console.error('❌ 水系图加载失败:', err);
  159. alert('水系图加载错误:' + err.message);
  160. });
  161. })
  162. .catch(err => {
  163. console.error('❌ 区县边界加载失败:', err);
  164. alert('区县边界加载错误:' + err.message);
  165. });
  166. });
  167. </script>
  168. <style scoped>
  169. .map-wrapper {
  170. width:100%;
  171. height: 100%;
  172. position: relative;
  173. z-index: 100;
  174. }
  175. .map-container {
  176. width: 100% !important;
  177. height: 500px !important;
  178. }
  179. /* 标题和分隔线 */
  180. ::v-deep .popup-title {
  181. text-align: center; /* 居中 */
  182. font-size: 16px; /* 减小字号 */
  183. font-weight: 700; /* 加粗 */
  184. color: #0066CC; /* 蓝色,匹配设计 */
  185. margin: 0 0 4px; /* 间距调整 */
  186. border-bottom: 2px solid #0066CC; /* 底部横线 */
  187. padding-bottom: 4px; /* 横线与文字间距 */
  188. }
  189. ::v-deep .popup-divider {
  190. height: 1px; /* 横线高度 */
  191. background: #0066CC; /* 横线颜色 */
  192. margin: 6px 0; /* 上下间距 */
  193. }
  194. /* 表格样式 */
  195. ::v-deep .popup-table {
  196. width: 100%; /* 占满容器 */
  197. border-collapse: collapse;/* 合并边框 */
  198. margin-top: 12px; /* 与段落间距 */
  199. }
  200. ::v-deep .popup-table th,
  201. ::v-deep .popup-table td {
  202. border: 1px solid #CCCCCC;/* 单元格边框 */
  203. padding: 4px 6px; /* 内边距 */
  204. text-align: center; /* 内容居中 */
  205. font-size: 12px; /* 字号调整 */
  206. }
  207. ::v-deep .popup-table th {
  208. background: #F5F5F5; /* 表头背景色 */
  209. font-weight: 600; /* 表头加粗 */
  210. }
  211. /* 美化弹窗(完整层级穿透) */
  212. ::v-deep .leaflet-popup-content-wrapper {
  213. padding: 0 !important;
  214. border-radius: 12px !important;
  215. box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
  216. }
  217. ::v-deep .leaflet-popup-content {
  218. margin: 0 !important;
  219. width: auto !important;
  220. max-width: 220px !important;
  221. }
  222. ::v-deep .popup-container {
  223. min-width: 180px;
  224. max-width: 220px;
  225. padding: 10px;/**内边距 */
  226. font-family: "Microsoft YaHei", sans-serif;
  227. }
  228. ::v-deep .popup-content p {
  229. margin: 6px 0;
  230. font-size: 15px;
  231. color: #666;
  232. line-height: 1.6;
  233. }
  234. ::v-deep .popup-content strong {
  235. color: #FF3333; /* 与标记点颜色呼应 */
  236. font-weight: 600;
  237. }
  238. /* 可选:隐藏弹窗箭头,更像卡片 */
  239. ::v-deep .leaflet-popup-tip {
  240. display: none;
  241. }
  242. /* 临时调试:确保标记点可见 */
  243. ::v-deep .leaflet-marker-icon {
  244. display: none !important; /* 隐藏默认标记图标 */
  245. }
  246. ::v-deep .leaflet-circle-marker {
  247. stroke-width: 2px !important;
  248. }
  249. </style>