mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-02 03:58:07 +08:00
parent
0e420dfa47
commit
ae11ca586d
357
docs/zh-CN/components/form/condition-builder.md
Normal file
357
docs/zh-CN/components/form/condition-builder.md
Normal file
@ -0,0 +1,357 @@
|
||||
---
|
||||
title: 组合条件
|
||||
description:
|
||||
type: 0
|
||||
group: null
|
||||
menuName: 组合条件
|
||||
icon:
|
||||
---
|
||||
|
||||
## 基本用法
|
||||
|
||||
用于设置复杂组合条件,支持添加条件,添加分组,设置组合方式,拖拽排序等功能。
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"debug": true,
|
||||
"controls": [
|
||||
{
|
||||
"type": "condition-builder",
|
||||
"label": "条件组件",
|
||||
"name": "conditions",
|
||||
"description": "适合让用户自己拼查询条件,然后后端根据数据生成 query where",
|
||||
"fields": [
|
||||
{
|
||||
"label": "文本",
|
||||
"type": "text",
|
||||
"name": "text"
|
||||
},
|
||||
{
|
||||
"label": "数字",
|
||||
"type": "number",
|
||||
"name": "number"
|
||||
},
|
||||
{
|
||||
"label": "布尔",
|
||||
"type": "boolean",
|
||||
"name": "boolean"
|
||||
},
|
||||
{
|
||||
"label": "选项",
|
||||
"type": "select",
|
||||
"name": "select",
|
||||
"options": [
|
||||
{
|
||||
"label": "A",
|
||||
"value": "a"
|
||||
},
|
||||
{
|
||||
"label": "B",
|
||||
"value": "b"
|
||||
},
|
||||
{
|
||||
"label": "C",
|
||||
"value": "c"
|
||||
},
|
||||
{
|
||||
"label": "D",
|
||||
"value": "d"
|
||||
},
|
||||
{
|
||||
"label": "E",
|
||||
"value": "e"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "动态选项",
|
||||
"type": "select",
|
||||
"name": "select2",
|
||||
"source": "https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/mock2/form/getOptions?waitSeconds=1"
|
||||
},
|
||||
{
|
||||
"label": "日期",
|
||||
"children": [
|
||||
{
|
||||
"label": "日期",
|
||||
"type": "date",
|
||||
"name": "date"
|
||||
},
|
||||
{
|
||||
"label": "时间",
|
||||
"type": "time",
|
||||
"name": "time"
|
||||
},
|
||||
{
|
||||
"label": "日期时间",
|
||||
"type": "datetime",
|
||||
"name": "datetime"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 值格式说明
|
||||
|
||||
```ts
|
||||
type ValueGroup = {
|
||||
conjunction: 'and' | 'or';
|
||||
children: Array<ValueGroup | ValueItem>;
|
||||
};
|
||||
type ValueItem = {
|
||||
// 左侧字段,这块有预留类型,不过目前基本上只是字段。
|
||||
left: {
|
||||
type: 'field';
|
||||
field: string;
|
||||
};
|
||||
|
||||
// 还有更多类型,暂不细说
|
||||
op: 'equals' | 'not_equal' | 'less' | 'less_or_equal';
|
||||
|
||||
// 根据字段类型和 op 的不同,值格式会不一样。
|
||||
// 如果 op 是范围,right 就是个数组 [开始值,结束值],否则就是值。
|
||||
right: any;
|
||||
};
|
||||
|
||||
type Value = ValueGroup;
|
||||
```
|
||||
|
||||
## 字段选项
|
||||
|
||||
字段选项为这个组件主要配置部分,通过 `fields` 字段来配置,有哪些字段,并且字段的类型是什么,支持哪些比较操作符。
|
||||
|
||||
`fields` 为数组类型,每个成员表示一个可选字段,支持多个层,配置示例
|
||||
|
||||
```json
|
||||
"fields": [
|
||||
{
|
||||
"label": "字段1"
|
||||
// 字段1
|
||||
},
|
||||
{
|
||||
"label": "字段2"
|
||||
// 字段2
|
||||
},
|
||||
{
|
||||
"label": "字段分组",
|
||||
"children": [
|
||||
{
|
||||
"label": "字段3"
|
||||
},
|
||||
{
|
||||
"label": "字段4"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 支持的字段类型
|
||||
|
||||
这里面能用的字段类型和表单项中的字段类型不一样,还没支持那么多,基本上只有一些基础的类型,其他复杂类型还需后续扩充,目前基本上支持以下这些类型。
|
||||
|
||||
### 文本
|
||||
|
||||
- `type` 字段配置中配置成 `"text"`
|
||||
- `label` 字段名称。
|
||||
- `placeholder` 占位符
|
||||
- `operators` 默认为 `[ 'equal', 'not_equal', 'is_empty', 'is_not_empty', 'like', 'not_like', 'starts_with', 'ends_with' ]` 如果不要那么多,可以配置覆盖。
|
||||
- `defaultOp` 默认为 `"equal"`
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"debug": true,
|
||||
"controls": [
|
||||
{
|
||||
"type": "condition-builder",
|
||||
"label": "条件组件",
|
||||
"name": "conditions",
|
||||
"description": "适合让用户自己拼查询条件,然后后端根据数据生成 query where",
|
||||
"fields": [
|
||||
{
|
||||
"label": "A",
|
||||
"type": "text",
|
||||
"name": "a"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 数字
|
||||
|
||||
- `type` 字段配置中配置成 `"number"`
|
||||
- `label` 字段名称。
|
||||
- `placeholder` 占位符
|
||||
- `operators` 默认为 `[ 'equal', 'not_equal', 'less', 'less_or_equal', 'greater', 'greater_or_equal', 'between', 'not_between', 'is_empty', 'is_not_empty' ]` 如果不要那么多,可以配置覆盖。
|
||||
- `defaultOp` 默认为 `"equal"`
|
||||
- `minimum` 最小值
|
||||
- `maximum` 最大值
|
||||
- `step` 步长
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"debug": true,
|
||||
"controls": [
|
||||
{
|
||||
"type": "condition-builder",
|
||||
"label": "条件组件",
|
||||
"name": "conditions",
|
||||
"description": "适合让用户自己拼查询条件,然后后端根据数据生成 query where",
|
||||
"fields": [
|
||||
{
|
||||
"label": "A",
|
||||
"type": "number",
|
||||
"name": "a",
|
||||
"minimum": 1,
|
||||
"maximum": 10,
|
||||
"step": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 日期
|
||||
|
||||
- `type` 字段配置中配置成 `"date"`
|
||||
- `label` 字段名称。
|
||||
- `placeholder` 占位符
|
||||
- `operators` 默认为 `[ 'equal', 'not_equal', 'less', 'less_or_equal', 'greater', 'greater_or_equal', 'between', 'not_between', 'is_empty', 'is_not_empty' ]` 如果不要那么多,可以配置覆盖。
|
||||
- `defaultOp` 默认为 `"equal"`
|
||||
- `defaultValue` 默认值
|
||||
- `format` 默认 `"YYYY-MM-DD"` 值格式
|
||||
- `inputFormat` 默认 `"YYYY-MM-DD"` 显示的日期格式。
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"debug": true,
|
||||
"controls": [
|
||||
{
|
||||
"type": "condition-builder",
|
||||
"label": "条件组件",
|
||||
"name": "conditions",
|
||||
"description": "适合让用户自己拼查询条件,然后后端根据数据生成 query where",
|
||||
"fields": [
|
||||
{
|
||||
"label": "A",
|
||||
"type": "date",
|
||||
"name": "a"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 日期时间
|
||||
|
||||
- `type` 字段配置中配置成 `"datetime"`
|
||||
- `label` 字段名称。
|
||||
- `placeholder` 占位符
|
||||
- `operators` 默认为 `[ 'equal', 'not_equal', 'less', 'less_or_equal', 'greater', 'greater_or_equal', 'between', 'not_between', 'is_empty', 'is_not_empty' ]` 如果不要那么多,可以配置覆盖。
|
||||
- `defaultOp` 默认为 `"equal"`
|
||||
- `defaultValue` 默认值
|
||||
- `format` 默认 `""` 值格式
|
||||
- `inputFormat` 默认 `"YYYY-MM-DD HH:mm"` 显示的日期格式。
|
||||
- `timeFormat` 默认 `"HH:mm"` 时间格式,决定输入框有哪些。
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"debug": true,
|
||||
"controls": [
|
||||
{
|
||||
"type": "condition-builder",
|
||||
"label": "条件组件",
|
||||
"name": "conditions",
|
||||
"description": "适合让用户自己拼查询条件,然后后端根据数据生成 query where",
|
||||
"fields": [
|
||||
{
|
||||
"label": "A",
|
||||
"type": "datetime",
|
||||
"name": "a"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 时间
|
||||
|
||||
- `type` 字段配置中配置成 `"time"`
|
||||
- `label` 字段名称。
|
||||
- `placeholder` 占位符
|
||||
- `operators` 默认为 `[ 'equal', 'not_equal', 'less', 'less_or_equal', 'greater', 'greater_or_equal', 'between', 'not_between', 'is_empty', 'is_not_empty' ]` 如果不要那么多,可以配置覆盖。
|
||||
- `defaultOp` 默认为 `"equal"`
|
||||
- `defaultValue` 默认值
|
||||
- `format` 默认 `"HH:mm"` 值格式
|
||||
- `inputFormat` 默认 `"HH:mm"` 显示的日期格式。
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"debug": true,
|
||||
"controls": [
|
||||
{
|
||||
"type": "condition-builder",
|
||||
"label": "条件组件",
|
||||
"name": "conditions",
|
||||
"description": "适合让用户自己拼查询条件,然后后端根据数据生成 query where",
|
||||
"fields": [
|
||||
{
|
||||
"label": "A",
|
||||
"type": "time",
|
||||
"name": "a"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 下拉选择
|
||||
|
||||
- `type` 字段配置中配置成 `"select"`
|
||||
- `label` 字段名称。
|
||||
- `placeholder` 占位符
|
||||
- `operators` 默认为 `[ 'select_equals', 'select_not_equals', 'select_any_in', 'select_not_any_in' ]` 如果不要那么多,可以配置覆盖。
|
||||
- `defaultOp`
|
||||
- `options` 选项列表,`Array<{label: string, value: any}>`
|
||||
- `source` 动态选项,请配置 api。
|
||||
- `searchable` 是否可以搜索
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"debug": true,
|
||||
"controls": [
|
||||
{
|
||||
"type": "condition-builder",
|
||||
"label": "条件组件",
|
||||
"name": "conditions",
|
||||
"description": "适合让用户自己拼查询条件,然后后端根据数据生成 query where",
|
||||
"fields": [
|
||||
{
|
||||
"label": "A",
|
||||
"type": "select",
|
||||
"name": "a",
|
||||
"source": "https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/mock2/form/getOptions?waitSeconds=1",
|
||||
"searchable": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
@ -306,6 +306,15 @@ export const components = [
|
||||
makeMarkdownRenderer
|
||||
)
|
||||
},
|
||||
{
|
||||
label: 'Condition-Builder 条件组合',
|
||||
path: '/zh-CN/components/form/condition-builder',
|
||||
getComponent: () =>
|
||||
// @ts-ignore
|
||||
import('../../docs/zh-CN/components/form/condition-builder.md').then(
|
||||
makeMarkdownRenderer
|
||||
)
|
||||
},
|
||||
{
|
||||
label: 'Date 日期选择器',
|
||||
path: '/zh-CN/components/form/date',
|
||||
|
@ -169,7 +169,7 @@
|
||||
cursor: move;
|
||||
width: 20px;
|
||||
margin-left: -5px;
|
||||
opacity: 0;
|
||||
opacity: 0.6;
|
||||
text-align: center;
|
||||
transition: opacity var(--animation-duration) ease-out;
|
||||
@include icon-color();
|
||||
|
@ -3,8 +3,9 @@ import qs from 'qs';
|
||||
import React from 'react';
|
||||
import Alert from './components/Alert2';
|
||||
import ImageGallery from './components/ImageGallery';
|
||||
import {RendererEnv} from './env';
|
||||
import {envOverwrite} from './envOverwrite';
|
||||
import {RendererEnv, RendererProps} from './factory';
|
||||
import {RendererProps} from './factory';
|
||||
import {LocaleContext, TranslateFn} from './locale';
|
||||
import {RootRenderer} from './RootRenderer';
|
||||
import {SchemaRenderer} from './SchemaRenderer';
|
||||
|
@ -454,4 +454,73 @@ export interface BaseSchema {
|
||||
visibleOn?: SchemaExpression;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
/**
|
||||
* 用来显示的文字
|
||||
*/
|
||||
label?: string;
|
||||
|
||||
/**
|
||||
* 可以用来给 Option 标记个范围,让数据展示更清晰。
|
||||
*
|
||||
* 这个只有在数值展示的时候显示。
|
||||
*/
|
||||
scopeLabel?: string;
|
||||
|
||||
/**
|
||||
* 请保证数值唯一,多个选项值一致会认为是同一个选项。
|
||||
*/
|
||||
value?: any;
|
||||
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* 支持嵌套
|
||||
*/
|
||||
children?: Options;
|
||||
|
||||
/**
|
||||
* 是否可见
|
||||
*/
|
||||
visible?: boolean;
|
||||
|
||||
/**
|
||||
* 最好不要用!因为有 visible 就够了。
|
||||
*
|
||||
* @deprecated 用 visible
|
||||
*/
|
||||
hidden?: boolean;
|
||||
|
||||
/**
|
||||
* 描述,部分控件支持
|
||||
*/
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* 标记后数据延时加载
|
||||
*/
|
||||
defer?: boolean;
|
||||
|
||||
/**
|
||||
* 如果设置了,优先级更高,不设置走 source 接口加载。
|
||||
*/
|
||||
deferApi?: SchemaApi;
|
||||
|
||||
/**
|
||||
* 标记正在加载。只有 defer 为 true 时有意义。内部字段不可以外部设置
|
||||
*/
|
||||
loading?: boolean;
|
||||
|
||||
/**
|
||||
* 只有设置了 defer 才有意义,内部字段不可以外部设置
|
||||
*/
|
||||
loaded?: boolean;
|
||||
|
||||
[propName: string]: any;
|
||||
}
|
||||
export interface Options extends Array<Option> {}
|
||||
|
||||
export {PageSchema};
|
||||
|
@ -26,76 +26,10 @@ import Input from './Input';
|
||||
import {Api} from '../types';
|
||||
import {LocaleProps, localeable} from '../locale';
|
||||
import Spinner from './Spinner';
|
||||
import {SchemaApi} from '../Schema';
|
||||
import {Option, Options} from '../Schema';
|
||||
import {withRemoteOptions} from './WithRemoteOptions';
|
||||
|
||||
export interface Option {
|
||||
/**
|
||||
* 用来显示的文字
|
||||
*/
|
||||
label?: string;
|
||||
|
||||
/**
|
||||
* 可以用来给 Option 标记个范围,让数据展示更清晰。
|
||||
*
|
||||
* 这个只有在数值展示的时候显示。
|
||||
*/
|
||||
scopeLabel?: string;
|
||||
|
||||
/**
|
||||
* 请保证数值唯一,多个选项值一致会认为是同一个选项。
|
||||
*/
|
||||
value?: any;
|
||||
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* 支持嵌套
|
||||
*/
|
||||
children?: Options;
|
||||
|
||||
/**
|
||||
* 是否可见
|
||||
*/
|
||||
visible?: boolean;
|
||||
|
||||
/**
|
||||
* 最好不要用!因为有 visible 就够了。
|
||||
*
|
||||
* @deprecated 用 visible
|
||||
*/
|
||||
hidden?: boolean;
|
||||
|
||||
/**
|
||||
* 描述,部分控件支持
|
||||
*/
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* 标记后数据延时加载
|
||||
*/
|
||||
defer?: boolean;
|
||||
|
||||
/**
|
||||
* 如果设置了,优先级更高,不设置走 source 接口加载。
|
||||
*/
|
||||
deferApi?: SchemaApi;
|
||||
|
||||
/**
|
||||
* 标记正在加载。只有 defer 为 true 时有意义。内部字段不可以外部设置
|
||||
*/
|
||||
loading?: boolean;
|
||||
|
||||
/**
|
||||
* 只有设置了 defer 才有意义,内部字段不可以外部设置
|
||||
*/
|
||||
loaded?: boolean;
|
||||
|
||||
[propName: string]: any;
|
||||
}
|
||||
export interface Options extends Array<Option> {}
|
||||
export {Option, Options};
|
||||
|
||||
export interface OptionProps {
|
||||
className?: string;
|
||||
@ -105,6 +39,7 @@ export interface OptionProps {
|
||||
labelField?: string;
|
||||
simpleValue?: boolean; // 默认onChange 出去是整个 option 节点,如果配置了 simpleValue 就只包含值。
|
||||
options: Options;
|
||||
loading: boolean;
|
||||
joinValues?: boolean;
|
||||
extractValue?: boolean;
|
||||
delimiter?: string;
|
||||
@ -330,7 +265,6 @@ interface SelectProps extends OptionProps, ThemeProps, LocaleProps {
|
||||
value: any;
|
||||
loadOptions?: Function;
|
||||
searchPromptText: string;
|
||||
loading?: boolean;
|
||||
loadingPlaceholder: string;
|
||||
spinnerClassName?: string;
|
||||
noResultsText: string;
|
||||
@ -1067,10 +1001,13 @@ export class Select extends React.Component<SelectProps, SelectState> {
|
||||
}
|
||||
}
|
||||
|
||||
export default themeable(
|
||||
const enhancedSelect = themeable(
|
||||
localeable(
|
||||
uncontrollable(Select, {
|
||||
value: 'onChange'
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
export default enhancedSelect;
|
||||
export const SelectWithRemoteOptions = withRemoteOptions(enhancedSelect);
|
||||
|
195
src/components/WithRemoteOptions.tsx
Normal file
195
src/components/WithRemoteOptions.tsx
Normal file
@ -0,0 +1,195 @@
|
||||
/**
|
||||
* 让选项类的组件支持远程加载选项。
|
||||
*
|
||||
* 目前这个逻辑其实在 renderer/form/options 中有
|
||||
* 但是那个里面耦合较多,没办法简单的在组件之间相互调用,
|
||||
* 所以先单独弄个 hoc 出来,后续再想个更加合理的方案。
|
||||
*/
|
||||
import React from 'react';
|
||||
import hoistNonReactStatic from 'hoist-non-react-statics';
|
||||
import {Api, Payload} from '../types';
|
||||
import {Option, SchemaApi, SchemaTokenizeableString} from '../Schema';
|
||||
import {withStore} from './WithStore';
|
||||
|
||||
import {EnvContext, RendererEnv} from '../env';
|
||||
|
||||
import {flow, Instance, types} from 'mobx-state-tree';
|
||||
import {buildApi, isEffectiveApi} from '../utils/api';
|
||||
import {isPureVariable, resolveVariableAndFilter} from '../utils/tpl-builtin';
|
||||
import {normalizeOptions} from './Select';
|
||||
import {reaction} from 'mobx';
|
||||
|
||||
export const Store = types
|
||||
.model('OptionsStore')
|
||||
.props({
|
||||
fetching: false,
|
||||
errorMsg: '',
|
||||
options: types.frozen<Array<Option>>([]),
|
||||
data: types.frozen({})
|
||||
})
|
||||
.actions(self => {
|
||||
const load: (env: RendererEnv, api: Api, data: any) => Promise<any> = flow(
|
||||
function* (env, api, data) {
|
||||
try {
|
||||
self.fetching = true;
|
||||
const ret: Payload = yield env.fetcher(api, data);
|
||||
|
||||
if (ret.ok) {
|
||||
const data = ret.data || {};
|
||||
let options = data.options || data.items || data.rows || data;
|
||||
(self as any).setOptions(options);
|
||||
} else {
|
||||
throw new Error(ret.msg || 'fetch error');
|
||||
}
|
||||
} catch (e) {
|
||||
self.errorMsg = e.message;
|
||||
} finally {
|
||||
self.fetching = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
load,
|
||||
setData(data: any) {
|
||||
self.data = data || {};
|
||||
},
|
||||
setOptions(options: any) {
|
||||
options = normalizeOptions(options);
|
||||
|
||||
if (Array.isArray(options)) {
|
||||
self.options = options.concat();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export type IStore = Instance<typeof Store>;
|
||||
|
||||
export interface RemoteOptionsProps {
|
||||
options: Array<Option>;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export interface OutterProps {
|
||||
env?: RendererEnv;
|
||||
data: any;
|
||||
source?: SchemaApi | SchemaTokenizeableString;
|
||||
options?: Array<Option>;
|
||||
}
|
||||
|
||||
export function withRemoteOptions<
|
||||
T extends React.ComponentType<React.ComponentProps<T> & RemoteOptionsProps>
|
||||
>(ComposedComponent: T) {
|
||||
type FinalOutterProps = JSX.LibraryManagedAttributes<
|
||||
T,
|
||||
Omit<React.ComponentProps<T>, keyof RemoteOptionsProps>
|
||||
> &
|
||||
OutterProps;
|
||||
|
||||
const result = hoistNonReactStatic(
|
||||
withStore(() => Store.create())(
|
||||
class extends React.Component<
|
||||
FinalOutterProps & {
|
||||
store: IStore;
|
||||
}
|
||||
> {
|
||||
static displayName = `WithRemoteOptions(${
|
||||
ComposedComponent.displayName || ComposedComponent.name
|
||||
})`;
|
||||
static ComposedComponent = ComposedComponent;
|
||||
static contextType = EnvContext;
|
||||
toDispose: Array<() => void> = [];
|
||||
|
||||
componentDidMount() {
|
||||
const env: RendererEnv = this.props.env || this.context;
|
||||
const {store, source, data, options} = this.props;
|
||||
|
||||
store.setData(data);
|
||||
options && store.setOptions(options);
|
||||
|
||||
if (isPureVariable(source)) {
|
||||
this.syncOptions();
|
||||
this.toDispose.push(
|
||||
reaction(
|
||||
() =>
|
||||
resolveVariableAndFilter(
|
||||
source as string,
|
||||
store.data,
|
||||
'| raw'
|
||||
),
|
||||
() => this.syncOptions()
|
||||
)
|
||||
);
|
||||
} else if (env && isEffectiveApi(source, data)) {
|
||||
this.loadOptions();
|
||||
this.toDispose.push(
|
||||
reaction(
|
||||
() =>
|
||||
buildApi(source as string, store.data, {
|
||||
ignoreData: true
|
||||
}).url,
|
||||
() => this.loadOptions()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: any) {
|
||||
const props = this.props;
|
||||
|
||||
if (props.data !== prevProps.data) {
|
||||
props.store.setData(props.data);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.toDispose.forEach(fn => fn());
|
||||
this.toDispose = [];
|
||||
}
|
||||
|
||||
loadOptions() {
|
||||
const env: RendererEnv = this.props.env || this.context;
|
||||
const {store, source, data, options} = this.props;
|
||||
|
||||
if (env && isEffectiveApi(source, data)) {
|
||||
store.load(env, source, data);
|
||||
}
|
||||
}
|
||||
|
||||
syncOptions() {
|
||||
const {store, source, data} = this.props;
|
||||
|
||||
if (isPureVariable(source)) {
|
||||
store.setOptions(
|
||||
resolveVariableAndFilter(source as string, data, '| raw') || []
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const store = this.props.store;
|
||||
const injectedProps: RemoteOptionsProps = {
|
||||
options: store.options,
|
||||
loading: store.fetching
|
||||
};
|
||||
|
||||
return (
|
||||
<ComposedComponent
|
||||
{...(this.props as JSX.LibraryManagedAttributes<
|
||||
T,
|
||||
React.ComponentProps<T>
|
||||
>)}
|
||||
{...injectedProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
),
|
||||
ComposedComponent
|
||||
);
|
||||
|
||||
return result as typeof result & {
|
||||
ComposedComponent: T;
|
||||
};
|
||||
}
|
73
src/components/WithStore.tsx
Normal file
73
src/components/WithStore.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 接管 store 的生命周期,这个比较轻量,适合在组件中使用。
|
||||
* 相比渲染器中的 withStore,这里面的 store 不会在一个大树中。
|
||||
* 而且不会知道父级和子级中还有哪些 store。
|
||||
*/
|
||||
import React from 'react';
|
||||
import hoistNonReactStatic from 'hoist-non-react-statics';
|
||||
import {destroy, IAnyStateTreeNode} from 'mobx-state-tree';
|
||||
import {observer} from 'mobx-react';
|
||||
|
||||
export function withStore<K extends IAnyStateTreeNode>(
|
||||
storeFactory: (props: any) => K
|
||||
) {
|
||||
return function <
|
||||
T extends React.ComponentType<
|
||||
React.ComponentProps<T> & {
|
||||
store: K;
|
||||
}
|
||||
>
|
||||
>(ComposedComponent: T) {
|
||||
ComposedComponent = observer(ComposedComponent);
|
||||
|
||||
type OuterProps = JSX.LibraryManagedAttributes<
|
||||
T,
|
||||
Omit<React.ComponentProps<T>, 'store'>
|
||||
>;
|
||||
|
||||
const result = hoistNonReactStatic(
|
||||
class extends React.Component<OuterProps> {
|
||||
static displayName = `WithStore(${
|
||||
ComposedComponent.displayName || 'Unkown'
|
||||
})`;
|
||||
static ComposedComponent = ComposedComponent;
|
||||
ref: any;
|
||||
store?: K = storeFactory(this.props);
|
||||
refFn = (ref: any) => {
|
||||
this.ref = ref;
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this.store && destroy(this.store);
|
||||
delete this.store;
|
||||
}
|
||||
|
||||
getWrappedInstance() {
|
||||
return this.ref;
|
||||
}
|
||||
|
||||
render() {
|
||||
const injectedProps = {
|
||||
store: this.store
|
||||
};
|
||||
|
||||
return (
|
||||
<ComposedComponent
|
||||
{...(this.props as JSX.LibraryManagedAttributes<
|
||||
T,
|
||||
React.ComponentProps<T>
|
||||
>)}
|
||||
{...injectedProps}
|
||||
ref={this.refFn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
ComposedComponent
|
||||
);
|
||||
|
||||
return result as typeof result & {
|
||||
ComposedComponent: T;
|
||||
};
|
||||
};
|
||||
}
|
@ -31,6 +31,7 @@ import Formula from './Formula';
|
||||
|
||||
export interface ExpressionProps extends ThemeProps {
|
||||
value: ExpressionComplex;
|
||||
data?: any;
|
||||
index?: number;
|
||||
onChange: (value: ExpressionComplex, index?: number) => void;
|
||||
valueField?: FieldSimple;
|
||||
@ -124,7 +125,8 @@ export class Expression extends React.Component<ExpressionProps> {
|
||||
fields,
|
||||
op,
|
||||
classnames: cx,
|
||||
config
|
||||
config,
|
||||
data
|
||||
} = this.props;
|
||||
const inputType =
|
||||
((value as any)?.type === 'field'
|
||||
@ -153,6 +155,7 @@ export class Expression extends React.Component<ExpressionProps> {
|
||||
value={value}
|
||||
onChange={this.handleValueChange}
|
||||
op={op}
|
||||
data={data}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
@ -1,13 +1,11 @@
|
||||
import React from 'react';
|
||||
import {Fields, ConditionGroupValue, Funcs} from './types';
|
||||
import {ClassNamesFn, ThemeProps, themeable} from '../../theme';
|
||||
import {ThemeProps, themeable} from '../../theme';
|
||||
import Button from '../Button';
|
||||
import GroupOrItem from './GroupOrItem';
|
||||
import {autobind, guid} from '../../utils/helper';
|
||||
import {Config} from './config';
|
||||
import {Icon} from '../icons';
|
||||
import PopOverContainer from '../PopOverContainer';
|
||||
import ListRadios from '../ListRadios';
|
||||
|
||||
export interface ConditionGroupProps extends ThemeProps {
|
||||
config: Config;
|
||||
@ -15,6 +13,7 @@ export interface ConditionGroupProps extends ThemeProps {
|
||||
fields: Fields;
|
||||
funcs?: Funcs;
|
||||
showNot?: boolean;
|
||||
data?: any;
|
||||
onChange: (value: ConditionGroupValue) => void;
|
||||
removeable?: boolean;
|
||||
onRemove?: (e: React.MouseEvent) => void;
|
||||
@ -113,6 +112,7 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
|
||||
const {
|
||||
classnames: cx,
|
||||
value,
|
||||
data,
|
||||
fields,
|
||||
funcs,
|
||||
config,
|
||||
@ -167,13 +167,12 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
|
||||
添加条件组
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{removeable ? (
|
||||
<a className={cx('CBDelete')} onClick={onRemove}>
|
||||
<Icon icon="close" className="icon" />
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
{removeable ? (
|
||||
<a className={cx('CBDelete')} onClick={onRemove}>
|
||||
<Icon icon="close" className="icon" />
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className={cx('CBGroup-body')}>
|
||||
@ -190,6 +189,7 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
|
||||
onChange={this.handleItemChange}
|
||||
funcs={funcs}
|
||||
onRemove={this.handleItemRemove}
|
||||
data={data}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
|
@ -13,6 +13,7 @@ export interface CBGroupOrItemProps extends ThemeProps {
|
||||
fields: Fields;
|
||||
funcs?: Funcs;
|
||||
index: number;
|
||||
data?: any;
|
||||
draggable?: boolean;
|
||||
onChange: (value: ConditionGroupValue, index: number) => void;
|
||||
removeable?: boolean;
|
||||
@ -39,6 +40,7 @@ export class CBGroupOrItem extends React.Component<CBGroupOrItemProps> {
|
||||
fields,
|
||||
funcs,
|
||||
draggable,
|
||||
data,
|
||||
onDragStart
|
||||
} = this.props;
|
||||
|
||||
@ -65,6 +67,7 @@ export class CBGroupOrItem extends React.Component<CBGroupOrItemProps> {
|
||||
funcs={funcs}
|
||||
removeable
|
||||
onRemove={this.handleItemRemove}
|
||||
data={data}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
@ -74,6 +77,7 @@ export class CBGroupOrItem extends React.Component<CBGroupOrItemProps> {
|
||||
value={value as ConditionValue}
|
||||
onChange={this.handleItemChange}
|
||||
funcs={funcs}
|
||||
data={data}
|
||||
/>
|
||||
<a className={cx('CBDelete')} onClick={this.handleItemRemove}>
|
||||
<Icon icon="close" className="icon" />
|
||||
|
@ -29,6 +29,7 @@ export interface ConditionItemProps extends ThemeProps {
|
||||
funcs?: Funcs;
|
||||
index?: number;
|
||||
value: ConditionRule;
|
||||
data?: any;
|
||||
onChange: (value: ConditionRule, index?: number) => void;
|
||||
}
|
||||
|
||||
@ -220,7 +221,7 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
|
||||
}
|
||||
|
||||
renderRightWidgets(type: string, op: OperatorType) {
|
||||
const {funcs, value, fields, config, classnames: cx} = this.props;
|
||||
const {funcs, value, data, fields, config, classnames: cx} = this.props;
|
||||
let field = {
|
||||
...config.types[type],
|
||||
type
|
||||
@ -250,6 +251,7 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
|
||||
funcs={funcs}
|
||||
valueField={field}
|
||||
value={(value.right as Array<ExpressionComplex>)?.[0]}
|
||||
data={data}
|
||||
onChange={this.handleRightSubChange.bind(this, 0)}
|
||||
fields={fields}
|
||||
allowedTypes={
|
||||
@ -265,6 +267,7 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
|
||||
funcs={funcs}
|
||||
valueField={field}
|
||||
value={(value.right as Array<ExpressionComplex>)?.[1]}
|
||||
data={data}
|
||||
onChange={this.handleRightSubChange.bind(this, 1)}
|
||||
fields={fields}
|
||||
allowedTypes={
|
||||
@ -283,6 +286,7 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
|
||||
funcs={funcs}
|
||||
valueField={field}
|
||||
value={value.right}
|
||||
data={data}
|
||||
onChange={this.handleRightChange}
|
||||
fields={fields}
|
||||
allowedTypes={
|
||||
|
@ -4,12 +4,13 @@ import {ThemeProps, themeable} from '../../theme';
|
||||
import InputBox from '../InputBox';
|
||||
import NumberInput from '../NumberInput';
|
||||
import DatePicker from '../DatePicker';
|
||||
import Select from '../Select';
|
||||
import {SelectWithRemoteOptions as Select} from '../Select';
|
||||
import Switch from '../Switch';
|
||||
import {localeable, LocaleProps} from '../../locale';
|
||||
|
||||
export interface ValueProps extends ThemeProps, LocaleProps {
|
||||
value: any;
|
||||
data?: any;
|
||||
onChange: (value: any) => void;
|
||||
field: FieldSimple;
|
||||
op?: OperatorType;
|
||||
@ -23,7 +24,8 @@ export class Value extends React.Component<ValueProps> {
|
||||
value,
|
||||
onChange,
|
||||
op,
|
||||
translate: __
|
||||
translate: __,
|
||||
data
|
||||
} = this.props;
|
||||
let input: JSX.Element | undefined = undefined;
|
||||
|
||||
@ -39,8 +41,10 @@ export class Value extends React.Component<ValueProps> {
|
||||
input = (
|
||||
<NumberInput
|
||||
placeholder={field.placeholder || __('NumberInput.placeholder')}
|
||||
step={field.step}
|
||||
min={field.minimum}
|
||||
max={field.maximum}
|
||||
precision={field.precision}
|
||||
value={value ?? field.defaultValue}
|
||||
onChange={onChange}
|
||||
/>
|
||||
@ -84,8 +88,11 @@ export class Value extends React.Component<ValueProps> {
|
||||
input = (
|
||||
<Select
|
||||
simpleValue
|
||||
options={field.options!}
|
||||
value={value ?? field.defaultValue}
|
||||
options={field.options}
|
||||
source={field.source}
|
||||
searchable={field.searchable}
|
||||
value={value ?? field.defaultValue ?? ''}
|
||||
data={data}
|
||||
onChange={onChange}
|
||||
multiple={op === 'select_any_in' || op === 'select_not_any_in'}
|
||||
/>
|
||||
|
@ -21,6 +21,7 @@ export interface ConditionBuilderProps extends ThemeProps, LocaleProps {
|
||||
funcs?: Funcs;
|
||||
showNot?: boolean;
|
||||
value?: ConditionGroupValue;
|
||||
data?: any;
|
||||
onChange: (value: ConditionGroupValue) => void;
|
||||
config?: Config;
|
||||
}
|
||||
@ -195,7 +196,8 @@ export class QueryBuilder extends React.Component<ConditionBuilderProps> {
|
||||
funcs,
|
||||
onChange,
|
||||
value,
|
||||
showNot
|
||||
showNot,
|
||||
data
|
||||
} = this.props;
|
||||
|
||||
const normalizedValue = Array.isArray(value?.children)
|
||||
@ -225,6 +227,7 @@ export class QueryBuilder extends React.Component<ConditionBuilderProps> {
|
||||
removeable={false}
|
||||
onDragStart={this.handleDragStart}
|
||||
showNot={showNot}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
import {SchemaApi} from '../../Schema';
|
||||
import {Api} from '../../types';
|
||||
|
||||
export type FieldTypes =
|
||||
| 'text'
|
||||
| 'number'
|
||||
@ -105,6 +108,8 @@ interface NumberField extends BaseField {
|
||||
type: 'number';
|
||||
maximum?: number;
|
||||
minimum?: number;
|
||||
step?: number;
|
||||
precision?: number;
|
||||
}
|
||||
|
||||
interface DateField extends BaseField {
|
||||
@ -138,6 +143,8 @@ interface SelectField extends BaseField {
|
||||
name: string;
|
||||
multiple?: boolean;
|
||||
options?: Array<any>;
|
||||
source?: SchemaApi;
|
||||
searchable?: boolean;
|
||||
}
|
||||
|
||||
interface BooleanField extends BaseField {
|
||||
|
111
src/env.tsx
Normal file
111
src/env.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
/**
|
||||
* @file 组件 Env,包括如何发送 ajax,如何通知,如何跳转等等。。
|
||||
*/
|
||||
import React from 'react';
|
||||
import Alert from './components/Alert2';
|
||||
import {RendererConfig} from './factory';
|
||||
import {ThemeInstance} from './theme';
|
||||
import {Action, Api, Payload, Schema} from './types';
|
||||
import hoistNonReactStatic from 'hoist-non-react-statics';
|
||||
|
||||
export interface RendererEnv {
|
||||
fetcher: (api: Api, data?: any, options?: object) => Promise<Payload>;
|
||||
isCancel: (val: any) => boolean;
|
||||
notify: (
|
||||
type: 'error' | 'success',
|
||||
msg: string,
|
||||
conf?: {
|
||||
closeButton?: boolean;
|
||||
timeout?: number;
|
||||
}
|
||||
) => void;
|
||||
jumpTo: (to: string, action?: Action, ctx?: object) => void;
|
||||
alert: (msg: string) => void;
|
||||
confirm: (msg: string, title?: string) => Promise<boolean>;
|
||||
updateLocation: (location: any, replace?: boolean) => void;
|
||||
|
||||
/**
|
||||
* 阻止路由跳转,有时候 form 没有保存,但是路由跳转了,导致页面没有更新,
|
||||
* 所以先让用户确认一下。
|
||||
*
|
||||
* 单页模式需要这个,如果非单页模式,不需要处理这个。
|
||||
*/
|
||||
blockRouting?: (fn: (targetLocation: any) => void | string) => () => void;
|
||||
isCurrentUrl: (link: string, ctx?: any) => boolean | {params?: object};
|
||||
|
||||
/**
|
||||
* 监控路由变化,如果 jssdk 需要做单页跳转需要实现这个。
|
||||
*/
|
||||
watchRouteChange?: (fn: () => void) => () => void;
|
||||
rendererResolver?: (
|
||||
path: string,
|
||||
schema: Schema,
|
||||
props: any
|
||||
) => null | RendererConfig;
|
||||
copy?: (contents: string) => void;
|
||||
getModalContainer?: () => HTMLElement;
|
||||
theme: ThemeInstance;
|
||||
affixOffsetTop: number;
|
||||
affixOffsetBottom: number;
|
||||
richTextToken: string;
|
||||
loadRenderer: (
|
||||
schema: Schema,
|
||||
path: string,
|
||||
reRender: Function
|
||||
) => Promise<React.ReactType> | React.ReactType | JSX.Element | void;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
export const EnvContext = React.createContext<RendererEnv | void>(undefined);
|
||||
|
||||
export interface EnvProps {
|
||||
env: RendererEnv;
|
||||
}
|
||||
|
||||
export function withRendererEnv<
|
||||
T extends React.ComponentType<React.ComponentProps<T> & EnvProps>
|
||||
>(ComposedComponent: T) {
|
||||
type OuterProps = JSX.LibraryManagedAttributes<
|
||||
T,
|
||||
Omit<React.ComponentProps<T>, keyof EnvProps>
|
||||
> & {
|
||||
env?: RendererEnv;
|
||||
};
|
||||
|
||||
const result = hoistNonReactStatic(
|
||||
class extends React.Component<OuterProps> {
|
||||
static displayName = `WithEnv(${
|
||||
ComposedComponent.displayName || ComposedComponent.name
|
||||
})`;
|
||||
static contextType = EnvContext;
|
||||
static ComposedComponent = ComposedComponent;
|
||||
|
||||
render() {
|
||||
const injectedProps: {
|
||||
env: RendererEnv;
|
||||
} = {
|
||||
env: this.props.env || this.context
|
||||
};
|
||||
|
||||
if (!injectedProps.env) {
|
||||
throw new Error('Env 信息获取失败,组件用法不正确');
|
||||
}
|
||||
|
||||
return (
|
||||
<ComposedComponent
|
||||
{...(this.props as JSX.LibraryManagedAttributes<
|
||||
T,
|
||||
React.ComponentProps<T>
|
||||
>)}
|
||||
{...injectedProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
ComposedComponent
|
||||
);
|
||||
|
||||
return result as typeof result & {
|
||||
ComposedComponent: T;
|
||||
};
|
||||
}
|
@ -16,6 +16,7 @@ import {alert, confirm, setRenderSchemaFn} from './components/Alert';
|
||||
import {getDefaultLocale, makeTranslator, LocaleProps} from './locale';
|
||||
import ScopedRootRenderer, {RootRenderProps} from './Root';
|
||||
import {HocStoreFactory} from './WithStore';
|
||||
import {EnvContext, RendererEnv} from './env';
|
||||
|
||||
export interface TestFunc {
|
||||
(
|
||||
@ -45,54 +46,6 @@ export interface RendererBasicConfig {
|
||||
// [propName:string]:any;
|
||||
}
|
||||
|
||||
export interface RendererEnv {
|
||||
fetcher: (api: Api, data?: any, options?: object) => Promise<Payload>;
|
||||
isCancel: (val: any) => boolean;
|
||||
notify: (
|
||||
type: 'error' | 'success',
|
||||
msg: string,
|
||||
conf?: {
|
||||
closeButton?: boolean;
|
||||
timeout?: number;
|
||||
}
|
||||
) => void;
|
||||
jumpTo: (to: string, action?: Action, ctx?: object) => void;
|
||||
alert: (msg: string) => void;
|
||||
confirm: (msg: string, title?: string) => Promise<boolean>;
|
||||
updateLocation: (location: any, replace?: boolean) => void;
|
||||
|
||||
/**
|
||||
* 阻止路由跳转,有时候 form 没有保存,但是路由跳转了,导致页面没有更新,
|
||||
* 所以先让用户确认一下。
|
||||
*
|
||||
* 单页模式需要这个,如果非单页模式,不需要处理这个。
|
||||
*/
|
||||
blockRouting?: (fn: (targetLocation: any) => void | string) => () => void;
|
||||
isCurrentUrl: (link: string, ctx?: any) => boolean | {params?: object};
|
||||
|
||||
/**
|
||||
* 监控路由变化,如果 jssdk 需要做单页跳转需要实现这个。
|
||||
*/
|
||||
watchRouteChange?: (fn: () => void) => () => void;
|
||||
rendererResolver?: (
|
||||
path: string,
|
||||
schema: Schema,
|
||||
props: any
|
||||
) => null | RendererConfig;
|
||||
copy?: (contents: string) => void;
|
||||
getModalContainer?: () => HTMLElement;
|
||||
theme: ThemeInstance;
|
||||
affixOffsetTop: number;
|
||||
affixOffsetBottom: number;
|
||||
richTextToken: string;
|
||||
loadRenderer: (
|
||||
schema: Schema,
|
||||
path: string,
|
||||
reRender: Function
|
||||
) => Promise<React.ReactType> | React.ReactType | JSX.Element | void;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
export interface RendererProps extends ThemeProps, LocaleProps {
|
||||
render: (region: string, node: SchemaNode, props?: any) => JSX.Element;
|
||||
env: RendererEnv;
|
||||
@ -402,16 +355,18 @@ export function render(
|
||||
}
|
||||
|
||||
return (
|
||||
<ScopedRootRenderer
|
||||
{...props}
|
||||
schema={schema}
|
||||
pathPrefix={pathPrefix}
|
||||
rootStore={store}
|
||||
env={env}
|
||||
theme={theme}
|
||||
locale={locale}
|
||||
translate={translate}
|
||||
/>
|
||||
<EnvContext.Provider value={env}>
|
||||
<ScopedRootRenderer
|
||||
{...props}
|
||||
schema={schema}
|
||||
pathPrefix={pathPrefix}
|
||||
rootStore={store}
|
||||
env={env}
|
||||
theme={theme}
|
||||
locale={locale}
|
||||
translate={translate}
|
||||
/>
|
||||
</EnvContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -535,3 +490,5 @@ setRenderSchemaFn((controls, value, callback, scopeRef, theme) => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export {RendererEnv};
|
||||
|
@ -16,9 +16,7 @@ interface ClassDictionary {
|
||||
[id: string]: any;
|
||||
}
|
||||
|
||||
// This is the only way I found to break circular references between ClassArray and ClassValue
|
||||
// https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540
|
||||
interface ClassArray extends Array<ClassValue> {} // tslint:disable-line no-empty-interface
|
||||
interface ClassArray extends Array<ClassValue> {}
|
||||
|
||||
export type ClassNamesFn = (...classes: ClassValue[]) => string;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user