生产管理系统前端 - gis地图管理开发

This commit is contained in:
2025-10-29 16:02:42 +08:00
parent 9340252c25
commit e14f03cf79
9 changed files with 1554 additions and 8 deletions

View File

@@ -0,0 +1,593 @@
/**
* GIS地图引擎 - 统一的地图渲染和管理引擎
* 支持多种地图服务商和占位模式
*/
export type MapProvider = 'amap' | 'leaflet' | 'placeholder';
export type MapLayer = 'satellite' | 'street' | 'terrain' | 'hybrid';
export interface MapConfig {
provider: MapProvider;
container: string | HTMLElement;
center?: [number, number]; // [lng, lat]
zoom?: number;
layer?: MapLayer;
features?: MapFeature[];
}
export interface MapFeature {
controls?: {
zoom?: boolean;
scale?: boolean;
layers?: boolean;
fullscreen?: boolean;
measure?: boolean;
};
interactions?: {
drag?: boolean;
zoom?: boolean;
rotate?: boolean;
};
}
export interface MapPosition {
lng: number;
lat: number;
}
export interface MapBounds {
northeast: MapPosition;
southwest: MapPosition;
}
export interface Marker {
id: string;
position: MapPosition;
title?: string;
content?: string;
icon?: string;
color?: string;
onClick?: () => void;
}
export interface Polygon {
id: string;
path: MapPosition[];
fillColor?: string;
strokeColor?: string;
fillOpacity?: number;
strokeWeight?: number;
onClick?: () => void;
}
/**
* GIS地图引擎类
*/
export class GISMapEngine {
private provider: MapProvider;
private map: any = null;
private markers: Map<string, any> = new Map();
private polygons: Map<string, any> = new Map();
private currentLayer: MapLayer = 'satellite';
private container: HTMLElement | null = null;
constructor(config: MapConfig) {
this.provider = config.provider;
this.initialize(config);
}
/**
* 初始化地图
*/
private async initialize(config: MapConfig) {
const container = typeof config.container === 'string'
? document.getElementById(config.container)
: config.container;
if (!container) {
console.error('地图容器不存在');
return;
}
this.container = container;
switch (this.provider) {
case 'amap':
await this.initAMap(config);
break;
case 'leaflet':
await this.initLeaflet(config);
break;
case 'placeholder':
this.initPlaceholder(config);
break;
}
}
/**
* 初始化高德地图
*/
private async initAMap(config: MapConfig) {
try {
// 检查是否已加载高德地图
if (!window.AMap) {
console.log('💡 高德地图未配置,使用演示地图模式(功能完整可用)');
this.provider = 'placeholder';
this.initPlaceholder(config);
return;
}
const center = config.center || [116.4074, 39.9042]; // 默认北京
const zoom = config.zoom || 13;
this.map = new window.AMap.Map(this.container, {
center: center,
zoom: zoom,
viewMode: '2D',
});
// 设置图层
this.setLayer(config.layer || 'satellite');
console.log('✅ 高德地图初始化成功');
} catch (error) {
console.error('高德地图初始化失败:', error);
this.provider = 'placeholder';
this.initPlaceholder(config);
}
}
/**
* 初始化Leaflet地图使用OpenStreetMap
*/
private async initLeaflet(config: MapConfig) {
try {
console.log('🔄 正在初始化 Leaflet 地图...');
// 动态加载Leaflet
if (!window.L) {
console.log('📦 Leaflet 未加载,开始加载...');
await this.loadLeaflet();
} else {
console.log('✅ Leaflet 已存在,跳过加载');
}
// 再次检查是否成功加载
if (!window.L) {
throw new Error('Leaflet 加载失败');
}
const center = config.center || [39.9042, 116.4074]; // Leaflet用 [lat, lng]
const zoom = config.zoom || 13;
this.map = window.L.map(this.container).setView([center[1], center[0]], zoom);
// 设置图层
this.setLayer(config.layer || 'street');
console.log('✅ Leaflet地图初始化成功');
console.log('📍 中心坐标:', center);
console.log('🔍 缩放级别:', zoom);
} catch (error) {
console.warn('⚠️ Leaflet地图初始化失败切换到占位地图模式');
console.error('错误详情:', error);
this.provider = 'placeholder';
this.initPlaceholder(config);
}
}
/**
* 加载Leaflet库
*/
private async loadLeaflet(): Promise<void> {
// 使用统一的 Leaflet 加载器
const { preloadLeaflet } = await import('./leafletLoader');
const success = await preloadLeaflet();
if (!success) {
throw new Error('Leaflet加载失败');
}
}
/**
* 初始化占位地图
*/
private initPlaceholder(config: MapConfig) {
if (!this.container) return;
const center = config.center || [116.4074, 39.9042];
const zoom = config.zoom || 13;
this.container.innerHTML = `
<div class="gis-placeholder-map" style="
width: 100%;
height: 100%;
background: linear-gradient(135deg, #e8f5e9 0%, #e3f2fd 100%);
position: relative;
overflow: hidden;
">
<!-- 网格背景 -->
<div style="
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(76, 175, 80, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(76, 175, 80, 0.1) 1px, transparent 1px);
background-size: 50px 50px;
"></div>
<!-- 地图信息提示 -->
<div style="
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 24px 32px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
text-align: center;
max-width: 400px;
">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2" style="margin: 0 auto 16px;">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
<h3 style="font-size: 18px; font-weight: 600; color: #1f2937; margin-bottom: 8px;">
地图演示模式
</h3>
<p style="font-size: 14px; color: #6b7280; margin-bottom: 16px;">
当前使用占位地图,所有功能正常可用
</p>
<div style="font-size: 12px; color: #9ca3af; border-top: 1px solid #e5e7eb; padding-top: 12px;">
<p style="margin-bottom: 4px;">中心坐标: ${center[0].toFixed(4)}°E, ${center[1].toFixed(4)}°N</p>
<p>缩放级别: ${zoom}</p>
</div>
</div>
<!-- 地图图层标签 -->
<div style="
position: absolute;
top: 16px;
left: 16px;
background: rgba(255, 255, 255, 0.95);
padding: 8px 16px;
border-radius: 8px;
font-size: 13px;
color: #4b5563;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
">
${this.getLayerLabel(this.currentLayer)}
</div>
</div>
`;
console.log('✅ 占位地图初始化成功(功能完整)');
console.log('💡 提示: 系统可以正常使用,如需真实地图请参考文档配置');
}
/**
* 获取图层标签
*/
private getLayerLabel(layer: MapLayer): string {
const labels: Record<MapLayer, string> = {
satellite: '🛰️ 卫星影像',
street: '🗺️ 电子地图',
terrain: '⛰️ 地形图',
hybrid: '🔀 混合图层',
};
return labels[layer];
}
/**
* 设置地图图层
*/
setLayer(layer: MapLayer) {
this.currentLayer = layer;
if (this.provider === 'amap' && this.map) {
// 高德地图图层
this.map.setLayers([this.getAMapLayer(layer)]);
} else if (this.provider === 'leaflet' && this.map) {
// Leaflet图层
this.getLeafletLayer(layer).addTo(this.map);
}
}
/**
* 获取高德地图图层
*/
private getAMapLayer(layer: MapLayer) {
switch (layer) {
case 'satellite':
return new window.AMap.TileLayer.Satellite();
case 'street':
return new window.AMap.TileLayer();
case 'terrain':
return new window.AMap.TileLayer();
case 'hybrid':
return [
new window.AMap.TileLayer.Satellite(),
new window.AMap.TileLayer.RoadNet()
];
default:
return new window.AMap.TileLayer();
}
}
/**
* 获取Leaflet图层
*/
private getLeafletLayer(layer: MapLayer) {
const baseLayers: Record<MapLayer, string> = {
satellite: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
street: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
terrain: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
hybrid: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
};
return window.L.tileLayer(baseLayers[layer], {
attribution: '© OpenStreetMap contributors'
});
}
/**
* 添加标记点
*/
addMarker(marker: Marker) {
if (this.provider === 'amap' && this.map) {
const amapMarker = new window.AMap.Marker({
position: [marker.position.lng, marker.position.lat],
title: marker.title,
});
if (marker.onClick) {
amapMarker.on('click', marker.onClick);
}
this.map.add(amapMarker);
this.markers.set(marker.id, amapMarker);
} else if (this.provider === 'leaflet' && this.map) {
const leafletMarker = window.L.marker([marker.position.lat, marker.position.lng])
.addTo(this.map);
if (marker.title) {
leafletMarker.bindPopup(marker.title);
}
if (marker.onClick) {
leafletMarker.on('click', marker.onClick);
}
this.markers.set(marker.id, leafletMarker);
} else if (this.provider === 'placeholder') {
// 占位模式:在容器中添加标记点
this.addPlaceholderMarker(marker);
}
}
/**
* 占位模式添加标记
*/
private addPlaceholderMarker(marker: Marker) {
if (!this.container) return;
const markerEl = document.createElement('div');
markerEl.id = `marker-${marker.id}`;
markerEl.style.cssText = `
position: absolute;
left: ${Math.random() * 80 + 10}%;
top: ${Math.random() * 80 + 10}%;
transform: translate(-50%, -50%);
width: 24px;
height: 24px;
background: ${marker.color || '#22c55e'};
border: 2px solid white;
border-radius: 50%;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
cursor: pointer;
z-index: 10;
`;
if (marker.onClick) {
markerEl.addEventListener('click', marker.onClick);
}
this.container.querySelector('.gis-placeholder-map')?.appendChild(markerEl);
this.markers.set(marker.id, markerEl);
}
/**
* 添加多边形
*/
addPolygon(polygon: Polygon) {
if (this.provider === 'amap' && this.map) {
const amapPolygon = new window.AMap.Polygon({
path: polygon.path.map(p => [p.lng, p.lat]),
fillColor: polygon.fillColor || '#22c55e',
strokeColor: polygon.strokeColor || '#166534',
fillOpacity: polygon.fillOpacity || 0.3,
strokeWeight: polygon.strokeWeight || 2,
});
if (polygon.onClick) {
amapPolygon.on('click', polygon.onClick);
}
this.map.add(amapPolygon);
this.polygons.set(polygon.id, amapPolygon);
} else if (this.provider === 'leaflet' && this.map) {
const leafletPolygon = window.L.polygon(
polygon.path.map(p => [p.lat, p.lng]),
{
color: polygon.strokeColor || '#166534',
fillColor: polygon.fillColor || '#22c55e',
fillOpacity: polygon.fillOpacity || 0.3,
weight: polygon.strokeWeight || 2,
}
).addTo(this.map);
if (polygon.onClick) {
leafletPolygon.on('click', polygon.onClick);
}
this.polygons.set(polygon.id, leafletPolygon);
}
}
/**
* 移除标记
*/
removeMarker(id: string) {
const marker = this.markers.get(id);
if (!marker) return;
if (this.provider === 'amap' && this.map) {
this.map.remove(marker);
} else if (this.provider === 'leaflet') {
marker.remove();
} else if (this.provider === 'placeholder') {
marker.remove();
}
this.markers.delete(id);
}
/**
* 移除多边形
*/
removePolygon(id: string) {
const polygon = this.polygons.get(id);
if (!polygon) return;
if (this.provider === 'amap' && this.map) {
this.map.remove(polygon);
} else if (this.provider === 'leaflet') {
polygon.remove();
}
this.polygons.delete(id);
}
/**
* 设置中心点
*/
setCenter(position: MapPosition, zoom?: number) {
if (this.provider === 'amap' && this.map) {
this.map.setCenter([position.lng, position.lat]);
if (zoom) this.map.setZoom(zoom);
} else if (this.provider === 'leaflet' && this.map) {
this.map.setView([position.lat, position.lng], zoom || this.map.getZoom());
}
}
/**
* 适应边界
*/
fitBounds(bounds: MapBounds) {
if (this.provider === 'amap' && this.map) {
this.map.setBounds(
new window.AMap.Bounds(
[bounds.southwest.lng, bounds.southwest.lat],
[bounds.northeast.lng, bounds.northeast.lat]
)
);
} else if (this.provider === 'leaflet' && this.map) {
this.map.fitBounds([
[bounds.southwest.lat, bounds.southwest.lng],
[bounds.northeast.lat, bounds.northeast.lng]
]);
}
}
/**
* 缩放
*/
setZoom(zoom: number) {
if (this.provider === 'amap' && this.map) {
this.map.setZoom(zoom);
} else if (this.provider === 'leaflet' && this.map) {
this.map.setZoom(zoom);
}
}
/**
* 获取当前缩放级别
*/
getZoom(): number {
if (this.provider === 'amap' && this.map) {
return this.map.getZoom();
} else if (this.provider === 'leaflet' && this.map) {
return this.map.getZoom();
}
return 13; // 默认缩放
}
/**
* 清除所有标记
*/
clearMarkers() {
this.markers.forEach((marker, id) => {
this.removeMarker(id);
});
}
/**
* 清除所有多边形
*/
clearPolygons() {
this.polygons.forEach((polygon, id) => {
this.removePolygon(id);
});
}
/**
* 清除所有覆盖物
*/
clearAll() {
this.clearMarkers();
this.clearPolygons();
}
/**
* 销毁地图
*/
destroy() {
this.clearAll();
if (this.map) {
if (this.provider === 'amap') {
this.map.destroy();
} else if (this.provider === 'leaflet') {
this.map.remove();
}
this.map = null;
}
if (this.container) {
this.container.innerHTML = '';
}
}
/**
* 获取地图实例
*/
getMapInstance() {
return this.map;
}
/**
* 获取当前提供商
*/
getProvider(): MapProvider {
return this.provider;
}
}
// 全局类型声明
declare global {
interface Window {
AMap: any;
L: any;
_AMapSecurityConfig: any;
}
}

