feat(map-plugin): supports connecting each point into a line (#2216)

* feat(map-plugin): supports connecting each point into a line

* feat: update

* feat: support startPoint and endPoint

* fix: data refresh is incorrect when sort changed

* feat: support google map
This commit is contained in:
Dunqing 2023-11-02 15:03:49 +08:00 committed by GitHub
parent 5c937500b7
commit a90d5fe6d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 151 additions and 45 deletions

View File

@ -3,7 +3,7 @@ import _ from 'lodash';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useFormBlockContext } from '../../../block-provider'; import { useFormBlockContext } from '../../../block-provider';
import { useCollection, useCollectionManager, useSortFields } from '../../../collection-manager'; import { useCollection, useCollectionManager } from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useCompile, useDesignable } from '../../hooks'; import { useCompile, useDesignable } from '../../hooks';
@ -47,20 +47,6 @@ export const AssociationFilterItemDesigner = (props) => {
dn.refresh(); dn.refresh();
}; };
const sortFields = useSortFields(collectionField?.target);
const defaultSort = fieldSchema?.['x-component-props']?.params?.sort || [];
const sort = defaultSort?.map((item: string) => {
return item.startsWith('-')
? {
field: item.substring(1),
direction: 'desc',
}
: {
field: item,
direction: 'asc',
};
});
return ( return (
<GeneralSchemaDesigner {...props} disableInitializer={true}> <GeneralSchemaDesigner {...props} disableInitializer={true}>
<SchemaSettings.ModalItem <SchemaSettings.ModalItem
@ -127,22 +113,7 @@ export const AssociationFilterItemDesigner = (props) => {
}); });
}} }}
/> />
<SchemaSettings.DefaultSortingRules <SchemaSettings.DefaultSortingRules name={collectionField?.target} />
sort={sort}
sortFields={sortFields}
onSubmit={({ sort }) => {
_.set(field.componentProps, 'params', {
...field.componentProps?.params,
sort: sort.map((item) => {
return item.direction === 'desc' ? `-${item.field}` : item.field;
}),
});
fieldSchema['x-component-props']['params'] = field.componentProps.params;
dn.emit('patch', {
schema: fieldSchema,
});
}}
/>
<SchemaSettings.SelectItem <SchemaSettings.SelectItem
key="title-field" key="title-field"
title={t('Title field')} title={t('Title field')}

View File

