# 地图页面设计文档 ## 1. 页面概述 地图页面是 EMS 系统的核心可视化界面,提供基于地理位置的直观信息展示。它主要用于显示城市的环境网格划分、实时污染数据(如热力图)、网格员位置以及特定任务的地理标记。此页面帮助主管和决策者监控区域状态,并为网格员提供清晰的工作区域指引。 ## 2. 页面布局 ![地图页面布局示意图](https://placeholder-for-map-page-mockup.png) ### 2.1 布局结构 页面采用全屏地图为主体的布局,辅以侧边栏和浮动控件。 - **主区域**: 全屏交互式地图,作为所有地理信息的载体。 - **左侧边栏**: 用于控制地图上显示的图层,如网格边界、热力图、网格员位置等。同时,也可能包含一个可搜索的兴趣点列表。 - **地图控件**: 地图右上角或左上角放置缩放、定位、全屏等标准地图控件。 - **信息弹窗 (Popup)**: 点击地图上的特定元素(如网格、标记点)时,会弹出信息窗口,显示该元素的详细信息。 ### 2.2 响应式设计 - **桌面端**: 显示完整的左侧边栏和地图。 - **平板端/移动端**: 左侧边栏默认收起,可通过按钮展开为抽屉或浮动面板,以最大化地图可视区域。 ## 3. 组件结构 ```vue ``` ## 4. 数据结构 ```typescript // 网格数据结构 interface GridData { id: number; name: string; // GeoJSON 格式的多边形坐标 geometry: { type: 'Polygon'; coordinates: number[][][]; }; manager: string; // 网格负责人 } // 热力图数据点 interface HeatmapPoint { lat: number; lng: number; value: number; // 污染指数 } // 网格员位置 interface WorkerPosition { id: number; name: string; lat: number; lng: number; lastUpdated: string; } // 任务标记 interface TaskMarker { id: number; title: string; lat: number; lng: number; status: string; } ``` ## 5. 状态管理 ```typescript // 地图服务实例 let mapInstance = null; let gridLayerGroup = null; let heatmapLayer = null; let workerMarkersGroup = null; // 组件内状态 const mapContainer = ref(null); const mapLoading = ref(true); const isPanelVisible = ref(true); const visibleLayers = ref(['grids']); // 默认显示的图层 const gridSearch = ref(''); // 全局状态 (Pinia Store) const mapStore = useMapStore(); const { grids, heatmapData, workers } = storeToRefs(mapStore); // 监听可见图层变化,动态更新地图 watch(visibleLayers, (newLayers, oldLayers) => { // ... 调用地图服务方法显示/隐藏图层 }); ``` ## 6. 交互逻辑 ### 6.1 地图初始化 ```typescript onMounted(() => { initializeMap(); mapStore.fetchInitialMapData(); }); const initializeMap = () => { if (mapContainer.value) { mapInstance = L.map(mapContainer.value).setView([39.9042, 116.4074], 10); // 默认北京 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(mapInstance); mapLoading.value = false; } }; ``` ### 6.2 图层控制 ```typescript const updateLayers = () => { // 网格图层 if (visibleLayers.value.includes('grids') && !mapInstance.hasLayer(gridLayerGroup)) { gridLayerGroup.addTo(mapInstance); } else if (!visibleLayers.value.includes('grids') && mapInstance.hasLayer(gridLayerGroup)) { gridLayerGroup.removeFrom(mapInstance); } // 其他图层同理... }; // 监听 Pinia store 中数据的变化,并更新地图 watch(grids, (newGrids) => { if (gridLayerGroup) { gridLayerGroup.clearLayers(); // 添加新的网格到图层 newGrids.forEach(grid => { const polygon = L.geoJSON(grid.geometry); polygon.bindPopup(`${grid.name}
负责人: ${grid.manager}`); gridLayerGroup.addLayer(polygon); }); } }); ``` ### 6.3 面板交互 ```typescript const togglePanel = () => { isPanelVisible.value = !isPanelVisible.value; }; const searchGrid = () => { const targetGrid = grids.value.find(g => g.name === gridSearch.value); if (targetGrid) { // 飞到目标网格位置 const bounds = L.geoJSON(targetGrid.geometry).getBounds(); mapInstance.flyToBounds(bounds); } else { ElMessage.info('未找到该网格'); } }; ``` ## 7. API 调用 ```typescript // api/map.ts export const mapApi = { getGrids: () => apiClient.get('/map/grids'), getHeatmapData: () => apiClient.get('/map/heatmap'), getWorkerPositions: () => apiClient.get('/map/workers'), }; // stores/map.ts export const useMapStore = defineStore('map', { state: () => ({ grids: [] as GridData[], heatmapData: [] as HeatmapPoint[], workers: [] as WorkerPosition[], }), actions: { async fetchInitialMapData() { // 并发请求所有地图数据 const [gridsRes, heatmapRes, workersRes] = await Promise.all([ mapApi.getGrids(), mapApi.getHeatmapData(), mapApi.getWorkerPositions(), ]); this.grids = gridsRes.data; this.heatmapData = heatmapRes.data; this.workers = workersRes.data; } } }); ``` ## 8. 样式设计 ```scss .map-page { position: relative; width: 100%; height: calc(100vh - 50px); /* 减去顶部导航栏高度 */ .map-container { width: 100%; height: 100%; z-index: 1; } .layer-control-panel { position: absolute; top: 20px; left: 20px; z-index: 2; width: 250px; background: white; transition: transform 0.3s ease; .panel-header { display: flex; justify-content: space-between; align-items: center; } } .map-loading-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.7); display: flex; justify-content: center; align-items: center; z-index: 3; flex-direction: column; gap: 10px; } } ``` ## 9. 测试用例 1. **单元测试**: - 测试 Pinia store 的 action 是否能正确获取和存储地图数据。 - 测试地图交互函数,如 `searchGrid` 的逻辑。 2. **集成测试**: - 验证地图是否能成功初始化并显示底图。 - 验证图层控制复选框是否能正确显示/隐藏对应的地理要素。 - 验证点击网格是否能弹出正确的信息窗口。 - 验证搜索功能是否能正确地将地图视图定位到指定网格。 ## 10. 性能优化 1. **大数据渲染**: - 对于大量的标记点(如网格员、任务),使用 `L.markerClusterGroup` 插件进行聚合,提高性能。 - 对于复杂的网格边界(Polygon),在低缩放级别下使用简化的几何图形(可通过后端服务实现)。 2. **懒加载**: 地图库(Leaflet)和相关插件(热力图、聚合)应按需异步加载。 3. **数据更新**: 网格员位置等实时数据应使用 WebSocket 或定时轮询(配合 `requestAnimationFrame`)进行高效更新,而不是频繁重绘整个图层。