mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 11:56:29 +00:00
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:
parent
5c937500b7
commit
a90d5fe6d6
@ -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')}
|
||||||
|
@ -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',
|
||||||
|
@ -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={
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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());
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user