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",
|
"main": "./lib/index.js",
|
||||||
"types": "./lib/index.d.ts",
|
"types": "./lib/index.d.ts",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nocobase/server": "0.10.0-alpha.5",
|
"@types/google.maps": "^3.53.4",
|
||||||
"@nocobase/test": "0.10.0-alpha.5"
|
"@types/react": "^17.0.0",
|
||||||
|
"@types/react-dom": "^17.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@amap/amap-jsapi-loader": "^1.0.1",
|
"@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 React, { useState } from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { MapBlockComponent } from '../components';
|
||||||
import { RecursionField, Schema, useFieldSchema } from '@formily/react';
|
import { useCollection, useProps } from '@nocobase/client';
|
||||||
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==';
|
|
||||||
|
|
||||||
export const MapBlock = (props) => {
|
export const MapBlock = (props) => {
|
||||||
const { fieldNames, dataSource = [], fixedBlock, zoom, setSelectedRecordKeys } = useProps(props);
|
const { fieldNames } = useProps(props);
|
||||||
const { getField, getPrimaryKey } = useCollection();
|
const { getField } = useCollection();
|
||||||
const field = getField(fieldNames?.field);
|
const field = getField(fieldNames?.field);
|
||||||
const [isMapInitialization, setIsMapInitialization] = useState(false);
|
const fieldComponentProps = field?.uiSchema?.['x-component-props'];
|
||||||
const mapRef = useRef<AMapForwardedRefProps>();
|
return <MapBlockComponent {...fieldComponentProps} {...props} />;
|
||||||
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 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 { 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 { FormDialog, FormLayout } from '@formily/antd';
|
||||||
|
import { SchemaOptionsContext } from '@formily/react';
|
||||||
|
import { DataBlockInitializer, SchemaComponent, SchemaComponentOptions, useCollectionManager } from '@nocobase/client';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useMapTranslation } from '../locale';
|
import { useMapTranslation } from '../locale';
|
||||||
import { createMapBlockSchema } from './utils';
|
import { createMapBlockSchema } from './utils';
|
||||||
@ -9,6 +9,7 @@ import { createMapBlockSchema } from './utils';
|
|||||||
export const MapBlockInitializer = (props) => {
|
export const MapBlockInitializer = (props) => {
|
||||||
const { insert } = props;
|
const { insert } = props;
|
||||||
const options = useContext(SchemaOptionsContext);
|
const options = useContext(SchemaOptionsContext);
|
||||||
|
console.log('🚀 ~ file: MapBlockInitializer.tsx:12 ~ MapBlockInitializer ~ options:', options);
|
||||||
const { getCollectionFieldsOptions } = useCollectionManager();
|
const { getCollectionFieldsOptions } = useCollectionManager();
|
||||||
const { t } = useMapTranslation();
|
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 { useCollection } from '@nocobase/client';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import { Alert, Button, Modal, Spin } from 'antd';
|
import { Alert, Button, Modal, Spin } from 'antd';
|
||||||
import React, { useEffect, useCallback, useRef, useState, useMemo, useImperativeHandle } from 'react';
|
import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router';
|
||||||
import { useMapConfiguration } from '../hooks';
|
import { useMapConfiguration } from '../../hooks';
|
||||||
import { useMapTranslation } from '../locale';
|
import { useMapTranslation } from '../../locale';
|
||||||
import Search from './Search';
|
import { MapEditorType } from '../../types';
|
||||||
|
import { Search } from './Search';
|
||||||
export type MapEditorType = 'point' | 'polygon' | 'lineString' | 'circle';
|
|
||||||
|
|
||||||
export interface AMapComponentProps {
|
export interface AMapComponentProps {
|
||||||
value?: any;
|
value?: any;
|
||||||
@ -80,9 +79,10 @@ export interface AMapForwardedRefProps {
|
|||||||
close: (clear?: boolean) => void;
|
close: (clear?: boolean) => void;
|
||||||
};
|
};
|
||||||
overlay: AMap.Polygon;
|
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 { accessKey, securityJsCode } = useMapConfiguration(props.mapType) || {};
|
||||||
const { value, onChange, block = false, readonly, disabled = block, zoom = 13, overlayCommonOptions } = props;
|
const { value, onChange, block = false, readonly, disabled = block, zoom = 13, overlayCommonOptions } = props;
|
||||||
const { t } = useMapTranslation();
|
const { t } = useMapTranslation();
|
||||||
@ -358,13 +358,14 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
|
|||||||
overlay: overlay.current,
|
overlay: overlay.current,
|
||||||
mouseTool: () => mouseTool.current,
|
mouseTool: () => mouseTool.current,
|
||||||
editor: () => editor.current,
|
editor: () => editor.current,
|
||||||
|
errMessage,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!accessKey || errMessage) {
|
if (!accessKey || errMessage) {
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
action={
|
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')}
|
{t('Go to the configuration page')}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@ -458,5 +459,3 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default AMapComponent;
|
|
@ -2,14 +2,14 @@ import { css } from '@emotion/css';
|
|||||||
import { useDebounceFn } from 'ahooks';
|
import { useDebounceFn } from 'ahooks';
|
||||||
import { message, Select } from 'antd';
|
import { message, Select } from 'antd';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { useMapTranslation } from '../locale';
|
import { useMapTranslation } from '../../locale';
|
||||||
|
|
||||||
interface SearchProps {
|
interface SearchProps {
|
||||||
aMap: any;
|
aMap: any;
|
||||||
toCenter: (p: any) => void;
|
toCenter: (p: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Search = (props: SearchProps) => {
|
export const Search = (props: SearchProps) => {
|
||||||
const { aMap, toCenter } = props;
|
const { aMap, toCenter } = props;
|
||||||
const { t } = useMapTranslation();
|
const { t } = useMapTranslation();
|
||||||
const placeSearch = useRef<any>();
|
const placeSearch = useRef<any>();
|
||||||
@ -90,5 +90,3 @@ const Search = (props: SearchProps) => {
|
|||||||
</div>
|
</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 { useAPIClient, useCompile } from '@nocobase/client';
|
||||||
import { useBoolean } from 'ahooks';
|
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 React, { useEffect, useMemo } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import { MapTypes } from '../constants';
|
import { MapTypes } from '../constants';
|
||||||
import { MapConfigurationResourceKey, getSSKey, useMapConfiguration } from '../hooks';
|
import { getSSKey, MapConfigurationResourceKey, useMapConfiguration } from '../hooks';
|
||||||
import { useMapTranslation } from '../locale';
|
import { useMapTranslation } from '../locale';
|
||||||
|
|
||||||
const AMapConfiguration = ({ type }) => {
|
interface BaseConfigurationProps {
|
||||||
|
type: 'amap' | 'google';
|
||||||
|
}
|
||||||
|
const BaseConfiguration: React.FC<BaseConfigurationProps> = ({ type, children }) => {
|
||||||
const { t } = useMapTranslation();
|
const { t } = useMapTranslation();
|
||||||
const [isDisabled, disableAction] = useBoolean(false);
|
const [isDisabled, disableAction] = useBoolean(false);
|
||||||
const apiClient = useAPIClient();
|
const apiClient = useAPIClient();
|
||||||
@ -31,6 +35,7 @@ const AMapConfiguration = ({ type }) => {
|
|||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
sessionStorage.removeItem(getSSKey(type));
|
sessionStorage.removeItem(getSSKey(type));
|
||||||
|
disableAction.toggle();
|
||||||
message.success(t('Saved successfully'));
|
message.success(t('Saved successfully'));
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -38,20 +43,15 @@ const AMapConfiguration = ({ type }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Form form={form} layout="vertical" onFinish={onSubmit}>
|
<Form disabled={isDisabled} form={form} layout="vertical" onFinish={onSubmit}>
|
||||||
<Form.Item required name="accessKey" label={t('Access key')}>
|
{children}
|
||||||
<Input disabled={isDisabled} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item required name="securityJsCode" label={t('securityJsCode or serviceHost')}>
|
|
||||||
<Input disabled={isDisabled} />
|
|
||||||
</Form.Item>
|
|
||||||
{isDisabled ? (
|
{isDisabled ? (
|
||||||
<Button onClick={disableAction.toggle} type="ghost">
|
<Button disabled={false} onClick={disableAction.toggle} type="ghost">
|
||||||
{t('Edit')}
|
{t('Edit')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button type="primary" htmlType="submit">
|
<Button disabled={false} type="primary" htmlType="submit">
|
||||||
{t('Save')}
|
{t('Save')}
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</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 = {
|
const components = {
|
||||||
amap: AMapConfiguration,
|
amap: AMapConfiguration,
|
||||||
google: () => <div>Coming soon</div>,
|
google: GoogleMapConfiguration,
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabList = MapTypes.map((item) => {
|
const tabList = MapTypes.map((item) => {
|
||||||
@ -72,22 +97,21 @@ const tabList = MapTypes.map((item) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const Configuration = () => {
|
export const Configuration = () => {
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
|
const location = useLocation();
|
||||||
|
const search = new URLSearchParams(location.search);
|
||||||
return (
|
return (
|
||||||
<Card bordered>
|
<Card bordered>
|
||||||
<Tabs
|
<Tabs type="card" defaultActiveKey={search.get('tab')}>
|
||||||
type="card"
|
{tabList.map((tab) => {
|
||||||
items={tabList.map((tab) => {
|
return (
|
||||||
return {
|
<Tabs.TabPane key={tab.value} tab={compile(tab.label)}>
|
||||||
key: tab.value,
|
<tab.component type={tab.value} />
|
||||||
label: compile(tab.label),
|
</Tabs.TabPane>
|
||||||
children: <tab.component type={tab.value} />,
|
);
|
||||||
};
|
|
||||||
})}
|
})}
|
||||||
/>
|
</Tabs>
|
||||||
</Card>
|
</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 { connect, mapReadPretty } from '@formily/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AMapComponent, { AMapComponentProps } from './AMap';
|
import { AMapComponentProps } from './AMap';
|
||||||
import ReadPretty from './ReadPretty';
|
|
||||||
import { css } from '@emotion/css';
|
|
||||||
import Designer from './Designer';
|
import Designer from './Designer';
|
||||||
|
import { MapComponent } from './MapComponent';
|
||||||
|
import ReadPretty from './ReadPretty';
|
||||||
|
|
||||||
type MapProps = AMapComponentProps;
|
type MapProps = AMapComponentProps;
|
||||||
|
|
||||||
@ -18,7 +19,7 @@ const InternalMap = connect((props: MapProps) => {
|
|||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{props.mapType ? <AMapComponent {...props} /> : null}
|
<MapComponent {...props} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, mapReadPretty(ReadPretty));
|
}, mapReadPretty(ReadPretty));
|
||||||
@ -29,4 +30,4 @@ const Map = InternalMap as typeof InternalMap & {
|
|||||||
|
|
||||||
Map.Designer = Designer;
|
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 { useFieldSchema, useForm } from '@formily/react';
|
||||||
import { EllipsisWithTooltip, useCollection } from '@nocobase/client';
|
import { EllipsisWithTooltip, useCollection, useFieldTitle } from '@nocobase/client';
|
||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
import AMapComponent from './AMap';
|
import { MapComponent } from './MapComponent';
|
||||||
|
|
||||||
const ReadPretty = (props) => {
|
const ReadPretty = (props) => {
|
||||||
const { value } = props;
|
const { value } = props;
|
||||||
@ -9,13 +9,8 @@ const ReadPretty = (props) => {
|
|||||||
const { getField } = useCollection();
|
const { getField } = useCollection();
|
||||||
const collectionField = getField(fieldSchema.name);
|
const collectionField = getField(fieldSchema.name);
|
||||||
const mapType = props.mapType || collectionField?.uiSchema['x-component-props']?.mapType;
|
const mapType = props.mapType || collectionField?.uiSchema['x-component-props']?.mapType;
|
||||||
const field = useField();
|
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
useEffect(() => {
|
useFieldTitle();
|
||||||
if (!field.title && collectionField?.uiSchema?.title) {
|
|
||||||
field.title = collectionField?.uiSchema.title;
|
|
||||||
}
|
|
||||||
}, collectionField?.title);
|
|
||||||
|
|
||||||
if (!form.readPretty) {
|
if (!form.readPretty) {
|
||||||
return (
|
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;
|
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('AMap'), value: 'amap' },
|
||||||
{ label: generateNTemplate('Google Maps'), value: 'google' },
|
{ 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';
|
} from '@nocobase/client';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { MapBlockOptions } from './block';
|
import { MapBlockOptions } from './block';
|
||||||
import Configuration from './components/Configuration';
|
import { Configuration, Map } from './components';
|
||||||
import Map from './components/Map';
|
|
||||||
import { interfaces } from './fields';
|
import { interfaces } from './fields';
|
||||||
import { MapInitializer } from './initialize';
|
import { MapInitializer } from './initialize';
|
||||||
import { useMapTranslation } from './locale';
|
|
||||||
import './locale';
|
import './locale';
|
||||||
|
import { useMapTranslation } from './locale';
|
||||||
|
|
||||||
const MapProvider = React.memo((props) => {
|
const MapProvider = React.memo((props) => {
|
||||||
const ctx = useContext(CollectionManagerContext);
|
const ctx = useContext(CollectionManagerContext);
|
||||||
|
@ -43,6 +43,8 @@ const locale = {
|
|||||||
'Edit description': '编辑描述',
|
'Edit description': '编辑描述',
|
||||||
'Map field': '地图字段',
|
'Map field': '地图字段',
|
||||||
'Marker field': '标记字段',
|
'Marker field': '标记字段',
|
||||||
|
'Load google maps failed, Please check the Api key and refresh the page':
|
||||||
|
'加载谷歌地图失败,请检查 Api key 并刷新页面',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default locale;
|
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"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
|
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":
|
"@hapi/hoek@^11.0.2":
|
||||||
version "11.0.2"
|
version "11.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-11.0.2.tgz#cb3ea547daac7de5c9cf1d960c3f35c34f065427"
|
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-11.0.2.tgz#cb3ea547daac7de5c9cf1d960c3f35c34f065427"
|
||||||
@ -6459,6 +6466,11 @@
|
|||||||
"@types/minimatch" "*"
|
"@types/minimatch" "*"
|
||||||
"@types/node" "*"
|
"@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":
|
"@types/graceful-fs@^4.1.2", "@types/graceful-fs@^4.1.3":
|
||||||
version "4.1.6"
|
version "4.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae"
|
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"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom-factories/-/react-dom-factories-1.0.2.tgz#eb7705c4db36fb501b3aa38ff759616aa0ff96e0"
|
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"
|
version "18.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||||
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
||||||
@ -22156,7 +22168,7 @@ react-to-print@^2.14.7:
|
|||||||
version "2.14.13"
|
version "2.14.13"
|
||||||
resolved "https://registry.yarnpkg.com/react-to-print/-/react-to-print-2.14.13.tgz#cd0349f7ef93c8af5120fac0ef6c4f3d100df490"
|
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"
|
version "18.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||||
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||||
|
Loading…
Reference in New Issue
Block a user