diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index f9fd0e53d..b12a751bc 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -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 parse from 'json-templates'; import { cloneDeep } from 'lodash'; import get from 'lodash/get'; import omit from 'lodash/omit'; +import { useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; import { useReactToPrint } from 'react-to-print'; @@ -247,11 +248,13 @@ export const useCustomizeUpdateActionProps = () => { export const useCustomizeBulkUpdateActionProps = () => { const { field, resource, __parent, service } = useBlockRequestContext(); + const expressionScope = useContext(SchemaExpressionScopeContext); const actionSchema = useFieldSchema(); const currentRecord = useRecord(); const tableBlockContext = useTableBlockContext(); const { rowKey } = tableBlockContext; - const { selectedRowKeys } = tableBlockContext.field?.data ?? {}; + const selectedRecordKeys = + tableBlockContext.field?.data?.selectedRowKeys ?? expressionScope?.selectedRecordKeys ?? {}; const currentUserContext = useCurrentUserContext(); const currentUser = currentUserContext?.data?.data; const history = useHistory(); @@ -280,12 +283,12 @@ export const useCustomizeBulkUpdateActionProps = () => { forceUpdate: false, }; if (updateMode === 'selected') { - if (!selectedRowKeys?.length) { + if (!selectedRecordKeys?.length) { message.error(t('Please select the records to be updated')); actionField.data.loading = false; return; } - updateData.filter = { $and: [{ [rowKey || 'id']: { $in: selectedRowKeys } }] }; + updateData.filter = { $and: [{ [rowKey || 'id']: { $in: selectedRecordKeys } }] }; } if (!updateData.filter) { updateData.forceUpdate = true; @@ -328,17 +331,19 @@ export const useCustomizeBulkUpdateActionProps = () => { }; }; -export const useCustomizeBulkEditActionProps = () => { +export const useCustomizeBulkEditActionProps = (props) => { const form = useForm(); const { t } = useTranslation(); const { field, resource, __parent } = useBlockRequestContext(); + const expressionScope = useContext(SchemaExpressionScopeContext); const actionContext = useActionContext(); const history = useHistory(); const compile = useCompile(); const actionField = useField(); const tableBlockContext = useTableBlockContext(); const { rowKey } = tableBlockContext; - const { selectedRowKeys } = tableBlockContext.field?.data ?? {}; + const selectedRecordKeys = + tableBlockContext.field?.data?.selectedRowKeys ?? expressionScope?.selectedRecordKeys ?? {}; const { setVisible, fieldSchema: actionSchema } = actionContext; return { async onClick() { @@ -369,11 +374,11 @@ export const useCustomizeBulkEditActionProps = () => { forceUpdate: false, }; if (updateMode === 'selected') { - if (!selectedRowKeys?.length) { + if (!selectedRecordKeys?.length) { message.error(t('Please select the records to be updated')); return; } - updateData.filter = { $and: [{ [rowKey || 'id']: { $in: selectedRowKeys } }] }; + updateData.filter = { $and: [{ [rowKey || 'id']: { $in: selectedRecordKeys } }] }; } if (!updateData.filter) { updateData.forceUpdate = true; diff --git a/packages/core/client/src/block-provider/index.tsx b/packages/core/client/src/block-provider/index.tsx index 6d7e3aff1..259064ecd 100644 --- a/packages/core/client/src/block-provider/index.tsx +++ b/packages/core/client/src/block-provider/index.tsx @@ -1,4 +1,3 @@ - export * from './BlockProvider'; export * from './BlockSchemaComponentProvider'; export * from './CalendarBlockProvider'; @@ -8,4 +7,4 @@ export * from './TableBlockProvider'; export * from './TableFieldProvider'; export * from './TableSelectorProvider'; export * from './FormFieldProvider'; - +export * from './SharedFilterProvider'; diff --git a/packages/core/client/src/collection-manager/action-hooks.ts b/packages/core/client/src/collection-manager/action-hooks.ts index b50556804..f9fcd5249 100644 --- a/packages/core/client/src/collection-manager/action-hooks.ts +++ b/packages/core/client/src/collection-manager/action-hooks.ts @@ -81,7 +81,7 @@ export const useSortFields = (collectionName: string) => { return false; } const fieldInterface = getInterface(field.interface); - if (fieldInterface.sortable) { + if (fieldInterface?.sortable) { return true; } return false; @@ -113,7 +113,7 @@ export const useCollectionFilterOptions = (collectionName: string) => { return; } const fieldInterface = getInterface(field.interface); - if (!fieldInterface.filterable) { + if (!fieldInterface?.filterable) { return; } const { nested, children, operators } = fieldInterface.filterable; diff --git a/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts b/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts index 29bd148f7..06b0b502c 100644 --- a/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts +++ b/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts @@ -119,6 +119,7 @@ export const useCollectionManager = () => { const result: CascaderProps['options'][0] = { value: field.name, label: compile(field?.uiSchema?.title) || field.name, + ...field, }; if (association && field.target) { result.children = getCollectionFieldsOptions(field.target, type, opts); diff --git a/packages/core/client/src/schema-component/hooks/index.ts b/packages/core/client/src/schema-component/hooks/index.ts index 077be1346..b39505ce3 100644 --- a/packages/core/client/src/schema-component/hooks/index.ts +++ b/packages/core/client/src/schema-component/hooks/index.ts @@ -7,3 +7,4 @@ export * from './useFieldProps'; export * from './useSchemaComponentContext'; export * from './useFieldComponentOptions'; export * from './useFieldTitle'; +export * from './useProps'; diff --git a/packages/core/client/src/schema-component/hooks/useProps.ts b/packages/core/client/src/schema-component/hooks/useProps.ts index aa10a0771..c0dad3703 100644 --- a/packages/core/client/src/schema-component/hooks/useProps.ts +++ b/packages/core/client/src/schema-component/hooks/useProps.ts @@ -1,4 +1,5 @@ import { merge } from '@formily/shared'; +import { useEffect } from 'react'; interface Options { arrayMerge?(target: any[], source: any[], options?: Options): any[]; diff --git a/packages/core/client/src/schema-initializer/components/BulkEditField.tsx b/packages/core/client/src/schema-initializer/components/BulkEditField.tsx index d7567b12f..f860d8ea9 100644 --- a/packages/core/client/src/schema-initializer/components/BulkEditField.tsx +++ b/packages/core/client/src/schema-initializer/components/BulkEditField.tsx @@ -1,3 +1,4 @@ +import { css } from '@emotion/css'; import { Field } from '@formily/core'; import { connect, useField, useFieldSchema } from '@formily/react'; import { merge, uid } from '@formily/shared'; @@ -117,8 +118,15 @@ export const BulkEditField = (props: any) => { }; return ( - - {t('Remains the same')} {t('Changed to')} {t('Clear')} diff --git a/packages/plugins/map/src/client/block/MapActionInitializers.tsx b/packages/plugins/map/src/client/block/MapActionInitializers.tsx new file mode 100644 index 000000000..78292db2f --- /dev/null +++ b/packages/plugins/map/src/client/block/MapActionInitializers.tsx @@ -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, + }, + }, + }, + ], + }, + ], +}; diff --git a/packages/plugins/map/src/client/block/MapBlock.tsx b/packages/plugins/map/src/client/block/MapBlock.tsx new file mode 100644 index 000000000..a6ffcf738 --- /dev/null +++ b/packages/plugins/map/src/client/block/MapBlock.tsx @@ -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(); + 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 ( +
+
+ + + + {selectingMode === 'selection' ? ( + + ) : null} + +
+ + + + +
+ ); +}; + +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 && ( + + + + ) + ); +}; diff --git a/packages/plugins/map/src/client/block/MapBlockDesigner.tsx b/packages/plugins/map/src/client/block/MapBlockDesigner.tsx new file mode 100644 index 000000000..f894e53cf --- /dev/null +++ b/packages/plugins/map/src/client/block/MapBlockDesigner.tsx @@ -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 ( + + + {fixedBlockDesignerSetting} + { + 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(); + }} + /> + { + 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(); + }} + /> + { + 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(); + }} + > + { + 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'], + }, + }); + }} + /> + + + + + + ); +}; diff --git a/packages/plugins/map/src/client/block/MapBlockInitializer.tsx b/packages/plugins/map/src/client/block/MapBlockInitializer.tsx new file mode 100644 index 000000000..6cdfe16ac --- /dev/null +++ b/packages/plugins/map/src/client/block/MapBlockInitializer.tsx @@ -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 ( + } + 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 ( + + + { + 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' + }, + }, + }, + }} + /> + + + ); + }).open({ + initialValues: {}, + }); + insert( + createMapBlockSchema({ + collection: item.name, + fieldNames: { + ...values, + }, + }), + ); + }} + title={t('Map block')} + /> + ); +}; diff --git a/packages/plugins/map/src/client/block/MapBlockProvider.tsx b/packages/plugins/map/src/client/block/MapBlockProvider.tsx new file mode 100644 index 000000000..2348f9948 --- /dev/null +++ b/packages/plugins/map/src/client/block/MapBlockProvider.tsx @@ -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({}); + +const InternalMapBlockProvider = (props) => { + const { fieldNames } = props; + const fieldSchema = useFieldSchema(); + const field = useField(); + const { resource, service } = useBlockRequestContext(); + const [selectedRecordKeys, setSelectedRecordKeys] = useState([]); + + useFixedSchema(); + return ( + + + {props.children} + + + ); +}; + +export const MapBlockProvider = (props) => { + return ( + + + + ); +}; + +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, + }; +}; diff --git a/packages/plugins/map/src/client/block/index.tsx b/packages/plugins/map/src/client/block/index.tsx new file mode 100644 index 000000000..365373dad --- /dev/null +++ b/packages/plugins/map/src/client/block/index.tsx @@ -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 ( + + + {props.children} + + + ); +}; diff --git a/packages/plugins/map/src/client/block/utils.ts b/packages/plugins/map/src/client/block/utils.ts new file mode 100644 index 000000000..ac6031573 --- /dev/null +++ b/packages/plugins/map/src/client/block/utils.ts @@ -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; +}; diff --git a/packages/plugins/map/src/client/components/AMap.tsx b/packages/plugins/map/src/client/components/AMap.tsx index 788cd399a..134af781f 100644 --- a/packages/plugins/map/src/client/components/AMap.tsx +++ b/packages/plugins/map/src/client/components/AMap.tsx @@ -6,20 +6,24 @@ import { useFieldSchema } from '@formily/react'; import { useCollection } from '@nocobase/client'; import { useMemoizedFn } from 'ahooks'; 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 { useMapConfiguration } from '../hooks'; import { useMapTranslation } from '../locale'; import Search from './Search'; -interface AMapComponentProps { - accessKey: string; - securityJsCode: string; - value: any; - onChange: (value: number[]) => void; +export type MapEditorType = 'point' | 'polygon' | 'lineString' | 'circle'; + +export interface AMapComponentProps { + value?: any; + onChange?: (value: number[]) => void; disabled?: boolean; mapType: string; zoom: number; + type: MapEditorType; + style?: React.CSSProperties; + overlayCommonOptions?: AMap.PolylineOptions & AMap.PolygonOptions; + block?: boolean; } const methodMapping = { @@ -53,9 +57,30 @@ const methodMapping = { }, }; -const AMapComponent: React.FC = (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((props, ref) => { 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 fieldSchema = useFieldSchema(); const aMap = useRef(); @@ -64,9 +89,13 @@ const AMapComponent: React.FC = (props) => { const [needUpdateFlag, forceUpdate] = useState([]); const [errMessage, setErrMessage] = useState(''); const { getField } = useCollection(); - const collectionField = getField(fieldSchema.name); - const type = collectionField?.interface; - const overlay = useRef(); + const type = useMemo(() => { + if (props.type) return props.type; + const collectionField = getField(fieldSchema?.name); + return collectionField?.interface; + }, [props?.type, fieldSchema?.name]); + + const overlay = useRef(); const editor = useRef(null); const history = useHistory(); const id = useRef(`nocobase-map-${type}-${Date.now().toString(32)}`); @@ -76,16 +105,17 @@ const AMapComponent: React.FC = (props) => { strokeColor: '#4e9bff', fillColor: '#4e9bff', strokeOpacity: 1, + ...overlayCommonOptions, }); const toRemoveOverlay = useMemoizedFn(() => { if (overlay.current) { - map.current?.remove(overlay.current); + overlay.current.remove(); } }); const setTarget = useMemoizedFn(() => { - if (!disabled && type !== 'point' && editor.current) { + if ((!disabled || block) && type !== 'point' && editor.current) { editor.current.setTarget(overlay.current); editor.current.open(); } @@ -113,25 +143,38 @@ const AMapComponent: React.FC = (props) => { overlay.current = target; setTarget(); } - onChange(nextValue); + onChange?.(nextValue); }); - const createEditor = useMemoizedFn(() => { - const mapping = methodMapping[type as keyof typeof methodMapping]; + const createEditor = useMemoizedFn((curType = type) => { + const mapping = methodMapping[curType]; 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 }) { onMapChange(target, true); }); editor.current.on('move', function ({ target }) { onMapChange(target, true); }); + return editor.current; } }); - const executeMouseTool = useMemoizedFn(() => { + const executeMouseTool = useMemoizedFn((curType = type) => { if (!mouseTool.current || editor.current?.getTarget()) return; - const mapping = methodMapping[type as keyof typeof methodMapping]; + const mapping = methodMapping[curType]; if (!mapping) { return; } @@ -140,15 +183,13 @@ const AMapComponent: React.FC = (props) => { } as AMap.PolylineOptions); }); - const createMouseTool = useMemoizedFn(() => { + const createMouseTool = useMemoizedFn((curType: MapEditorType = type) => { if (mouseTool.current) return; - mouseTool.current = new aMap.current.MouseTool(map.current); mouseTool.current.on('draw', function ({ obj }) { onMapChange(obj); }); - - executeMouseTool(); + executeMouseTool(curType); }); const toCenter = (position, imm?: boolean) => { @@ -164,7 +205,7 @@ const AMapComponent: React.FC = (props) => { editor.current.setTarget(); editor.current.close(); } - onChange(null); + onChange?.(null); }; Modal.confirm({ title: t('Clear the canvas'), @@ -183,28 +224,41 @@ const AMapComponent: React.FC = (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(() => { if (!aMap.current) return; if (!value || overlay.current) { 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]); - - nextOverlay.setMap(map.current); overlay.current = nextOverlay; createEditor(); @@ -283,6 +337,19 @@ const AMapComponent: React.FC = (props) => { }; }, [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) { return ( = (props) => {
- {/* bottom: 20px; right: 50%; transform: translateX(50%); z-index: 2; */} -
- -
{!disabled ? ( <> +
+ +
= (props) => { ) : null}
); -}; +}); export default AMapComponent; diff --git a/packages/plugins/map/src/client/components/Map.tsx b/packages/plugins/map/src/client/components/Map.tsx index 3602f2999..fa68da2eb 100644 --- a/packages/plugins/map/src/client/components/Map.tsx +++ b/packages/plugins/map/src/client/components/Map.tsx @@ -1,14 +1,17 @@ import { connect, mapReadPretty } from '@formily/react'; import React from 'react'; -import AMapComponent from './AMap'; +import AMapComponent, { AMapComponentProps } from './AMap'; import ReadPretty from './ReadPretty'; import { css } from '@emotion/css'; import Designer from './Designer'; -const InternalMap = connect((props) => { +interface MapProps extends AMapComponentProps {} + +const InternalMap = connect((props: MapProps) => { return (
{ - const { value, readOnly } = props; + const { value } = props; const fieldSchema = useFieldSchema(); const { getField } = useCollection(); const collectionField = getField(fieldSchema.name); const mapType = props.mapType || collectionField?.uiSchema['x-component-props']?.mapType; const field = useField(); + const form = useForm() useEffect(() => { if (!field.title && collectionField?.uiSchema?.title) { @@ -17,7 +18,7 @@ const ReadPretty = (props) => { } }, collectionField?.title); - if (!readOnly) + if (!form.readPretty) { return (
@@ -25,6 +26,7 @@ const ReadPretty = (props) => {
); + } return mapType === 'amap' ? : null; }; diff --git a/packages/plugins/map/src/client/fields/schema.ts b/packages/plugins/map/src/client/fields/schema.ts index 7e3630ce1..9513533d3 100644 --- a/packages/plugins/map/src/client/fields/schema.ts +++ b/packages/plugins/map/src/client/fields/schema.ts @@ -41,17 +41,14 @@ export const commonSchema = { }, 'x-disabled': '{{ isOverride || !createOnly }}', default: 'amap', - enum: MapTypes - } + enum: MapTypes, + }, }, - schemaInitialize(schema: ISchema, { readPretty, block }) { + schemaInitialize(schema: ISchema, { block }) { if (block === 'Form') { Object.assign(schema, { - 'x-component-props': { - readOnly: readPretty ? true : false - }, 'x-designer': 'Map.Designer', }); } }, -} +}; diff --git a/packages/plugins/map/src/client/index.tsx b/packages/plugins/map/src/client/index.tsx index 16ab0d9c1..b41142822 100644 --- a/packages/plugins/map/src/client/index.tsx +++ b/packages/plugins/map/src/client/index.tsx @@ -2,21 +2,23 @@ import { CollectionManagerContext, CurrentAppInfoProvider, SchemaComponentOptions, - SettingsCenterProvider + SettingsCenterProvider, } from '@nocobase/client'; import React, { useContext } from 'react'; +import { MapBlockOptions } from './block'; import Configuration from './components/Configuration'; import Map from './components/Map'; import { interfaces } from './fields'; -import { Initialize } from './initialize'; +import { MapInitializer } from './initialize'; import { useMapTranslation } from './locale'; +import './locale'; export default React.memo((props) => { const ctx = useContext(CollectionManagerContext); const { t } = useMapTranslation(); return ( - + { }} > - - {props.children} - + + + {props.children} + + - + ); }); diff --git a/packages/plugins/map/src/client/initialize.tsx b/packages/plugins/map/src/client/initialize.tsx index 8e1daf8dd..00d8aa152 100644 --- a/packages/plugins/map/src/client/initialize.tsx +++ b/packages/plugins/map/src/client/initialize.tsx @@ -1,8 +1,8 @@ import { registerField, registerGroup, useCurrentAppInfo } from '@nocobase/client'; import React, { useEffect } from 'react'; import { fields } from './fields'; -import './locale'; import { generateNTemplate } from './locale'; +import './locale'; export const useRegisterInterface = () => { const { data } = useCurrentAppInfo() || {}; @@ -26,7 +26,7 @@ export const useRegisterInterface = () => { }, [data]); }; -export const Initialize: React.FC = (props) => { +export const MapInitializer: React.FC = (props) => { useRegisterInterface(); return {props.children}; }; diff --git a/packages/plugins/map/src/client/locale/index.ts b/packages/plugins/map/src/client/locale/index.ts index 55e629f62..fecd9a532 100644 --- a/packages/plugins/map/src/client/locale/index.ts +++ b/packages/plugins/map/src/client/locale/index.ts @@ -1,11 +1,12 @@ import { i18n } from '@nocobase/client'; import { useTranslation } from 'react-i18next'; - +import enUS from './en-US'; +import zhCN from './zh-CN'; export const NAMESPACE = 'map'; -// i18n.addResources('zh-CN', NAMESPACE, zhCN); -// i18n.addResources('en-US', NAMESPACE, enUS); +i18n.addResources('zh-CN', NAMESPACE, zhCN); +i18n.addResources('en-US', NAMESPACE, enUS); export function lang(key: string) { return i18n.t(key, { ns: NAMESPACE }); diff --git a/packages/plugins/map/src/client/locale/zh-CN.ts b/packages/plugins/map/src/client/locale/zh-CN.ts index ee98b04fa..5884b784b 100644 --- a/packages/plugins/map/src/client/locale/zh-CN.ts +++ b/packages/plugins/map/src/client/locale/zh-CN.ts @@ -1,32 +1,32 @@ const locale = { 'Map-based geometry': '基于地图的几何图形', 'Map type': '地图类型', - 'Point': '点', - 'Line': '线', - 'Circle': '圆', - 'Polygon': '多边形', + Point: '点', + Line: '线', + Circle: '圆', + Polygon: '多边形', 'Access key': '访问密钥', 'securityJsCode or serviceHost': 'securityJsCode 或 serviceHost', - 'AMap': '高德地图', + AMap: '高德地图', 'Google Maps': '谷歌地图', - 'Clear': '清空', + Clear: '清空', 'Click to select the starting point and double-click to end the drawing': '点击选择起点,双击结束绘制', 'Clear the canvas': '清空画布', 'Are you sure to clear the canvas?': '您确定要清空画布吗?', - 'Confirm': '确定', - 'Cancel': '取消', + Confirm: '确定', + Cancel: '取消', 'Enter keywords to search': '输入地方名关键字搜索(必须包含省/市)', 'The AccessKey is incorrect, please check it': '访问密钥不正确,请检查', 'Please configure the AMap securityCode or serviceHost correctly': '请正确配置高德地图 securityCode 或 serviceHost', 'Map Manager': '地图管理', - 'Configuration': '配置', + Configuration: '配置', 'Saved successfully': '保存成功', 'Saved failed': '保存失败', - 'Edit': '编辑', - 'Save': '保存', + Edit: '编辑', + Save: '保存', 'Please configure the AccessKey and SecurityJsCode first': '请先配置 AccessKey 和 SecurityJsCode', 'Go to the configuration page': '前往配置页面', - 'Zoom': '缩放', + Zoom: '缩放', 'Set default zoom level': '设置默认缩放级别', 'The default zoom level of the map': '地图默认缩放级别', // Designer @@ -34,12 +34,14 @@ const locale = { 'Field title': '字段标题', 'Edit tooltip': '编辑提示信息', 'Delete field': '删除字段', - "Required": "必填", - 'Pattern': '模式', - "Editable": "可编辑", - "Readonly": "只读(禁止编辑)", - "Easy-reading": "只读(阅读模式)", - "Edit description": "编辑描述", -} + Required: '必填', + Pattern: '模式', + Editable: '可编辑', + Readonly: '只读(禁止编辑)', + 'Easy-reading': '只读(阅读模式)', + 'Edit description': '编辑描述', + 'Map field': '地图字段', + 'Marker field': '标记字段', +}; export default locale;