feat: improve cascader component

This commit is contained in:
chenos 2021-08-16 12:35:52 +08:00
parent e9ddd4fd7e
commit 69d47a0331
8 changed files with 474 additions and 111 deletions

View File

@ -48,7 +48,7 @@ const plugins = [
'@nocobase/plugin-export',
'@nocobase/plugin-system-settings',
// '@nocobase/plugin-automations',
// '@nocobase/plugin-china-region',
'@nocobase/plugin-china-region',
];
for (const plugin of plugins) {

View File

@ -90,5 +90,9 @@ import * as uiSchema from './ui-schema';
default: process.env.STORAGE_TYPE === 'ali-oss',
});
// 导入地域数据
const ChinaRegion = database.getModel('china_regions');
ChinaRegion && await ChinaRegion.importData();
await database.close();
})();

View File

@ -1,9 +1,10 @@
import { createForm } from '@formily/core';
import { ArrayField, createForm } from '@formily/core';
import {
createSchemaField,
FormProvider,
Schema,
SchemaOptionsContext,
useField,
useFieldSchema,
useForm,
} from '@formily/react';
@ -18,7 +19,12 @@ import { useMemo } from 'react';
import { CodeOutlined } from '@ant-design/icons';
import Editor from '@monaco-editor/react';
import { ArrayItems, ArrayCollapse, FormLayout, FormItem as FormilyFormItem } from '@formily/antd';
import {
ArrayItems,
ArrayCollapse,
FormLayout,
FormItem as FormilyFormItem,
} from '@formily/antd';
import { Space, Card, Modal, Spin } from 'antd';
import { ArrayTable } from '../../schemas/array-table';
@ -27,7 +33,10 @@ import { AddNew } from '../../schemas/add-new';
import { Cascader } from '../../schemas/cascader';
import { Checkbox } from '../../schemas/checkbox';
import { ColorSelect } from '../../schemas/color-select';
import { DatabaseField, DatabaseCollection } from '../../schemas/database-field';
import {
DatabaseField,
DatabaseCollection,
} from '../../schemas/database-field';
import { DatePicker } from '../../schemas/date-picker';
import { Filter } from '../../schemas/filter';
import { Form } from '../../schemas/form';
@ -54,6 +63,9 @@ import { Chart } from '../../schemas/chart';
import { useDesignableSwitchContext } from '../../constate/DesignableSwitch';
import { action } from '@formily/reactive';
import { ISchema, FormilyISchema } from '../../schemas';
import { Resource } from '../../resource';
import { useRequest } from 'ahooks';
import { CascaderOptionType } from 'antd/lib/cascader';
export const BlockContext = createContext({ dragRef: null });
@ -74,10 +86,68 @@ export const useAsyncDataSource = (service: any) => (field: any) => {
);
};
export const useChinaRegionDataSource = ({ onSuccess }) => {
const field = useField<ArrayField>();
const maxLevel = field.componentProps.maxLevel || 3;
const resource = Resource.make('china_regions');
console.log('useChinaRegionDataSource');
useRequest(
() =>
resource.list({
perPage: -1,
filter: {
level: 1,
},
}),
{
formatResult: (data) =>
data?.data?.map((item) => {
if (maxLevel !== 1) {
item.isLeaf = false;
}
return item;
}),
onSuccess,
},
);
};
export const loadChinaRegionData = (
selectedOptions: CascaderOptionType[],
field: ArrayField,
) => {
const maxLevel = field.componentProps.maxLevel || 3;
const targetOption = selectedOptions[selectedOptions.length - 1];
targetOption.loading = true;
const resource = Resource.make('china_regions');
resource
.list({
perPage: -1,
filter: {
parent_code: targetOption['code'],
},
})
.then((data) => {
targetOption.loading = false;
targetOption.children =
data?.data?.map((item) => {
if (maxLevel > item.level) {
item.isLeaf = false;
}
return item;
}) || [];
field.dataSource = [...field.dataSource];
});
};
export const SchemaField = createSchemaField({
scope: {
Table,
useAsyncDataSource,
ChinaRegion: {
loadData: loadChinaRegionData,
useDataSource: useChinaRegionDataSource,
},
},
components: {
Card,
@ -163,7 +233,10 @@ export function findPropertyByPath(schema: Schema, path?: any): Schema {
return property;
}
export function addPropertyBefore(target: Schema, data: ISchema | FormilyISchema) {
export function addPropertyBefore(
target: Schema,
data: ISchema | FormilyISchema,
) {
Object.keys(target.parent.properties).forEach((name) => {
if (name === target.name) {
target.parent.addProperty(data.name, data);
@ -174,7 +247,10 @@ export function addPropertyBefore(target: Schema, data: ISchema | FormilyISchema
});
}
export function addPropertyAfter(target: Schema, data: ISchema | FormilyISchema) {
export function addPropertyAfter(
target: Schema,
data: ISchema | FormilyISchema,
) {
Object.keys(target.parent.properties).forEach((name) => {
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
@ -211,8 +287,8 @@ export function useDesignable(path?: any) {
const schemaPath = path || useSchemaPath();
const fieldSchema = useFieldSchema();
let current;
let currentSchema = current =
findPropertyByPath(schema, schemaPath) || ({} as Schema);
let currentSchema = (current =
findPropertyByPath(schema, schemaPath) || ({} as Schema));
if (!currentSchema) {
currentSchema = fieldSchema;
}
@ -221,7 +297,10 @@ export function useDesignable(path?: any) {
}
// console.log('useDesignable', { schema, schemaPath, currentSchema });
const options = useContext(SchemaOptionsContext);
let DesignableBar = get(options.components, currentSchema['x-designable-bar']);
let DesignableBar = get(
options.components,
currentSchema['x-designable-bar'],
);
if (!designable) {
DesignableBar = () => null;
}
@ -267,7 +346,10 @@ export function useDesignable(path?: any) {
refresh();
return target.properties[property.name];
},
appendChild: (property: ISchema | FormilyISchema, targetPath?: any): Schema => {
appendChild: (
property: ISchema | FormilyISchema,
targetPath?: any,
): Schema => {
let target = currentSchema;
if (targetPath) {
target = findPropertyByPath(schema, targetPath);
@ -294,7 +376,10 @@ export function useDesignable(path?: any) {
refresh();
return target.properties[property.name];
},
insertAfter: (property: ISchema | FormilyISchema, targetPath?: any): Schema => {
insertAfter: (
property: ISchema | FormilyISchema,
targetPath?: any,
): Schema => {
let target = currentSchema;
if (targetPath) {
target = findPropertyByPath(schema, targetPath);
@ -360,7 +445,7 @@ export function useDesignable(path?: any) {
remove(s.parent);
}
};
console.log({ removed })
console.log({ removed });
remove(target);
refresh();
return removed;
@ -533,7 +618,7 @@ export const SchemaRenderer = (props: SchemaRendererProps) => {
// useEffect(() => {
// refresh(uid());
// }, [designable]);
console.log({ designable }, 'designable')
console.log({ designable }, 'designable');
const defaultRender = ({ schema }) => (
<FormProvider form={form}>
<SchemaField

View File

@ -0,0 +1,78 @@
import React from 'react';
import { SchemaRenderer } from '../..';
const options = [
{
value: 'zhejiang',
label: 'Zhejiang',
children: [
{
value: 'hangzhou',
label: 'Hangzhou',
children: [
{
value: 'xihu',
label: 'West Lake',
},
],
},
],
},
{
value: 'jiangsu',
label: 'Jiangsu',
children: [
{
value: 'nanjing',
label: 'Nanjing',
children: [
{
value: 'zhonghuamen',
label: 'Zhong Hua Men',
},
],
},
],
},
];
const schema = {
type: 'object',
properties: {
input: {
type: 'string',
title: `编辑模式`,
name: 'name1',
enum: options,
'x-decorator': 'FormItem',
'x-component': 'Cascader',
'x-component-props': {
// changeOnSelect: true,
},
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
type: 'string',
title: `阅读模式`,
enum: options,
name: 'name2',
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Cascader',
'x-component-props': {
changeOnSelect: true,
},
},
},
};
export default () => {
return <SchemaRenderer schema={schema} />;
};

View File

@ -0,0 +1,136 @@
import React from 'react';
import { SchemaRenderer } from '../..';
import { CascaderOptionType } from 'antd/lib/cascader';
import { ArrayField } from '@formily/core';
import { action } from '@formily/reactive';
import { uid } from '@formily/shared';
import { useField } from '@formily/react';
import { useEffect } from 'react';
import { useRequest } from 'ahooks';
import { Resource } from '@nocobase/client/src/resource';
const options = [
{
value: 'zhejiang',
label: 'Zhejiang',
isLeaf: false,
},
{
value: 'jiangsu',
label: 'Jiangsu',
isLeaf: false,
},
];
const schema = {
type: 'object',
properties: {
input: {
type: 'string',
title: `编辑模式`,
name: 'name1',
// enum: options,
'x-decorator': 'FormItem',
'x-component': 'Cascader',
'x-component-props': {
changeOnSelect: true,
loadData: '{{ loadData }}',
useDataSource: '{{ useDataSource }}',
labelInValue: true,
fieldNames: {
label: 'name',
value: 'code',
children: 'children',
},
// changeOnSelect: true,
},
'x-reactions': [
// '{{useAsyncDataSource(loadOptions)}}',
{
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
],
},
read: {
type: 'string',
title: `阅读模式`,
enum: options,
name: 'name2',
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Cascader',
'x-component-props': {
changeOnSelect: true,
loadData: '{{ loadData }}',
useDataSource: '{{ useDataSource }}',
labelInValue: true,
fieldNames: {
label: 'name',
value: 'code',
children: 'children',
},
},
},
},
};
const useDataSource = ({ onSuccess }) => {
const field = useField<ArrayField>();
const maxLevel = field.componentProps.maxLevel || 3;
const resource = Resource.make('china_regions');
useRequest(
() =>
resource.list({
perPage: -1,
filter: {
level: 1,
},
}),
{
formatResult: (data) =>
data?.data?.map((item) => {
if (maxLevel !== 1) {
item.isLeaf = false;
}
return item;
}),
onSuccess,
},
);
};
const loadData = (selectedOptions: CascaderOptionType[], field: ArrayField) => {
const maxLevel = field.componentProps.maxLevel || 3;
const targetOption = selectedOptions[selectedOptions.length - 1];
targetOption.loading = true;
const resource = Resource.make('china_regions');
resource
.list({
perPage: -1,
filter: {
parent_code: targetOption['code'],
},
})
.then((data) => {
targetOption.loading = false;
targetOption.children =
data?.data?.map((item) => {
if (maxLevel > item.level) {
item.isLeaf = false;
}
return item;
}) || [];
field.dataSource = [...field.dataSource];
});
};
export default () => {
return (
<SchemaRenderer debug scope={{ useDataSource, loadData }} schema={schema} />
);
};

View File

@ -23,90 +23,10 @@ group:
## Examples
### 基本用法
<code src="./demos/demo1.tsx" />
### 省市区级联
```tsx
/**
* title: 省市区级联
*/
import React from 'react';
import { SchemaRenderer } from '../';
const options = [
{
value: 'zhejiang',
label: 'Zhejiang',
children: [
{
value: 'hangzhou',
label: 'Hangzhou',
children: [
{
value: 'xihu',
label: 'West Lake',
},
],
},
],
},
{
value: 'jiangsu',
label: 'Jiangsu',
children: [
{
value: 'nanjing',
label: 'Nanjing',
children: [
{
value: 'zhonghuamen',
label: 'Zhong Hua Men',
},
],
},
],
},
];
const schema = {
type: 'object',
properties: {
input: {
type: 'string',
title: `编辑模式`,
name: 'name1',
enum: options,
'x-decorator': 'FormItem',
'x-component': 'Cascader',
'x-component-props': {
changeOnSelect: true,
},
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
type: 'string',
title: `阅读模式`,
enum: options,
name: 'name2',
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Cascader',
'x-component-props': {
changeOnSelect: true,
},
},
},
};
export default () => {
return (
<SchemaRenderer schema={schema} />
);
};
```
<code src="./demos/demo2.tsx" />

View File

@ -1,13 +1,113 @@
import React from 'react'
import { connect, mapReadPretty, mapProps } from '@formily/react'
import { Cascader as AntdCascader } from 'antd'
import { Display } from '../display'
import { LoadingOutlined } from '@ant-design/icons'
import React from 'react';
import {
connect,
mapReadPretty,
mapProps,
useField,
observer,
} from '@formily/react';
import { Cascader as AntdCascader } from 'antd';
import { Display } from '../display';
import { LoadingOutlined } from '@ant-design/icons';
import { CascaderOptionType } from 'antd/lib/cascader';
import { ArrayField } from '@formily/core';
import { toArr } from '@formily/shared';
import { omit } from 'lodash';
const defaultFieldNames = {
label: 'label',
value: 'value',
children: 'children',
};
export const Cascader = connect(
(props) => {
console.log('props.title', props.title);
return <AntdCascader {...props}/>
(props: any) => {
const field = useField<ArrayField>();
const {
value,
onChange,
loadData,
labelInValue,
useDataSource,
fieldNames = defaultFieldNames,
...others
} = props;
// 兼容值为 object[] 的情况
const toValue = () => {
return toArr(value).map((item) => {
if (typeof item === 'object') {
return item[fieldNames.value];
}
return item;
});
};
const displayRender = (labels: string[], selectedOptions: any[]) => {
const values = toArr(value);
if (values.length !== labels.length) {
labels = toValue();
}
if (selectedOptions.length === 0) {
selectedOptions = values;
}
return labels.map((label, i) => {
let option = selectedOptions[i];
if (
!option ||
typeof option === 'string' ||
typeof option === 'number'
) {
option = { [fieldNames.label]: label, [fieldNames.value]: label };
}
if (i === labels.length - 1) {
return (
<span key={option[fieldNames.value]}>
{option[fieldNames.label]}
</span>
);
}
return (
<span key={option[fieldNames.value]}>
{option[fieldNames.label]} /{' '}
</span>
);
});
};
// 这里没有用 x-reactions 是因为 readyPretty=true 时不需要
if (useDataSource) {
useDataSource({
onSuccess: (data) => {
field.dataSource = data;
},
});
}
if (loadData) {
Object.assign(others, {
loadData: (selectedOptions: CascaderOptionType[]) => {
// 将 field 传给 loadData
loadData(selectedOptions, field);
},
});
}
return (
<AntdCascader
{...others}
value={toValue()}
fieldNames={fieldNames}
displayRender={displayRender}
onChange={(value, selectedOptions) => {
if (labelInValue) {
onChange(
selectedOptions.map((option) =>
omit(option, [fieldNames.children]),
),
);
} else {
onChange(value);
}
console.log({ value, selectedOptions });
}}
/>
);
},
mapProps(
{
@ -22,10 +122,39 @@ export const Cascader = connect(
) : (
props.suffixIcon
),
};
},
),
mapReadPretty((props) => {
const { fieldNames = defaultFieldNames } = props;
const values = toArr(props.value);
const len = values.length;
const field = useField<ArrayField>();
let dataSource = field.dataSource;
const data = [];
for (const item of values) {
if (typeof item === 'object') {
data.push(item);
} else {
const curr = dataSource?.find((v) => v[fieldNames.value] === item);
dataSource = curr?.[fieldNames.children] || [];
data.push(curr || { label: item, value: item });
}
}
),
mapReadPretty(Display.Cascader)
)
// console.log({ data });
return (
<div>
{data.map((item, index) => {
return (
<span>
{typeof item === 'object' ? item[fieldNames.label] : item}
{len > index + 1 && ' / '}
</span>
);
})}
</div>
);
}),
);
export default Cascader
export default Cascader;

View File

@ -16,7 +16,18 @@ export const chinaRegion: FieldOptions = {
type: 'array',
// title,
'x-component': 'Cascader',
'x-component-props': {},
'x-component-props': {
changeOnSelect: true,
loadData: '{{ ChinaRegion.loadData }}',
useDataSource: '{{ ChinaRegion.useDataSource }}',
labelInValue: true,
maxLevel: 3,
fieldNames: {
label: 'name',
value: 'code',
children: 'children',
},
},
'x-decorator': 'FormItem',
'x-designable-bar': 'Cascader.DesignableBar',
},