@ -19,7 +19,7 @@ import {
Space, Space,
Switch, Switch,
} from 'antd'; } from 'antd';
import _, { cloneDeep } from 'lodash'; import _, { cloneDeep, get, set } from 'lodash';
import React, { import React, {
ReactNode, ReactNode,
createContext, createContext,
@ -1059,17 +1059,56 @@ SchemaSettings.BlockTitleItem = function BlockTitleItem() {
}; };
SchemaSettings.DefaultSortingRules = function DefaultSortingRules(props) { SchemaSettings.DefaultSortingRules = function DefaultSortingRules(props) {
const { sort, sortFields, onSubmit } = props; const { path = 'x-component-props.params.sort' } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const { dn } = useDesignable();
const fieldSchema = useFieldSchema();
const field = useField();
const title = props.title || t('Set default sorting rules');
const { name } = useCollection();
const defaultSort = get(fieldSchema, path) || [];
const sort = defaultSort?.map((item: string) => {
return item.startsWith('-')
? {
field: item.substring(1),
direction: 'desc',
}
: {
field: item,
direction: 'asc',
};
});
const sortFields = useSortFields(props.name || name);
const onSubmit = async ({ sort }) => {
if (props?.onSubmit) {
return props.onSubmit({ sort });
}
const value = sort.map((item) => {
return item.direction === 'desc' ? `-${item.field}` : item.field;
});
set(
field,
path.replace('x-component-props', 'componentProps').replace('x-decorator-props', 'decoratorProps'),
value,
);
set(fieldSchema, path, value);
await dn.emit('patch', {
schema: fieldSchema,
});
return props.onSubmitAfter?.();
};
return ( return (
<SchemaSettings.ModalItem <SchemaSettings.ModalItem
title={t('Set default sorting rules')} title={title}
components={{ ArrayItems }} components={{ ArrayItems }}
schema={ schema={
{ {
type: 'object', type: 'object',
title: t('Set default sorting rules'), title,
properties: { properties: {
sort: { sort: {
type: 'array', type: 'array',

View File

@ -84,6 +84,7 @@ export const MapBlockDesigner = () => {
}} }}
/> />
) : null} ) : null}
<SchemaSettings.DefaultSortingRules path="x-component-props.lineSort" title={t('Concatenation order field')} />
<SchemaSettings.ModalItem <SchemaSettings.ModalItem
title={t('The default zoom level of the map')} title={t('The default zoom level of the map')}
schema={ schema={

View File

@ -33,6 +33,7 @@ const InternalMapBlockProvider = (props) => {
}; };
export const MapBlockProvider = (props) => { export const MapBlockProvider = (props) => {
const uField = useField();
const { params, fieldNames } = props; const { params, fieldNames } = props;
const appends = params.appends || []; const appends = params.appends || [];
const { field } = fieldNames || {}; const { field } = fieldNames || {};
@ -40,7 +41,12 @@ export const MapBlockProvider = (props) => {
appends.push(field[0]); appends.push(field[0]);
} }
return ( return (
<BlockProvider name="map" {...props} params={{ ...params, appends, paginate: false }}> <BlockProvider
name="map"
{...props}
runWhenParamsChanged
params={{ ...params, appends, paginate: false, sort: uField.componentProps.lineSort }}
>
<InternalMapBlockProvider {...props} /> <InternalMapBlockProvider {...props} />
</BlockProvider> </BlockProvider>
); );
@ -57,5 +63,6 @@ export const useMapBlockProps = () => {
...ctx, ...ctx,
dataSource: ctx?.service?.data?.data, dataSource: ctx?.service?.data?.data,
zoom: ctx?.field?.componentProps?.zoom || 13, zoom: ctx?.field?.componentProps?.zoom || 13,
lineSort: ctx?.field?.componentProps?.lineSort,
}; };
}; };

View File

@ -1,5 +1,5 @@
import { CheckOutlined, EnvironmentOutlined, ExpandOutlined } from '@ant-design/icons'; import { CheckOutlined, EnvironmentOutlined, ExpandOutlined } from '@ant-design/icons';
import { RecursionField, Schema, useFieldSchema } from '@formily/react'; import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
import { import {
ActionContextProvider, ActionContextProvider,
RecordProvider, RecordProvider,
@ -19,7 +19,8 @@ import { AMapComponent, AMapForwardedRefProps } from './Map';
import { getSource } from '../../utils'; import { getSource } from '../../utils';
export const AMapBlock = (props) => { export const AMapBlock = (props) => {
const { collectionField, fieldNames, dataSource, fixedBlock, zoom, setSelectedRecordKeys } = useProps(props); const { collectionField, fieldNames, dataSource, fixedBlock, zoom, setSelectedRecordKeys, lineSort } =
useProps(props);
const { name, getPrimaryKey } = useCollection(); const { name, getPrimaryKey } = useCollection();
const { getCollectionJoinField } = useCollectionManager(); const { getCollectionJoinField } = useCollectionManager();
const primaryKey = getPrimaryKey(); const primaryKey = getPrimaryKey();
@ -118,9 +119,9 @@ export const AMapBlock = (props) => {
const cf = getCollectionJoinField([name, ...fieldPaths].flat().join('.')); const cf = getCollectionJoinField([name, ...fieldPaths].flat().join('.'));
const overlays = dataSource const overlays = dataSource
.map((item) => { .map((item) => {
const data = getSource(item, fieldNames?.field, cf?.interface); const data = getSource(item, fieldNames?.field, cf?.interface)?.filter(Boolean);
if (!data?.length) return []; if (!data?.length) return [];
return data?.filter(Boolean).map((mapItem) => { return data.map((mapItem) => {
const overlay = mapRef.current?.setOverlay(collectionField.type, mapItem, { const overlay = mapRef.current?.setOverlay(collectionField.type, mapItem, {
strokeColor: '#4e9bff', strokeColor: '#4e9bff',
fillColor: '#4e9bff', fillColor: '#4e9bff',
@ -188,13 +189,46 @@ export const AMapBlock = (props) => {
return () => o.off('click', onClick); return () => o.off('click', onClick);
}); });
if (collectionField.type === 'point' && lineSort?.length && overlays?.length > 1) {
const positions = overlays.map((o: AMap.Marker) => o.getPosition());
(overlays[0] as AMap.Marker).setzIndex(13);
(overlays[overlays.length - 1] as AMap.Marker).setzIndex(13);
const createText = (start = true) => {
if (!mapRef.current?.map) return;
return new AMap.Text({
label: {
direction: 'top',
offset: [0, 0],
content: start ? t('Start point') : t('End point'),
},
position: positions[start ? 0 : positions.length - 1],
map: mapRef.current?.map,
});
};
overlays.push(
...[
mapRef.current?.setOverlay('lineString', positions, {
strokeColor: '#4e9bff',
fillColor: '#4e9bff',
strokeWeight: 2,
cursor: 'pointer',
}),
createText(),
createText(false),
].filter(Boolean),
);
}
return () => { return () => {
overlays.forEach((ov) => { overlays.forEach((ov) => {
ov.remove(); ov.remove();
}); });
events.forEach((e) => e()); events.forEach((e) => e());
}; };
}, [dataSource, isMapInitialization, fieldNames, name, primaryKey, collectionField.type, isConnected]); }, [dataSource, isMapInitialization, fieldNames, name, primaryKey, collectionField.type, isConnected, lineSort]);
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {

View File

@ -29,8 +29,16 @@ const labelClass = css`
border: 1px solid #0000f5; border: 1px solid #0000f5;
`; `;
const pointClass = css`
margin-top: -64px;
padding: 2px 4px;
background: #fff;
border: 1px solid #0000f5;
`;
export const GoogleMapsBlock = (props) => { export const GoogleMapsBlock = (props) => {
const { collectionField, fieldNames, dataSource, fixedBlock, zoom, setSelectedRecordKeys } = useProps(props); const { collectionField, fieldNames, dataSource, fixedBlock, zoom, setSelectedRecordKeys, lineSort } =
useProps(props);
const { getPrimaryKey } = useCollection(); const { getPrimaryKey } = useCollection();
const primaryKey = getPrimaryKey(); const primaryKey = getPrimaryKey();
const { marker: markerName = 'id' } = fieldNames; const { marker: markerName = 'id' } = fieldNames;
@ -151,7 +159,7 @@ export const GoogleMapsBlock = (props) => {
: fieldNames?.field; : fieldNames?.field;
const cf = getCollectionJoinField([name, ...fieldPaths].flat().join('.')); const cf = getCollectionJoinField([name, ...fieldPaths].flat().join('.'));
const overlays: google.maps.Polygon[] = dataSource const overlays: google.maps.MVCObject[] = dataSource
.map((item) => { .map((item) => {
const data = getSource(item, fieldNames?.field, cf?.interface); const data = getSource(item, fieldNames?.field, cf?.interface);
if (!data?.length) return []; if (!data?.length) return [];
@ -177,7 +185,6 @@ export const GoogleMapsBlock = (props) => {
.filter(Boolean); .filter(Boolean);
overlaysRef.current = overlays; overlaysRef.current = overlays;
mapRef.current?.setFitView(overlays);
const events = overlays.map((o: google.maps.MVCObject) => { const events = overlays.map((o: google.maps.MVCObject) => {
const onClick = (event) => { const onClick = (event) => {
@ -217,9 +224,53 @@ export const GoogleMapsBlock = (props) => {
return () => o.unbindAll(); return () => o.unbindAll();
}); });
if (collectionField.type === 'point' && lineSort?.length && overlays?.length > 1) {
const positions = overlays.map((o: google.maps.Marker) => o.getPosition());
(overlays[0] as google.maps.Marker).setZIndex(138);
(overlays[overlays.length - 1] as google.maps.Marker).setZIndex(138);
const createText = (start = true) => {
if (!mapRef.current?.map) return;
return new google.maps.Marker({
label: {
// direction: 'top',
// offset: [0, 0],
className: pointClass,
fontFamily: 'inherit',
fontSize: '13px',
color: '#333',
text: start ? t('Start point') : t('End point'),
},
icon: getIcon(defaultImage),
position: start ? positions[0] : positions[positions.length - 1],
map: mapRef.current.map,
});
};
overlays.push(
...[
mapRef.current.setOverlay(
'lineString',
positions.map((p) => [p.lng(), p.lat()]),
{
strokeColor: '#4e9bff',
fillColor: '#4e9bff',
strokeWeight: 2,
cursor: 'pointer',
},
),
createText(),
createText(false),
].filter(Boolean),
);
}
mapRef.current?.setFitView(overlays);
return () => { return () => {
overlays.forEach((ov) => { overlays.forEach((ov) => {
ov.setMap(null); (ov as google.maps.Polygon).setMap(null);
ov.unbindAll(); ov.unbindAll();
}); });
events.forEach((e) => e()); events.forEach((e) => e());

View File

@ -47,6 +47,9 @@ const locale = {
'Load google maps failed, Please check the Api key and refresh the page': 'Load google maps failed, Please check the Api key and refresh the page':
'加载谷歌地图失败,请检查 Api key 并刷新页面', '加载谷歌地图失败,请检查 Api key 并刷新页面',
'Create map block': '创建地图区块', 'Create map block': '创建地图区块',
'Start point': '起点',
'End point': '终点',
'Concatenation order field': '连接顺序字段',
}; };
export default locale; export default locale;