296 lines
8.3 KiB
Markdown
296 lines
8.3 KiB
Markdown
# 地图页面设计文档
|
||
|
||
## 1. 页面概述
|
||
|
||
地图页面是 EMS 系统的核心可视化界面,提供基于地理位置的直观信息展示。它主要用于显示城市的环境网格划分、实时污染数据(如热力图)、网格员位置以及特定任务的地理标记。此页面帮助主管和决策者监控区域状态,并为网格员提供清晰的工作区域指引。
|
||
|
||
## 2. 页面布局
|
||
|
||

|
||
|
||
### 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: '© 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`)进行高效更新,而不是频繁重绘整个图层。 |