mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 09:17:23 +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 { useTranslation } from 'react-i18next';
|
||||
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 { useCompile, useDesignable } from '../../hooks';
|
||||
|
||||
@ -47,20 +47,6 @@ export const AssociationFilterItemDesigner = (props) => {
|
||||
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 (
|
||||
<GeneralSchemaDesigner {...props} disableInitializer={true}>
|
||||
<SchemaSettings.ModalItem
|
||||
@ -127,22 +113,7 @@ export const AssociationFilterItemDesigner = (props) => {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SchemaSettings.DefaultSortingRules
|
||||
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.DefaultSortingRules name={collectionField?.target} />
|
||||
<SchemaSettings.SelectItem
|
||||
key="title-field"
|
||||
title={t('Title field')}
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
Space,
|
||||
Switch,
|
||||
} from 'antd';
|
||||
import _, { cloneDeep } from 'lodash';
|
||||
import _, { cloneDeep, get, set } from 'lodash';
|
||||
import React, {
|
||||
ReactNode,
|
||||
createContext,
|
||||
@ -1059,17 +1059,56 @@ SchemaSettings.BlockTitleItem = function BlockTitleItem() {
|
||||
};
|
||||
|
||||
SchemaSettings.DefaultSortingRules = function DefaultSortingRules(props) {
|
||||
const { sort, sortFields, onSubmit } = props;
|
||||
const { path = 'x-component-props.params.sort' } = props;
|
||||
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 (
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Set default sorting rules')}
|
||||
title={title}
|
||||
components={{ ArrayItems }}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Set default sorting rules'),
|
||||
title,
|
||||
properties: {
|
||||
sort: {
|
||||
type: 'array',
|
||||
|
@ -84,6 +84,7 @@ export const MapBlockDesigner = () => {
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<SchemaSettings.DefaultSortingRules path="x-component-props.lineSort" title={t('Concatenation order field')} />
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('The default zoom level of the map')}
|
||||
schema={
|
||||
|
@ -33,6 +33,7 @@ const InternalMapBlockProvider = (props) => {
|
||||
};
|
||||
|
||||
export const MapBlockProvider = (props) => {
|
||||
const uField = useField();
|
||||
const { params, fieldNames } = props;
|
||||
const appends = params.appends || [];
|
||||
const { field } = fieldNames || {};
|
||||
@ -40,7 +41,12 @@ export const MapBlockProvider = (props) => {
|
||||
appends.push(field[0]);
|
||||
}
|
||||
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} />
|
||||
</BlockProvider>
|
||||
);
|
||||
@ -57,5 +63,6 @@ export const useMapBlockProps = () => {
|
||||
...ctx,
|
||||
dataSource: ctx?.service?.data?.data,
|
||||
zoom: ctx?.field?.componentProps?.zoom || 13,
|
||||
lineSort: ctx?.field?.componentProps?.lineSort,
|
||||
};
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { CheckOutlined, EnvironmentOutlined, ExpandOutlined } from '@ant-design/icons';
|
||||
import { RecursionField, Schema, useFieldSchema } from '@formily/react';
|
||||
import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import {
|
||||
ActionContextProvider,
|
||||
RecordProvider,
|
||||
@ -19,7 +19,8 @@ import { AMapComponent, AMapForwardedRefProps } from './Map';
|
||||
import { getSource } from '../../utils';
|
||||
|
||||
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 { getCollectionJoinField } = useCollectionManager();
|
||||
const primaryKey = getPrimaryKey();
|
||||
@ -118,9 +119,9 @@ export const AMapBlock = (props) => {
|
||||
const cf = getCollectionJoinField([name, ...fieldPaths].flat().join('.'));
|
||||
const overlays = dataSource
|
||||
.map((item) => {
|
||||
const data = getSource(item, fieldNames?.field, cf?.interface);
|
||||
const data = getSource(item, fieldNames?.field, cf?.interface)?.filter(Boolean);
|
||||
if (!data?.length) return [];
|
||||
return data?.filter(Boolean).map((mapItem) => {
|
||||
return data.map((mapItem) => {
|
||||
const overlay = mapRef.current?.setOverlay(collectionField.type, mapItem, {
|
||||
strokeColor: '#4e9bff',
|
||||
fillColor: '#4e9bff',
|
||||
@ -188,13 +189,46 @@ export const AMapBlock = (props) => {
|
||||
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 () => {
|
||||
overlays.forEach((ov) => {
|
||||
ov.remove();
|
||||
});
|
||||
events.forEach((e) => e());
|
||||
};
|
||||
}, [dataSource, isMapInitialization, fieldNames, name, primaryKey, collectionField.type, isConnected]);
|
||||
}, [dataSource, isMapInitialization, fieldNames, name, primaryKey, collectionField.type, isConnected, lineSort]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
|
@ -29,8 +29,16 @@ const labelClass = css`
|
||||
border: 1px solid #0000f5;
|
||||
`;
|
||||
|
||||
const pointClass = css`
|
||||
margin-top: -64px;
|
||||
padding: 2px 4px;
|
||||
background: #fff;
|
||||
border: 1px solid #0000f5;
|
||||
`;
|
||||
|
||||
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 primaryKey = getPrimaryKey();
|
||||
const { marker: markerName = 'id' } = fieldNames;
|
||||
@ -151,7 +159,7 @@ export const GoogleMapsBlock = (props) => {
|
||||
: fieldNames?.field;
|
||||
const cf = getCollectionJoinField([name, ...fieldPaths].flat().join('.'));
|
||||
|
||||
const overlays: google.maps.Polygon[] = dataSource
|
||||
const overlays: google.maps.MVCObject[] = dataSource
|
||||
.map((item) => {
|
||||
const data = getSource(item, fieldNames?.field, cf?.interface);
|
||||
if (!data?.length) return [];
|
||||
@ -177,7 +185,6 @@ export const GoogleMapsBlock = (props) => {
|
||||
.filter(Boolean);
|
||||
|
||||
overlaysRef.current = overlays;
|
||||
mapRef.current?.setFitView(overlays);
|
||||
|
||||
const events = overlays.map((o: google.maps.MVCObject) => {
|
||||
const onClick = (event) => {
|
||||
@ -217,9 +224,53 @@ export const GoogleMapsBlock = (props) => {
|
||||
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 () => {
|
||||
overlays.forEach((ov) => {
|
||||
ov.setMap(null);
|
||||
(ov as google.maps.Polygon).setMap(null);
|
||||
ov.unbindAll();
|
||||
});
|
||||
events.forEach((e) => e());
|
||||
|
@ -47,6 +47,9 @@ const locale = {
|
||||
'Load google maps failed, Please check the Api key and refresh the page':
|
||||
'加载谷歌地图失败,请检查 Api key 并刷新页面',
|
||||
'Create map block': '创建地图区块',
|
||||
'Start point': '起点',
|
||||
'End point': '终点',
|
||||
'Concatenation order field': '连接顺序字段',
|
||||
};
|
||||
|
||||
export default locale;
|
||||
|
Loading…
Reference in New Issue
Block a user