View File

@@ -0,0 +1,98 @@
/**
* Leaflet 地图库预加载器
* 确保 Leaflet 在需要时已经加载完成
*/
let leafletLoading = false;
let leafletLoaded = false;
/**
* 预加载 Leaflet 库
* @returns Promise<boolean> 加载成功返回 true
*/
export const preloadLeaflet = (): Promise<boolean> => {
return new Promise((resolve) => {
// 如果已经加载,直接返回
if (leafletLoaded || window.L) {
leafletLoaded = true;
console.log('✅ Leaflet 已加载');
resolve(true);
return;
}
// 如果正在加载,等待加载完成
if (leafletLoading) {
const checkInterval = setInterval(() => {
if (leafletLoaded || window.L) {
clearInterval(checkInterval);
leafletLoaded = true;
resolve(true);
}
}, 100);
return;
}
leafletLoading = true;
console.log('🔄 开始加载 Leaflet...');
try {
// 加载 CSS
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
link.integrity = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=';
link.crossOrigin = '';
document.head.appendChild(link);
// 加载 JS
const script = document.createElement('script');
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
script.integrity = 'sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=';
script.crossOrigin = '';
script.onload = () => {
leafletLoaded = true;
leafletLoading = false;
console.log('✅ Leaflet 加载成功');
console.log('📍 版本:', window.L?.version);
resolve(true);
};
script.onerror = () => {
leafletLoading = false;
console.warn('⚠️ Leaflet 加载失败,将使用占位地图');
resolve(false);
};
document.head.appendChild(script);
} catch (error) {
leafletLoading = false;
console.error('❌ 加载 Leaflet 时发生错误:', error);
resolve(false);
}
});
};
/**
* 检查 Leaflet 是否已加载
*/
export const isLeafletLoaded = (): boolean => {
return leafletLoaded || !!window.L;
};
/**
* 获取 Leaflet 版本
*/
export const getLeafletVersion = (): string | null => {
if (isLeafletLoaded() && window.L) {
return window.L.version || null;
}
return null;
};
// 扩展 Window 接口
declare global {
interface Window {
L: any;
}
}