feat(map-plugin): support google map (#2027)
* feat(map-plugin): support google map * fix: should render current position * feat: support google map in field * feat: support google map block * feat: support connect data * fix: update * feat: support autocomplete instated of search * feat: support language * fix: drawing not work * feat: support same labe lstyle * feat: use better configuration * fix: avoid rerender * chore: update deps * feat: add tips for lineString, Close T-591 * fix: zoom to big * feat: improve api key incorrect flow * fix: remove unreleated file * feat: improve setFitView * feat: update tips * feat: improve code * fix: avoid crash, Close T-713 --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
		
							parent
							
								
									717b21d37a
								
							
						
					
					
						commit
						4bf722326c
					
				@ -9,11 +9,22 @@
 | 
			
		||||
  "main": "./lib/index.js",
 | 
			
		||||
  "types": "./lib/index.d.ts",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@nocobase/server": "0.10.0-alpha.5",
 | 
			
		||||
    "@nocobase/test": "0.10.0-alpha.5"
 | 
			
		||||
    "@types/google.maps": "^3.53.4",
 | 
			
		||||
    "@types/react": "^17.0.0",
 | 
			
		||||
    "@types/react-dom": "^17.0.0"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@amap/amap-jsapi-loader": "^1.0.1",
 | 
			
		||||
    "@amap/amap-jsapi-types": "^0.0.10"
 | 
			
		||||
    "@amap/amap-jsapi-types": "^0.0.10",
 | 
			
		||||
    "@googlemaps/js-api-loader": "^1.16.1",
 | 
			
		||||
    "@emotion/css": "^11.7.1",
 | 
			
		||||
    "@formily/react": "2.2.26",
 | 
			
		||||
    "@formily/shared": "2.2.26",
 | 
			
		||||
    "@formily/antd": "2.2.26",
 | 
			
		||||
    "@nocobase/server": "0.10.0-alpha.5",
 | 
			
		||||
    "@nocobase/test": "0.10.0-alpha.5",
 | 
			
		||||
    "react": "18.x",
 | 
			
		||||
    "react-dom": "18.x",
 | 
			
		||||
    "antd": "^4.24.8"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,315 +1,11 @@
 | 
			
		||||
import { CheckOutlined, EnvironmentOutlined, ExpandOutlined } from '@ant-design/icons';
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
import { RecursionField, Schema, useFieldSchema } from '@formily/react';
 | 
			
		||||
import {
 | 
			
		||||
  ActionContextProvider,
 | 
			
		||||
  RecordProvider,
 | 
			
		||||
  useCollection,
 | 
			
		||||
  useCompile,
 | 
			
		||||
  useFilterAPI,
 | 
			
		||||
  useProps,
 | 
			
		||||
} from '@nocobase/client';
 | 
			
		||||
import { useMemoizedFn } from 'ahooks';
 | 
			
		||||
import { Button, Space } from 'antd';
 | 
			
		||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
 | 
			
		||||
import AMap, { AMapForwardedRefProps } from '../components/AMap';
 | 
			
		||||
import { useMapTranslation } from '../locale';
 | 
			
		||||
 | 
			
		||||
const selectedImage =
 | 
			
		||||
  'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAA/CAMAAAC7OkrPAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAEJQTFRFAAAA8Yti8Yti8Itj8Iti74tj8Itj8YtiKwADKwADKhw5Kh07KwADKwADKwADKwAD4odn1YBlKwADKwADKwADKwAD/5y7LQAAABZ0Uk5TAP/8/f/B/PYOHjY3CCozLP3XCSkvMhA05K4AAAC4SURBVHic7dXLDoIwEIXhwSsoLSrw/q8qbSAiMy3/oivj2czmS5omnVORdapVJJFKBSEL2mrrUurbpdXa5dTH5dXi9tTsGNtX0TFG1OT+7PdY2YdEGd0FuFmUwXWm5UAZbCTKYA3SUrXZQTHLHbUy2OlsMO0ultLsajLlbEWZMEUZ+ig5E6YKM2GqMBOmGKubW1D3ps6g1nnvA5uGa5Os85E9nmF2ebYkzeKhc9wre4V+GMeh317hDfXgCWigIGJbAAAAAElFTkSuQmCC';
 | 
			
		||||
const defaultImage =
 | 
			
		||||
  'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAA/CAYAAACM5Lr9AAAFkklEQVR42s3VaWxUVRjG8ddiEKPRxAQTCVSWLnZaWmjpTgulpYUBExP9Agou4IKAogUKAgItlNJCgUIXINFPfsFoDCjibkQRI0QIICAQWUpZugKtpcv0+DxyYqaTe2/vTLf58Etu7znnPf+0k44opQw5i5qsjIFs2APHoBZatFq+02vZeq94yyKs0dOjsAzOTCtuVtNKWtX0UpeaXqHU07tgt4ZnvuMa93Avz/CsniE2WIQV3iEaCDnQ8F/MTgZ4h2d4ljNgmZ4pFszDpmIRIuFP57YWDMcFu7uHMziLMyEKxJBl2MbbM6DZub2NQ3sUZ3K2vkMMGIdNKbi1ADqcZe1q2i7VKzibd8BCEE8GUQ3PQ4ez3KWcGNCbeAfv0neKm85hWRvqo+Hu1LJ2HuwTvAt3tkAciKbDIDO/7iE4l7X9rpqCA30pa3uLwt3nYRAIuIfV5mcWN6msnapf8G405IGQMGry+prB0DiloqPfwng3G3SLDltXnTt5S6PK3NnRr9jAFhDJyLsZAJcz8SGcjOr+xAa0XGGTpOfeSMrYUKcyKlx+gS1oSkbY9VXpm2+rjHKXX2ALm2TS2qq96duaVHp5u19gC5skbc3V05N23FWT8NIfsIVNkra6siGttE2llbX7h9JWhaZ6mfj+lfaJZW3Kn6DJJRNWXWqbgN+Y39jRqtDUKKkrL15PxfdjammrfyhpVmiqlNQVfx9KwX/cFJT6A7awSVLeu1CeUlinxu9o8QsphbUKTWUyfvn5Wcm511QSXvoDtrBJkpedewzaE/E58wdsYZMopSQ55+z+pKK6fo9KKqpXaDkAIgxLWnomK3H1JZVQ0tyv2ICW6XAvLHHJ6QD4K6GgRsVjQ3/g3WzQLffCKGHJqfHgisOXaNy2f/oU7+TdukGIUW5xJ0vic6+oWGzsS7yTd4NoncPiF594GC7FFlSrcVub+gTv4p36btE6h1Fc9nFn7IpzKmZrY5/gXbwTxI0O8xD77rGPYtZeVtE42Jt4B+8C8WAcNu6dPwZDzVh8PYzdcqdXcDbvgMdBPDHEOG7R0Xkxqy6oKAzpDZzNO0AMmIfFvH1kABwZk1epIotv9yjOxOyj+g4xYB4W/dbvlAAdozGsJ3Gmni0mzMPGLvyN6IMo/NojNt/qEZyFmR+CWLAIW3CYaDDUR+D/TTgGdwdncJaeKRbMw8bM/9XdosjlZ5VjU0O3cAZngXTBPCzqzV/cPQAXHeuqVBgu8IVj/TXFGXqWdME8LHLez55mRSw9qUI31fuEZzFjNogNFmFvHPQUACfC8IUbUlTvlbDcSsWzeobYYB42+vWfjDwbnn3c6zCewdnnQGwyD4t47UcjAXA2JO+qCi6ss4V7eUafFZuswn4wM8ex+LgKwqV2cC/OzAXxgnlY+KvfmxkIVaPyr6tRhbWWuId79RnxgnmYY+53VlaHLj2lRm6stRSSc0ph7xoQL1mEzfnWyhPQNrKgGgE1hrjGPTAExEvmYWGvfNOVPUErz6vhiDDCNez5GMQHVmFfd2Vq6MLD6smCGkNc4x4QH5iHPfXyV125H64NX1eFkOpO+I5reo/4wDws9KUDdhSPxFdNIGLc8R3XQHxkFfalHYnB8w6qYRuqO+E7roH4yDws5MX9dtwHlYF5lQi6qYjPfKfXxEcWYbO/sKtsBP50QxFFI/D/De8qQLrBPCwYizY9M2r+of/D+Mx3ID6zDJv1uaVwGfAIBDqGJsbhZ9eQ/BuK+OwYlhzPNe7hXh9Yhe0zwqBB4IAYouCZn1wIXH5GEZ/d1sjBMzzrBfOwoBf2GWFYBMS4C03JWRA087OaoBmf3uCz5zrP8Kx9lmF7jXiE2RbBs95ghB0MIo8/pS0OeJAzvOBLmKY//BAGURBN+jmMa3qPkLdh/wKOL8SpLbnYFgAAAABJRU5ErkJggg==';
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import { MapBlockComponent } from '../components';
 | 
			
		||||
import { useCollection, useProps } from '@nocobase/client';
 | 
			
		||||
 | 
			
		||||
export const MapBlock = (props) => {
 | 
			
		||||
  const { fieldNames, dataSource = [], fixedBlock, zoom, setSelectedRecordKeys } = useProps(props);
 | 
			
		||||
  const { getField, getPrimaryKey } = useCollection();
 | 
			
		||||
  const { fieldNames } = useProps(props);
 | 
			
		||||
  const { getField } = useCollection();
 | 
			
		||||
  const field = getField(fieldNames?.field);
 | 
			
		||||
  const [isMapInitialization, setIsMapInitialization] = useState(false);
 | 
			
		||||
  const mapRef = useRef<AMapForwardedRefProps>();
 | 
			
		||||
  const geometryUtils: AMap.IGeometryUtil = mapRef.current?.aMap?.GeometryUtil;
 | 
			
		||||
  const [record, setRecord] = useState();
 | 
			
		||||
  const [selectingMode, setSelecting] = useState('');
 | 
			
		||||
  const { t } = useMapTranslation();
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
  const { isConnected, doFilter } = useFilterAPI();
 | 
			
		||||
  const [, setPrevSelected] = useState(null);
 | 
			
		||||
  const selectingModeRef = useRef(selectingMode);
 | 
			
		||||
  selectingModeRef.current = selectingMode;
 | 
			
		||||
 | 
			
		||||
  const setOverlayOptions = (overlay: AMap.Polygon | AMap.Marker, state?: boolean) => {
 | 
			
		||||
    const extData = overlay.getExtData();
 | 
			
		||||
    const selected = typeof state === 'undefined' ? extData.selected : !state;
 | 
			
		||||
    extData.selected = !selected;
 | 
			
		||||
    if ('setIcon' in overlay) {
 | 
			
		||||
      overlay.setIcon(
 | 
			
		||||
        new mapRef.current.aMap.Icon({
 | 
			
		||||
          imageSize: [19, 32],
 | 
			
		||||
          image: selected ? defaultImage : selectedImage,
 | 
			
		||||
        } as AMap.IconOpts),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    (overlay as AMap.Polygon).setOptions({
 | 
			
		||||
      extData,
 | 
			
		||||
      ...(selected
 | 
			
		||||
        ? { strokeColor: '#4e9bff', fillColor: '#4e9bff' }
 | 
			
		||||
        : { strokeColor: '#F18b62', fillColor: '#F18b62' }),
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const removeSelection = () => {
 | 
			
		||||
    mapRef.current.mouseTool().close(true);
 | 
			
		||||
    mapRef.current.editor().setTarget(null);
 | 
			
		||||
    mapRef.current.editor().close();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // selection
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (selectingMode !== 'selection') {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!mapRef.current.editor()) {
 | 
			
		||||
      mapRef.current.createEditor('polygon');
 | 
			
		||||
      mapRef.current.createMouseTool('polygon');
 | 
			
		||||
    } else {
 | 
			
		||||
      mapRef.current.executeMouseTool('polygon');
 | 
			
		||||
    }
 | 
			
		||||
    return () => {
 | 
			
		||||
      removeSelection();
 | 
			
		||||
    };
 | 
			
		||||
  }, [selectingMode]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (selectingMode) {
 | 
			
		||||
      return () => {
 | 
			
		||||
        if (!selectingModeRef.current) {
 | 
			
		||||
          mapRef.current.map.getAllOverlays().forEach((o) => {
 | 
			
		||||
            setOverlayOptions(o, false);
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }, [selectingMode]);
 | 
			
		||||
 | 
			
		||||
  const onSelectingComplete = useMemoizedFn(() => {
 | 
			
		||||
    const selectingOverlay = mapRef.current.editor().getTarget();
 | 
			
		||||
    const overlays = mapRef.current.map.getAllOverlays();
 | 
			
		||||
    const selectedOverlays = overlays.filter((o) => {
 | 
			
		||||
      if (o === selectingOverlay || o.getExtData().id === undefined) return;
 | 
			
		||||
      if ('getPosition' in o) {
 | 
			
		||||
        return geometryUtils.isPointInRing(o.getPosition(), selectingOverlay.getPath() as any);
 | 
			
		||||
      }
 | 
			
		||||
      return geometryUtils.doesRingRingIntersect(o.getPath(), selectingOverlay.getPath() as any);
 | 
			
		||||
    });
 | 
			
		||||
    const ids = selectedOverlays.map((o) => {
 | 
			
		||||
      setOverlayOptions(o, true);
 | 
			
		||||
      return o.getExtData().id;
 | 
			
		||||
    });
 | 
			
		||||
    setSelectedRecordKeys((lastIds) => ids.concat(lastIds));
 | 
			
		||||
    selectingOverlay.remove();
 | 
			
		||||
    mapRef.current.editor().close();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!field || !mapRef.current) return;
 | 
			
		||||
    const overlays = dataSource
 | 
			
		||||
      .map((item) => {
 | 
			
		||||
        const data = item[fieldNames?.field];
 | 
			
		||||
        if (!data) return;
 | 
			
		||||
        const overlay = mapRef.current.setOverlay(field.type, data, {
 | 
			
		||||
          strokeColor: '#4e9bff',
 | 
			
		||||
          fillColor: '#4e9bff',
 | 
			
		||||
          cursor: 'pointer',
 | 
			
		||||
          label: {
 | 
			
		||||
            direction: 'bottom',
 | 
			
		||||
            offset: [0, 5],
 | 
			
		||||
            content: fieldNames?.marker ? compile(item[fieldNames.marker]) : undefined,
 | 
			
		||||
          },
 | 
			
		||||
          extData: {
 | 
			
		||||
            id: item[getPrimaryKey()],
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
        return overlay;
 | 
			
		||||
      })
 | 
			
		||||
      .filter(Boolean);
 | 
			
		||||
    mapRef.current.map?.setFitView(overlays);
 | 
			
		||||
 | 
			
		||||
    const events = overlays.map((o: AMap.Marker) => {
 | 
			
		||||
      const onClick = (e) => {
 | 
			
		||||
        const overlay: AMap.Polygon | AMap.Marker = e.target;
 | 
			
		||||
        const extData = overlay.getExtData();
 | 
			
		||||
        if (!extData) return;
 | 
			
		||||
        if (selectingModeRef.current) {
 | 
			
		||||
          if (selectingModeRef.current === 'click') {
 | 
			
		||||
            setSelectedRecordKeys((keys) =>
 | 
			
		||||
              extData.selected ? keys.filter((key) => key !== extData.id) : [...keys, extData.id],
 | 
			
		||||
            );
 | 
			
		||||
            setOverlayOptions(overlay);
 | 
			
		||||
          }
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        const data = dataSource?.find((item) => {
 | 
			
		||||
          return extData.id === item[getPrimaryKey()];
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 筛选区块模式
 | 
			
		||||
        if (isConnected) {
 | 
			
		||||
          setPrevSelected((prev) => {
 | 
			
		||||
            prev && clearSelected(prev);
 | 
			
		||||
            if (prev === o) {
 | 
			
		||||
              clearSelected(o);
 | 
			
		||||
 | 
			
		||||
              // 删除过滤参数
 | 
			
		||||
              doFilter(null);
 | 
			
		||||
              return null;
 | 
			
		||||
            } else {
 | 
			
		||||
              selectMarker(o);
 | 
			
		||||
              doFilter(data[getPrimaryKey()], (target) => target.field || getPrimaryKey(), '$eq');
 | 
			
		||||
            }
 | 
			
		||||
            return o;
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (data) {
 | 
			
		||||
          setRecord(data);
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      o.on('click', onClick);
 | 
			
		||||
      return () => o.off('click', onClick);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      overlays.forEach((ov) => {
 | 
			
		||||
        ov.remove();
 | 
			
		||||
      });
 | 
			
		||||
      events.forEach((e) => e());
 | 
			
		||||
    };
 | 
			
		||||
  }, [dataSource, isMapInitialization, fieldNames, field.type, isConnected]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      setSelectedRecordKeys([]);
 | 
			
		||||
    });
 | 
			
		||||
  }, [dataSource]);
 | 
			
		||||
 | 
			
		||||
  const mapRefCallback = (instance: AMapForwardedRefProps) => {
 | 
			
		||||
    mapRef.current = instance;
 | 
			
		||||
    setIsMapInitialization(!!instance?.aMap);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={css`
 | 
			
		||||
        position: relative;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
      `}
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        className={css`
 | 
			
		||||
          position: absolute;
 | 
			
		||||
          left: 10px;
 | 
			
		||||
          top: 10px;
 | 
			
		||||
          z-index: 999;
 | 
			
		||||
        `}
 | 
			
		||||
      >
 | 
			
		||||
        <Space direction="vertical">
 | 
			
		||||
          <Button
 | 
			
		||||
            style={{
 | 
			
		||||
              color: !selectingMode ? '#F18b62' : undefined,
 | 
			
		||||
              borderColor: 'currentcolor',
 | 
			
		||||
            }}
 | 
			
		||||
            onClick={(e) => {
 | 
			
		||||
              e.stopPropagation();
 | 
			
		||||
              setSelecting('');
 | 
			
		||||
            }}
 | 
			
		||||
            icon={<EnvironmentOutlined />}
 | 
			
		||||
          ></Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            style={{
 | 
			
		||||
              color: selectingMode === 'selection' ? '#F18b62' : undefined,
 | 
			
		||||
              borderColor: 'currentcolor',
 | 
			
		||||
            }}
 | 
			
		||||
            onClick={(e) => {
 | 
			
		||||
              e.stopPropagation();
 | 
			
		||||
              setSelecting('selection');
 | 
			
		||||
            }}
 | 
			
		||||
            icon={<ExpandOutlined />}
 | 
			
		||||
          ></Button>
 | 
			
		||||
          {selectingMode === 'selection' ? (
 | 
			
		||||
            <Button
 | 
			
		||||
              type="primary"
 | 
			
		||||
              icon={<CheckOutlined />}
 | 
			
		||||
              title={t('Confirm selection')}
 | 
			
		||||
              onClick={onSelectingComplete}
 | 
			
		||||
            ></Button>
 | 
			
		||||
          ) : null}
 | 
			
		||||
        </Space>
 | 
			
		||||
      </div>
 | 
			
		||||
      <MapBlockDrawer record={record} setVisible={setRecord} />
 | 
			
		||||
      <AMap
 | 
			
		||||
        {...field?.uiSchema?.['x-component-props']}
 | 
			
		||||
        ref={mapRefCallback}
 | 
			
		||||
        style={{ height: fixedBlock ? '100%' : null }}
 | 
			
		||||
        zoom={zoom}
 | 
			
		||||
        disabled
 | 
			
		||||
        block
 | 
			
		||||
        overlayCommonOptions={{
 | 
			
		||||
          strokeColor: '#F18b62',
 | 
			
		||||
          fillColor: '#F18b62',
 | 
			
		||||
        }}
 | 
			
		||||
      ></AMap>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
  const fieldComponentProps = field?.uiSchema?.['x-component-props'];
 | 
			
		||||
  return <MapBlockComponent {...fieldComponentProps} {...props} />;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const MapBlockDrawer = (props) => {
 | 
			
		||||
  const { setVisible, record } = props;
 | 
			
		||||
  const fieldSchema = useFieldSchema();
 | 
			
		||||
  const schema: Schema = useMemo(
 | 
			
		||||
    () =>
 | 
			
		||||
      fieldSchema.reduceProperties((buf, current) => {
 | 
			
		||||
        if (current.name === 'drawer') {
 | 
			
		||||
          return current;
 | 
			
		||||
        }
 | 
			
		||||
        return buf;
 | 
			
		||||
      }, null),
 | 
			
		||||
    [fieldSchema],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    schema && (
 | 
			
		||||
      <ActionContextProvider value={{ visible: !!record, setVisible }}>
 | 
			
		||||
        <RecordProvider record={record}>
 | 
			
		||||
          <RecursionField schema={schema} name={schema.name} />
 | 
			
		||||
        </RecordProvider>
 | 
			
		||||
      </ActionContextProvider>
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function clearSelected(marker: AMap.Marker | AMap.Polygon | AMap.Polyline | AMap.Circle) {
 | 
			
		||||
  if ((marker as AMap.Marker).dom) {
 | 
			
		||||
    (marker as AMap.Marker).dom.style.filter = 'none';
 | 
			
		||||
 | 
			
		||||
    // AMap.Polygon | AMap.Polyline | AMap.Circle 都有 setOptions 方法
 | 
			
		||||
  } else if ((marker as AMap.Polygon).setOptions) {
 | 
			
		||||
    (marker as AMap.Polygon).setOptions({
 | 
			
		||||
      strokeColor: '#4e9bff',
 | 
			
		||||
      fillColor: '#4e9bff',
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function selectMarker(marker: AMap.Marker | AMap.Polygon | AMap.Polyline | AMap.Circle) {
 | 
			
		||||
  if ((marker as AMap.Marker).dom) {
 | 
			
		||||
    (marker as AMap.Marker).dom.style.filter = 'brightness(1.2) contrast(1.2) hue-rotate(180deg)';
 | 
			
		||||
 | 
			
		||||
    // AMap.Polygon | AMap.Polyline | AMap.Circle 都有 setOptions 方法
 | 
			
		||||
  } else if ((marker as AMap.Polygon).setOptions) {
 | 
			
		||||
    (marker as AMap.Polygon).setOptions({
 | 
			
		||||
      strokeColor: '#F18b62',
 | 
			
		||||
      fillColor: '#F18b62',
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { TableOutlined } from '@ant-design/icons';
 | 
			
		||||
import { DataBlockInitializer, SchemaComponent, SchemaComponentOptions, useCollectionManager } from '@nocobase/client';
 | 
			
		||||
import { SchemaOptionsContext } from '@formily/react';
 | 
			
		||||
import { FormDialog, FormLayout } from '@formily/antd';
 | 
			
		||||
import { SchemaOptionsContext } from '@formily/react';
 | 
			
		||||
import { DataBlockInitializer, SchemaComponent, SchemaComponentOptions, useCollectionManager } from '@nocobase/client';
 | 
			
		||||
import React, { useContext } from 'react';
 | 
			
		||||
import { useMapTranslation } from '../locale';
 | 
			
		||||
import { createMapBlockSchema } from './utils';
 | 
			
		||||
@ -9,6 +9,7 @@ import { createMapBlockSchema } from './utils';
 | 
			
		||||
export const MapBlockInitializer = (props) => {
 | 
			
		||||
  const { insert } = props;
 | 
			
		||||
  const options = useContext(SchemaOptionsContext);
 | 
			
		||||
  console.log('🚀 ~ file: MapBlockInitializer.tsx:12 ~ MapBlockInitializer ~ options:', options);
 | 
			
		||||
  const { getCollectionFieldsOptions } = useCollectionManager();
 | 
			
		||||
  const { t } = useMapTranslation();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										314
									
								
								packages/plugins/map/src/client/components/AMap/Block.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										314
									
								
								packages/plugins/map/src/client/components/AMap/Block.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,314 @@
 | 
			
		||||
import { CheckOutlined, EnvironmentOutlined, ExpandOutlined } from '@ant-design/icons';
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
import { RecursionField, Schema, useFieldSchema } from '@formily/react';
 | 
			
		||||
import {
 | 
			
		||||
  ActionContextProvider,
 | 
			
		||||
  RecordProvider,
 | 
			
		||||
  useCollection,
 | 
			
		||||
  useCompile,
 | 
			
		||||
  useFilterAPI,
 | 
			
		||||
  useProps,
 | 
			
		||||
} from '@nocobase/client';
 | 
			
		||||
import { useMemoizedFn } from 'ahooks';
 | 
			
		||||
import { Button, Space } from 'antd';
 | 
			
		||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
 | 
			
		||||
import { defaultImage, selectedImage } from '../../constants';
 | 
			
		||||
import { useMapTranslation } from '../../locale';
 | 
			
		||||
import { AMapComponent, AMapForwardedRefProps } from './Map';
 | 
			
		||||
 | 
			
		||||
export const AMapBlock = (props) => {
 | 
			
		||||
  const { fieldNames, dataSource = [], fixedBlock, zoom, setSelectedRecordKeys } = useProps(props);
 | 
			
		||||
  const { getField, getPrimaryKey } = useCollection();
 | 
			
		||||
  const field = getField(fieldNames?.field);
 | 
			
		||||
  const [isMapInitialization, setIsMapInitialization] = useState(false);
 | 
			
		||||
  const mapRef = useRef<AMapForwardedRefProps>();
 | 
			
		||||
  const geometryUtils: AMap.IGeometryUtil = mapRef.current?.aMap?.GeometryUtil;
 | 
			
		||||
  const [record, setRecord] = useState();
 | 
			
		||||
  const [selectingMode, setSelecting] = useState('');
 | 
			
		||||
  const { t } = useMapTranslation();
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
  const { isConnected, doFilter } = useFilterAPI();
 | 
			
		||||
  const [, setPrevSelected] = useState(null);
 | 
			
		||||
  const selectingModeRef = useRef(selectingMode);
 | 
			
		||||
  selectingModeRef.current = selectingMode;
 | 
			
		||||
 | 
			
		||||
  const setOverlayOptions = (overlay: AMap.Polygon | AMap.Marker, state?: boolean) => {
 | 
			
		||||
    const extData = overlay.getExtData();
 | 
			
		||||
    const selected = typeof state === 'undefined' ? extData.selected : !state;
 | 
			
		||||
    extData.selected = !selected;
 | 
			
		||||
    if ('setIcon' in overlay) {
 | 
			
		||||
      overlay.setIcon(
 | 
			
		||||
        new mapRef.current.aMap.Icon({
 | 
			
		||||
          imageSize: [19, 32],
 | 
			
		||||
          image: selected ? defaultImage : selectedImage,
 | 
			
		||||
        } as AMap.IconOpts),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    (overlay as AMap.Polygon).setOptions({
 | 
			
		||||
      extData,
 | 
			
		||||
      ...(selected
 | 
			
		||||
        ? { strokeColor: '#4e9bff', fillColor: '#4e9bff' }
 | 
			
		||||
        : { strokeColor: '#F18b62', fillColor: '#F18b62' }),
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const removeSelection = () => {
 | 
			
		||||
    if (!mapRef.current) return;
 | 
			
		||||
    mapRef.current.mouseTool().close(true);
 | 
			
		||||
    mapRef.current.editor().setTarget(null);
 | 
			
		||||
    mapRef.current.editor().close();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // selection
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (selectingMode !== 'selection') {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!mapRef.current.editor()) {
 | 
			
		||||
      mapRef.current.createEditor('polygon');
 | 
			
		||||
      mapRef.current.createMouseTool('polygon');
 | 
			
		||||
    } else {
 | 
			
		||||
      mapRef.current.executeMouseTool('polygon');
 | 
			
		||||
    }
 | 
			
		||||
    return () => {
 | 
			
		||||
      removeSelection();
 | 
			
		||||
    };
 | 
			
		||||
  }, [selectingMode]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (selectingMode) {
 | 
			
		||||
      return () => {
 | 
			
		||||
        if (!selectingModeRef.current) {
 | 
			
		||||
          mapRef.current.map.getAllOverlays().forEach((o) => {
 | 
			
		||||
            setOverlayOptions(o, false);
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }, [selectingMode]);
 | 
			
		||||
 | 
			
		||||
  const onSelectingComplete = useMemoizedFn(() => {
 | 
			
		||||
    const selectingOverlay = mapRef.current.editor().getTarget();
 | 
			
		||||
    const overlays = mapRef.current.map.getAllOverlays();
 | 
			
		||||
    const selectedOverlays = overlays.filter((o) => {
 | 
			
		||||
      if (o === selectingOverlay || o.getExtData().id === undefined) return;
 | 
			
		||||
      if ('getPosition' in o) {
 | 
			
		||||
        return geometryUtils.isPointInRing(o.getPosition(), selectingOverlay.getPath() as any);
 | 
			
		||||
      }
 | 
			
		||||
      return geometryUtils.doesRingRingIntersect(o.getPath(), selectingOverlay.getPath() as any);
 | 
			
		||||
    });
 | 
			
		||||
    const ids = selectedOverlays.map((o) => {
 | 
			
		||||
      setOverlayOptions(o, true);
 | 
			
		||||
      return o.getExtData().id;
 | 
			
		||||
    });
 | 
			
		||||
    setSelectedRecordKeys((lastIds) => ids.concat(lastIds));
 | 
			
		||||
    selectingOverlay.remove();
 | 
			
		||||
    mapRef.current.editor().close();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!field || !mapRef.current) return;
 | 
			
		||||
    const overlays = dataSource
 | 
			
		||||
      .map((item) => {
 | 
			
		||||
        const data = item[fieldNames?.field];
 | 
			
		||||
        if (!data) return;
 | 
			
		||||
        const overlay = mapRef.current.setOverlay(field.type, data, {
 | 
			
		||||
          strokeColor: '#4e9bff',
 | 
			
		||||
          fillColor: '#4e9bff',
 | 
			
		||||
          cursor: 'pointer',
 | 
			
		||||
          label: {
 | 
			
		||||
            direction: 'bottom',
 | 
			
		||||
            offset: [0, 5],
 | 
			
		||||
            content: fieldNames?.marker ? compile(item[fieldNames.marker]) : undefined,
 | 
			
		||||
          },
 | 
			
		||||
          extData: {
 | 
			
		||||
            id: item[getPrimaryKey()],
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
        return overlay;
 | 
			
		||||
      })
 | 
			
		||||
      .filter(Boolean);
 | 
			
		||||
    mapRef.current.map?.setFitView(overlays);
 | 
			
		||||
 | 
			
		||||
    const events = overlays.map((o: AMap.Marker) => {
 | 
			
		||||
      const onClick = (e) => {
 | 
			
		||||
        const overlay: AMap.Polygon | AMap.Marker = e.target;
 | 
			
		||||
        const extData = overlay.getExtData();
 | 
			
		||||
        if (!extData) return;
 | 
			
		||||
        if (selectingModeRef.current) {
 | 
			
		||||
          if (selectingModeRef.current === 'click') {
 | 
			
		||||
            setSelectedRecordKeys((keys) =>
 | 
			
		||||
              extData.selected ? keys.filter((key) => key !== extData.id) : [...keys, extData.id],
 | 
			
		||||
            );
 | 
			
		||||
            setOverlayOptions(overlay);
 | 
			
		||||
          }
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        const data = dataSource?.find((item) => {
 | 
			
		||||
          return extData.id === item[getPrimaryKey()];
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 筛选区块模式
 | 
			
		||||
        if (isConnected) {
 | 
			
		||||
          setPrevSelected((prev) => {
 | 
			
		||||
            prev && clearSelected(prev);
 | 
			
		||||
            if (prev === o) {
 | 
			
		||||
              clearSelected(o);
 | 
			
		||||
 | 
			
		||||
              // 删除过滤参数
 | 
			
		||||
              doFilter(null);
 | 
			
		||||
              return null;
 | 
			
		||||
            } else {
 | 
			
		||||
              selectMarker(o);
 | 
			
		||||
              doFilter(data[getPrimaryKey()], (target) => target.field || getPrimaryKey(), '$eq');
 | 
			
		||||
            }
 | 
			
		||||
            return o;
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (data) {
 | 
			
		||||
          setRecord(data);
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      o.on('click', onClick);
 | 
			
		||||
      return () => o.off('click', onClick);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      overlays.forEach((ov) => {
 | 
			
		||||
        ov.remove();
 | 
			
		||||
      });
 | 
			
		||||
      events.forEach((e) => e());
 | 
			
		||||
    };
 | 
			
		||||
  }, [dataSource, isMapInitialization, fieldNames, field.type, isConnected]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      setSelectedRecordKeys([]);
 | 
			
		||||
    });
 | 
			
		||||
  }, [dataSource]);
 | 
			
		||||
 | 
			
		||||
  const mapRefCallback = (instance: AMapForwardedRefProps) => {
 | 
			
		||||
    mapRef.current = instance;
 | 
			
		||||
    setIsMapInitialization(!!instance?.map && !instance.errMessage);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={css`
 | 
			
		||||
        position: relative;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
      `}
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        className={css`
 | 
			
		||||
          position: absolute;
 | 
			
		||||
          left: 10px;
 | 
			
		||||
          top: 10px;
 | 
			
		||||
          z-index: 999;
 | 
			
		||||
        `}
 | 
			
		||||
      >
 | 
			
		||||
        {isMapInitialization && !mapRef.current.errMessage ? (
 | 
			
		||||
          <Space direction="vertical">
 | 
			
		||||
            <Button
 | 
			
		||||
              style={{
 | 
			
		||||
                color: !selectingMode ? '#F18b62' : undefined,
 | 
			
		||||
                borderColor: 'currentcolor',
 | 
			
		||||
              }}
 | 
			
		||||
              onClick={(e) => {
 | 
			
		||||
                e.stopPropagation();
 | 
			
		||||
                setSelecting('');
 | 
			
		||||
              }}
 | 
			
		||||
              icon={<EnvironmentOutlined />}
 | 
			
		||||
            ></Button>
 | 
			
		||||
            <Button
 | 
			
		||||
              style={{
 | 
			
		||||
                color: selectingMode === 'selection' ? '#F18b62' : undefined,
 | 
			
		||||
                borderColor: 'currentcolor',
 | 
			
		||||
              }}
 | 
			
		||||
              onClick={(e) => {
 | 
			
		||||
                e.stopPropagation();
 | 
			
		||||
                setSelecting('selection');
 | 
			
		||||
              }}
 | 
			
		||||
              icon={<ExpandOutlined />}
 | 
			
		||||
            ></Button>
 | 
			
		||||
            {selectingMode === 'selection' ? (
 | 
			
		||||
              <Button
 | 
			
		||||
                type="primary"
 | 
			
		||||
                icon={<CheckOutlined />}
 | 
			
		||||
                title={t('Confirm selection')}
 | 
			
		||||
                onClick={onSelectingComplete}
 | 
			
		||||
              ></Button>
 | 
			
		||||
            ) : null}
 | 
			
		||||
          </Space>
 | 
			
		||||
        ) : null}
 | 
			
		||||
      </div>
 | 
			
		||||
      <MapBlockDrawer record={record} setVisible={setRecord} />
 | 
			
		||||
      <AMapComponent
 | 
			
		||||
        {...field?.uiSchema?.['x-component-props']}
 | 
			
		||||
        ref={mapRefCallback}
 | 
			
		||||
        style={{ height: fixedBlock ? '100%' : null }}
 | 
			
		||||
        zoom={zoom}
 | 
			
		||||
        disabled
 | 
			
		||||
        block
 | 
			
		||||
        overlayCommonOptions={{
 | 
			
		||||
          strokeColor: '#F18b62',
 | 
			
		||||
          fillColor: '#F18b62',
 | 
			
		||||
        }}
 | 
			
		||||
      ></AMapComponent>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const MapBlockDrawer = (props) => {
 | 
			
		||||
  const { setVisible, record } = props;
 | 
			
		||||
  const fieldSchema = useFieldSchema();
 | 
			
		||||
  const schema: Schema = useMemo(
 | 
			
		||||
    () =>
 | 
			
		||||
      fieldSchema.reduceProperties((buf, current) => {
 | 
			
		||||
        if (current.name === 'drawer') {
 | 
			
		||||
          return current;
 | 
			
		||||
        }
 | 
			
		||||
        return buf;
 | 
			
		||||
      }, null),
 | 
			
		||||
    [fieldSchema],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    schema && (
 | 
			
		||||
      <ActionContextProvider value={{ visible: !!record, setVisible }}>
 | 
			
		||||
        <RecordProvider record={record}>
 | 
			
		||||
          <RecursionField schema={schema} name={schema.name} />
 | 
			
		||||
        </RecordProvider>
 | 
			
		||||
      </ActionContextProvider>
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function clearSelected(marker: AMap.Marker | AMap.Polygon | AMap.Polyline | AMap.Circle) {
 | 
			
		||||
  if ((marker as AMap.Marker).dom) {
 | 
			
		||||
    (marker as AMap.Marker).dom.style.filter = 'none';
 | 
			
		||||
 | 
			
		||||
    // AMap.Polygon | AMap.Polyline | AMap.Circle 都有 setOptions 方法
 | 
			
		||||
  } else if ((marker as AMap.Polygon).setOptions) {
 | 
			
		||||
    (marker as AMap.Polygon).setOptions({
 | 
			
		||||
      strokeColor: '#4e9bff',
 | 
			
		||||
      fillColor: '#4e9bff',
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function selectMarker(marker: AMap.Marker | AMap.Polygon | AMap.Polyline | AMap.Circle) {
 | 
			
		||||
  if ((marker as AMap.Marker).dom) {
 | 
			
		||||
    (marker as AMap.Marker).dom.style.filter = 'brightness(1.2) contrast(1.2) hue-rotate(180deg)';
 | 
			
		||||
 | 
			
		||||
    // AMap.Polygon | AMap.Polyline | AMap.Circle 都有 setOptions 方法
 | 
			
		||||
  } else if ((marker as AMap.Polygon).setOptions) {
 | 
			
		||||
    (marker as AMap.Polygon).setOptions({
 | 
			
		||||
      strokeColor: '#F18b62',
 | 
			
		||||
      fillColor: '#F18b62',
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -6,13 +6,12 @@ import { useFieldSchema } from '@formily/react';
 | 
			
		||||
import { useCollection } from '@nocobase/client';
 | 
			
		||||
import { useMemoizedFn } from 'ahooks';
 | 
			
		||||
import { Alert, Button, Modal, Spin } from 'antd';
 | 
			
		||||
import React, { useEffect, useCallback, useRef, useState, useMemo, useImperativeHandle } from 'react';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
import { useMapConfiguration } from '../hooks';
 | 
			
		||||
import { useMapTranslation } from '../locale';
 | 
			
		||||
import Search from './Search';
 | 
			
		||||
 | 
			
		||||
export type MapEditorType = 'point' | 'polygon' | 'lineString' | 'circle';
 | 
			
		||||
import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
 | 
			
		||||
import { useNavigate } from 'react-router';
 | 
			
		||||
import { useMapConfiguration } from '../../hooks';
 | 
			
		||||
import { useMapTranslation } from '../../locale';
 | 
			
		||||
import { MapEditorType } from '../../types';
 | 
			
		||||
import { Search } from './Search';
 | 
			
		||||
 | 
			
		||||
export interface AMapComponentProps {
 | 
			
		||||
  value?: any;
 | 
			
		||||
@ -80,9 +79,10 @@ export interface AMapForwardedRefProps {
 | 
			
		||||
    close: (clear?: boolean) => void;
 | 
			
		||||
  };
 | 
			
		||||
  overlay: AMap.Polygon;
 | 
			
		||||
  errMessage?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps>((props, ref) => {
 | 
			
		||||
export const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps>((props, ref) => {
 | 
			
		||||
  const { accessKey, securityJsCode } = useMapConfiguration(props.mapType) || {};
 | 
			
		||||
  const { value, onChange, block = false, readonly, disabled = block, zoom = 13, overlayCommonOptions } = props;
 | 
			
		||||
  const { t } = useMapTranslation();
 | 
			
		||||
@ -358,13 +358,14 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
 | 
			
		||||
    overlay: overlay.current,
 | 
			
		||||
    mouseTool: () => mouseTool.current,
 | 
			
		||||
    editor: () => editor.current,
 | 
			
		||||
    errMessage,
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  if (!accessKey || errMessage) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Alert
 | 
			
		||||
        action={
 | 
			
		||||
          <Button type="primary" onClick={() => navigate('/admin/settings/map-configuration/configuration')}>
 | 
			
		||||
          <Button type="primary" onClick={() => navigate('/admin/settings/map/configuration')}>
 | 
			
		||||
            {t('Go to the configuration page')}
 | 
			
		||||
          </Button>
 | 
			
		||||
        }
 | 
			
		||||
@ -458,5 +459,3 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default AMapComponent;
 | 
			
		||||
@ -2,14 +2,14 @@ import { css } from '@emotion/css';
 | 
			
		||||
import { useDebounceFn } from 'ahooks';
 | 
			
		||||
import { message, Select } from 'antd';
 | 
			
		||||
import React, { useEffect, useRef, useState } from 'react';
 | 
			
		||||
import { useMapTranslation } from '../locale';
 | 
			
		||||
import { useMapTranslation } from '../../locale';
 | 
			
		||||
 | 
			
		||||
interface SearchProps {
 | 
			
		||||
  aMap: any;
 | 
			
		||||
  toCenter: (p: any) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Search = (props: SearchProps) => {
 | 
			
		||||
export const Search = (props: SearchProps) => {
 | 
			
		||||
  const { aMap, toCenter } = props;
 | 
			
		||||
  const { t } = useMapTranslation();
 | 
			
		||||
  const placeSearch = useRef<any>();
 | 
			
		||||
@ -90,5 +90,3 @@ const Search = (props: SearchProps) => {
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Search;
 | 
			
		||||
							
								
								
									
										2
									
								
								packages/plugins/map/src/client/components/AMap/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								packages/plugins/map/src/client/components/AMap/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
export * from './Block';
 | 
			
		||||
export * from './Map';
 | 
			
		||||
@ -1,12 +1,16 @@
 | 
			
		||||
import { useAPIClient, useCompile } from '@nocobase/client';
 | 
			
		||||
import { useBoolean } from 'ahooks';
 | 
			
		||||
import { Button, Card, Form, Input, Tabs, message } from 'antd';
 | 
			
		||||
import { Button, Card, Form, Input, message, Tabs } from 'antd';
 | 
			
		||||
import React, { useEffect, useMemo } from 'react';
 | 
			
		||||
import { useLocation } from 'react-router-dom';
 | 
			
		||||
import { MapTypes } from '../constants';
 | 
			
		||||
import { MapConfigurationResourceKey, getSSKey, useMapConfiguration } from '../hooks';
 | 
			
		||||
import { getSSKey, MapConfigurationResourceKey, useMapConfiguration } from '../hooks';
 | 
			
		||||
import { useMapTranslation } from '../locale';
 | 
			
		||||
 | 
			
		||||
const AMapConfiguration = ({ type }) => {
 | 
			
		||||
interface BaseConfigurationProps {
 | 
			
		||||
  type: 'amap' | 'google';
 | 
			
		||||
}
 | 
			
		||||
const BaseConfiguration: React.FC<BaseConfigurationProps> = ({ type, children }) => {
 | 
			
		||||
  const { t } = useMapTranslation();
 | 
			
		||||
  const [isDisabled, disableAction] = useBoolean(false);
 | 
			
		||||
  const apiClient = useAPIClient();
 | 
			
		||||
@ -31,6 +35,7 @@ const AMapConfiguration = ({ type }) => {
 | 
			
		||||
      })
 | 
			
		||||
      .then((res) => {
 | 
			
		||||
        sessionStorage.removeItem(getSSKey(type));
 | 
			
		||||
        disableAction.toggle();
 | 
			
		||||
        message.success(t('Saved successfully'));
 | 
			
		||||
      })
 | 
			
		||||
      .catch((err) => {
 | 
			
		||||
@ -38,20 +43,15 @@ const AMapConfiguration = ({ type }) => {
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <Form form={form} layout="vertical" onFinish={onSubmit}>
 | 
			
		||||
      <Form.Item required name="accessKey" label={t('Access key')}>
 | 
			
		||||
        <Input disabled={isDisabled} />
 | 
			
		||||
      </Form.Item>
 | 
			
		||||
      <Form.Item required name="securityJsCode" label={t('securityJsCode or serviceHost')}>
 | 
			
		||||
        <Input disabled={isDisabled} />
 | 
			
		||||
      </Form.Item>
 | 
			
		||||
    <Form disabled={isDisabled} form={form} layout="vertical" onFinish={onSubmit}>
 | 
			
		||||
      {children}
 | 
			
		||||
      {isDisabled ? (
 | 
			
		||||
        <Button onClick={disableAction.toggle} type="ghost">
 | 
			
		||||
        <Button disabled={false} onClick={disableAction.toggle} type="ghost">
 | 
			
		||||
          {t('Edit')}
 | 
			
		||||
        </Button>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <Form.Item>
 | 
			
		||||
          <Button type="primary" htmlType="submit">
 | 
			
		||||
          <Button disabled={false} type="primary" htmlType="submit">
 | 
			
		||||
            {t('Save')}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
@ -60,9 +60,34 @@ const AMapConfiguration = ({ type }) => {
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const AMapConfiguration = () => {
 | 
			
		||||
  const { t } = useMapTranslation();
 | 
			
		||||
  return (
 | 
			
		||||
    <BaseConfiguration type="amap">
 | 
			
		||||
      <Form.Item required name="accessKey" label={t('Access key')}>
 | 
			
		||||
        <Input />
 | 
			
		||||
      </Form.Item>
 | 
			
		||||
      <Form.Item required name="securityJsCode" label={t('securityJsCode or serviceHost')}>
 | 
			
		||||
        <Input />
 | 
			
		||||
      </Form.Item>
 | 
			
		||||
    </BaseConfiguration>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const GoogleMapConfiguration = () => {
 | 
			
		||||
  const { t } = useMapTranslation();
 | 
			
		||||
  return (
 | 
			
		||||
    <BaseConfiguration type="google">
 | 
			
		||||
      <Form.Item required name="accessKey" label={t('Api key')}>
 | 
			
		||||
        <Input />
 | 
			
		||||
      </Form.Item>
 | 
			
		||||
    </BaseConfiguration>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const components = {
 | 
			
		||||
  amap: AMapConfiguration,
 | 
			
		||||
  google: () => <div>Coming soon</div>,
 | 
			
		||||
  google: GoogleMapConfiguration,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const tabList = MapTypes.map((item) => {
 | 
			
		||||
@ -72,22 +97,21 @@ const tabList = MapTypes.map((item) => {
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const Configuration = () => {
 | 
			
		||||
export const Configuration = () => {
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
  const location = useLocation();
 | 
			
		||||
  const search = new URLSearchParams(location.search);
 | 
			
		||||
  return (
 | 
			
		||||
    <Card bordered>
 | 
			
		||||
      <Tabs
 | 
			
		||||
        type="card"
 | 
			
		||||
        items={tabList.map((tab) => {
 | 
			
		||||
          return {
 | 
			
		||||
            key: tab.value,
 | 
			
		||||
            label: compile(tab.label),
 | 
			
		||||
            children: <tab.component type={tab.value} />,
 | 
			
		||||
          };
 | 
			
		||||
      <Tabs type="card" defaultActiveKey={search.get('tab')}>
 | 
			
		||||
        {tabList.map((tab) => {
 | 
			
		||||
          return (
 | 
			
		||||
            <Tabs.TabPane key={tab.value} tab={compile(tab.label)}>
 | 
			
		||||
              <tab.component type={tab.value} />
 | 
			
		||||
            </Tabs.TabPane>
 | 
			
		||||
          );
 | 
			
		||||
        })}
 | 
			
		||||
      />
 | 
			
		||||
      </Tabs>
 | 
			
		||||
    </Card>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Configuration;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										341
									
								
								packages/plugins/map/src/client/components/GoogleMaps/Block.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										341
									
								
								packages/plugins/map/src/client/components/GoogleMaps/Block.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,341 @@
 | 
			
		||||
import { CheckOutlined, EnvironmentOutlined, ExpandOutlined } from '@ant-design/icons';
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
import { RecursionField, Schema, useFieldSchema } from '@formily/react';
 | 
			
		||||
import {
 | 
			
		||||
  ActionContextProvider,
 | 
			
		||||
  RecordProvider,
 | 
			
		||||
  useCollection,
 | 
			
		||||
  useCompile,
 | 
			
		||||
  useFilterAPI,
 | 
			
		||||
  useProps,
 | 
			
		||||
} from '@nocobase/client';
 | 
			
		||||
import { useMemoizedFn } from 'ahooks';
 | 
			
		||||
import { Button, Space } from 'antd';
 | 
			
		||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
 | 
			
		||||
import { defaultImage, selectedImage } from '../../constants';
 | 
			
		||||
import { useMapTranslation } from '../../locale';
 | 
			
		||||
import { GoogleMapForwardedRefProps, GoogleMapsComponent, OverlayOptions } from './Map';
 | 
			
		||||
import { getIcon } from './utils';
 | 
			
		||||
 | 
			
		||||
const OVERLAY_KEY = 'google-maps-overlay-id';
 | 
			
		||||
const OVERLAY_SELECtED = 'google-maps-overlay-selected';
 | 
			
		||||
 | 
			
		||||
const labelClass = css`
 | 
			
		||||
  margin-top: 6px;
 | 
			
		||||
  padding: 2px 4px;
 | 
			
		||||
  background: #fff;
 | 
			
		||||
  border: 1px solid #0000f5;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const GoogleMapsBlock = (props) => {
 | 
			
		||||
  const { fieldNames, dataSource = [], fixedBlock, zoom, setSelectedRecordKeys } = useProps(props);
 | 
			
		||||
  const { getField, getPrimaryKey } = useCollection();
 | 
			
		||||
  const { marker: markerName, field: fieldName } = fieldNames || {
 | 
			
		||||
    marker: 'id',
 | 
			
		||||
    field: 'id',
 | 
			
		||||
  };
 | 
			
		||||
  const field = getField(fieldName);
 | 
			
		||||
  const [isMapInitialization, setIsMapInitialization] = useState(false);
 | 
			
		||||
  const mapRef = useRef<GoogleMapForwardedRefProps>();
 | 
			
		||||
  const [record, setRecord] = useState();
 | 
			
		||||
  const [selectingMode, setSelecting] = useState('');
 | 
			
		||||
  const { t } = useMapTranslation();
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
  const { isConnected, doFilter } = useFilterAPI();
 | 
			
		||||
  const [, setPrevSelected] = useState(null);
 | 
			
		||||
  const selectingModeRef = useRef(selectingMode);
 | 
			
		||||
  const selectionOverlayRef = useRef<google.maps.Polygon>();
 | 
			
		||||
  const overlaysRef = useRef<google.maps.MVCObject[]>([]);
 | 
			
		||||
  selectingModeRef.current = selectingMode;
 | 
			
		||||
 | 
			
		||||
  const setOverlayOptions = (overlay: google.maps.MVCObject, state?: boolean) => {
 | 
			
		||||
    const selected = typeof state !== 'undefined' ? !state : overlay.get(OVERLAY_SELECtED);
 | 
			
		||||
    overlay.set(OVERLAY_SELECtED, !selected);
 | 
			
		||||
    (overlay as google.maps.Marker).setOptions({
 | 
			
		||||
      ...(selected
 | 
			
		||||
        ? {
 | 
			
		||||
            icon: getIcon(defaultImage),
 | 
			
		||||
            strokeColor: '#4e9bff',
 | 
			
		||||
            fillColor: '#4e9bff',
 | 
			
		||||
          }
 | 
			
		||||
        : {
 | 
			
		||||
            icon: getIcon(selectedImage),
 | 
			
		||||
            strokeColor: '#F18b62',
 | 
			
		||||
            fillColor: '#F18b62',
 | 
			
		||||
          }),
 | 
			
		||||
    } as OverlayOptions);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // selection
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (selectingMode !== 'selection') {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!mapRef.current.drawingManager) {
 | 
			
		||||
      mapRef.current.drawingManager = mapRef.current.createDraw(true, {
 | 
			
		||||
        editable: true,
 | 
			
		||||
        draggable: true,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    const listenerSet = new Set<() => void>();
 | 
			
		||||
    mapRef.current.drawingManager.setDrawingMode(google.maps.drawing.OverlayType.POLYGON);
 | 
			
		||||
    mapRef.current.drawingManager.addListener('overlaycomplete', (event) => {
 | 
			
		||||
      const polygon = event.overlay as google.maps.Polygon;
 | 
			
		||||
      mapRef.current.drawingManager.setDrawingMode(null);
 | 
			
		||||
      selectionOverlayRef.current = polygon;
 | 
			
		||||
      const path = polygon.getPath();
 | 
			
		||||
      ['insert_at', 'remove_at', 'set_at'].forEach((key) => {
 | 
			
		||||
        listenerSet.add(path.addListener(key, () => {}).remove);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    return () => {
 | 
			
		||||
      listenerSet.forEach((i) => {
 | 
			
		||||
        i();
 | 
			
		||||
      });
 | 
			
		||||
      if (!mapRef.current) return;
 | 
			
		||||
      selectionOverlayRef.current?.unbindAll();
 | 
			
		||||
      selectionOverlayRef.current?.setMap(null);
 | 
			
		||||
      selectionOverlayRef.current = null;
 | 
			
		||||
      mapRef.current.drawingManager.setDrawingMode(null);
 | 
			
		||||
      mapRef.current.drawingManager.unbindAll();
 | 
			
		||||
    };
 | 
			
		||||
  }, [selectingMode]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (selectingMode) {
 | 
			
		||||
      return () => {
 | 
			
		||||
        if (!selectingModeRef.current) {
 | 
			
		||||
          overlaysRef.current.forEach((o) => {
 | 
			
		||||
            setOverlayOptions(o, false);
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }, [selectingMode]);
 | 
			
		||||
 | 
			
		||||
  const onSelectingComplete = useMemoizedFn(() => {
 | 
			
		||||
    const overlay = selectionOverlayRef.current;
 | 
			
		||||
    const overlays = overlaysRef.current;
 | 
			
		||||
    const poly = google.maps.geometry.poly;
 | 
			
		||||
    const selectedOverlays = overlays.filter((o) => {
 | 
			
		||||
      if (o === overlay || o.get(OVERLAY_KEY) === undefined) return;
 | 
			
		||||
      if (o instanceof google.maps.Marker) {
 | 
			
		||||
        return poly.containsLocation(o.getPosition(), overlay);
 | 
			
		||||
      } else if (o instanceof google.maps.Circle) {
 | 
			
		||||
        return poly.containsLocation(o.getCenter(), overlay);
 | 
			
		||||
      } else {
 | 
			
		||||
        return (o as google.maps.Polygon)
 | 
			
		||||
          .getPath()
 | 
			
		||||
          .getArray()
 | 
			
		||||
          .some((position) => {
 | 
			
		||||
            return poly.containsLocation(position, overlay);
 | 
			
		||||
          });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    const ids = selectedOverlays.map((o) => {
 | 
			
		||||
      setOverlayOptions(o, true);
 | 
			
		||||
      return o.get(OVERLAY_KEY);
 | 
			
		||||
    });
 | 
			
		||||
    setSelectedRecordKeys((lastIds) => ids.concat(lastIds));
 | 
			
		||||
    overlay.unbindAll();
 | 
			
		||||
    overlay.setMap(null);
 | 
			
		||||
    mapRef.current.drawingManager.setDrawingMode(google.maps.drawing.OverlayType.POLYGON);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!field || !mapRef.current?.map) return;
 | 
			
		||||
    const overlays: google.maps.Polygon[] = dataSource
 | 
			
		||||
      .map((item) => {
 | 
			
		||||
        const data = item[fieldNames?.field];
 | 
			
		||||
        if (!data) return;
 | 
			
		||||
        const overlay = mapRef.current.setOverlay(field.type, data, {
 | 
			
		||||
          strokeColor: '#4e9bff',
 | 
			
		||||
          fillColor: '#4e9bff',
 | 
			
		||||
          cursor: 'pointer',
 | 
			
		||||
          label: {
 | 
			
		||||
            className: labelClass,
 | 
			
		||||
            fontFamily: 'inherit',
 | 
			
		||||
            fontSize: '13px',
 | 
			
		||||
            color: '#333',
 | 
			
		||||
            text: fieldNames?.marker ? compile(item[markerName]) : undefined,
 | 
			
		||||
          } as google.maps.MarkerLabel,
 | 
			
		||||
        });
 | 
			
		||||
        overlay.set(OVERLAY_KEY, item[getPrimaryKey()]);
 | 
			
		||||
        return overlay;
 | 
			
		||||
      })
 | 
			
		||||
      .filter(Boolean);
 | 
			
		||||
 | 
			
		||||
    overlaysRef.current = overlays;
 | 
			
		||||
    mapRef.current.setFitView(overlays);
 | 
			
		||||
 | 
			
		||||
    const events = overlays.map((o: google.maps.MVCObject) => {
 | 
			
		||||
      const onClick = (event) => {
 | 
			
		||||
        const overlay = o as google.maps.Polygon;
 | 
			
		||||
        const id = overlay.get(OVERLAY_KEY);
 | 
			
		||||
        if (!id) return;
 | 
			
		||||
 | 
			
		||||
        const data = dataSource?.find((item) => {
 | 
			
		||||
          return id === item[getPrimaryKey()];
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 筛选区块模式
 | 
			
		||||
        if (isConnected) {
 | 
			
		||||
          setPrevSelected((prev) => {
 | 
			
		||||
            prev && clearSelected(overlay);
 | 
			
		||||
            if (prev === o) {
 | 
			
		||||
              clearSelected(overlay);
 | 
			
		||||
 | 
			
		||||
              // 删除过滤参数
 | 
			
		||||
              doFilter(null);
 | 
			
		||||
              return null;
 | 
			
		||||
            } else {
 | 
			
		||||
              selectMarker(overlay);
 | 
			
		||||
              doFilter(data[getPrimaryKey()], (target) => target.field || getPrimaryKey(), '$eq');
 | 
			
		||||
            }
 | 
			
		||||
            return overlay;
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (data) {
 | 
			
		||||
          setRecord(data);
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      o.addListener('click', onClick);
 | 
			
		||||
      return () => o.unbindAll();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      overlays.forEach((ov) => {
 | 
			
		||||
        ov.setMap(null);
 | 
			
		||||
        ov.unbindAll();
 | 
			
		||||
      });
 | 
			
		||||
      events.forEach((e) => e());
 | 
			
		||||
    };
 | 
			
		||||
  }, [dataSource, isMapInitialization, markerName, field.type, isConnected]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      setSelectedRecordKeys([]);
 | 
			
		||||
    });
 | 
			
		||||
  }, [dataSource]);
 | 
			
		||||
 | 
			
		||||
  const mapRefCallback = (instance: GoogleMapForwardedRefProps) => {
 | 
			
		||||
    mapRef.current = instance;
 | 
			
		||||
    setIsMapInitialization(!!instance?.map && !instance.errMessage);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={css`
 | 
			
		||||
        position: relative;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
      `}
 | 
			
		||||
    >
 | 
			
		||||
      {isMapInitialization && (
 | 
			
		||||
        <>
 | 
			
		||||
          <div
 | 
			
		||||
            className={css`
 | 
			
		||||
              position: absolute;
 | 
			
		||||
              left: 10px;
 | 
			
		||||
              top: 10px;
 | 
			
		||||
              z-index: 999;
 | 
			
		||||
            `}
 | 
			
		||||
          >
 | 
			
		||||
            <Space direction="vertical">
 | 
			
		||||
              <Button
 | 
			
		||||
                style={{
 | 
			
		||||
                  color: !selectingMode ? '#F18b62' : undefined,
 | 
			
		||||
                  borderColor: 'currentcolor',
 | 
			
		||||
                }}
 | 
			
		||||
                onClick={(e) => {
 | 
			
		||||
                  e.stopPropagation();
 | 
			
		||||
                  setSelecting('');
 | 
			
		||||
                }}
 | 
			
		||||
                icon={<EnvironmentOutlined />}
 | 
			
		||||
              ></Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                style={{
 | 
			
		||||
                  color: selectingMode === 'selection' ? '#F18b62' : undefined,
 | 
			
		||||
                  borderColor: 'currentcolor',
 | 
			
		||||
                }}
 | 
			
		||||
                onClick={(e) => {
 | 
			
		||||
                  e.stopPropagation();
 | 
			
		||||
                  setSelecting('selection');
 | 
			
		||||
                }}
 | 
			
		||||
                icon={<ExpandOutlined />}
 | 
			
		||||
              ></Button>
 | 
			
		||||
              {selectingMode === 'selection' ? (
 | 
			
		||||
                <Button
 | 
			
		||||
                  type="primary"
 | 
			
		||||
                  icon={<CheckOutlined />}
 | 
			
		||||
                  title={t('Confirm selection')}
 | 
			
		||||
                  onClick={onSelectingComplete}
 | 
			
		||||
                ></Button>
 | 
			
		||||
              ) : null}
 | 
			
		||||
            </Space>
 | 
			
		||||
          </div>
 | 
			
		||||
          <MapBlockDrawer record={record} setVisible={setRecord} />
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
      <GoogleMapsComponent
 | 
			
		||||
        {...props}
 | 
			
		||||
        ref={mapRefCallback}
 | 
			
		||||
        style={{ height: fixedBlock ? '100%' : null }}
 | 
			
		||||
        zoom={zoom}
 | 
			
		||||
        disabled
 | 
			
		||||
        block
 | 
			
		||||
        overlayCommonOptions={{
 | 
			
		||||
          strokeColor: '#F18b62',
 | 
			
		||||
          fillColor: '#F18b62',
 | 
			
		||||
        }}
 | 
			
		||||
      ></GoogleMapsComponent>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const MapBlockDrawer = (props) => {
 | 
			
		||||
  const { setVisible, record } = props;
 | 
			
		||||
  const fieldSchema = useFieldSchema();
 | 
			
		||||
  const schema: Schema = useMemo(
 | 
			
		||||
    () =>
 | 
			
		||||
      fieldSchema.reduceProperties((buf, current) => {
 | 
			
		||||
        if (current.name === 'drawer') {
 | 
			
		||||
          return current;
 | 
			
		||||
        }
 | 
			
		||||
        return buf;
 | 
			
		||||
      }, null),
 | 
			
		||||
    [fieldSchema],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    schema && (
 | 
			
		||||
      <ActionContextProvider value={{ visible: !!record, setVisible }}>
 | 
			
		||||
        <RecordProvider record={record}>
 | 
			
		||||
          <RecursionField schema={schema} name={schema.name} />
 | 
			
		||||
        </RecordProvider>
 | 
			
		||||
      </ActionContextProvider>
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function clearSelected(target: google.maps.Polygon) {
 | 
			
		||||
  if (target instanceof google.maps.Marker) {
 | 
			
		||||
    return target.setIcon(getIcon(defaultImage));
 | 
			
		||||
  }
 | 
			
		||||
  target.setOptions({
 | 
			
		||||
    strokeColor: '#4e9bff',
 | 
			
		||||
    fillColor: '#4e9bff',
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function selectMarker(target: google.maps.Polygon) {
 | 
			
		||||
  if (target instanceof google.maps.Marker) {
 | 
			
		||||
    return target.setIcon(getIcon(selectedImage));
 | 
			
		||||
  }
 | 
			
		||||
  target.setOptions({
 | 
			
		||||
    strokeColor: '#F18b62',
 | 
			
		||||
    fillColor: '#F18b62',
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										470
									
								
								packages/plugins/map/src/client/components/GoogleMaps/Map.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										470
									
								
								packages/plugins/map/src/client/components/GoogleMaps/Map.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,470 @@
 | 
			
		||||
import { SyncOutlined } from '@ant-design/icons';
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
import { useFieldSchema } from '@formily/react';
 | 
			
		||||
import { Loader } from '@googlemaps/js-api-loader';
 | 
			
		||||
import { useAPIClient, useCollection } from '@nocobase/client';
 | 
			
		||||
import { useMemoizedFn } from 'ahooks';
 | 
			
		||||
import { Alert, Button, Modal, Spin } from 'antd';
 | 
			
		||||
import React, { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
 | 
			
		||||
import { useNavigate } from 'react-router';
 | 
			
		||||
import { defaultImage } from '../../constants';
 | 
			
		||||
import { useMapConfiguration } from '../../hooks';
 | 
			
		||||
import { useMapTranslation } from '../../locale';
 | 
			
		||||
import { MapEditorType } from '../../types';
 | 
			
		||||
import { Search } from './Search';
 | 
			
		||||
import { getCurrentPosition, getIcon } from './utils';
 | 
			
		||||
 | 
			
		||||
export type OverlayOptions = google.maps.PolygonOptions & google.maps.MarkerOptions & google.maps.PolylineOptions;
 | 
			
		||||
 | 
			
		||||
export const getDrawingMode = (type: MapEditorType) => {
 | 
			
		||||
  if (type === 'point') {
 | 
			
		||||
    return 'marker';
 | 
			
		||||
  } else if (type === 'lineString') {
 | 
			
		||||
    return 'polyline';
 | 
			
		||||
  }
 | 
			
		||||
  return type;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const methodMapping = {
 | 
			
		||||
  point: {
 | 
			
		||||
    propertyKey: 'position',
 | 
			
		||||
    overlay: 'Marker',
 | 
			
		||||
  },
 | 
			
		||||
  polygon: {
 | 
			
		||||
    propertyKey: 'paths',
 | 
			
		||||
    overlay: 'Polygon',
 | 
			
		||||
  },
 | 
			
		||||
  lineString: {
 | 
			
		||||
    propertyKey: 'path',
 | 
			
		||||
    overlay: 'Polyline',
 | 
			
		||||
  },
 | 
			
		||||
  circle: {
 | 
			
		||||
    transformOptions(value) {
 | 
			
		||||
      return {
 | 
			
		||||
        center: new google.maps.LatLng(value[1], value[0]),
 | 
			
		||||
        radius: value[2],
 | 
			
		||||
      } as google.maps.CircleOptions;
 | 
			
		||||
    },
 | 
			
		||||
    overlay: 'Circle',
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface GoogleMapsComponentProps {
 | 
			
		||||
  value?: any;
 | 
			
		||||
  onChange?: (value: number[]) => void;
 | 
			
		||||
  disabled?: boolean;
 | 
			
		||||
  mapType: string;
 | 
			
		||||
  /**
 | 
			
		||||
   * only ReadPretty
 | 
			
		||||
   */
 | 
			
		||||
  readonly: string;
 | 
			
		||||
  zoom: number;
 | 
			
		||||
  type: MapEditorType;
 | 
			
		||||
  style?: React.CSSProperties;
 | 
			
		||||
  overlayCommonOptions?: OverlayOptions;
 | 
			
		||||
  block?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GoogleMapForwardedRefProps {
 | 
			
		||||
  setOverlay: (t: MapEditorType, v: any, o?: OverlayOptions) => google.maps.MVCObject;
 | 
			
		||||
  getOverlay: (t: MapEditorType, v: any, o?: OverlayOptions) => google.maps.MVCObject;
 | 
			
		||||
  setFitView: (overlays: google.maps.MVCObject[]) => void;
 | 
			
		||||
  createDraw: (onlyCreate?: boolean, additionalOptions?: OverlayOptions) => any;
 | 
			
		||||
  map: google.maps.Map;
 | 
			
		||||
  overlay: google.maps.MVCObject;
 | 
			
		||||
  drawingManager: google.maps.drawing.DrawingManager;
 | 
			
		||||
  errMessage?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const GoogleMapsComponent = React.forwardRef<GoogleMapForwardedRefProps, GoogleMapsComponentProps>(
 | 
			
		||||
  (props, ref) => {
 | 
			
		||||
    const { value, onChange, block = false, readonly, disabled = block, zoom = 13, overlayCommonOptions } = props;
 | 
			
		||||
    const { accessKey } = useMapConfiguration(props.mapType) || {};
 | 
			
		||||
    const { t } = useMapTranslation();
 | 
			
		||||
    const { getField } = useCollection();
 | 
			
		||||
    const fieldSchema = useFieldSchema();
 | 
			
		||||
    const drawingManagerRef = useRef<google.maps.drawing.DrawingManager>();
 | 
			
		||||
    const map = useRef<google.maps.Map>();
 | 
			
		||||
    const overlayRef = useRef<google.maps.Marker | google.maps.Polygon | google.maps.Polyline | google.maps.Circle>();
 | 
			
		||||
    const [needUpdateFlag, forceUpdate] = useState([]);
 | 
			
		||||
    const [errMessage, setErrMessage] = useState('');
 | 
			
		||||
    const api = useAPIClient();
 | 
			
		||||
 | 
			
		||||
    const type = useMemo<MapEditorType>(() => {
 | 
			
		||||
      if (props.type) return props.type;
 | 
			
		||||
      const collectionField = getField(fieldSchema?.name);
 | 
			
		||||
      return collectionField?.interface;
 | 
			
		||||
    }, [props?.type, fieldSchema?.name]);
 | 
			
		||||
 | 
			
		||||
    const drawingMode = useRef(getDrawingMode(type) as google.maps.drawing.OverlayType);
 | 
			
		||||
 | 
			
		||||
    const [commonOptions] = useState<OverlayOptions>({
 | 
			
		||||
      strokeWeight: 5,
 | 
			
		||||
      strokeColor: '#4e9bff',
 | 
			
		||||
      fillColor: '#4e9bff',
 | 
			
		||||
      strokeOpacity: 1,
 | 
			
		||||
      editable: !disabled,
 | 
			
		||||
      draggable: !disabled,
 | 
			
		||||
      ...overlayCommonOptions,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const navigate = useNavigate();
 | 
			
		||||
    const mapContainerRef = useRef<HTMLDivElement>();
 | 
			
		||||
    const cleanupOverlayListenersRef = useRef<Set<() => void>>(new Set());
 | 
			
		||||
 | 
			
		||||
    const onAndOffListenOverlay = useMemoizedFn((target: typeof overlayRef.current) => {
 | 
			
		||||
      cleanupOverlayListenersRef.current.forEach((cb) => {
 | 
			
		||||
        cleanupOverlayListenersRef.current.delete(cb);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if ('getPath' in target) {
 | 
			
		||||
        const mvcArray = target.getPath();
 | 
			
		||||
        ['insert_at', 'remove_at', 'set_at'].forEach((event) => {
 | 
			
		||||
          cleanupOverlayListenersRef.current.add(
 | 
			
		||||
            mvcArray.addListener(event, () => {
 | 
			
		||||
              onMapChange(target, true);
 | 
			
		||||
            }).remove,
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
      } else if (target instanceof google.maps.Circle) {
 | 
			
		||||
        ['center_changed', 'radius_changed'].forEach((event) => {
 | 
			
		||||
          cleanupOverlayListenersRef.current.add(
 | 
			
		||||
            target.addListener(event, () => {
 | 
			
		||||
              onMapChange(target, true);
 | 
			
		||||
            }).remove,
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const toRemoveOverlay = useMemoizedFn(() => {
 | 
			
		||||
      if (overlayRef.current) {
 | 
			
		||||
        overlayRef.current.unbindAll();
 | 
			
		||||
        overlayRef.current.setMap(null);
 | 
			
		||||
      }
 | 
			
		||||
      if (type !== 'point') {
 | 
			
		||||
        drawingManagerRef.current?.setDrawingMode(null);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const toCenter = useMemoizedFn((position) => {
 | 
			
		||||
      if (map.current) {
 | 
			
		||||
        map.current.setCenter(position);
 | 
			
		||||
        map.current.setZoom(zoom);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const setupOverlay = useMemoizedFn((nextOverlay: typeof overlayRef.current) => {
 | 
			
		||||
      toRemoveOverlay();
 | 
			
		||||
      onAndOffListenOverlay(nextOverlay);
 | 
			
		||||
      overlayRef.current = nextOverlay;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const setFitView = useMemoizedFn((overlays: google.maps.MVCObject[]) => {
 | 
			
		||||
      const bounds = new google.maps.LatLngBounds();
 | 
			
		||||
 | 
			
		||||
      overlays.forEach((overlay) => {
 | 
			
		||||
        if (overlay instanceof google.maps.Marker) {
 | 
			
		||||
          bounds.extend(overlay.getPosition());
 | 
			
		||||
        } else if (overlay instanceof google.maps.Polyline || overlay instanceof google.maps.Polygon) {
 | 
			
		||||
          const path = overlay.getPath();
 | 
			
		||||
          for (let i = 0; i < path.getLength(); i++) {
 | 
			
		||||
            bounds.extend(path.getAt(i));
 | 
			
		||||
          }
 | 
			
		||||
        } else if (overlay instanceof google.maps.Circle) {
 | 
			
		||||
          bounds.union(overlay.getBounds());
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      map.current.setCenter(bounds.getCenter());
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const onFocusOverlay = () => {
 | 
			
		||||
      if (overlayRef.current) {
 | 
			
		||||
        setFitView([overlayRef.current]);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onMapChange = useMemoizedFn((target: typeof overlayRef.current, onlyChange = false) => {
 | 
			
		||||
      let nextValue = null;
 | 
			
		||||
 | 
			
		||||
      if (type === 'point') {
 | 
			
		||||
        const { lat, lng } = (target as google.maps.Marker).getPosition();
 | 
			
		||||
        nextValue = [lng(), lat()];
 | 
			
		||||
      } else if (type === 'polygon' || type === 'lineString') {
 | 
			
		||||
        nextValue = (target as google.maps.Polyline)
 | 
			
		||||
          .getPath()
 | 
			
		||||
          .getArray()
 | 
			
		||||
          .map((item) => [item.lng(), item.lat()]);
 | 
			
		||||
        if (nextValue.length < 2) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
      } else if (type === 'circle') {
 | 
			
		||||
        const center = (target as google.maps.Circle).getCenter();
 | 
			
		||||
        const radius = (target as google.maps.Circle).getRadius();
 | 
			
		||||
        nextValue = [center.lng(), center.lat(), radius];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!onlyChange) {
 | 
			
		||||
        setupOverlay(target);
 | 
			
		||||
      }
 | 
			
		||||
      onChange?.(nextValue);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const createDraw = useMemoizedFn((onlyCreate = false, additionalOptions?: OverlayOptions) => {
 | 
			
		||||
      const currentOptions = {
 | 
			
		||||
        ...commonOptions,
 | 
			
		||||
        ...additionalOptions,
 | 
			
		||||
        map: map.current,
 | 
			
		||||
      };
 | 
			
		||||
      drawingManagerRef.current = new google.maps.drawing.DrawingManager({
 | 
			
		||||
        drawingMode: drawingMode.current,
 | 
			
		||||
        drawingControl: false,
 | 
			
		||||
        markerOptions: { ...currentOptions, icon: getIcon(defaultImage) },
 | 
			
		||||
        polygonOptions: currentOptions,
 | 
			
		||||
        polylineOptions: currentOptions,
 | 
			
		||||
        circleOptions: currentOptions,
 | 
			
		||||
        map: map.current,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (!onlyCreate) {
 | 
			
		||||
        drawingManagerRef.current.addListener('overlaycomplete', (event: { type: string; overlay: unknown }) => {
 | 
			
		||||
          const overlay = event.overlay as google.maps.Marker;
 | 
			
		||||
          onMapChange(overlay);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return drawingManagerRef.current;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const getOverlay = useMemoizedFn((t = type, v = value, o?: OverlayOptions) => {
 | 
			
		||||
      const mapping = methodMapping[t];
 | 
			
		||||
      if (!mapping) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const options = { ...commonOptions, icon: getIcon(defaultImage), ...o };
 | 
			
		||||
 | 
			
		||||
      if ('transformOptions' in mapping) {
 | 
			
		||||
        Object.assign(options, mapping.transformOptions(v));
 | 
			
		||||
      } else if ('propertyKey' in mapping) {
 | 
			
		||||
        options[mapping.propertyKey] = Array.isArray(v[0])
 | 
			
		||||
          ? v.map((item) => {
 | 
			
		||||
              return new google.maps.LatLng(item[1], item[0]);
 | 
			
		||||
            })
 | 
			
		||||
          : new google.maps.LatLng(v[1], v[0]);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const overlay = new google.maps[mapping.overlay](options);
 | 
			
		||||
      return overlay;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const setOverlay = useMemoizedFn((t = type, v = value, o?: OverlayOptions) => {
 | 
			
		||||
      if (!map.current) return;
 | 
			
		||||
      const nextOverlay = getOverlay(t, v, {
 | 
			
		||||
        ...o,
 | 
			
		||||
        map: map.current,
 | 
			
		||||
      }) as google.maps.Polyline;
 | 
			
		||||
      return nextOverlay;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // edit mode
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
      if (!map.current) return;
 | 
			
		||||
      if (!value || (!readonly && overlayRef.current)) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const nextOverlay = setOverlay();
 | 
			
		||||
      setupOverlay(nextOverlay);
 | 
			
		||||
      // Focus on the overlay
 | 
			
		||||
      setFitView([nextOverlay]);
 | 
			
		||||
    }, [value, needUpdateFlag, type, disabled, readonly, setOverlay, setFitView, setupOverlay]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
      if (!accessKey || map.current || !mapContainerRef.current) return;
 | 
			
		||||
      let loader: Loader;
 | 
			
		||||
      try {
 | 
			
		||||
        loader = new Loader({
 | 
			
		||||
          apiKey: accessKey,
 | 
			
		||||
          version: 'weekly',
 | 
			
		||||
          language: api.auth.getLocale(),
 | 
			
		||||
        });
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        setErrMessage(t('Load google maps failed, Please check the Api key and refresh the page'));
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // google maps api error
 | 
			
		||||
      const error = console.error;
 | 
			
		||||
      console.error = (err, ...args) => {
 | 
			
		||||
        if (err?.includes('InvalidKeyMapError')) {
 | 
			
		||||
          setErrMessage(t('Load google maps failed, Please check the Api key and refresh the page'));
 | 
			
		||||
        }
 | 
			
		||||
        error(err, ...args);
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      Promise.all([loader.importLibrary('drawing'), loader.importLibrary('core'), loader.importLibrary('geometry')])
 | 
			
		||||
        .then(async (res) => {
 | 
			
		||||
          const center = await getCurrentPosition();
 | 
			
		||||
          map.current = new google.maps.Map(mapContainerRef.current, {
 | 
			
		||||
            zoom,
 | 
			
		||||
            center,
 | 
			
		||||
            mapTypeId: google.maps.MapTypeId.ROADMAP,
 | 
			
		||||
            zoomControl: false,
 | 
			
		||||
            streetViewControl: false,
 | 
			
		||||
            panControl: false,
 | 
			
		||||
            mapTypeControl: false,
 | 
			
		||||
            fullscreenControl: false,
 | 
			
		||||
          });
 | 
			
		||||
          setErrMessage('');
 | 
			
		||||
          forceUpdate([]);
 | 
			
		||||
        })
 | 
			
		||||
        .catch((err) => {
 | 
			
		||||
          if (err instanceof Error) {
 | 
			
		||||
            setErrMessage(err.message);
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
      return () => {
 | 
			
		||||
        map.current?.unbindAll();
 | 
			
		||||
        map.current = null;
 | 
			
		||||
        drawingManagerRef.current?.unbindAll();
 | 
			
		||||
      };
 | 
			
		||||
    }, [accessKey, api.auth, type, zoom]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
      if (!map.current || !type || disabled || drawingManagerRef.current) return;
 | 
			
		||||
      createDraw();
 | 
			
		||||
    }, [createDraw, disabled, needUpdateFlag, type]);
 | 
			
		||||
 | 
			
		||||
    useImperativeHandle(ref, () => ({
 | 
			
		||||
      setOverlay,
 | 
			
		||||
      getOverlay,
 | 
			
		||||
      setFitView,
 | 
			
		||||
      createDraw,
 | 
			
		||||
      map: map.current,
 | 
			
		||||
      overlay: overlayRef.current,
 | 
			
		||||
      drawingManager: drawingManagerRef.current,
 | 
			
		||||
      errMessage,
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    const onReset = useMemoizedFn(() => {
 | 
			
		||||
      const ok = () => {
 | 
			
		||||
        toRemoveOverlay();
 | 
			
		||||
        drawingManagerRef.current.setDrawingMode(drawingMode.current);
 | 
			
		||||
        onChange?.(null);
 | 
			
		||||
      };
 | 
			
		||||
      Modal.confirm({
 | 
			
		||||
        title: t('Clear the canvas'),
 | 
			
		||||
        content: t('Are you sure to clear the canvas?'),
 | 
			
		||||
        okText: t('Confirm'),
 | 
			
		||||
        cancelText: t('Cancel'),
 | 
			
		||||
        onOk() {
 | 
			
		||||
          ok();
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!accessKey || errMessage) {
 | 
			
		||||
      return (
 | 
			
		||||
        <Alert
 | 
			
		||||
          action={
 | 
			
		||||
            <Button type="primary" onClick={() => navigate('/admin/settings/map/configuration?tab=google')}>
 | 
			
		||||
              {t('Go to the configuration page')}
 | 
			
		||||
            </Button>
 | 
			
		||||
          }
 | 
			
		||||
          message={errMessage || t('Please configure the Api key first')}
 | 
			
		||||
          type="error"
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div
 | 
			
		||||
        className={css`
 | 
			
		||||
          position: relative;
 | 
			
		||||
          height: 500px;
 | 
			
		||||
        `}
 | 
			
		||||
      >
 | 
			
		||||
        {!map.current && (
 | 
			
		||||
          <div
 | 
			
		||||
            className={css`
 | 
			
		||||
              position: absolute;
 | 
			
		||||
              inset: 0;
 | 
			
		||||
              display: flex;
 | 
			
		||||
              align-items: center;
 | 
			
		||||
              justify-content: center;
 | 
			
		||||
            `}
 | 
			
		||||
          >
 | 
			
		||||
            <Spin />
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
        {!disabled ? (
 | 
			
		||||
          <>
 | 
			
		||||
            {map.current && <Search toCenter={toCenter} mapRef={map} />}
 | 
			
		||||
            <div
 | 
			
		||||
              className={css`
 | 
			
		||||
                position: absolute;
 | 
			
		||||
                bottom: 80px;
 | 
			
		||||
                right: 20px;
 | 
			
		||||
                z-index: 10;
 | 
			
		||||
              `}
 | 
			
		||||
            >
 | 
			
		||||
              <Button
 | 
			
		||||
                onClick={onFocusOverlay}
 | 
			
		||||
                disabled={!overlayRef.current}
 | 
			
		||||
                type="primary"
 | 
			
		||||
                shape="round"
 | 
			
		||||
                size="large"
 | 
			
		||||
                icon={<SyncOutlined />}
 | 
			
		||||
              ></Button>
 | 
			
		||||
            </div>
 | 
			
		||||
            {type === 'lineString' || type === 'polygon' ? (
 | 
			
		||||
              <div
 | 
			
		||||
                className={css`
 | 
			
		||||
                  position: absolute;
 | 
			
		||||
                  bottom: 20px;
 | 
			
		||||
                  left: 10px;
 | 
			
		||||
                  z-index: 2;
 | 
			
		||||
                  pointer-events: none;
 | 
			
		||||
                `}
 | 
			
		||||
              >
 | 
			
		||||
                <Alert
 | 
			
		||||
                  message={t('Click to select the starting point and double-click to end the drawing')}
 | 
			
		||||
                  type="info"
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
            ) : null}
 | 
			
		||||
            <div
 | 
			
		||||
              className={css`
 | 
			
		||||
                position: absolute;
 | 
			
		||||
                bottom: 20px;
 | 
			
		||||
                right: 20px;
 | 
			
		||||
                z-index: 2;
 | 
			
		||||
              `}
 | 
			
		||||
            >
 | 
			
		||||
              <Button
 | 
			
		||||
                disabled={!value}
 | 
			
		||||
                style={{
 | 
			
		||||
                  height: '40px',
 | 
			
		||||
                }}
 | 
			
		||||
                onClick={onReset}
 | 
			
		||||
                type="primary"
 | 
			
		||||
                danger
 | 
			
		||||
              >
 | 
			
		||||
                {t('Clear')}
 | 
			
		||||
              </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </>
 | 
			
		||||
        ) : null}
 | 
			
		||||
        <div
 | 
			
		||||
          ref={mapContainerRef}
 | 
			
		||||
          className={css`
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            height: 100%;
 | 
			
		||||
          `}
 | 
			
		||||
          style={props?.style}
 | 
			
		||||
        ></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										107
									
								
								packages/plugins/map/src/client/components/GoogleMaps/Search.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								packages/plugins/map/src/client/components/GoogleMaps/Search.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,107 @@
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
import { useDebounceFn } from 'ahooks';
 | 
			
		||||
import { message, RefSelectProps, Select } from 'antd';
 | 
			
		||||
import React, { useEffect, useRef, useState } from 'react';
 | 
			
		||||
import { useMapTranslation } from '../../locale';
 | 
			
		||||
 | 
			
		||||
interface SearchProps {
 | 
			
		||||
  toCenter: (p: any) => void;
 | 
			
		||||
  mapRef: React.RefObject<google.maps.Map>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Search = (props: SearchProps) => {
 | 
			
		||||
  const { toCenter, mapRef } = props;
 | 
			
		||||
  const { t } = useMapTranslation();
 | 
			
		||||
  const placeSearchRef = useRef<google.maps.places.AutocompleteService>();
 | 
			
		||||
  const [options, setOptions] = useState([]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    google.maps
 | 
			
		||||
      .importLibrary('places')
 | 
			
		||||
      .then((places: google.maps.PlacesLibrary) => {
 | 
			
		||||
        placeSearchRef.current = new places.AutocompleteService();
 | 
			
		||||
      })
 | 
			
		||||
      .catch(() => {
 | 
			
		||||
        message.error('Please configure the Google API Key correctly');
 | 
			
		||||
      });
 | 
			
		||||
  }, [mapRef]);
 | 
			
		||||
 | 
			
		||||
  const { run: onSearch } = useDebounceFn(
 | 
			
		||||
    (keyword) => {
 | 
			
		||||
      if (!placeSearchRef.current) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      placeSearchRef.current.getPlacePredictions(
 | 
			
		||||
        {
 | 
			
		||||
          input: keyword || ' ',
 | 
			
		||||
        },
 | 
			
		||||
        (result, status) => {
 | 
			
		||||
          if (status === google.maps.places.PlacesServiceStatus.OK) {
 | 
			
		||||
            setOptions(
 | 
			
		||||
              result.map((item: any) => {
 | 
			
		||||
                const structured = item.structured_formatting;
 | 
			
		||||
                return {
 | 
			
		||||
                  ...item,
 | 
			
		||||
                  label: `${structured.main_text}${structured.secondary_text ? ' ' + structured.secondary_text : ''}`,
 | 
			
		||||
                  value: item.place_id,
 | 
			
		||||
                };
 | 
			
		||||
              }),
 | 
			
		||||
            );
 | 
			
		||||
          } else {
 | 
			
		||||
            setOptions([]);
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      wait: 300,
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const onSelect = (value) => {
 | 
			
		||||
    const place = options.find((o) => {
 | 
			
		||||
      return o.value === value;
 | 
			
		||||
    });
 | 
			
		||||
    const service = new google.maps.places.PlacesService(mapRef.current);
 | 
			
		||||
    service.getDetails(
 | 
			
		||||
      {
 | 
			
		||||
        placeId: place.place_id,
 | 
			
		||||
        fields: ['geometry'],
 | 
			
		||||
      },
 | 
			
		||||
      (result, status) => {
 | 
			
		||||
        if (status === google.maps.places.PlacesServiceStatus.OK) {
 | 
			
		||||
          toCenter(result.geometry.location);
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={css`
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        top: 10px;
 | 
			
		||||
        left: 10px;
 | 
			
		||||
        z-index: 10;
 | 
			
		||||
        width: calc(100% - 20px);
 | 
			
		||||
      `}
 | 
			
		||||
    >
 | 
			
		||||
      <Select
 | 
			
		||||
        id="google-map-search"
 | 
			
		||||
        showSearch
 | 
			
		||||
        allowClear
 | 
			
		||||
        style={{
 | 
			
		||||
          background: 'rgba(255, 255, 255, 0.8)',
 | 
			
		||||
        }}
 | 
			
		||||
        placeholder={t('Enter keywords to search')}
 | 
			
		||||
        filterOption={false}
 | 
			
		||||
        onSearch={onSearch}
 | 
			
		||||
        onSelect={onSelect}
 | 
			
		||||
        options={options}
 | 
			
		||||
      ></Select>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Search;
 | 
			
		||||
@ -0,0 +1,2 @@
 | 
			
		||||
export * from './Block';
 | 
			
		||||
export * from './Map';
 | 
			
		||||
@ -0,0 +1,33 @@
 | 
			
		||||
export const getIcon = (url) => {
 | 
			
		||||
  return {
 | 
			
		||||
    url,
 | 
			
		||||
    scaledSize: {
 | 
			
		||||
      width: 19,
 | 
			
		||||
      height: 32,
 | 
			
		||||
    },
 | 
			
		||||
    labelOrigin: new google.maps.Point(19 / 2, 32 + 10),
 | 
			
		||||
  } as google.maps.Icon;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getCurrentPosition: () => Promise<{ lat: number; lng: number }> = () => {
 | 
			
		||||
  return new Promise((resolve) => {
 | 
			
		||||
    const defaultLatLng = () => {
 | 
			
		||||
      // google company lat and lng
 | 
			
		||||
      resolve({ lat: 37.4224764, lng: -122.0842499 });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (navigator.geolocation) {
 | 
			
		||||
      navigator.geolocation.getCurrentPosition(
 | 
			
		||||
        (position) => {
 | 
			
		||||
          const { latitude, longitude } = position.coords;
 | 
			
		||||
          resolve({ lat: latitude, lng: longitude });
 | 
			
		||||
        },
 | 
			
		||||
        () => {
 | 
			
		||||
          defaultLatLng();
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      defaultLatLng();
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
@ -1,9 +1,10 @@
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
import { connect, mapReadPretty } from '@formily/react';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import AMapComponent, { AMapComponentProps } from './AMap';
 | 
			
		||||
import ReadPretty from './ReadPretty';
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
import { AMapComponentProps } from './AMap';
 | 
			
		||||
import Designer from './Designer';
 | 
			
		||||
import { MapComponent } from './MapComponent';
 | 
			
		||||
import ReadPretty from './ReadPretty';
 | 
			
		||||
 | 
			
		||||
type MapProps = AMapComponentProps;
 | 
			
		||||
 | 
			
		||||
@ -18,7 +19,7 @@ const InternalMap = connect((props: MapProps) => {
 | 
			
		||||
        }
 | 
			
		||||
      `}
 | 
			
		||||
    >
 | 
			
		||||
      {props.mapType ? <AMapComponent {...props} /> : null}
 | 
			
		||||
      <MapComponent {...props} />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}, mapReadPretty(ReadPretty));
 | 
			
		||||
@ -29,4 +30,4 @@ const Map = InternalMap as typeof InternalMap & {
 | 
			
		||||
 | 
			
		||||
Map.Designer = Designer;
 | 
			
		||||
 | 
			
		||||
export default Map;
 | 
			
		||||
export { Map };
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								packages/plugins/map/src/client/components/MapBlock.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								packages/plugins/map/src/client/components/MapBlock.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
import React, { useMemo } from 'react';
 | 
			
		||||
import { useMapTranslation } from '../locale';
 | 
			
		||||
import { AMapBlock } from './AMap';
 | 
			
		||||
import { GoogleMapsBlock } from './GoogleMaps';
 | 
			
		||||
 | 
			
		||||
const MapBlocks = {
 | 
			
		||||
  amap: AMapBlock,
 | 
			
		||||
  google: GoogleMapsBlock,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const MapBlockComponent: React.FC<any> = (props) => {
 | 
			
		||||
  const { t } = useMapTranslation();
 | 
			
		||||
  const { mapType } = props;
 | 
			
		||||
 | 
			
		||||
  const Component = useMemo(() => {
 | 
			
		||||
    return MapBlocks[mapType];
 | 
			
		||||
  }, [mapType]);
 | 
			
		||||
 | 
			
		||||
  if (!Component) {
 | 
			
		||||
    return <div>{t(`The ${mapType} cannot found`)}</div>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <Component {...props} />;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										23
									
								
								packages/plugins/map/src/client/components/MapComponent.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								packages/plugins/map/src/client/components/MapComponent.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
import React, { useMemo } from 'react';
 | 
			
		||||
import { useMapTranslation } from '../locale';
 | 
			
		||||
import { AMapComponent } from './AMap';
 | 
			
		||||
import { GoogleMapsComponent } from './GoogleMaps';
 | 
			
		||||
 | 
			
		||||
const MapComponents = {
 | 
			
		||||
  amap: AMapComponent,
 | 
			
		||||
  google: GoogleMapsComponent,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const MapComponent = React.forwardRef<any, any>((props, ref) => {
 | 
			
		||||
  const { t } = useMapTranslation();
 | 
			
		||||
  const { mapType } = props;
 | 
			
		||||
  const Component = useMemo(() => {
 | 
			
		||||
    return MapComponents[mapType];
 | 
			
		||||
  }, [mapType]);
 | 
			
		||||
 | 
			
		||||
  if (!Component) {
 | 
			
		||||
    return <div>{t(`The ${mapType} cannot found`)}</div>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <Component ref={ref} {...props} />;
 | 
			
		||||
});
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { useField, useFieldSchema, useForm } from '@formily/react';
 | 
			
		||||
import { EllipsisWithTooltip, useCollection } from '@nocobase/client';
 | 
			
		||||
import React, { useEffect } from 'react';
 | 
			
		||||
import AMapComponent from './AMap';
 | 
			
		||||
import { useFieldSchema, useForm } from '@formily/react';
 | 
			
		||||
import { EllipsisWithTooltip, useCollection, useFieldTitle } from '@nocobase/client';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { MapComponent } from './MapComponent';
 | 
			
		||||
 | 
			
		||||
const ReadPretty = (props) => {
 | 
			
		||||
  const { value } = props;
 | 
			
		||||
@ -9,13 +9,8 @@ const ReadPretty = (props) => {
 | 
			
		||||
  const { getField } = useCollection();
 | 
			
		||||
  const collectionField = getField(fieldSchema.name);
 | 
			
		||||
  const mapType = props.mapType || collectionField?.uiSchema['x-component-props']?.mapType;
 | 
			
		||||
  const field = useField();
 | 
			
		||||
  const form = useForm();
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!field.title && collectionField?.uiSchema?.title) {
 | 
			
		||||
      field.title = collectionField?.uiSchema.title;
 | 
			
		||||
    }
 | 
			
		||||
  }, collectionField?.title);
 | 
			
		||||
  useFieldTitle();
 | 
			
		||||
 | 
			
		||||
  if (!form.readPretty) {
 | 
			
		||||
    return (
 | 
			
		||||
@ -27,7 +22,7 @@ const ReadPretty = (props) => {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return mapType === 'amap' ? <AMapComponent readonly mapType={mapType} {...props}></AMapComponent> : null;
 | 
			
		||||
  return <MapComponent readonly mapType={mapType} {...props}></MapComponent>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ReadPretty;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										6
									
								
								packages/plugins/map/src/client/components/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								packages/plugins/map/src/client/components/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
export * from './AMap';
 | 
			
		||||
export * from './Configuration';
 | 
			
		||||
export * from './GoogleMaps';
 | 
			
		||||
export * from './Map';
 | 
			
		||||
export * from './MapBlock';
 | 
			
		||||
export * from './MapComponent';
 | 
			
		||||
@ -4,3 +4,8 @@ export const MapTypes = [
 | 
			
		||||
  { label: generateNTemplate('AMap'), value: 'amap' },
 | 
			
		||||
  { label: generateNTemplate('Google Maps'), value: 'google' },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const selectedImage =
 | 
			
		||||
  'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAA/CAMAAAC7OkrPAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAEJQTFRFAAAA8Yti8Yti8Itj8Iti74tj8Itj8YtiKwADKwADKhw5Kh07KwADKwADKwADKwAD4odn1YBlKwADKwADKwADKwAD/5y7LQAAABZ0Uk5TAP/8/f/B/PYOHjY3CCozLP3XCSkvMhA05K4AAAC4SURBVHic7dXLDoIwEIXhwSsoLSrw/q8qbSAiMy3/oivj2czmS5omnVORdapVJJFKBSEL2mrrUurbpdXa5dTH5dXi9tTsGNtX0TFG1OT+7PdY2YdEGd0FuFmUwXWm5UAZbCTKYA3SUrXZQTHLHbUy2OlsMO0ultLsajLlbEWZMEUZ+ig5E6YKM2GqMBOmGKubW1D3ps6g1nnvA5uGa5Os85E9nmF2ebYkzeKhc9wre4V+GMeh317hDfXgCWigIGJbAAAAAElFTkSuQmCC';
 | 
			
		||||
export const defaultImage =
 | 
			
		||||
  'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAA/CAYAAACM5Lr9AAAFkklEQVR42s3VaWxUVRjG8ddiEKPRxAQTCVSWLnZaWmjpTgulpYUBExP9Agou4IKAogUKAgItlNJCgUIXINFPfsFoDCjibkQRI0QIICAQWUpZugKtpcv0+DxyYqaTe2/vTLf58Etu7znnPf+0k44opQw5i5qsjIFs2APHoBZatFq+02vZeq94yyKs0dOjsAzOTCtuVtNKWtX0UpeaXqHU07tgt4ZnvuMa93Avz/CsniE2WIQV3iEaCDnQ8F/MTgZ4h2d4ljNgmZ4pFszDpmIRIuFP57YWDMcFu7uHMziLMyEKxJBl2MbbM6DZub2NQ3sUZ3K2vkMMGIdNKbi1ADqcZe1q2i7VKzibd8BCEE8GUQ3PQ4ez3KWcGNCbeAfv0neKm85hWRvqo+Hu1LJ2HuwTvAt3tkAciKbDIDO/7iE4l7X9rpqCA30pa3uLwt3nYRAIuIfV5mcWN6msnapf8G405IGQMGry+prB0DiloqPfwng3G3SLDltXnTt5S6PK3NnRr9jAFhDJyLsZAJcz8SGcjOr+xAa0XGGTpOfeSMrYUKcyKlx+gS1oSkbY9VXpm2+rjHKXX2ALm2TS2qq96duaVHp5u19gC5skbc3V05N23FWT8NIfsIVNkra6siGttE2llbX7h9JWhaZ6mfj+lfaJZW3Kn6DJJRNWXWqbgN+Y39jRqtDUKKkrL15PxfdjammrfyhpVmiqlNQVfx9KwX/cFJT6A7awSVLeu1CeUlinxu9o8QsphbUKTWUyfvn5Wcm511QSXvoDtrBJkpedewzaE/E58wdsYZMopSQ55+z+pKK6fo9KKqpXaDkAIgxLWnomK3H1JZVQ0tyv2ICW6XAvLHHJ6QD4K6GgRsVjQ3/g3WzQLffCKGHJqfHgisOXaNy2f/oU7+TdukGIUW5xJ0vic6+oWGzsS7yTd4NoncPiF594GC7FFlSrcVub+gTv4p36btE6h1Fc9nFn7IpzKmZrY5/gXbwTxI0O8xD77rGPYtZeVtE42Jt4B+8C8WAcNu6dPwZDzVh8PYzdcqdXcDbvgMdBPDHEOG7R0Xkxqy6oKAzpDZzNO0AMmIfFvH1kABwZk1epIotv9yjOxOyj+g4xYB4W/dbvlAAdozGsJ3Gmni0mzMPGLvyN6IMo/NojNt/qEZyFmR+CWLAIW3CYaDDUR+D/TTgGdwdncJaeKRbMw8bM/9XdosjlZ5VjU0O3cAZngXTBPCzqzV/cPQAXHeuqVBgu8IVj/TXFGXqWdME8LHLez55mRSw9qUI31fuEZzFjNogNFmFvHPQUACfC8IUbUlTvlbDcSsWzeobYYB42+vWfjDwbnn3c6zCewdnnQGwyD4t47UcjAXA2JO+qCi6ss4V7eUafFZuswn4wM8ex+LgKwqV2cC/OzAXxgnlY+KvfmxkIVaPyr6tRhbWWuId79RnxgnmYY+53VlaHLj2lRm6stRSSc0ph7xoQL1mEzfnWyhPQNrKgGgE1hrjGPTAExEvmYWGvfNOVPUErz6vhiDDCNez5GMQHVmFfd2Vq6MLD6smCGkNc4x4QH5iHPfXyV125H64NX1eFkOpO+I5reo/4wDws9KUDdhSPxFdNIGLc8R3XQHxkFfalHYnB8w6qYRuqO+E7roH4yDws5MX9dtwHlYF5lQi6qYjPfKfXxEcWYbO/sKtsBP50QxFFI/D/De8qQLrBPCwYizY9M2r+of/D+Mx3ID6zDJv1uaVwGfAIBDqGJsbhZ9eQ/BuK+OwYlhzPNe7hXh9Yhe0zwqBB4IAYouCZn1wIXH5GEZ/d1sjBMzzrBfOwoBf2GWFYBMS4C03JWRA087OaoBmf3uCz5zrP8Kx9lmF7jXiE2RbBs95ghB0MIo8/pS0OeJAzvOBLmKY//BAGURBN+jmMa3qPkLdh/wKOL8SpLbnYFgAAAABJRU5ErkJggg==';
 | 
			
		||||
 | 
			
		||||
@ -6,12 +6,11 @@ import {
 | 
			
		||||
} from '@nocobase/client';
 | 
			
		||||
import React, { useContext } from 'react';
 | 
			
		||||
import { MapBlockOptions } from './block';
 | 
			
		||||
import Configuration from './components/Configuration';
 | 
			
		||||
import Map from './components/Map';
 | 
			
		||||
import { Configuration, Map } from './components';
 | 
			
		||||
import { interfaces } from './fields';
 | 
			
		||||
import { MapInitializer } from './initialize';
 | 
			
		||||
import { useMapTranslation } from './locale';
 | 
			
		||||
import './locale';
 | 
			
		||||
import { useMapTranslation } from './locale';
 | 
			
		||||
 | 
			
		||||
const MapProvider = React.memo((props) => {
 | 
			
		||||
  const ctx = useContext(CollectionManagerContext);
 | 
			
		||||
 | 
			
		||||
@ -43,6 +43,8 @@ const locale = {
 | 
			
		||||
  'Edit description': '编辑描述',
 | 
			
		||||
  'Map field': '地图字段',
 | 
			
		||||
  'Marker field': '标记字段',
 | 
			
		||||
  'Load google maps failed, Please check the Api key and refresh the page':
 | 
			
		||||
    '加载谷歌地图失败,请检查 Api key 并刷新页面',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default locale;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								packages/plugins/map/src/client/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/plugins/map/src/client/types.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
export type MapEditorType = 'point' | 'polygon' | 'lineString' | 'circle';
 | 
			
		||||
							
								
								
									
										0
									
								
								packages/plugins/map/src/client/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								packages/plugins/map/src/client/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										16
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								yarn.lock
									
									
									
									
									
								
							@ -4007,6 +4007,13 @@
 | 
			
		||||
  version "1.1.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
 | 
			
		||||
 | 
			
		||||
"@googlemaps/js-api-loader@^1.16.1":
 | 
			
		||||
  version "1.16.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@googlemaps/js-api-loader/-/js-api-loader-1.16.1.tgz#db74c851f8e26a50f578a07c195a87e1fb60ae90"
 | 
			
		||||
  integrity sha512-x6M48e2nkIrp1KGNwXEVMRTCzruBw/w1HItlhVXL6Lp5pKuTcJEfOCE4tjPk3n6kLcMuJYXE8aluYMmakn7tTw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    fast-deep-equal "^3.1.3"
 | 
			
		||||
 | 
			
		||||
"@hapi/hoek@^11.0.2":
 | 
			
		||||
  version "11.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-11.0.2.tgz#cb3ea547daac7de5c9cf1d960c3f35c34f065427"
 | 
			
		||||
@ -6459,6 +6466,11 @@
 | 
			
		||||
    "@types/minimatch" "*"
 | 
			
		||||
    "@types/node" "*"
 | 
			
		||||
 | 
			
		||||
"@types/google.maps@^3.53.4":
 | 
			
		||||
  version "3.53.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/google.maps/-/google.maps-3.53.4.tgz#741442764ebaef1a6705f3ab2c047ffeba333020"
 | 
			
		||||
  integrity sha512-IiDAYTONQEKCBssPtoM0XqWF8YIIk2leba4NOf9qVQ/d8l5gLuS3QT87TrX6/u8rMonQAXC9KEDmM4q7sT6MWg==
 | 
			
		||||
 | 
			
		||||
"@types/graceful-fs@^4.1.2", "@types/graceful-fs@^4.1.3":
 | 
			
		||||
  version "4.1.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae"
 | 
			
		||||
@ -21935,7 +21947,7 @@ react-dom-factories@^1.0.0:
 | 
			
		||||
  version "1.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react-dom-factories/-/react-dom-factories-1.0.2.tgz#eb7705c4db36fb501b3aa38ff759616aa0ff96e0"
 | 
			
		||||
 | 
			
		||||
react-dom@18.1.0, react-dom@^16.8.6, react-dom@^18.0.0:
 | 
			
		||||
react-dom@17.x, react-dom@18.1.0, react-dom@^16.8.6, react-dom@^18.0.0:
 | 
			
		||||
  version "18.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
 | 
			
		||||
  integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
 | 
			
		||||
@ -22156,7 +22168,7 @@ react-to-print@^2.14.7:
 | 
			
		||||
  version "2.14.13"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react-to-print/-/react-to-print-2.14.13.tgz#cd0349f7ef93c8af5120fac0ef6c4f3d100df490"
 | 
			
		||||
 | 
			
		||||
react@18.1.0, react@^16.8.6, react@^18.0.0:
 | 
			
		||||
react@17.x, react@18.1.0, react@^16.8.6, react@^18.0.0:
 | 
			
		||||
  version "18.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
 | 
			
		||||
  integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user