| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446 |
- import React, { useEffect, useRef } from 'react';
- import { useQuery, useQueryClient } from '@tanstack/react-query';
- import { getGlobalConfig } from './utils.ts';
- import type { GlobalConfig } from '../share/types.ts';
- import { Spin } from 'antd';
- import './style_amap.css';
- import { MapMode, MarkerData } from '../share/types.ts';
- // 在线地图配置
- export const AMAP_ONLINE_CONFIG = {
- // 高德地图 Web API 密钥
- API_KEY: getGlobalConfig('MAP_CONFIG')?.KEY || '',
- // 主JS文件路径
- MAIN_JS: 'https://webapi.amap.com/maps?v=2.0&key=' + (getGlobalConfig('MAP_CONFIG')?.KEY || ''),
- // 插件列表
- PLUGINS: ['AMap.MouseTool', 'AMap.RangingTool', 'AMap.Scale', 'AMap.ToolBar', 'AMap.MarkerCluster'],
- };
- export const AMAP_OFFLINE_CONFIG = {
- // 主JS文件路径
- MAIN_JS: '/amap/amap3.js?v=2.0',
- // 插件目录
- PLUGINS_PATH: '/amap/plugins',
- // 插件列表
- PLUGINS: ['AMap.MouseTool', 'AMap.RangingTool', 'AMap.Scale', 'AMap.ToolBar', 'AMap.MarkerCluster'],
- };
- // 离线瓦片配置
- export const TILE_CONFIG = {
- // 瓦片地图基础路径
- BASE_URL: '/amap/tiles',
- // 缩放级别范围
- ZOOMS: [3, 20] as [number, number],
- // 默认中心点
- DEFAULT_CENTER: [108.25910334, 27.94292459] as [number, number],
- // 默认缩放级别
- DEFAULT_ZOOM: 15
- } as const;
- // 地图控件配置
- export const MAP_CONTROLS = {
- scale: true,
- toolbar: true,
- mousePosition: true,
- } as const;
- export interface AMapProps {
- style?: React.CSSProperties;
- width?: string | number;
- height?: string | number;
- center?: [number, number];
- zoom?: number;
- mode?: MapMode;
- onMarkerClick?: (markerData: MarkerData) => void;
- onClick?: (lnglat: [number, number]) => void;
- markers?: MarkerData[];
- showCluster?: boolean;
- queryKey?: string;
- }
- export interface MapConfig {
- zoom: number;
- center: [number, number];
- zooms: [number, number];
- resizeEnable: boolean;
- rotateEnable: boolean;
- pitchEnable: boolean;
- defaultCursor: string;
- showLabel: boolean;
- layers?: any[];
- }
- export interface AMapInstance {
- map: any;
- setZoomAndCenter: (zoom: number, center: [number, number]) => void;
- setCenter: (center: [number, number]) => void;
- setZoom: (zoom: number) => void;
- destroy: () => void;
- clearMap: () => void;
- getAllOverlays: (type: string) => any[];
- on: (event: string, handler: Function) => void;
- }
- declare global {
- interface Window {
- AMap: any;
- CONFIG?: GlobalConfig;
- }
- }
- const loadScript = (url: string,plugins:string[]): Promise<void> => {
- return new Promise((resolve, reject) => {
- const script = document.createElement('script');
- script.type = 'text/javascript';
- script.src = url + (plugins.length > 0 ? `&plugin=${plugins.join(',')}` : '');
- script.onerror = (e) => reject(e);
- script.onload = () => resolve();
- document.head.appendChild(script);
- });
- };
- export const useAMapLoader = (mode: MapMode = MapMode.ONLINE) => {
- return useQuery({
- queryKey: ['amap-loader', mode],
- queryFn: async () => {
- if (typeof window === 'undefined') return null;
-
- if (!window.AMap) {
- const config = mode === MapMode.OFFLINE ? AMAP_OFFLINE_CONFIG : AMAP_ONLINE_CONFIG;
- await loadScript(config.MAIN_JS,config.PLUGINS);
- }
-
- return window.AMap;
- },
- staleTime: Infinity, // 地图脚本加载后永不过期
- gcTime: Infinity,
- retry: 2,
- });
- };
- export const useAMapClick = (
- map: any,
- onClick?: (lnglat: [number, number]) => void
- ) => {
- const mouseTool = useRef<any>(null);
- const clickHandlerRef = useRef<((e: any) => void) | null>(null);
- useEffect(() => {
- if (!map) return;
- // 清理旧的点击处理器
- if (clickHandlerRef.current) {
- map.off('click', clickHandlerRef.current);
- clickHandlerRef.current = null;
- }
- // 如果有点击回调,设置新的点击处理器
- if (onClick) {
- clickHandlerRef.current = (e: any) => {
- const lnglat = e.lnglat.getLng ?
- [e.lnglat.getLng(), e.lnglat.getLat()] as [number, number] :
- [e.lnglat.lng, e.lnglat.lat] as [number, number];
- onClick(lnglat);
- };
- map.on('click', clickHandlerRef.current);
- }
- return () => {
- if (clickHandlerRef.current) {
- map.off('click', clickHandlerRef.current);
- clickHandlerRef.current = null;
- }
- };
- }, [map, onClick]);
- return {
- mouseTool: mouseTool.current
- };
- };
- // 定义图标配置的类型
- interface MarkerIconConfig {
- size: [number, number];
- content: string;
- }
- // 默认图标配置
- const DEFAULT_MARKER_ICON: MarkerIconConfig = {
- size: [25, 34],
- content: `
- <svg width="25" height="34" viewBox="0 0 25 34" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M12.5 0C5.59644 0 0 5.59644 0 12.5C0 21.875 12.5 34 12.5 34C12.5 34 25 21.875 25 12.5C25 5.59644 19.4036 0 12.5 0ZM12.5 17C10.0147 17 8 14.9853 8 12.5C8 10.0147 10.0147 8 12.5 8C14.9853 8 17 10.0147 17 12.5C17 14.9853 14.9853 17 12.5 17Z" fill="#1890ff"/>
- </svg>
- `
- };
- interface UseAMapMarkersProps {
- map: any;
- markers: MarkerData[];
- showCluster?: boolean;
- onMarkerClick?: (markerData: MarkerData) => void;
- }
- export const useAMapMarkers = ({
- map,
- markers,
- showCluster = true,
- onMarkerClick,
- }: UseAMapMarkersProps) => {
- const clusterInstance = useRef<any>(null);
- const markersRef = useRef<any[]>([]);
- // 优化经纬度格式化函数
- const toFixedDigit = (num: number, n: number): string => {
- if (typeof num !== "number") return "";
- return Number(num).toFixed(n);
- };
- // 创建标记点
- const createMarker = (markerData: MarkerData) => {
- const { longitude, latitude, title, iconUrl } = markerData;
-
- // 创建标记点
- const marker = new window.AMap.Marker({
- position: [longitude, latitude],
- title: title,
- icon: iconUrl ? new window.AMap.Icon({
- size: DEFAULT_MARKER_ICON.size,
- imageSize: DEFAULT_MARKER_ICON.size,
- image: iconUrl
- }) : new window.AMap.Icon({
- size: DEFAULT_MARKER_ICON.size,
- imageSize: DEFAULT_MARKER_ICON.size,
- image: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(DEFAULT_MARKER_ICON.content)}`
- }),
- label: title ? {
- content: title,
- direction: 'top'
- } : undefined
- });
- // 添加点击事件
- if (onMarkerClick) {
- marker.on('click', () => onMarkerClick(markerData));
- }
- return marker;
- };
- // 处理聚合点
- const handleCluster = () => {
- if (!map || !markers.length) return;
- const points = markers.map(item => ({
- weight: 1,
- lnglat: [
- toFixedDigit(item.longitude, 5),
- toFixedDigit(item.latitude, 5)
- ],
- ...item
- }));
- if (clusterInstance.current) {
- clusterInstance.current.setData(points);
- return;
- }
- if(window.AMap?.MarkerCluster){
- clusterInstance.current = new window.AMap.MarkerCluster(map, points, {
- gridSize: 60,
- renderMarker: (context: { marker: any; data: MarkerData[] }) => {
- const { marker, data } = context;
- const firstPoint = data[0];
-
- if (firstPoint.iconUrl) {
- marker.setContent(`<img src="${firstPoint.iconUrl}" style="width:${DEFAULT_MARKER_ICON.size[0]}px;height:${DEFAULT_MARKER_ICON.size[1]}px;">`);
- } else {
- marker.setContent(DEFAULT_MARKER_ICON.content);
- }
- marker.setAnchor('bottom-center');
- marker.setOffset(new window.AMap.Pixel(0, 0));
- if (firstPoint.title) {
- marker.setLabel({
- direction: 'top',
- offset: new window.AMap.Pixel(0, -5),
- content: firstPoint.title
- });
- }
-
- marker.on('click', () => onMarkerClick?.(firstPoint));
- }
- });
- }
- // 优化聚合点点击逻辑
- if(clusterInstance.current){
- clusterInstance.current.on('click', (item: any) => {
- if (item.clusterData.length <= 1) return;
- const center = item.clusterData.reduce(
- (acc: number[], curr: any) => [
- acc[0] + Number(curr.lnglat[0]),
- acc[1] + Number(curr.lnglat[1])
- ],
- [0, 0]
- ).map((coord: number) => coord / item.clusterData.length);
- map.setZoomAndCenter(map.getZoom() + 2, center);
- });
- }
- };
- // 处理普通标记点
- const handleMarkers = () => {
- if (!map || !markers.length) return;
-
- // 清除旧的标记点
- markersRef.current.forEach(marker => marker.setMap(null));
- markersRef.current = [];
- // 添加新的标记点
- markersRef.current = markers.map(markerData => {
- const marker = createMarker(markerData);
- marker.setMap(map);
- return marker;
- });
- };
- useEffect(() => {
- if (!map || !Array.isArray(markers)) return;
- // 清理旧的标记点和聚合点
- if (clusterInstance.current) {
- clusterInstance.current.setMap(null);
- clusterInstance.current = null;
- }
- markersRef.current.forEach(marker => marker.setMap(null));
- markersRef.current = [];
- // 根据配置添加新的标记点
- if (markers.length > 0) {
- if (showCluster) {
- handleCluster();
- } else {
- handleMarkers();
- }
- }
- return () => {
- if (clusterInstance.current) {
- clusterInstance.current.setMap(null);
- clusterInstance.current = null;
- }
- markersRef.current.forEach(marker => marker.setMap(null));
- markersRef.current = [];
- };
- }, [map, markers, showCluster]);
- };
- const AMapComponent: React.FC<AMapProps> = ({
- width = '100%',
- height = '400px',
- center = TILE_CONFIG.DEFAULT_CENTER as [number, number],
- zoom = TILE_CONFIG.DEFAULT_ZOOM,
- mode = (getGlobalConfig('MAP_CONFIG')?.MAP_MODE as MapMode) || MapMode.ONLINE,
- onMarkerClick,
- onClick,
- markers = [],
- showCluster = true,
- queryKey = 'amap-instance',
- }) => {
- const mapContainer = useRef<HTMLDivElement>(null);
- const mapInstance = useRef<AMapInstance | null>(null);
- const queryClient = useQueryClient();
- // 加载地图脚本
- const { data: AMap, isLoading: isLoadingScript } = useAMapLoader(mode);
- // 初始化地图实例
- const { data: map } = useQuery<AMapInstance>({
- queryKey: [ queryKey ],
- queryFn: async () => {
- if (!AMap || !mapContainer.current) return null;
- const config: MapConfig = {
- zoom,
- center,
- zooms: [3, 20],
- resizeEnable: true,
- rotateEnable: false,
- pitchEnable: false,
- defaultCursor: 'pointer',
- showLabel: true,
- };
- if (mode === 'offline') {
- config.layers = [
- new AMap.TileLayer({
- getTileUrl: (x: number, y: number, z: number) =>
- `${TILE_CONFIG.BASE_URL}/${z}/${x}/${y}.png`,
- zIndex: 100,
- })
- ];
- }
- const newMap = new AMap.Map(mapContainer.current, config);
- mapInstance.current = newMap;
- return newMap;
- },
- enabled: !!AMap && !!mapContainer.current && !isLoadingScript,
- gcTime: Infinity,
- staleTime: Infinity,
- });
- // 处理标记点
- useAMapMarkers({
- map,
- markers,
- showCluster,
- onMarkerClick,
- });
- // 处理点击事件
- useAMapClick(map, onClick);
- // 更新地图视图
- useEffect(() => {
- if (!map) return;
-
- if (center && zoom) {
- map.setZoomAndCenter(zoom, center);
- } else if (center) {
- map.setCenter(center);
- } else if (zoom) {
- map.setZoom(zoom);
- }
- }, [map, center, zoom]);
- // 清理地图实例和查询缓存
- useEffect(() => {
- return () => {
- if (mapInstance.current) {
- mapInstance.current.destroy();
- mapInstance.current = null;
- // 清理 React Query 缓存
- queryClient.removeQueries({ queryKey: [ queryKey ] });
- }
- };
- }, [queryClient]);
- return (
- <div
- ref={mapContainer}
- style={{
- width,
- height,
- position: 'relative',
- }}
- >
- {isLoadingScript && <div className="w-full h-full flex justify-center items-center"><Spin /></div>}
- </div>
- );
- };
- export default AMapComponent;
|