This commit is contained in:
ChuXun
2025-10-19 20:31:01 +08:00
parent cfd054f0d9
commit 4ce487588a
287 changed files with 59148 additions and 0 deletions

View File

@@ -0,0 +1,296 @@
# 地图页面设计文档
## 1. 页面概述
地图页面是 EMS 系统的核心可视化界面,提供基于地理位置的直观信息展示。它主要用于显示城市的环境网格划分、实时污染数据(如热力图)、网格员位置以及特定任务的地理标记。此页面帮助主管和决策者监控区域状态,并为网格员提供清晰的工作区域指引。
## 2. 页面布局
![地图页面布局示意图](https://placeholder-for-map-page-mockup.png)
### 2.1 布局结构
页面采用全屏地图为主体的布局,辅以侧边栏和浮动控件。
- **主区域**: 全屏交互式地图,作为所有地理信息的载体。
- **左侧边栏**: 用于控制地图上显示的图层,如网格边界、热力图、网格员位置等。同时,也可能包含一个可搜索的兴趣点列表。
- **地图控件**: 地图右上角或左上角放置缩放、定位、全屏等标准地图控件。
- **信息弹窗 (Popup)**: 点击地图上的特定元素(如网格、标记点)时,会弹出信息窗口,显示该元素的详细信息。
### 2.2 响应式设计
- **桌面端**: 显示完整的左侧边栏和地图。
- **平板端/移动端**: 左侧边栏默认收起,可通过按钮展开为抽屉或浮动面板,以最大化地图可视区域。
## 3. 组件结构
```vue
<template>
<div class="map-page">
<!-- 地图容器 -->
<div ref="mapContainer" class="map-container"></div>
<!-- 左侧图层控制面板 -->
<el-card class="layer-control-panel">
<template #header>
<div class="panel-header">
<span>图层控制</span>
<el-button link type="primary" @click="togglePanel" class="panel-toggle-btn">
<el-icon><ArrowLeft v-if="isPanelVisible" /><ArrowRight v-else /></el-icon>
</el-button>
</div>
</template>
<div v-show="isPanelVisible">
<el-checkbox-group v-model="visibleLayers">
<el-checkbox label="grids">显示网格</el-checkbox>
<el-checkbox label="heatmap">显示污染热力图</el-checkbox>
<el-checkbox label="workers">显示网格员位置</el-checkbox>
<el-checkbox label="tasks">显示任务标记</el-checkbox>
</el-checkbox-group>
<el-divider />
<h4>搜索网格</h4>
<el-input v-model="gridSearch" placeholder="输入网格名称/编号搜索" @keyup.enter="searchGrid" />
</div>
</el-card>
<!-- 地图加载状态 -->
<div v-if="mapLoading" class="map-loading-overlay">
<el-icon class="is-loading" size="30"><Loading /></el-icon>
<span>地图加载中...</span>
</div>
</div>
</template>
```
## 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<HTMLElement | null>(null);
const mapLoading = ref<boolean>(true);
const isPanelVisible = ref<boolean>(true);
const visibleLayers = ref<string[]>(['grids']); // 默认显示的图层
const gridSearch = ref<string>('');
// 全局状态 (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: '&copy; 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(`<b>${grid.name}</b><br>负责人: ${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<GridData[]>('/map/grids'),
getHeatmapData: () => apiClient.get<HeatmapPoint[]>('/map/heatmap'),
getWorkerPositions: () => apiClient.get<WorkerPosition[]>('/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`)进行高效更新,而不是频繁重绘整个图层。