feat(plugin-map): add map block (#1486)
* feat(plugin-map): add MapBlock * feat: improve implementation to better support multiple fields * feat: support click overlay * fix: the width of select is not 100% * fix: repeat MapBlock * fix: loss initializer * feat: support selected marker in map * feat: support select point use box * fix: fixedBlock not work * fix: template not work * feat: improve ui * feat: improve selecting * feat: update ui * feat: improve map readpretty * feat: support marker field
This commit is contained in:
parent
2de165c1eb
commit
46c736d7d3
@ -1,9 +1,10 @@
|
|||||||
import { useField, useFieldSchema, useForm } from '@formily/react';
|
import { SchemaExpressionScopeContext, useField, useFieldSchema, useForm } from '@formily/react';
|
||||||
import { message, Modal } from 'antd';
|
import { message, Modal } from 'antd';
|
||||||
import parse from 'json-templates';
|
import parse from 'json-templates';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import omit from 'lodash/omit';
|
import omit from 'lodash/omit';
|
||||||
|
import { useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { useReactToPrint } from 'react-to-print';
|
import { useReactToPrint } from 'react-to-print';
|
||||||
@ -247,11 +248,13 @@ export const useCustomizeUpdateActionProps = () => {
|
|||||||
|
|
||||||
export const useCustomizeBulkUpdateActionProps = () => {
|
export const useCustomizeBulkUpdateActionProps = () => {
|
||||||
const { field, resource, __parent, service } = useBlockRequestContext();
|
const { field, resource, __parent, service } = useBlockRequestContext();
|
||||||
|
const expressionScope = useContext(SchemaExpressionScopeContext);
|
||||||
const actionSchema = useFieldSchema();
|
const actionSchema = useFieldSchema();
|
||||||
const currentRecord = useRecord();
|
const currentRecord = useRecord();
|
||||||
const tableBlockContext = useTableBlockContext();
|
const tableBlockContext = useTableBlockContext();
|
||||||
const { rowKey } = tableBlockContext;
|
const { rowKey } = tableBlockContext;
|
||||||
const { selectedRowKeys } = tableBlockContext.field?.data ?? {};
|
const selectedRecordKeys =
|
||||||
|
tableBlockContext.field?.data?.selectedRowKeys ?? expressionScope?.selectedRecordKeys ?? {};
|
||||||
const currentUserContext = useCurrentUserContext();
|
const currentUserContext = useCurrentUserContext();
|
||||||
const currentUser = currentUserContext?.data?.data;
|
const currentUser = currentUserContext?.data?.data;
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@ -280,12 +283,12 @@ export const useCustomizeBulkUpdateActionProps = () => {
|
|||||||
forceUpdate: false,
|
forceUpdate: false,
|
||||||
};
|
};
|
||||||
if (updateMode === 'selected') {
|
if (updateMode === 'selected') {
|
||||||
if (!selectedRowKeys?.length) {
|
if (!selectedRecordKeys?.length) {
|
||||||
message.error(t('Please select the records to be updated'));
|
message.error(t('Please select the records to be updated'));
|
||||||
actionField.data.loading = false;
|
actionField.data.loading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateData.filter = { $and: [{ [rowKey || 'id']: { $in: selectedRowKeys } }] };
|
updateData.filter = { $and: [{ [rowKey || 'id']: { $in: selectedRecordKeys } }] };
|
||||||
}
|
}
|
||||||
if (!updateData.filter) {
|
if (!updateData.filter) {
|
||||||
updateData.forceUpdate = true;
|
updateData.forceUpdate = true;
|
||||||
@ -328,17 +331,19 @@ export const useCustomizeBulkUpdateActionProps = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCustomizeBulkEditActionProps = () => {
|
export const useCustomizeBulkEditActionProps = (props) => {
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { field, resource, __parent } = useBlockRequestContext();
|
const { field, resource, __parent } = useBlockRequestContext();
|
||||||
|
const expressionScope = useContext(SchemaExpressionScopeContext);
|
||||||
const actionContext = useActionContext();
|
const actionContext = useActionContext();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const actionField = useField();
|
const actionField = useField();
|
||||||
const tableBlockContext = useTableBlockContext();
|
const tableBlockContext = useTableBlockContext();
|
||||||
const { rowKey } = tableBlockContext;
|
const { rowKey } = tableBlockContext;
|
||||||
const { selectedRowKeys } = tableBlockContext.field?.data ?? {};
|
const selectedRecordKeys =
|
||||||
|
tableBlockContext.field?.data?.selectedRowKeys ?? expressionScope?.selectedRecordKeys ?? {};
|
||||||
const { setVisible, fieldSchema: actionSchema } = actionContext;
|
const { setVisible, fieldSchema: actionSchema } = actionContext;
|
||||||
return {
|
return {
|
||||||
async onClick() {
|
async onClick() {
|
||||||
@ -369,11 +374,11 @@ export const useCustomizeBulkEditActionProps = () => {
|
|||||||
forceUpdate: false,
|
forceUpdate: false,
|
||||||
};
|
};
|
||||||
if (updateMode === 'selected') {
|
if (updateMode === 'selected') {
|
||||||
if (!selectedRowKeys?.length) {
|
if (!selectedRecordKeys?.length) {
|
||||||
message.error(t('Please select the records to be updated'));
|
message.error(t('Please select the records to be updated'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateData.filter = { $and: [{ [rowKey || 'id']: { $in: selectedRowKeys } }] };
|
updateData.filter = { $and: [{ [rowKey || 'id']: { $in: selectedRecordKeys } }] };
|
||||||
}
|
}
|
||||||
if (!updateData.filter) {
|
if (!updateData.filter) {
|
||||||
updateData.forceUpdate = true;
|
updateData.forceUpdate = true;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
export * from './BlockProvider';
|
export * from './BlockProvider';
|
||||||
export * from './BlockSchemaComponentProvider';
|
export * from './BlockSchemaComponentProvider';
|
||||||
export * from './CalendarBlockProvider';
|
export * from './CalendarBlockProvider';
|
||||||
@ -8,4 +7,4 @@ export * from './TableBlockProvider';
|
|||||||
export * from './TableFieldProvider';
|
export * from './TableFieldProvider';
|
||||||
export * from './TableSelectorProvider';
|
export * from './TableSelectorProvider';
|
||||||
export * from './FormFieldProvider';
|
export * from './FormFieldProvider';
|
||||||
|
export * from './SharedFilterProvider';
|
||||||
|
@ -81,7 +81,7 @@ export const useSortFields = (collectionName: string) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const fieldInterface = getInterface(field.interface);
|
const fieldInterface = getInterface(field.interface);
|
||||||
if (fieldInterface.sortable) {
|
if (fieldInterface?.sortable) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -113,7 +113,7 @@ export const useCollectionFilterOptions = (collectionName: string) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fieldInterface = getInterface(field.interface);
|
const fieldInterface = getInterface(field.interface);
|
||||||
if (!fieldInterface.filterable) {
|
if (!fieldInterface?.filterable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { nested, children, operators } = fieldInterface.filterable;
|
const { nested, children, operators } = fieldInterface.filterable;
|
||||||
|
@ -119,6 +119,7 @@ export const useCollectionManager = () => {
|
|||||||
const result: CascaderProps<any>['options'][0] = {
|
const result: CascaderProps<any>['options'][0] = {
|
||||||
value: field.name,
|
value: field.name,
|
||||||
label: compile(field?.uiSchema?.title) || field.name,
|
label: compile(field?.uiSchema?.title) || field.name,
|
||||||
|
...field,
|
||||||
};
|
};
|
||||||
if (association && field.target) {
|
if (association && field.target) {
|
||||||
result.children = getCollectionFieldsOptions(field.target, type, opts);
|
result.children = getCollectionFieldsOptions(field.target, type, opts);
|
||||||
|
@ -7,3 +7,4 @@ export * from './useFieldProps';
|
|||||||
export * from './useSchemaComponentContext';
|
export * from './useSchemaComponentContext';
|
||||||
export * from './useFieldComponentOptions';
|
export * from './useFieldComponentOptions';
|
||||||
export * from './useFieldTitle';
|
export * from './useFieldTitle';
|
||||||
|
export * from './useProps';
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { merge } from '@formily/shared';
|
import { merge } from '@formily/shared';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
arrayMerge?(target: any[], source: any[], options?: Options): any[];
|
arrayMerge?(target: any[], source: any[], options?: Options): any[];
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
import { Field } from '@formily/core';
|
import { Field } from '@formily/core';
|
||||||
import { connect, useField, useFieldSchema } from '@formily/react';
|
import { connect, useField, useFieldSchema } from '@formily/react';
|
||||||
import { merge, uid } from '@formily/shared';
|
import { merge, uid } from '@formily/shared';
|
||||||
@ -117,8 +118,15 @@ export const BulkEditField = (props: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space>
|
<Space
|
||||||
<Select defaultValue={type} value={type} style={{ width: 150 }} onChange={typeChangeHandler}>
|
className={css`
|
||||||
|
display: flex;
|
||||||
|
> .ant-space-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Select defaultValue={type} value={type} onChange={typeChangeHandler}>
|
||||||
<Select.Option value={BulkEditFormItemValueType.RemainsTheSame}>{t('Remains the same')}</Select.Option>
|
<Select.Option value={BulkEditFormItemValueType.RemainsTheSame}>{t('Remains the same')}</Select.Option>
|
||||||
<Select.Option value={BulkEditFormItemValueType.ChangedTo}>{t('Changed to')}</Select.Option>
|
<Select.Option value={BulkEditFormItemValueType.ChangedTo}>{t('Changed to')}</Select.Option>
|
||||||
<Select.Option value={BulkEditFormItemValueType.Clear}>{t('Clear')}</Select.Option>
|
<Select.Option value={BulkEditFormItemValueType.Clear}>{t('Clear')}</Select.Option>
|
||||||
|
@ -0,0 +1,97 @@
|
|||||||
|
// 表格操作配置
|
||||||
|
export const MapActionInitializers = {
|
||||||
|
title: "{{t('Configure actions')}}",
|
||||||
|
icon: 'SettingOutlined',
|
||||||
|
style: {
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: "{{t('Enable actions')}}",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
title: "{{t('Filter')}}",
|
||||||
|
component: 'FilterActionInitializer',
|
||||||
|
schema: {
|
||||||
|
'x-align': 'left',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
title: "{{t('Add new')}}",
|
||||||
|
component: 'CreateActionInitializer',
|
||||||
|
schema: {
|
||||||
|
'x-align': 'right',
|
||||||
|
'x-decorator': 'ACLActionProvider',
|
||||||
|
'x-acl-action-props': {
|
||||||
|
skipScopeCheck: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
title: "{{t('Refresh')}}",
|
||||||
|
component: 'RefreshActionInitializer',
|
||||||
|
schema: {
|
||||||
|
'x-align': 'right',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'subMenu',
|
||||||
|
title: '{{t("Customize")}}',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
title: '{{t("Bulk update")}}',
|
||||||
|
component: 'CustomizeActionInitializer',
|
||||||
|
schema: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Bulk update") }}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-align': 'right',
|
||||||
|
'x-acl-action': 'update',
|
||||||
|
'x-decorator': 'ACLActionProvider',
|
||||||
|
'x-acl-action-props': {
|
||||||
|
skipScopeCheck: true,
|
||||||
|
},
|
||||||
|
'x-action': 'customize:bulkUpdate',
|
||||||
|
'x-designer': 'Action.Designer',
|
||||||
|
'x-action-settings': {
|
||||||
|
assignedValues: {},
|
||||||
|
updateMode: 'selected',
|
||||||
|
onSuccess: {
|
||||||
|
manualClose: true,
|
||||||
|
redirecting: false,
|
||||||
|
successMessage: '{{t("Updated successfully")}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-component-props': {
|
||||||
|
icon: 'EditOutlined',
|
||||||
|
useProps: '{{ useCustomizeBulkUpdateActionProps }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
title: '{{t("Bulk edit")}}',
|
||||||
|
component: 'CustomizeBulkEditActionInitializer',
|
||||||
|
schema: {
|
||||||
|
'x-align': 'right',
|
||||||
|
'x-decorator': 'ACLActionProvider',
|
||||||
|
'x-acl-action': 'update',
|
||||||
|
'x-acl-action-props': {
|
||||||
|
skipScopeCheck: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
256
packages/plugins/map/src/client/block/MapBlock.tsx
Normal file
256
packages/plugins/map/src/client/block/MapBlock.tsx
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
import { useCollection, useProps, ActionContext, RecordProvider, useRecord, useCompile } from '@nocobase/client';
|
||||||
|
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
||||||
|
import AMap, { AMapForwardedRefProps } from '../components/AMap';
|
||||||
|
import { RecursionField, useFieldSchema, Schema } from '@formily/react';
|
||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { Button, Space } from 'antd';
|
||||||
|
import { ExpandOutlined, EnvironmentOutlined, CheckOutlined } from '@ant-design/icons';
|
||||||
|
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) => {
|
||||||
|
const { fieldNames, dataSource = [], fixedBlock, zoom, selectedRecordKeys, 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 [visible, setVisible] = useState(false);
|
||||||
|
const [selectingMode, setSelecting] = useState('');
|
||||||
|
const { t } = useMapTranslation();
|
||||||
|
const compile = useCompile()
|
||||||
|
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',
|
||||||
|
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 (data) {
|
||||||
|
setVisible(true);
|
||||||
|
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]);
|
||||||
|
|
||||||
|
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>
|
||||||
|
<RecordProvider record={record}>
|
||||||
|
<MapBlockDrawer visible={visible} setVisible={setVisible} record={null} />
|
||||||
|
</RecordProvider>
|
||||||
|
<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 { visible, setVisible } = props;
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const schema: Schema = useMemo(
|
||||||
|
() =>
|
||||||
|
fieldSchema.reduceProperties((buf, current) => {
|
||||||
|
if (current.name === 'drawer') {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}, null),
|
||||||
|
[fieldSchema],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
schema && (
|
||||||
|
<ActionContext.Provider value={{ visible, setVisible }}>
|
||||||
|
<RecursionField schema={schema} name={schema.name} />
|
||||||
|
</ActionContext.Provider>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
160
packages/plugins/map/src/client/block/MapBlockDesigner.tsx
Normal file
160
packages/plugins/map/src/client/block/MapBlockDesigner.tsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import {
|
||||||
|
useCollection,
|
||||||
|
useCollectionFilterOptions,
|
||||||
|
useDesignable,
|
||||||
|
useSchemaTemplate,
|
||||||
|
useFixedBlockDesignerSetting,
|
||||||
|
GeneralSchemaDesigner,
|
||||||
|
SchemaSettings,
|
||||||
|
mergeFilter,
|
||||||
|
useCollectionManager,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import set from 'lodash/set';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation} from 'react-i18next'
|
||||||
|
import { useMapTranslation } from '../locale';
|
||||||
|
import { useMapBlockContext } from './MapBlockProvider';
|
||||||
|
|
||||||
|
export const MapBlockDesigner = () => {
|
||||||
|
const { name, title } = useCollection();
|
||||||
|
const field = useField();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const dataSource = useCollectionFilterOptions(name);
|
||||||
|
const { service } = useMapBlockContext();
|
||||||
|
const { t: mapT } = useMapTranslation();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const { getCollectionFieldsOptions } = useCollectionManager();
|
||||||
|
const collection = useCollection();
|
||||||
|
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
|
||||||
|
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||||
|
const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {};
|
||||||
|
const defaultZoom = fieldSchema?.['x-component-props']?.['zoom'] || 13;
|
||||||
|
|
||||||
|
const template = useSchemaTemplate();
|
||||||
|
const fixedBlockDesignerSetting = useFixedBlockDesignerSetting();
|
||||||
|
|
||||||
|
const mapFieldOptions = getCollectionFieldsOptions(collection?.name, ['point', 'lineString', 'polygon']);
|
||||||
|
const markerFieldOptions = getCollectionFieldsOptions(collection?.name, 'string');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||||
|
<SchemaSettings.BlockTitleItem />
|
||||||
|
{fixedBlockDesignerSetting}
|
||||||
|
<SchemaSettings.SelectItem
|
||||||
|
title={mapT('Map field')}
|
||||||
|
value={fieldNames.field}
|
||||||
|
options={mapFieldOptions}
|
||||||
|
onChange={(v) => {
|
||||||
|
const fieldNames = field.decoratorProps.fieldNames || {};
|
||||||
|
fieldNames['field'] = v;
|
||||||
|
field.decoratorProps.fieldNames = fieldNames;
|
||||||
|
fieldSchema['x-decorator-props']['fieldNames'] = fieldNames;
|
||||||
|
service.refresh();
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-decorator-props': field.decoratorProps,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettings.SelectItem
|
||||||
|
title={mapT('Marker field')}
|
||||||
|
value={fieldNames.marker}
|
||||||
|
options={markerFieldOptions}
|
||||||
|
onChange={(v) => {
|
||||||
|
const fieldNames = field.decoratorProps.fieldNames || {};
|
||||||
|
fieldNames['marker'] = v;
|
||||||
|
field.decoratorProps.fieldNames = fieldNames;
|
||||||
|
fieldSchema['x-decorator-props']['fieldNames'] = fieldNames;
|
||||||
|
service.refresh();
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-decorator-props': field.decoratorProps,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettings.ModalItem
|
||||||
|
title={mapT('The default zoom level of the map')}
|
||||||
|
schema={
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
title: mapT('Set default zoom level'),
|
||||||
|
properties: {
|
||||||
|
zoom: {
|
||||||
|
title: mapT('Zoom'),
|
||||||
|
default: defaultZoom,
|
||||||
|
'x-component': 'InputNumber',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component-props': {
|
||||||
|
precision: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ISchema
|
||||||
|
}
|
||||||
|
onSubmit={({ zoom }) => {
|
||||||
|
set(fieldSchema, 'x-component-props.zoom', zoom);
|
||||||
|
Object.assign(field.componentProps, fieldSchema['x-component-props']);
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
'x-uid': fieldSchema['x-uid'],
|
||||||
|
'x-component-props': field.componentProps,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
></SchemaSettings.ModalItem>
|
||||||
|
<SchemaSettings.ModalItem
|
||||||
|
title={t('Set the data scope')}
|
||||||
|
schema={
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
title: t('Set the data scope'),
|
||||||
|
properties: {
|
||||||
|
filter: {
|
||||||
|
default: defaultFilter,
|
||||||
|
// title: '数据范围',
|
||||||
|
enum: dataSource,
|
||||||
|
'x-component': 'Filter',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ISchema
|
||||||
|
}
|
||||||
|
onSubmit={({ filter }) => {
|
||||||
|
const params = field.decoratorProps.params || {};
|
||||||
|
params.filter = filter;
|
||||||
|
field.decoratorProps.params = params;
|
||||||
|
fieldSchema['x-decorator-props']['params'] = params;
|
||||||
|
const filters = service.params?.[1]?.filters || {};
|
||||||
|
service.run(
|
||||||
|
{ ...service.params?.[0], filter: mergeFilter([...Object.values(filters), filter]), page: 1 },
|
||||||
|
{ filters },
|
||||||
|
);
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettings.Divider />
|
||||||
|
<SchemaSettings.Template componentName={'Map'} collectionName={name} resourceName={defaultResource} />
|
||||||
|
<SchemaSettings.Divider />
|
||||||
|
<SchemaSettings.Remove
|
||||||
|
removeParentsIfNoChildren
|
||||||
|
breakRemoveOn={{
|
||||||
|
'x-component': 'Grid',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</GeneralSchemaDesigner>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,76 @@
|
|||||||
|
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 React, { useContext } from 'react';
|
||||||
|
import { useMapTranslation } from '../locale';
|
||||||
|
import { createMapBlockSchema } from './utils';
|
||||||
|
|
||||||
|
export const MapBlockInitializer = (props) => {
|
||||||
|
const { insert } = props;
|
||||||
|
const options = useContext(SchemaOptionsContext);
|
||||||
|
const { getCollectionFieldsOptions } = useCollectionManager();
|
||||||
|
const { t } = useMapTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataBlockInitializer
|
||||||
|
{...props}
|
||||||
|
componentType={'Map'}
|
||||||
|
icon={<TableOutlined />}
|
||||||
|
onCreateBlockSchema={async ({ item }) => {
|
||||||
|
const mapFieldOptions = getCollectionFieldsOptions(item.name, ['point', 'lineString', 'polygon']);
|
||||||
|
const markerFieldOptions = getCollectionFieldsOptions(item.name, 'string');
|
||||||
|
const values = await FormDialog(t('Create map block'), () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
|
||||||
|
<FormLayout layout={'vertical'}>
|
||||||
|
<SchemaComponent
|
||||||
|
schema={{
|
||||||
|
properties: {
|
||||||
|
field: {
|
||||||
|
title: t('Map field'),
|
||||||
|
enum: mapFieldOptions,
|
||||||
|
required: true,
|
||||||
|
'x-component': 'Select',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
default: mapFieldOptions[0]?.value
|
||||||
|
},
|
||||||
|
marker: {
|
||||||
|
title: t('Marker field'),
|
||||||
|
enum: markerFieldOptions,
|
||||||
|
'x-component': 'Select',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-reactions': (field) => {
|
||||||
|
const value = field.form.values.field
|
||||||
|
console.log("🚀 ~ file: MapBlockInitializer.tsx:45 ~ values ~ value:", value)
|
||||||
|
console.log("🚀 ~ file: MapBlockInitializer.tsx:50 ~ values ~ mapFieldOptions:", mapFieldOptions)
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const item = mapFieldOptions.find((item) => item.value === value).type
|
||||||
|
field.hidden = item !== 'point'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormLayout>
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
);
|
||||||
|
}).open({
|
||||||
|
initialValues: {},
|
||||||
|
});
|
||||||
|
insert(
|
||||||
|
createMapBlockSchema({
|
||||||
|
collection: item.name,
|
||||||
|
fieldNames: {
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
title={t('Map block')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
55
packages/plugins/map/src/client/block/MapBlockProvider.tsx
Normal file
55
packages/plugins/map/src/client/block/MapBlockProvider.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { ArrayField } from '@formily/core';
|
||||||
|
import { useField, useFieldSchema } from '@formily/react';
|
||||||
|
import { BlockProvider, SchemaComponentOptions, useBlockRequestContext, useFixedSchema } from '@nocobase/client';
|
||||||
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const MapBlockContext = createContext<any>({});
|
||||||
|
|
||||||
|
const InternalMapBlockProvider = (props) => {
|
||||||
|
const { fieldNames } = props;
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const field = useField();
|
||||||
|
const { resource, service } = useBlockRequestContext();
|
||||||
|
const [selectedRecordKeys, setSelectedRecordKeys] = useState([]);
|
||||||
|
|
||||||
|
useFixedSchema();
|
||||||
|
return (
|
||||||
|
<SchemaComponentOptions scope={{ selectedRecordKeys }}>
|
||||||
|
<MapBlockContext.Provider
|
||||||
|
value={{
|
||||||
|
field,
|
||||||
|
service,
|
||||||
|
resource,
|
||||||
|
fieldNames,
|
||||||
|
fixedBlock: fieldSchema?.['x-decorator-props']?.fixedBlock,
|
||||||
|
selectedRecordKeys,
|
||||||
|
setSelectedRecordKeys,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</MapBlockContext.Provider>
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MapBlockProvider = (props) => {
|
||||||
|
return (
|
||||||
|
<BlockProvider {...props} params={{ ...props.params, paginate: false }}>
|
||||||
|
<InternalMapBlockProvider {...props} />
|
||||||
|
</BlockProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMapBlockContext = () => {
|
||||||
|
return useContext(MapBlockContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMapBlockProps = () => {
|
||||||
|
const ctx = useMapBlockContext();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...ctx,
|
||||||
|
dataSource: ctx?.service?.data?.data,
|
||||||
|
zoom: ctx?.field?.componentProps?.zoom || 13,
|
||||||
|
};
|
||||||
|
};
|
39
packages/plugins/map/src/client/block/index.tsx
Normal file
39
packages/plugins/map/src/client/block/index.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
SchemaComponent,
|
||||||
|
SchemaComponentOptions,
|
||||||
|
SchemaInitializerContext,
|
||||||
|
SchemaInitializerProvider,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import React, { useContext, useEffect } from 'react';
|
||||||
|
import { generateNTemplate } from '../locale';
|
||||||
|
import { MapActionInitializers } from './MapActionInitializers';
|
||||||
|
import { MapBlock } from './MapBlock';
|
||||||
|
import { MapBlockDesigner } from './MapBlockDesigner';
|
||||||
|
import { MapBlockInitializer } from './MapBlockInitializer';
|
||||||
|
import { MapBlockProvider, useMapBlockProps } from './MapBlockProvider';
|
||||||
|
|
||||||
|
export const MapBlockOptions: React.FC = (props) => {
|
||||||
|
const items = useContext(SchemaInitializerContext);
|
||||||
|
const children = items.BlockInitializers.items[0].children;
|
||||||
|
const schemaInitializer = useContext(SchemaInitializerContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
children.push({
|
||||||
|
key: 'mapBlock',
|
||||||
|
type: 'item',
|
||||||
|
title: generateNTemplate('Map'),
|
||||||
|
component: 'MapBlockInitializer',
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SchemaInitializerProvider initializers={{ ...schemaInitializer, MapActionInitializers }}>
|
||||||
|
<SchemaComponentOptions
|
||||||
|
scope={{ useMapBlockProps }}
|
||||||
|
components={{ MapBlockInitializer, MapBlockDesigner, MapBlockProvider, MapBlock }}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
</SchemaInitializerProvider>
|
||||||
|
);
|
||||||
|
};
|
80
packages/plugins/map/src/client/block/utils.ts
Normal file
80
packages/plugins/map/src/client/block/utils.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { ISchema } from '@formily/react';
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
|
|
||||||
|
export const createMapBlockSchema = (options) => {
|
||||||
|
const { collection, resource, fieldNames, ...others } = options;
|
||||||
|
const schema: ISchema = {
|
||||||
|
type: 'void',
|
||||||
|
'x-acl-action': `${resource || collection}:list`,
|
||||||
|
'x-decorator': 'MapBlockProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
collection: collection,
|
||||||
|
resource: resource || collection,
|
||||||
|
action: 'list',
|
||||||
|
fieldNames,
|
||||||
|
params: {
|
||||||
|
paginate: false,
|
||||||
|
},
|
||||||
|
...others,
|
||||||
|
},
|
||||||
|
'x-designer': 'MapBlockDesigner',
|
||||||
|
'x-component': 'CardItem',
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-initializer': 'MapActionInitializers',
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
'x-component-props': {
|
||||||
|
style: {
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
[uid()]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'MapBlock',
|
||||||
|
'x-component-props': {
|
||||||
|
useProps: '{{ useMapBlockProps }}',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
'x-component-props': {
|
||||||
|
className: 'nb-action-popup',
|
||||||
|
},
|
||||||
|
title: '{{ t("View record") }}',
|
||||||
|
properties: {
|
||||||
|
tabs: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Tabs',
|
||||||
|
'x-component-props': {},
|
||||||
|
'x-initializer': 'TabPaneInitializers',
|
||||||
|
properties: {
|
||||||
|
tab1: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Details")}}',
|
||||||
|
'x-component': 'Tabs.TabPane',
|
||||||
|
'x-designer': 'Tabs.Designer',
|
||||||
|
'x-component-props': {},
|
||||||
|
properties: {
|
||||||
|
grid: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'RecordBlockInitializers',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
console.log(JSON.stringify(schema, null, 2));
|
||||||
|
return schema;
|
||||||
|
};
|
@ -6,20 +6,24 @@ 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 } from 'antd';
|
import { Alert, Button, Modal } from 'antd';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useCallback, useRef, useState, useMemo, useImperativeHandle } from 'react';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { useMapConfiguration } from '../hooks';
|
import { useMapConfiguration } from '../hooks';
|
||||||
import { useMapTranslation } from '../locale';
|
import { useMapTranslation } from '../locale';
|
||||||
import Search from './Search';
|
import Search from './Search';
|
||||||
|
|
||||||
interface AMapComponentProps {
|
export type MapEditorType = 'point' | 'polygon' | 'lineString' | 'circle';
|
||||||
accessKey: string;
|
|
||||||
securityJsCode: string;
|
export interface AMapComponentProps {
|
||||||
value: any;
|
value?: any;
|
||||||
onChange: (value: number[]) => void;
|
onChange?: (value: number[]) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
mapType: string;
|
mapType: string;
|
||||||
zoom: number;
|
zoom: number;
|
||||||
|
type: MapEditorType;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
overlayCommonOptions?: AMap.PolylineOptions & AMap.PolygonOptions;
|
||||||
|
block?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const methodMapping = {
|
const methodMapping = {
|
||||||
@ -53,9 +57,30 @@ const methodMapping = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const AMapComponent: React.FC<AMapComponentProps> = (props) => {
|
export interface AMapForwardedRefProps {
|
||||||
|
setOverlay: (t: MapEditorType, v: any, o?: AMap.PolylineOptions & AMap.PolygonOptions & AMap.MarkerOptions) => any;
|
||||||
|
getOverlay: (t: MapEditorType, v: any, o?: AMap.PolylineOptions & AMap.PolygonOptions & AMap.MarkerOptions) => any;
|
||||||
|
createMouseTool: (type: MapEditorType) => void;
|
||||||
|
createEditor: (type: MapEditorType) => void;
|
||||||
|
executeMouseTool: (type: MapEditorType) => void;
|
||||||
|
|
||||||
|
aMap: any;
|
||||||
|
map: AMap.Map;
|
||||||
|
editor: () => {
|
||||||
|
getTarget: () => AMap.Polygon;
|
||||||
|
setTarget: (o: any) => void;
|
||||||
|
close: () => void;
|
||||||
|
on: (event: string, callback: (e: any) => void) => void;
|
||||||
|
};
|
||||||
|
mouseTool: () => {
|
||||||
|
close: (clear?: boolean) => void;
|
||||||
|
};
|
||||||
|
overlay: AMap.Polygon;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps>((props, ref) => {
|
||||||
const { accessKey, securityJsCode } = useMapConfiguration(props.mapType) || {};
|
const { accessKey, securityJsCode } = useMapConfiguration(props.mapType) || {};
|
||||||
const { value, onChange, disabled, zoom = 13 } = props;
|
const { value, onChange, block = false, disabled = block, zoom = 13, overlayCommonOptions } = props;
|
||||||
const { t } = useMapTranslation();
|
const { t } = useMapTranslation();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const aMap = useRef<any>();
|
const aMap = useRef<any>();
|
||||||
@ -64,9 +89,13 @@ const AMapComponent: React.FC<AMapComponentProps> = (props) => {
|
|||||||
const [needUpdateFlag, forceUpdate] = useState([]);
|
const [needUpdateFlag, forceUpdate] = useState([]);
|
||||||
const [errMessage, setErrMessage] = useState('');
|
const [errMessage, setErrMessage] = useState('');
|
||||||
const { getField } = useCollection();
|
const { getField } = useCollection();
|
||||||
const collectionField = getField(fieldSchema.name);
|
const type = useMemo<MapEditorType>(() => {
|
||||||
const type = collectionField?.interface;
|
if (props.type) return props.type;
|
||||||
const overlay = useRef<any>();
|
const collectionField = getField(fieldSchema?.name);
|
||||||
|
return collectionField?.interface;
|
||||||
|
}, [props?.type, fieldSchema?.name]);
|
||||||
|
|
||||||
|
const overlay = useRef<AMap.Polygon>();
|
||||||
const editor = useRef(null);
|
const editor = useRef(null);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const id = useRef(`nocobase-map-${type}-${Date.now().toString(32)}`);
|
const id = useRef(`nocobase-map-${type}-${Date.now().toString(32)}`);
|
||||||
@ -76,16 +105,17 @@ const AMapComponent: React.FC<AMapComponentProps> = (props) => {
|
|||||||
strokeColor: '#4e9bff',
|
strokeColor: '#4e9bff',
|
||||||
fillColor: '#4e9bff',
|
fillColor: '#4e9bff',
|
||||||
strokeOpacity: 1,
|
strokeOpacity: 1,
|
||||||
|
...overlayCommonOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
const toRemoveOverlay = useMemoizedFn(() => {
|
const toRemoveOverlay = useMemoizedFn(() => {
|
||||||
if (overlay.current) {
|
if (overlay.current) {
|
||||||
map.current?.remove(overlay.current);
|
overlay.current.remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const setTarget = useMemoizedFn(() => {
|
const setTarget = useMemoizedFn(() => {
|
||||||
if (!disabled && type !== 'point' && editor.current) {
|
if ((!disabled || block) && type !== 'point' && editor.current) {
|
||||||
editor.current.setTarget(overlay.current);
|
editor.current.setTarget(overlay.current);
|
||||||
editor.current.open();
|
editor.current.open();
|
||||||
}
|
}
|
||||||
@ -113,25 +143,38 @@ const AMapComponent: React.FC<AMapComponentProps> = (props) => {
|
|||||||
overlay.current = target;
|
overlay.current = target;
|
||||||
setTarget();
|
setTarget();
|
||||||
}
|
}
|
||||||
onChange(nextValue);
|
onChange?.(nextValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
const createEditor = useMemoizedFn(() => {
|
const createEditor = useMemoizedFn((curType = type) => {
|
||||||
const mapping = methodMapping[type as keyof typeof methodMapping];
|
const mapping = methodMapping[curType];
|
||||||
if (mapping && 'editor' in mapping && !editor.current) {
|
if (mapping && 'editor' in mapping && !editor.current) {
|
||||||
editor.current = new aMap.current[mapping.editor](map.current);
|
editor.current = new aMap.current[mapping.editor](map.current, null, {
|
||||||
|
createOptions: commonOptions,
|
||||||
|
editOptions: commonOptions,
|
||||||
|
controlPoint: {
|
||||||
|
...commonOptions,
|
||||||
|
strokeWeight: 3,
|
||||||
|
},
|
||||||
|
midControlPoint: {
|
||||||
|
...commonOptions,
|
||||||
|
strokeWeight: 2,
|
||||||
|
fillColor: '#fff',
|
||||||
|
},
|
||||||
|
});
|
||||||
editor.current.on('adjust', function ({ target }) {
|
editor.current.on('adjust', function ({ target }) {
|
||||||
onMapChange(target, true);
|
onMapChange(target, true);
|
||||||
});
|
});
|
||||||
editor.current.on('move', function ({ target }) {
|
editor.current.on('move', function ({ target }) {
|
||||||
onMapChange(target, true);
|
onMapChange(target, true);
|
||||||
});
|
});
|
||||||
|
return editor.current;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const executeMouseTool = useMemoizedFn(() => {
|
const executeMouseTool = useMemoizedFn((curType = type) => {
|
||||||
if (!mouseTool.current || editor.current?.getTarget()) return;
|
if (!mouseTool.current || editor.current?.getTarget()) return;
|
||||||
const mapping = methodMapping[type as keyof typeof methodMapping];
|
const mapping = methodMapping[curType];
|
||||||
if (!mapping) {
|
if (!mapping) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -140,15 +183,13 @@ const AMapComponent: React.FC<AMapComponentProps> = (props) => {
|
|||||||
} as AMap.PolylineOptions);
|
} as AMap.PolylineOptions);
|
||||||
});
|
});
|
||||||
|
|
||||||
const createMouseTool = useMemoizedFn(() => {
|
const createMouseTool = useMemoizedFn((curType: MapEditorType = type) => {
|
||||||
if (mouseTool.current) return;
|
if (mouseTool.current) return;
|
||||||
|
|
||||||
mouseTool.current = new aMap.current.MouseTool(map.current);
|
mouseTool.current = new aMap.current.MouseTool(map.current);
|
||||||
mouseTool.current.on('draw', function ({ obj }) {
|
mouseTool.current.on('draw', function ({ obj }) {
|
||||||
onMapChange(obj);
|
onMapChange(obj);
|
||||||
});
|
});
|
||||||
|
executeMouseTool(curType);
|
||||||
executeMouseTool();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const toCenter = (position, imm?: boolean) => {
|
const toCenter = (position, imm?: boolean) => {
|
||||||
@ -164,7 +205,7 @@ const AMapComponent: React.FC<AMapComponentProps> = (props) => {
|
|||||||
editor.current.setTarget();
|
editor.current.setTarget();
|
||||||
editor.current.close();
|
editor.current.close();
|
||||||
}
|
}
|
||||||
onChange(null);
|
onChange?.(null);
|
||||||
};
|
};
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: t('Clear the canvas'),
|
title: t('Clear the canvas'),
|
||||||
@ -183,28 +224,41 @@ const AMapComponent: React.FC<AMapComponentProps> = (props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getOverlay = useCallback(
|
||||||
|
(t = type, v = value, o?: AMap.PolylineOptions & AMap.PolygonOptions) => {
|
||||||
|
const mapping = methodMapping[t];
|
||||||
|
if (!mapping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const options = { ...commonOptions, ...o } as AMap.MarkerOptions;
|
||||||
|
if ('transformOptions' in mapping) {
|
||||||
|
Object.assign(options, mapping.transformOptions(v));
|
||||||
|
} else if ('propertyKey' in mapping) {
|
||||||
|
options[mapping.propertyKey] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new aMap.current[mapping.overlay](options);
|
||||||
|
},
|
||||||
|
[commonOptions],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setOverlay = (t = type, v = value, o?: AMap.PolylineOptions & AMap.PolygonOptions) => {
|
||||||
|
if (!aMap.current) return;
|
||||||
|
const nextOverlay = getOverlay(t, v, o);
|
||||||
|
nextOverlay.setMap(map.current);
|
||||||
|
return nextOverlay;
|
||||||
|
};
|
||||||
|
|
||||||
// 编辑时
|
// 编辑时
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!aMap.current) return;
|
if (!aMap.current) return;
|
||||||
if (!value || overlay.current) {
|
if (!value || overlay.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const mapping = methodMapping[type as keyof typeof methodMapping];
|
|
||||||
if (!mapping) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const options = { ...commonOptions };
|
|
||||||
if ('transformOptions' in mapping) {
|
|
||||||
Object.assign(options, mapping.transformOptions(value));
|
|
||||||
} else if ('propertyKey' in mapping) {
|
|
||||||
options[mapping.propertyKey] = value;
|
|
||||||
}
|
|
||||||
const nextOverlay = new aMap.current[mapping.overlay](options);
|
|
||||||
|
|
||||||
|
const nextOverlay = setOverlay();
|
||||||
// 聚焦在编辑的位置
|
// 聚焦在编辑的位置
|
||||||
map.current.setFitView([nextOverlay]);
|
map.current.setFitView([nextOverlay]);
|
||||||
|
|
||||||
nextOverlay.setMap(map.current);
|
|
||||||
overlay.current = nextOverlay;
|
overlay.current = nextOverlay;
|
||||||
|
|
||||||
createEditor();
|
createEditor();
|
||||||
@ -283,6 +337,19 @@ const AMapComponent: React.FC<AMapComponentProps> = (props) => {
|
|||||||
};
|
};
|
||||||
}, [accessKey, type, securityJsCode]);
|
}, [accessKey, type, securityJsCode]);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
setOverlay,
|
||||||
|
getOverlay,
|
||||||
|
createMouseTool,
|
||||||
|
createEditor,
|
||||||
|
executeMouseTool,
|
||||||
|
aMap: aMap.current,
|
||||||
|
map: map.current,
|
||||||
|
overlay: overlay.current,
|
||||||
|
mouseTool: () => mouseTool.current,
|
||||||
|
editor: () => editor.current,
|
||||||
|
}));
|
||||||
|
|
||||||
if (!accessKey || errMessage) {
|
if (!accessKey || errMessage) {
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
@ -301,33 +368,31 @@ const AMapComponent: React.FC<AMapComponentProps> = (props) => {
|
|||||||
<div
|
<div
|
||||||
className={css`
|
className={css`
|
||||||
position: relative;
|
position: relative;
|
||||||
|
height: 500px;
|
||||||
`}
|
`}
|
||||||
id={id.current}
|
id={id.current}
|
||||||
style={{
|
style={props?.style}
|
||||||
height: '500px',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{/* bottom: 20px; right: 50%; transform: translateX(50%); z-index: 2; */}
|
|
||||||
<div
|
|
||||||
className={css`
|
|
||||||
position: absolute;
|
|
||||||
bottom: 80px;
|
|
||||||
right: 20px;
|
|
||||||
z-index: 10;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={onFocusOverlay}
|
|
||||||
disabled={!overlay.current}
|
|
||||||
type="primary"
|
|
||||||
shape="round"
|
|
||||||
size="large"
|
|
||||||
icon={<SyncOutlined />}
|
|
||||||
></Button>
|
|
||||||
</div>
|
|
||||||
{!disabled ? (
|
{!disabled ? (
|
||||||
<>
|
<>
|
||||||
<Search toCenter={toCenter} aMap={aMap.current} />
|
<Search toCenter={toCenter} aMap={aMap.current} />
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
position: absolute;
|
||||||
|
bottom: 80px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 10;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={onFocusOverlay}
|
||||||
|
disabled={!overlay.current}
|
||||||
|
type="primary"
|
||||||
|
shape="round"
|
||||||
|
size="large"
|
||||||
|
icon={<SyncOutlined />}
|
||||||
|
></Button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className={css`
|
className={css`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -363,6 +428,6 @@ const AMapComponent: React.FC<AMapComponentProps> = (props) => {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default AMapComponent;
|
export default AMapComponent;
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import { connect, mapReadPretty } from '@formily/react';
|
import { connect, mapReadPretty } from '@formily/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AMapComponent from './AMap';
|
import AMapComponent, { AMapComponentProps } from './AMap';
|
||||||
import ReadPretty from './ReadPretty';
|
import ReadPretty from './ReadPretty';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import Designer from './Designer';
|
import Designer from './Designer';
|
||||||
|
|
||||||
const InternalMap = connect((props) => {
|
interface MapProps extends AMapComponentProps {}
|
||||||
|
|
||||||
|
const InternalMap = connect((props: MapProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={css`
|
className={css`
|
||||||
|
height: 100%;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
.ant-formily-item-error & {
|
.ant-formily-item-error & {
|
||||||
border: 1px solid #ff4d4f;
|
border: 1px solid #ff4d4f;
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { useField, useFieldSchema } from '@formily/react';
|
import { useField, useFieldSchema, useForm } from '@formily/react';
|
||||||
import { EllipsisWithTooltip, useCollection } from '@nocobase/client';
|
import { EllipsisWithTooltip, useCollection, useTableBlockContext } from '@nocobase/client';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import AMapComponent from './AMap';
|
import AMapComponent from './AMap';
|
||||||
|
|
||||||
const ReadPretty = (props) => {
|
const ReadPretty = (props) => {
|
||||||
const { value, readOnly } = props;
|
const { value } = props;
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
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 field = useField();
|
||||||
|
const form = useForm()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!field.title && collectionField?.uiSchema?.title) {
|
if (!field.title && collectionField?.uiSchema?.title) {
|
||||||
@ -17,7 +18,7 @@ const ReadPretty = (props) => {
|
|||||||
}
|
}
|
||||||
}, collectionField?.title);
|
}, collectionField?.title);
|
||||||
|
|
||||||
if (!readOnly)
|
if (!form.readPretty) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<EllipsisWithTooltip ellipsis={true}>
|
<EllipsisWithTooltip ellipsis={true}>
|
||||||
@ -25,6 +26,7 @@ const ReadPretty = (props) => {
|
|||||||
</EllipsisWithTooltip>
|
</EllipsisWithTooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return mapType === 'amap' ? <AMapComponent mapType={mapType} {...props}></AMapComponent> : null;
|
return mapType === 'amap' ? <AMapComponent mapType={mapType} {...props}></AMapComponent> : null;
|
||||||
};
|
};
|
||||||
|
@ -41,17 +41,14 @@ export const commonSchema = {
|
|||||||
},
|
},
|
||||||
'x-disabled': '{{ isOverride || !createOnly }}',
|
'x-disabled': '{{ isOverride || !createOnly }}',
|
||||||
default: 'amap',
|
default: 'amap',
|
||||||
enum: MapTypes
|
enum: MapTypes,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
schemaInitialize(schema: ISchema, { readPretty, block }) {
|
schemaInitialize(schema: ISchema, { block }) {
|
||||||
if (block === 'Form') {
|
if (block === 'Form') {
|
||||||
Object.assign(schema, {
|
Object.assign(schema, {
|
||||||
'x-component-props': {
|
|
||||||
readOnly: readPretty ? true : false
|
|
||||||
},
|
|
||||||
'x-designer': 'Map.Designer',
|
'x-designer': 'Map.Designer',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
@ -2,21 +2,23 @@ import {
|
|||||||
CollectionManagerContext,
|
CollectionManagerContext,
|
||||||
CurrentAppInfoProvider,
|
CurrentAppInfoProvider,
|
||||||
SchemaComponentOptions,
|
SchemaComponentOptions,
|
||||||
SettingsCenterProvider
|
SettingsCenterProvider,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
|
import { MapBlockOptions } from './block';
|
||||||
import Configuration from './components/Configuration';
|
import Configuration from './components/Configuration';
|
||||||
import Map from './components/Map';
|
import Map from './components/Map';
|
||||||
import { interfaces } from './fields';
|
import { interfaces } from './fields';
|
||||||
import { Initialize } from './initialize';
|
import { MapInitializer } from './initialize';
|
||||||
import { useMapTranslation } from './locale';
|
import { useMapTranslation } from './locale';
|
||||||
|
import './locale';
|
||||||
|
|
||||||
export default React.memo((props) => {
|
export default React.memo((props) => {
|
||||||
const ctx = useContext(CollectionManagerContext);
|
const ctx = useContext(CollectionManagerContext);
|
||||||
const { t } = useMapTranslation();
|
const { t } = useMapTranslation();
|
||||||
return (
|
return (
|
||||||
<CurrentAppInfoProvider>
|
<CurrentAppInfoProvider>
|
||||||
<Initialize>
|
<MapInitializer>
|
||||||
<SettingsCenterProvider
|
<SettingsCenterProvider
|
||||||
settings={{
|
settings={{
|
||||||
map: {
|
map: {
|
||||||
@ -32,12 +34,14 @@ export default React.memo((props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SchemaComponentOptions components={{ Map }}>
|
<SchemaComponentOptions components={{ Map }}>
|
||||||
<CollectionManagerContext.Provider value={{ ...ctx, interfaces: { ...ctx.interfaces, ...interfaces } }}>
|
<MapBlockOptions>
|
||||||
{props.children}
|
<CollectionManagerContext.Provider value={{ ...ctx, interfaces: { ...ctx.interfaces, ...interfaces } }}>
|
||||||
</CollectionManagerContext.Provider>
|
{props.children}
|
||||||
|
</CollectionManagerContext.Provider>
|
||||||
|
</MapBlockOptions>
|
||||||
</SchemaComponentOptions>
|
</SchemaComponentOptions>
|
||||||
</SettingsCenterProvider>
|
</SettingsCenterProvider>
|
||||||
</Initialize>
|
</MapInitializer>
|
||||||
</CurrentAppInfoProvider>
|
</CurrentAppInfoProvider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { registerField, registerGroup, useCurrentAppInfo } from '@nocobase/client';
|
import { registerField, registerGroup, useCurrentAppInfo } from '@nocobase/client';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { fields } from './fields';
|
import { fields } from './fields';
|
||||||
import './locale';
|
|
||||||
import { generateNTemplate } from './locale';
|
import { generateNTemplate } from './locale';
|
||||||
|
import './locale';
|
||||||
|
|
||||||
export const useRegisterInterface = () => {
|
export const useRegisterInterface = () => {
|
||||||
const { data } = useCurrentAppInfo() || {};
|
const { data } = useCurrentAppInfo() || {};
|
||||||
@ -26,7 +26,7 @@ export const useRegisterInterface = () => {
|
|||||||
}, [data]);
|
}, [data]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Initialize: React.FC = (props) => {
|
export const MapInitializer: React.FC = (props) => {
|
||||||
useRegisterInterface();
|
useRegisterInterface();
|
||||||
return <React.Fragment>{props.children}</React.Fragment>;
|
return <React.Fragment>{props.children}</React.Fragment>;
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { i18n } from '@nocobase/client';
|
import { i18n } from '@nocobase/client';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import enUS from './en-US';
|
||||||
|
import zhCN from './zh-CN';
|
||||||
|
|
||||||
export const NAMESPACE = 'map';
|
export const NAMESPACE = 'map';
|
||||||
|
|
||||||
// i18n.addResources('zh-CN', NAMESPACE, zhCN);
|
i18n.addResources('zh-CN', NAMESPACE, zhCN);
|
||||||
// i18n.addResources('en-US', NAMESPACE, enUS);
|
i18n.addResources('en-US', NAMESPACE, enUS);
|
||||||
|
|
||||||
export function lang(key: string) {
|
export function lang(key: string) {
|
||||||
return i18n.t(key, { ns: NAMESPACE });
|
return i18n.t(key, { ns: NAMESPACE });
|
||||||
|
@ -1,32 +1,32 @@
|
|||||||
const locale = {
|
const locale = {
|
||||||
'Map-based geometry': '基于地图的几何图形',
|
'Map-based geometry': '基于地图的几何图形',
|
||||||
'Map type': '地图类型',
|
'Map type': '地图类型',
|
||||||
'Point': '点',
|
Point: '点',
|
||||||
'Line': '线',
|
Line: '线',
|
||||||
'Circle': '圆',
|
Circle: '圆',
|
||||||
'Polygon': '多边形',
|
Polygon: '多边形',
|
||||||
'Access key': '访问密钥',
|
'Access key': '访问密钥',
|
||||||
'securityJsCode or serviceHost': 'securityJsCode 或 serviceHost',
|
'securityJsCode or serviceHost': 'securityJsCode 或 serviceHost',
|
||||||
'AMap': '高德地图',
|
AMap: '高德地图',
|
||||||
'Google Maps': '谷歌地图',
|
'Google Maps': '谷歌地图',
|
||||||
'Clear': '清空',
|
Clear: '清空',
|
||||||
'Click to select the starting point and double-click to end the drawing': '点击选择起点,双击结束绘制',
|
'Click to select the starting point and double-click to end the drawing': '点击选择起点,双击结束绘制',
|
||||||
'Clear the canvas': '清空画布',
|
'Clear the canvas': '清空画布',
|
||||||
'Are you sure to clear the canvas?': '您确定要清空画布吗?',
|
'Are you sure to clear the canvas?': '您确定要清空画布吗?',
|
||||||
'Confirm': '确定',
|
Confirm: '确定',
|
||||||
'Cancel': '取消',
|
Cancel: '取消',
|
||||||
'Enter keywords to search': '输入地方名关键字搜索(必须包含省/市)',
|
'Enter keywords to search': '输入地方名关键字搜索(必须包含省/市)',
|
||||||
'The AccessKey is incorrect, please check it': '访问密钥不正确,请检查',
|
'The AccessKey is incorrect, please check it': '访问密钥不正确,请检查',
|
||||||
'Please configure the AMap securityCode or serviceHost correctly': '请正确配置高德地图 securityCode 或 serviceHost',
|
'Please configure the AMap securityCode or serviceHost correctly': '请正确配置高德地图 securityCode 或 serviceHost',
|
||||||
'Map Manager': '地图管理',
|
'Map Manager': '地图管理',
|
||||||
'Configuration': '配置',
|
Configuration: '配置',
|
||||||
'Saved successfully': '保存成功',
|
'Saved successfully': '保存成功',
|
||||||
'Saved failed': '保存失败',
|
'Saved failed': '保存失败',
|
||||||
'Edit': '编辑',
|
Edit: '编辑',
|
||||||
'Save': '保存',
|
Save: '保存',
|
||||||
'Please configure the AccessKey and SecurityJsCode first': '请先配置 AccessKey 和 SecurityJsCode',
|
'Please configure the AccessKey and SecurityJsCode first': '请先配置 AccessKey 和 SecurityJsCode',
|
||||||
'Go to the configuration page': '前往配置页面',
|
'Go to the configuration page': '前往配置页面',
|
||||||
'Zoom': '缩放',
|
Zoom: '缩放',
|
||||||
'Set default zoom level': '设置默认缩放级别',
|
'Set default zoom level': '设置默认缩放级别',
|
||||||
'The default zoom level of the map': '地图默认缩放级别',
|
'The default zoom level of the map': '地图默认缩放级别',
|
||||||
// Designer
|
// Designer
|
||||||
@ -34,12 +34,14 @@ const locale = {
|
|||||||
'Field title': '字段标题',
|
'Field title': '字段标题',
|
||||||
'Edit tooltip': '编辑提示信息',
|
'Edit tooltip': '编辑提示信息',
|
||||||
'Delete field': '删除字段',
|
'Delete field': '删除字段',
|
||||||
"Required": "必填",
|
Required: '必填',
|
||||||
'Pattern': '模式',
|
Pattern: '模式',
|
||||||
"Editable": "可编辑",
|
Editable: '可编辑',
|
||||||
"Readonly": "只读(禁止编辑)",
|
Readonly: '只读(禁止编辑)',
|
||||||
"Easy-reading": "只读(阅读模式)",
|
'Easy-reading': '只读(阅读模式)',
|
||||||
"Edit description": "编辑描述",
|
'Edit description': '编辑描述',
|
||||||
}
|
'Map field': '地图字段',
|
||||||
|
'Marker field': '标记字段',
|
||||||
|
};
|
||||||
|
|
||||||
export default locale;
|
export default locale;
|
||||||
|
Loading…
Reference in New Issue
Block a user