atmsamplemap.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  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, watch } from 'vue'; // 新增watch用于监听计算方式变化
  8. import L from 'leaflet';
  9. import 'leaflet/dist/leaflet.css';
  10. // 1. 接收父组件传递的计算方式(重量/体积)
  11. const props = defineProps({
  12. calculationMethod: {
  13. type: String,
  14. required: true,
  15. default: 'weight' // 默认按重量计算
  16. }
  17. });
  18. const mapContainer = ref(null);
  19. const mapInstance = ref(null); // 保存地图实例
  20. const markers = ref([]); // 保存所有标记点实例及对应数据,用于后续更新弹窗
  21. // 2. 定义重量/体积对应的指标分类(核心:区分需要展示的字段)
  22. const metricsMap = {
  23. weight: [ // 重量相关指标
  24. { label: 'Cr mg/kg', key: 'Cr mg/kg' },
  25. { label: 'As mg/kg', key: 'As mg/kg' },
  26. { label: 'Cd mg/kg', key: 'Cd mg/kg' },
  27. { label: 'Hg mg/kg', key: 'Hg mg/kg' },
  28. { label: 'Pb mg/kg', key: 'Pb mg/kg' },
  29. { label: '颗粒物重量 mg', key: '颗粒物的重量 mg' }
  30. ],
  31. volume: [ // 体积相关指标
  32. { label: 'Cr ug/m³', key: 'Cr ug/m3' },
  33. { label: 'As ug/m³', key: 'As ug/m3' },
  34. { label: 'Cd ug/m³', key: 'Cd ug/m3' },
  35. { label: 'Hg ug/m³', key: 'Hg ug/m3' },
  36. { label: 'Pb ug/m³', key: 'Pb ug/m3' },
  37. { label: '标准体积 m³', key: '标准体积 m3' },
  38. { label: '颗粒物浓度 ug/m³', key: '颗粒物浓度ug/m3' },
  39. ]
  40. };
  41. // 辅助函数:数值格式化
  42. function formatValue(value) {
  43. if (value === undefined || value === null || value === '') return '未知';
  44. return parseFloat(value);
  45. }
  46. // 辅助函数:位置格式化(处理"广东省韶关市"前缀)
  47. function formatLocation(fullLocation) {
  48. if (!fullLocation) return '未知位置';
  49. const processed = fullLocation.replace(/^广东省韶关市/, '').trim().replace(/^韶关市/, '');
  50. return processed || '未知位置';
  51. }
  52. // 3. 生成弹窗内容(根据计算方式动态生成)
  53. function generatePopupContent(item, method) {
  54. const metrics = metricsMap[method]; // 获取当前计算方式对应的指标
  55. // 生成指标HTML片段
  56. const metricsHtml = metrics.map(metric => `
  57. <div class="data-item">
  58. <span class="item-label">${metric.label}:</span>
  59. <span class="item-value">${formatValue(item[metric.key])}</span>
  60. </div>
  61. `).join('');
  62. // 弹窗整体结构(非指标信息保持不变)
  63. return `
  64. <div class="popup-container">
  65. <div class="popup-header">
  66. <h3 class="popup-title">${formatLocation(item.采样 || '未知采样点')}</h3>
  67. </div>
  68. <ul class="popup-info-list">
  69. <li>
  70. <span class="info-label">采样点ID:</span>
  71. <span class="info-value">${item.样品名称 || '未知'}</span>
  72. </li>
  73. </ul>
  74. <div class="grid-container">
  75. <div class="grid-item">${metricsHtml}</div>
  76. </div>
  77. </div>
  78. `;
  79. }
  80. onMounted(() => {
  81. if (!mapContainer.value) {
  82. console.error('❌ 地图容器未找到!');
  83. return;
  84. }
  85. // 初始化地图
  86. const map = L.map(mapContainer.value, {
  87. center: [24.7, 114], // 韶关大致中心
  88. zoom: 8.5,
  89. minZoom: 8.3,
  90. });
  91. mapInstance.value = map;
  92. // 区县颜色映射(保持不变)
  93. const districtColorMap = {
  94. "武江区": "#FF6B6B",
  95. "浈江区": "#4ECDC4",
  96. "曲江区": "#FFD166",
  97. "始兴县": "#A0DAA9",
  98. "仁化县": "#6A0572",
  99. "翁源县": "#1A535C",
  100. "乳源瑶族自治县": "#FF9F1C",
  101. "新丰县": "#87CEEB",
  102. "乐昌市": "#118AB2",
  103. "南雄市": "#06D6A0",
  104. };
  105. // 加载区县边界(保持不变)
  106. fetch('/data/韶关市各区县边界图.geojson')
  107. .then(res => {
  108. if (!res.ok) throw new Error(`区县边界加载失败:${res.status}`);
  109. return res.json();
  110. })
  111. .then(geojson => {
  112. console.log('✅ 区县边界数据加载完成,要素数:', geojson.features.length);
  113. L.geoJSON(geojson, {
  114. style: (feature) => {
  115. const districtName = feature.properties.name;
  116. const color = districtColorMap[districtName] || '#cccccc';
  117. return {
  118. fillColor: color,
  119. fillOpacity: 0.7,
  120. color: '#333333',
  121. weight: 2,
  122. };
  123. },
  124. }).addTo(map);
  125. fetch('/data/乐昌市.geoJson') // 假设文件路径为 /data/乐昌市.geojson
  126. .then(res => {
  127. if (!res.ok) throw new Error(`乐昌市边界加载失败:${res.status}`);
  128. return res.json();
  129. })
  130. .then(lechangGeojson => {
  131. console.log('✅ 乐昌市边界数据加载完成', lechangGeojson);
  132. // 为乐昌市设置更突出的样式(与原有边界区分)
  133. L.geoJSON(lechangGeojson, {
  134. style: () => {
  135. return {
  136. fillColor: districtColorMap["乐昌市"], // 复用原有颜色
  137. fillOpacity: 0.5, // 透明度略低,避免覆盖原有边界
  138. color: '#000000', // 边框颜色加深
  139. weight: 4, // 边框加粗,突出显示
  140. dashArray: '5, 5', // 可选:添加虚线效果,进一步区分
  141. };
  142. },
  143. }).addTo(map);
  144. })
  145. .catch(err => {
  146. console.warn('⚠️ 乐昌市边界加载失败(不影响主地图):', err);
  147. });
  148. // 加载大气数据并创建标记点
  149. fetch('http://localhost:3000/table/Atmosphere_summary_data')
  150. .then(res => {
  151. if (!res.ok) throw new Error(`大气数据加载失败:${res.status}`);
  152. return res.json();
  153. })
  154. .then(atmosphereData => {
  155. console.log('✅ 大气数据加载完成,记录数:', atmosphereData.length);
  156. markers.value = []; // 清空标记点数组
  157. atmosphereData.forEach((item, idx) => {
  158. try {
  159. // 提取经纬度(保持不变)
  160. const latField = ['latitude', 'lat', '纬度'].find(key => item[key] !== undefined);
  161. const lngField = ['longitude', 'lng', '经度'].find(key => item[key] !== undefined);
  162. if (!latField || !lngField) {
  163. console.error(`❌ 未找到经纬度字段(第${idx}条)`);
  164. return;
  165. }
  166. // 清理经纬度数据(保持不变)
  167. const cleanLat = String(item[latField]).replace(/[^\d.-]/g, '');
  168. const cleanLng = String(item[lngField]).replace(/[^\d.-]/g, '');
  169. const lat = parseFloat(parseFloat(cleanLat).toFixed(6));
  170. const lng = parseFloat(parseFloat(cleanLng).toFixed(6));
  171. // 坐标范围校验(保持不变)
  172. if (isNaN(lat) || isNaN(lng) || lat < 22.7 || lat > 25.5 || lng < 112.7 || lng > 115.3) {
  173. console.warn(`❌ 坐标超出范围(第${idx}条):`, lat, lng);
  174. return;
  175. }
  176. // 创建标记点(保持不变)
  177. const marker = L.circleMarker([lat, lng], {
  178. radius: 3.5,
  179. color: '#FF3333',
  180. fillColor: '#FF3333',
  181. fillOpacity: 0.9,
  182. weight: 1.5,
  183. zIndexOffset: 1000,
  184. }).addTo(map);
  185. // 绑定初始弹窗内容(根据默认计算方式)
  186. marker.bindPopup(generatePopupContent(item, props.calculationMethod));
  187. // 保存标记点实例和对应数据,用于后续更新
  188. markers.value.push({ marker, item });
  189. } catch (err) {
  190. console.error(`❌ 处理大气数据失败(第${idx}条):`, err);
  191. }
  192. });
  193. console.log(`✅ 成功创建 ${markers.value.length} 个大气数据标记点`);
  194. })
  195. .catch(err => {
  196. console.error('❌ 大气数据加载失败:', err);
  197. alert('大气数据接口错误:' + err.message);
  198. });
  199. })
  200. .catch(err => {
  201. console.error('❌ 区县边界加载失败:', err);
  202. alert('区县边界加载错误:' + err.message);
  203. });
  204. });
  205. // 4. 监听计算方式变化,更新所有标记点的弹窗内容
  206. watch(
  207. () => props.calculationMethod,
  208. (newMethod) => {
  209. markers.value.forEach(({ marker, item }) => {
  210. // 重新绑定弹窗内容(使用新的计算方式)
  211. marker.bindPopup(generatePopupContent(item, newMethod));
  212. });
  213. console.log(`✅ 已切换为${newMethod === 'weight' ? '重量' : '体积'}计算方式,弹窗内容已更新`);
  214. }
  215. );
  216. </script>
  217. <style scoped>
  218. /* 样式保持不变,仅需确保弹窗内容布局适配单组数据 */
  219. .map-wrapper {
  220. width: 100%;
  221. height: 100%;
  222. position: relative;
  223. }
  224. .map-container {
  225. width: 100% !important;
  226. height: 100% !important;
  227. }
  228. /* 弹窗样式 */
  229. ::v-deep .leaflet-popup-content-wrapper {
  230. padding: 0 !important;
  231. border-radius: 10px !important;
  232. box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
  233. }
  234. ::v-deep .leaflet-popup-content {
  235. margin: 0 !important;
  236. width: auto !important;
  237. max-width: 300px; /* 适当减小最大宽度,适配单列布局 */
  238. }
  239. ::v-deep .popup-container {
  240. min-width: 280px;
  241. padding: 12px;
  242. font-family: "Microsoft YaHei", sans-serif;
  243. }
  244. ::v-deep .popup-header {
  245. margin-bottom: 10px;
  246. }
  247. ::v-deep .popup-title {
  248. text-align: center;
  249. font-size: 16px;
  250. font-weight: 700;
  251. color: #0066CC;
  252. margin: 0 0 5px;
  253. padding-bottom: 6px;
  254. border-bottom: 1.5px solid #0066CC;
  255. }
  256. ::v-deep .popup-info-list {
  257. list-style: none;
  258. padding: 0;
  259. margin: 0 0 10px;
  260. display: grid;
  261. grid-template-columns: 1fr 1fr;
  262. gap: 6px;
  263. }
  264. ::v-deep .popup-info-list li {
  265. display: flex;
  266. margin: 0;
  267. padding: 3px 6px;
  268. background: #f9f9f9;
  269. border-radius: 3px;
  270. }
  271. ::v-deep .info-label {
  272. flex: 0 0 85px;
  273. font-weight: 600;
  274. color: #333;
  275. font-size: 13px;
  276. }
  277. ::v-deep .info-value {
  278. flex: 1;
  279. color: #666;
  280. font-size: 13px;
  281. white-space: nowrap;
  282. }
  283. ::v-deep .grid-container {
  284. display: grid;
  285. grid-template-columns: 1fr; /* 改为单列布局,适配分类后的指标 */
  286. gap: 6px;
  287. }
  288. ::v-deep .grid-item {
  289. display: flex;
  290. flex-direction: column;
  291. gap: 6px;
  292. }
  293. ::v-deep .data-item {
  294. display: flex;
  295. justify-content: space-between;
  296. padding: 6px 8px;
  297. background: #f9f9f9;
  298. border-radius: 3px;
  299. }
  300. ::v-deep .item-label {
  301. font-weight: 600;
  302. color: #555;
  303. font-size: 13px;
  304. }
  305. ::v-deep .item-value {
  306. color: #000;
  307. font-size: 13px;
  308. }
  309. /* 隐藏弹窗箭头 */
  310. ::v-deep .leaflet-popup-tip {
  311. display: none;
  312. }
  313. /* 标记点样式 */
  314. ::v-deep .leaflet-circle-marker {
  315. stroke-width: 1.5px !important;
  316. }
  317. /* 大气数据标记点的悬停效果 */
  318. ::v-deep .leaflet-marker-pane .leaflet-circle-marker[fill="#118AB2"]:hover {
  319. fill-opacity: 1 !important;
  320. stroke-width: 2.5px !important;
  321. }
  322. </style>