crosssectionmap.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  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. // 初始化地图(保持不变)
  22. if (!mapContainer.value) {
  23. console.error('❌ 地图容器未找到!');
  24. return;
  25. }
  26. const map = L.map(mapContainer.value, {
  27. center: [24.9, 114], // 韶关大致中心 前大往下,后大往左
  28. zoom: 8.5,
  29. minZoom: 8.3,
  30. });
  31. // 区县颜色映射 + 增强匹配(保持不变)
  32. const districtColorMap = {
  33. "武江区": "#FF6B6B",
  34. "浈江区": "#4ECDC4",
  35. "曲江区": "#FFD166",
  36. "始兴县": "#A0DAA9",
  37. "仁化县": "#6A0572",
  38. "翁源县": "#1A535C",
  39. "乳源瑶族自治县": "#FF9F1C",
  40. "新丰县": "#87CEEB",
  41. "乐昌市": "#118AB2",
  42. "南雄市": "#06D6A0",
  43. "韶关市": "#cccccc",
  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. const districtName = feature.properties.name || '';
  65. const color = getDistrictColor(districtName);
  66. return {
  67. fillColor: color,
  68. fillOpacity: 0.7,
  69. color: '#333333',
  70. weight: 2,
  71. };
  72. },
  73. }).addTo(map);
  74. // 加载水系图 + 新增接口数据加载(核心修改)
  75. fetch('/data/韶关市河流水系图.geojson')
  76. .then(res => {
  77. if (!res.ok) throw new Error(`水系图加载失败:${res.status}`);
  78. return res.json();
  79. })
  80. .then(waterGeojson => {
  81. L.geoJSON(waterGeojson, {
  82. style: {
  83. color: '#0066CC',
  84. weight: 2,
  85. opacity: 0.8,
  86. },
  87. }).addTo(map);
  88. // ========================
  89. // 从接口加载数据(替换本地rawData)
  90. // ========================
  91. fetch('http://localhost:8000/api/vector/export/all?table_name=cross_section')
  92. .then(res => {
  93. if (!res.ok) throw new Error(`数据加载失败:HTTP ${res.status}`);
  94. return res.json();
  95. })
  96. .then(geoJSONData => {
  97. // 提取GeoJSON的features.properties作为数据项
  98. const dataItems = geoJSONData.features.map(feature => feature.properties);
  99. console.log('✅ 接口数据加载完成,要素数:', dataItems.length);
  100. let markerCount = 0;
  101. dataItems.forEach((item, idx) => {
  102. try {
  103. // 字段映射(接口字段 → 原逻辑字段)
  104. const mappedItem = {
  105. "断面编号": item.id,
  106. "所属河流": item.river_name,
  107. "断面位置": item.position,
  108. "所属区县": item.county,
  109. "经度": item.longitude,
  110. "纬度": item.latitude,
  111. "Cd(ug/L)": item.cd_concentration
  112. };
  113. // 经纬度校验(保持原有逻辑)
  114. const lng = parseFloat(mappedItem.经度);
  115. const lat = parseFloat(mappedItem.纬度);
  116. if (isNaN(lat) || isNaN(lng) || lat < 22.7 || lat > 25.5 || lng < 112.7 || lng > 115.3) {
  117. console.warn(`❌ 坐标越界(第${idx}条):`, lat, lng, mappedItem);
  118. return;
  119. }
  120. // 创建标记(保持原有样式)
  121. const marker = L.marker([lat, lng], {
  122. icon: blueTriangle,
  123. zIndexOffset: 1000,
  124. }).addTo(map);
  125. // 镉含量格式化(保持原有逻辑)
  126. const cdValue = parseFloat(mappedItem["Cd(ug/L)"]);
  127. const formattedCd = isNaN(cdValue) ? '未知' : cdValue.toFixed(2) + ' μg/L';
  128. // 弹窗内容(保持原有结构)
  129. marker.bindPopup(`
  130. <div class="popup-container">
  131. <h3 class="popup-title">所属河流: ${mappedItem.所属河流}</h3>
  132. <div class="popup-divider"></div>
  133. <p><strong>断面编号:</strong> ${mappedItem.断面编号}</p>
  134. <p><strong>断面位置:</strong> ${mappedItem.断面位置}</p>
  135. <p><strong>所属区县:</strong> ${mappedItem.所属区县}</p>
  136. <p><strong>镉(Cd)含量:</strong> ${formattedCd}</p>
  137. </div>
  138. `);
  139. // 鼠标交互(保持原有逻辑)
  140. marker.on('mouseover', () => {
  141. marker.getElement().querySelector('svg').style.transform = 'scale(1.2)';
  142. }).on('mouseout', () => {
  143. marker.getElement().querySelector('svg').style.transform = 'scale(1)';
  144. });
  145. markerCount++;
  146. } catch (err) {
  147. console.error(`❌ 处理第${idx}条数据失败:`, err);
  148. }
  149. });
  150. console.log(`✅ 成功创建 ${markerCount} 个标记点`);
  151. })
  152. .catch(err => {
  153. console.error('❌ 采样点数据加载失败:', err);
  154. alert('采样点数据加载错误:' + err.message);
  155. });
  156. // ========================
  157. })
  158. .catch(err => {
  159. console.error('❌ 水系图加载失败:', err);
  160. alert('水系图加载错误:' + err.message);
  161. });
  162. })
  163. .catch(err => {
  164. console.error('❌ 区县边界加载失败:', err);
  165. alert('区县边界加载错误:' + err.message);
  166. });
  167. });
  168. </script>
  169. <style scoped>
  170. /* 原有样式保持不变 */
  171. .map-wrapper {
  172. width: 100%;
  173. height: 80%;
  174. position: relative;
  175. }
  176. .map-container {
  177. width: 100% !important;
  178. height: 100% !important;
  179. }
  180. ::v-deep .popup-title {
  181. text-align: center;
  182. font-size: 18px;
  183. font-weight: 700;
  184. color: #0066CC;
  185. margin: 0 0 6px;
  186. border-bottom: none;
  187. padding-bottom: 8px;
  188. }
  189. ::v-deep .popup-divider {
  190. height: 1px;
  191. background: #0066CC;
  192. margin: 8px 0;
  193. }
  194. ::v-deep .popup-container {
  195. min-width: 240px;
  196. max-width: 300px;
  197. padding: 16px;
  198. font-family: "Microsoft YaHei", sans-serif;
  199. }
  200. ::v-deep .popup-container p {
  201. margin: 6px 0;
  202. font-size: 15px;
  203. color: #666;
  204. line-height: 1.6;
  205. }
  206. ::v-deep .popup-container strong {
  207. color: #0066CC;
  208. font-weight: 600;
  209. }
  210. ::v-deep .exceeding {
  211. color: #FF3333;
  212. font-weight: bold;
  213. }
  214. ::v-deep .leaflet-popup-content-wrapper {
  215. padding: 0 !important;
  216. border-radius: 12px !important;
  217. box-shadow: 0 6px 16px rgba(0,0,0,0.2) !important;
  218. }
  219. ::v-deep .leaflet-popup-content {
  220. margin: 0 !important;
  221. width: auto !important;
  222. }
  223. ::v-deep .leaflet-popup-tip {
  224. display: none;
  225. }
  226. ::v-deep .info {
  227. padding: 6px 8px;
  228. background: white;
  229. background: rgba(255,255,255,0.9);
  230. box-shadow: 0 0 15px rgba(0,0,0,0.2);
  231. border-radius: 5px;
  232. }
  233. ::v-deep .custom-div-icon svg {
  234. transition: transform 0.2s;
  235. display: block;
  236. }
  237. </style>