irrwatermap.vue 10 KB

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