feat(map): support to filter other blocks (#1691)

* feat(Map): support to filter other blocks

* feat: highlight marker when selected

* feat: clear filter params when cancel connect

* feat: support line and polygon
This commit is contained in:
被雨水过滤的空气-Rairn 2023-04-17 10:12:49 +08:00 committed by GitHub
parent e8bb962389
commit 06d67a93ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 188 additions and 24 deletions

View File

@ -203,11 +203,11 @@ export const useCreateActionProps = () => {
};
};
interface FilterTarget {
export interface FilterTarget {
targets?: {
/** field uid */
uid: string;
/** associated fields */
/** associated field */
field?: string;
}[];
uid?: string;

View File

@ -1,8 +1,9 @@
import { useField, useFieldSchema } from '@formily/react';
import React, { createContext, useEffect, useRef } from 'react';
import { useBlockRequestContext } from '../block-provider';
import { SharedFilter } from '../block-provider/SharedFilterProvider';
import { SharedFilter, mergeFilter } from '../block-provider/SharedFilterProvider';
import { CollectionFieldOptions, useCollection } from '../collection-manager';
import { removeNullCondition } from '../schema-component';
import { useAssociatedFields } from './utils';
type Collection = ReturnType<typeof useCollection>;
@ -16,6 +17,8 @@ export interface DataBlock {
collection: Collection;
/** 根据提供的参数执行该方法即可刷新数据区块的数据 */
doFilter: (params: any, params2?: any) => Promise<void>;
/** 清除筛选区块设置的筛选参数 */
clearFilter: (uid: string) => void;
/** 数据区块表中所有的关系字段 */
associatedFields?: CollectionFieldOptions[];
/** 通过右上角菜单设置的过滤条件 */
@ -72,6 +75,24 @@ export const FilterBlockRecord = ({
defaultFilter: params?.filter || {},
service,
dom: container.current,
clearFilter(uid: string) {
const param = this.service.params?.[0] || {};
const storedFilter = this.service.params?.[1]?.filters || {};
delete storedFilter[uid];
const mergedFilter = mergeFilter([
...Object.values(storedFilter).map((filter) => removeNullCondition(filter)),
params?.filter || {},
]);
this.service.run(
{
...param,
page: 1,
filter: mergedFilter,
},
{ filters: storedFilter },
);
},
});
};

View File

@ -0,0 +1,3 @@
export * from './FilterProvider';
export * from './utils';

View File

@ -1,6 +1,11 @@
import { Schema, useFieldSchema } from '@formily/react';
import { isPlainObject, isEmpty } from '@nocobase/utils/client';
import { isEmpty, isPlainObject } from '@nocobase/utils/client';
import _ from 'lodash';
import { useCallback, useEffect, useState } from 'react';
import { mergeFilter } from '../block-provider';
import { FilterTarget, findFilterTargets } from '../block-provider/hooks';
import { Collection, FieldOptions, useCollection } from '../collection-manager';
import { removeNullCondition } from '../schema-component';
import { findFilterOperators } from '../schema-component/antd/form-item/SchemaSettingOptions';
import { useFilterBlock } from './FilterProvider';
@ -87,3 +92,78 @@ export const isAssocField = (field?: FieldOptions) => {
export const isSameCollection = (c1: Collection, c2: Collection) => {
return c1.name === c2.name;
};
export const useFilterAPI = () => {
const fieldSchema = useFieldSchema();
const { getDataBlocks } = useFilterBlock();
const { targets, uid } = findFilterTargets(fieldSchema);
const dataBlocks = getDataBlocks();
const [isConnected, setIsConnected] = useState(() => {
return targets && targets.some((target) => dataBlocks.some((dataBlock) => dataBlock.uid === target.uid));
});
const targetsKeys = Object.keys(targets || {});
useEffect(() => {
setIsConnected(targets && targets.some((target) => dataBlocks.some((dataBlock) => dataBlock.uid === target.uid)));
}, [targetsKeys.length, dataBlocks]);
const doFilter = useCallback(
(
value,
field: string | ((target: FilterTarget['targets'][0]) => string) = 'id',
operator: string | ((target: FilterTarget['targets'][0]) => string) = '$eq',
) => {
dataBlocks.forEach((block) => {
const target = targets.find((target) => target.uid === block.uid);
if (!target) return;
if (_.isFunction(field)) {
field = field(target);
}
if (_.isFunction(operator)) {
operator = operator(target);
}
const param = block.service.params?.[0] || {};
// 保留原有的 filter
const storedFilter = block.service.params?.[1]?.filters || {};
if (value !== undefined) {
storedFilter[uid] = {
$and: [
{
[field]: {
[operator]: value,
},
},
],
};
} else {
delete storedFilter[uid];
}
const mergedFilter = mergeFilter([
...Object.values(storedFilter).map((filter) => removeNullCondition(filter)),
block.defaultFilter,
]);
block.doFilter(
{
...param,
page: 1,
filter: mergedFilter,
},
{ filters: storedFilter },
);
});
},
[dataBlocks],
);
return {
/** 当前区块是否已连接其它区块 */
isConnected,
/** 调用该方法进行过滤 */
doFilter,
};
};

View File

@ -11,6 +11,7 @@ export * from './board';
export * from './china-region';
export * from './collection-manager';
export * from './document-title';
export * from './filter-provider';
export * from './formula';
export * from './i18n';
export * from './icon';

View File

@ -496,6 +496,7 @@ SchemaSettings.ConnectDataBlocks = (props: { type: FilterBlockType; emptyDescrip
targets.push({ uid: block.uid });
} else {
targets = targets.filter((target) => target.uid !== block.uid);
block.clearFilter(uid);
}
updateFilterTargets(fieldSchema, targets);
@ -537,6 +538,7 @@ SchemaSettings.ConnectDataBlocks = (props: { type: FilterBlockType; emptyDescrip
onChange={(value) => {
if (value === '') {
targets = targets.filter((target) => target.uid !== block.uid);
block.clearFilter(uid);
} else {
targets = targets.filter((target) => target.uid !== block.uid);
targets.push({ uid: block.uid, field: value });

View File

@ -1,15 +1,17 @@
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 { CheckOutlined, EnvironmentOutlined, ExpandOutlined } from '@ant-design/icons';
import { css } from '@emotion/css';
import { RecursionField, Schema, useFieldSchema } from '@formily/react';
import { ActionContext, RecordProvider, useCollection, useCompile, useFilterAPI, useProps } from '@nocobase/client';
import { useMemoizedFn } from 'ahooks';
import { Button, Space } from 'antd';
import { ExpandOutlined, EnvironmentOutlined, CheckOutlined } from '@ant-design/icons';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import AMap, { AMapForwardedRefProps } from '../components/AMap';
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=='
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);
@ -22,7 +24,9 @@ export const MapBlock = (props) => {
const [visible, setVisible] = useState(false);
const [selectingMode, setSelecting] = useState('');
const { t } = useMapTranslation();
const compile = useCompile()
const compile = useCompile();
const { isConnected, doFilter } = useFilterAPI();
const [, setPrevSelected] = useState(null);
const selectingModeRef = useRef(selectingMode);
selectingModeRef.current = selectingMode;
@ -31,10 +35,12 @@ export const MapBlock = (props) => {
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.setIcon(
new mapRef.current.aMap.Icon({
imageSize: [19, 32],
image: selected ? defaultImage : selectedImage,
} as AMap.IconOpts),
);
}
(overlay as AMap.Polygon).setOptions({
extData,
@ -106,6 +112,7 @@ export const MapBlock = (props) => {
const overlay = mapRef.current.setOverlay(field.type, data, {
strokeColor: '#4e9bff',
fillColor: '#4e9bff',
cursor: 'pointer',
label: {
direction: 'bottom',
offset: [0, 5],
@ -137,6 +144,27 @@ export const MapBlock = (props) => {
const data = dataSource?.find((item) => {
return extData.id === item[getPrimaryKey()];
});
// 筛选区块模式
if (isConnected) {
setPrevSelected((prev) => {
prev && clearSelected(prev);
if (prev === o) {
clearSelected(o);
// 删除过滤参数
doFilter(null);
return null;
} else {
selectMarker(o);
doFilter(data[getPrimaryKey()], (target) => target.field || getPrimaryKey(), '$eq');
}
return o;
});
return;
}
if (data) {
setVisible(true);
setRecord(data);
@ -152,7 +180,7 @@ export const MapBlock = (props) => {
});
events.forEach((e) => e());
};
}, [dataSource, isMapInitialization, fieldNames, field.type]);
}, [dataSource, isMapInitialization, fieldNames, field.type, isConnected]);
useEffect(() => {
setTimeout(() => {
@ -254,3 +282,29 @@ const MapBlockDrawer = (props) => {
)
);
};
function clearSelected(marker: AMap.Marker | AMap.Polygon | AMap.Polyline | AMap.Circle) {
if ((marker as AMap.Marker).dom) {
(marker as AMap.Marker).dom.style.filter = 'none';
// AMap.Polygon | AMap.Polyline | AMap.Circle 都有 setOptions 方法
} else if ((marker as AMap.Polygon).setOptions) {
(marker as AMap.Polygon).setOptions({
strokeColor: '#4e9bff',
fillColor: '#4e9bff',
});
}
}
function selectMarker(marker: AMap.Marker | AMap.Polygon | AMap.Polyline | AMap.Circle) {
if ((marker as AMap.Marker).dom) {
(marker as AMap.Marker).dom.style.filter = 'brightness(1.2) contrast(1.2) hue-rotate(180deg)';
// AMap.Polygon | AMap.Polyline | AMap.Circle 都有 setOptions 方法
} else if ((marker as AMap.Polygon).setOptions) {
(marker as AMap.Polygon).setOptions({
strokeColor: '#F18b62',
fillColor: '#F18b62',
});
}
}

View File

@ -1,14 +1,15 @@
import { ISchema, useField, useFieldSchema } from '@formily/react';
import {
useCollection,
useCollectionFilterOptions,
useDesignable,
useSchemaTemplate,
FilterBlockType,
FixedBlockDesignerItem,
GeneralSchemaDesigner,
SchemaSettings,
mergeFilter,
useCollection,
useCollectionFilterOptions,
useCollectionManager,
useDesignable,
useSchemaTemplate,
} from '@nocobase/client';
import set from 'lodash/set';
import React from 'react';
@ -145,6 +146,7 @@ export const MapBlockDesigner = () => {
});
}}
/>
<SchemaSettings.ConnectDataBlocks type={FilterBlockType.TABLE} emptyDescription={t('No blocks to connect')} />
<SchemaSettings.Divider />
<SchemaSettings.Template componentName={'Map'} collectionName={name} resourceName={defaultResource} />
<SchemaSettings.Divider />

View File

@ -1,6 +1,6 @@
import { useField, useFieldSchema } from '@formily/react';
import { BlockProvider, FixedBlockWrapper, SchemaComponentOptions, useBlockRequestContext } from '@nocobase/client';
import React, { createContext, useContext, useEffect, useState } from 'react';
import React, { createContext, useContext, useState } from 'react';
export const MapBlockContext = createContext<any>({});

View File

@ -19,6 +19,8 @@ export const createMapBlockSchema = (options) => {
},
'x-designer': 'MapBlockDesigner',
'x-component': 'CardItem',
// 保存当前筛选区块所能过滤的数据区块
'x-filter-targets': [],
properties: {
actions: {
type: 'void',
@ -75,6 +77,5 @@ export const createMapBlockSchema = (options) => {
},
},
};
console.log(JSON.stringify(schema, null, 2));
return schema;
};