mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-02 03:48:13 +08:00
feat:formItem增加autoUpdate功能 (#3845)
Co-authored-by: dqc <qianchuan.deng@gmail.com>
This commit is contained in:
parent
f1899524b7
commit
f507e88496
@ -1001,27 +1001,96 @@ Table 类型的表单项,要实现服务端校验,可以使用 `路径key`
|
||||
- `status`: 返回 `0` 表示校验成功,`422` 表示校验失败;
|
||||
- `errors`: 返回 `status` 为 `422` 时,显示的校验失败信息;
|
||||
|
||||
### 配置自动填充
|
||||
|
||||
通过配置 "autoUpdate" 来开启自动填充;其中 api 为自动填充数据源接口地址;mapping 为返回结果需要自动填充的 key value 映射关系;
|
||||
|
||||
```schema:scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"body": [
|
||||
{
|
||||
"type": "input-text",
|
||||
"label": "浏览器",
|
||||
"name": "browser",
|
||||
"autoUpdate": {
|
||||
api: "/api/mock2/form/autoUpdate?browser=$browser",
|
||||
showToast: true,
|
||||
mapping: {
|
||||
browser: "browser",
|
||||
version: "version",
|
||||
platform1: "${browser}",
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input-text",
|
||||
"label": "版本",
|
||||
"name": "version"
|
||||
},
|
||||
{
|
||||
"type": "input-text",
|
||||
"label": "平台",
|
||||
"name": "platform1"
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
自动填充接口返回格式如下:
|
||||
注意:amis 仅处理接口返回结果仅有一项的数据,默认自动填充相关字段
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 0,
|
||||
"data": {
|
||||
"rows": [
|
||||
{
|
||||
"key": "value",
|
||||
"key1": "value1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
或者是
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 0,
|
||||
"data": {
|
||||
"key": "value",
|
||||
"key1": "value1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 属性表
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| -------------- | -------------------------------------------------- | ------ | ---------------------------------------------------------- |
|
||||
| type | `string` | | 指定表单项类型 |
|
||||
| className | `string` | | 表单最外层类名 |
|
||||
| inputClassName | `string` | | 表单控制器类名 |
|
||||
| labelClassName | `string` | | label 的类名 |
|
||||
| name | `string` | | 字段名,指定该表单项提交时的 key |
|
||||
| value | `string` | | 表单默认值 |
|
||||
| label | [模板](../../../docs/concepts/template) 或 `false` | | 表单项标签 |
|
||||
| labelRemark | [Remark](../remark) | | 表单项标签描述 |
|
||||
| description | [模板](../../../docs/concepts/template) | | 表单项描述 |
|
||||
| placeholder | `string` | | 表单项描述 |
|
||||
| inline | `boolean` | | 是否为 内联 模式 |
|
||||
| submitOnChange | `boolean` | | 是否该表单项值发生变化时就提交当前表单。 |
|
||||
| disabled | `boolean` | | 当前表单项是否是禁用状态 |
|
||||
| disabledOn | [表达式](../../../docs/concepts/expression) | | 当前表单项是否禁用的条件 |
|
||||
| visible | [表达式](../../../docs/concepts/expression) | | 当前表单项是否禁用的条件 |
|
||||
| visibleOn | [表达式](../../../docs/concepts/expression) | | 当前表单项是否禁用的条件 |
|
||||
| required | `boolean` | | 是否为必填。 |
|
||||
| requiredOn | [表达式](../../../docs/concepts/expression) | | 过[表达式](../Types.md#表达式)来配置当前表单项是否为必填。 |
|
||||
| validations | [表达式](../../../docs/concepts/expression) | | 表单项值格式验证,支持设置多个,多个规则用英文逗号隔开。 |
|
||||
| validateApi | [表达式](../../../docs/types/api) | | 表单校验接口 |
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| -------------------- | -------------------------------------------------- | ------ | ---------------------------------------------------------- |
|
||||
| type | `string` | | 指定表单项类型 |
|
||||
| className | `string` | | 表单最外层类名 |
|
||||
| inputClassName | `string` | | 表单控制器类名 |
|
||||
| labelClassName | `string` | | label 的类名 |
|
||||
| name | `string` | | 字段名,指定该表单项提交时的 key |
|
||||
| value | `string` | | 表单默认值 |
|
||||
| label | [模板](../../../docs/concepts/template) 或 `false` | | 表单项标签 |
|
||||
| labelRemark | [Remark](../remark) | | 表单项标签描述 |
|
||||
| description | [模板](../../../docs/concepts/template) | | 表单项描述 |
|
||||
| placeholder | `string` | | 表单项描述 |
|
||||
| inline | `boolean` | | 是否为 内联 模式 |
|
||||
| submitOnChange | `boolean` | | 是否该表单项值发生变化时就提交当前表单。 |
|
||||
| disabled | `boolean` | | 当前表单项是否是禁用状态 |
|
||||
| disabledOn | [表达式](../../../docs/concepts/expression) | | 当前表单项是否禁用的条件 |
|
||||
| visible | [表达式](../../../docs/concepts/expression) | | 当前表单项是否禁用的条件 |
|
||||
| visibleOn | [表达式](../../../docs/concepts/expression) | | 当前表单项是否禁用的条件 |
|
||||
| required | `boolean` | | 是否为必填。 |
|
||||
| requiredOn | [表达式](../../../docs/concepts/expression) | | 过[表达式](../Types.md#表达式)来配置当前表单项是否为必填。 |
|
||||
| validations | [表达式](../../../docs/concepts/expression) | | 表单项值格式验证,支持设置多个,多个规则用英文逗号隔开。 |
|
||||
| validateApi | [表达式](../../../docs/types/api) | | 表单校验接口 |
|
||||
| autoUpdate | Object | | 自动填充配置 |
|
||||
| autoUpdate.api | [api](../../types/api) | | 自动填充数据接口地址 |
|
||||
| autoUpdate.mapping | Object | | 自动填充字段映射关系 |
|
||||
| autoUpdate.showToast | `boolean` | | 是否展示数据格式错误提示,默认为 false |
|
||||
|
@ -63,8 +63,7 @@ export default {
|
||||
label: '级联选项',
|
||||
source:
|
||||
'/api/mock2/options/chainedOptions?waitSeconds=1&parentId=$parentId&level=$level&maxLevel=4&waiSeconds=1',
|
||||
desc:
|
||||
'无限级别, 只要 api 返回数据就能继续往下选择. 当没有下级时请返回 null.',
|
||||
desc: '无限级别, 只要 api 返回数据就能继续往下选择. 当没有下级时请返回 null.',
|
||||
value: 'a,b'
|
||||
},
|
||||
{
|
||||
|
56
mock/cfc/mock/form/autoUpdate.js
Normal file
56
mock/cfc/mock/form/autoUpdate.js
Normal file
@ -0,0 +1,56 @@
|
||||
const result = async (req, res) => {
|
||||
const sleep = ms => {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
};
|
||||
const data = [
|
||||
{
|
||||
browser: 'Chrome',
|
||||
version: '1',
|
||||
platform: 'linux',
|
||||
Chrome: 'xxx1'
|
||||
},
|
||||
{
|
||||
browser: 'Chrome',
|
||||
version: '2',
|
||||
platform: 'windows',
|
||||
Chrome: 'xxx2'
|
||||
},
|
||||
{
|
||||
browser: 'Firefox',
|
||||
version: '3',
|
||||
platform: 'linux',
|
||||
Chrome: 'xxx3'
|
||||
},
|
||||
{
|
||||
browser: 'Firefox',
|
||||
version: '4',
|
||||
platform: 'windows',
|
||||
Chrome: 'xxx4'
|
||||
},
|
||||
{
|
||||
browser: 'Opera',
|
||||
version: '5',
|
||||
platform: 'linux',
|
||||
Chrome: 'xxx5'
|
||||
},
|
||||
{
|
||||
browser: 'Opera',
|
||||
version: '6',
|
||||
platform: 'windows',
|
||||
Chrome: 'xxx6'
|
||||
}
|
||||
];
|
||||
|
||||
await sleep(1000);
|
||||
const browser = req.query.browser;
|
||||
res.json({
|
||||
status: 0,
|
||||
msg: '',
|
||||
data:
|
||||
data.filter(function (item) {
|
||||
return browser ? ~item.browser.indexOf(browser) : false;
|
||||
})[0] || data[0]
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = result;
|
@ -1,44 +1,45 @@
|
||||
const db = [
|
||||
{
|
||||
"label": "zhugeliang",
|
||||
"value": "1"
|
||||
},
|
||||
const db = [
|
||||
{
|
||||
label: 'zhugeliang',
|
||||
value: '1'
|
||||
},
|
||||
|
||||
{
|
||||
"label": "zhongwuyan",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
label: 'zhongwuyan',
|
||||
value: '2'
|
||||
},
|
||||
|
||||
{
|
||||
"label": "buzhihuowu",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
label: 'buzhihuowu',
|
||||
value: '3'
|
||||
},
|
||||
|
||||
{
|
||||
"label": "zhongkui",
|
||||
"value": "4"
|
||||
},
|
||||
{
|
||||
label: 'zhongkui',
|
||||
value: '4'
|
||||
},
|
||||
|
||||
{
|
||||
"label": "luna",
|
||||
"value": "5"
|
||||
},
|
||||
{
|
||||
label: 'luna',
|
||||
value: '5'
|
||||
},
|
||||
|
||||
{
|
||||
"label": "wangzhaojun",
|
||||
"value": "6"
|
||||
}
|
||||
{
|
||||
label: 'wangzhaojun',
|
||||
value: '6'
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = function (req, res) {
|
||||
const term = req.query.term || '';
|
||||
|
||||
module.exports = function(req, res) {
|
||||
const term = req.query.term || '';
|
||||
|
||||
res.json({
|
||||
status: 0,
|
||||
msg: '',
|
||||
data: term ? db.filter(function(item) {
|
||||
return term ? ~item.label.indexOf(term) : false;
|
||||
}) : db
|
||||
});
|
||||
}
|
||||
res.json({
|
||||
status: 0,
|
||||
msg: '',
|
||||
data: term
|
||||
? db.filter(function (item) {
|
||||
return term ? ~item.label.indexOf(term) : false;
|
||||
})
|
||||
: db
|
||||
});
|
||||
};
|
||||
|
@ -127,6 +127,7 @@ register('de-DE', {
|
||||
'File.upload': 'Hochladen',
|
||||
'File.uploadFailed': 'Zurückgegebene Daten der Upload-API sind leer',
|
||||
'File.uploading': 'Wird hochgeladen...',
|
||||
'FormItem.autoUpdateloadFaild': 'Die Schnittstelle hat einen Fehler zurückgegeben, bitte sorgfältig prüfen',
|
||||
'Form.loadOptionsFailed': 'Optionen wurden auf folgendem Grund nicht geladen: {{reason}}',
|
||||
'Form.submit': 'Absenden',
|
||||
'Form.title': 'Formular',
|
||||
|
@ -129,6 +129,7 @@ register('en-US', {
|
||||
'File.upload': 'Upload',
|
||||
'File.uploadFailed': 'return data of udpload api is empty',
|
||||
'File.uploading': 'Uploading',
|
||||
'FormItem.autoUpdateloadFaild': 'return data of autoUpdate api is error',
|
||||
'Form.loadOptionsFailed': 'Failed to load options because: {{reason}}',
|
||||
'Form.submit': 'Submit',
|
||||
'Form.title': 'Form',
|
||||
|
@ -134,6 +134,7 @@ register('zh-CN', {
|
||||
'File.upload': '文件上传',
|
||||
'File.uploadFailed': '接口返回错误,请仔细检查',
|
||||
'File.uploading': '上传中...',
|
||||
'FormItem.autoUpdateloadFaild': '接口返回错误,请仔细检查',
|
||||
'Form.loadOptionsFailed': '加载选项失败,原因:{{reason}}',
|
||||
'Form.submit': '提交',
|
||||
'Form.title': '表单',
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import hoistNonReactStatic from 'hoist-non-react-statics';
|
||||
import {IFormItemStore, IFormStore} from '../../store/form';
|
||||
import {reaction} from 'mobx';
|
||||
import {IMapEntries, reaction} from 'mobx';
|
||||
|
||||
import {
|
||||
RendererProps,
|
||||
@ -14,7 +14,9 @@ import {
|
||||
ucFirst,
|
||||
getWidthRate,
|
||||
autobind,
|
||||
isMobile
|
||||
isMobile,
|
||||
createObject,
|
||||
getVariable
|
||||
} from '../../utils/helper';
|
||||
import {observer} from 'mobx-react';
|
||||
import {FormHorizontal, FormSchema, FormSchemaHorizontal} from '.';
|
||||
@ -32,6 +34,10 @@ import {
|
||||
import {HocStoreFactory} from '../../WithStore';
|
||||
import {wrapControl} from './wrapControl';
|
||||
import type {OnEventProps} from '../../utils/renderer-event';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import debounce from 'lodash/debounce';
|
||||
import {isEffectiveApi} from '../../utils/api';
|
||||
import {dataMapping} from '../../utils/tpl-builtin';
|
||||
|
||||
export type FormControlSchemaAlias = SchemaObject;
|
||||
|
||||
@ -396,7 +402,8 @@ export interface FormItemConfig extends FormItemBasicConfig {
|
||||
}
|
||||
|
||||
export class FormItemWrap extends React.Component<FormItemProps> {
|
||||
reaction: any;
|
||||
reaction: Array<() => void> = [];
|
||||
lastSearchTerm: any;
|
||||
|
||||
constructor(props: FormItemProps) {
|
||||
super(props);
|
||||
@ -404,15 +411,25 @@ export class FormItemWrap extends React.Component<FormItemProps> {
|
||||
const {formItem: model} = props;
|
||||
|
||||
if (model) {
|
||||
this.reaction = reaction(
|
||||
() => `${model.errors.join('')}${model.isFocused}${model.dialogOpen}`,
|
||||
() => this.forceUpdate()
|
||||
this.reaction.push(
|
||||
reaction(
|
||||
() => `${model.errors.join('')}${model.isFocused}${model.dialogOpen}`,
|
||||
() => this.forceUpdate()
|
||||
)
|
||||
);
|
||||
this.reaction.push(
|
||||
reaction(
|
||||
() => JSON.stringify(model.tmpValue),
|
||||
() => this.syncAutoUpdate(model.tmpValue)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.reaction && this.reaction();
|
||||
this.reaction.forEach(fn => fn());
|
||||
this.reaction = [];
|
||||
this.syncAutoUpdate.cancel();
|
||||
}
|
||||
|
||||
@autobind
|
||||
@ -429,6 +446,46 @@ export class FormItemWrap extends React.Component<FormItemProps> {
|
||||
this.props.onBlur && this.props.onBlur(e);
|
||||
}
|
||||
|
||||
syncAutoUpdate = debounce(
|
||||
(term: any) => {
|
||||
(async (term: string) => {
|
||||
const {autoUpdate, onBulkChange, formItem, data} = this.props;
|
||||
if (!autoUpdate || (autoUpdate && !autoUpdate.mapping)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {api, showToast} = autoUpdate;
|
||||
let mapping = {...autoUpdate.mapping};
|
||||
const itemName = formItem?.name;
|
||||
const ctx = createObject(data, {
|
||||
[itemName || '']: term
|
||||
});
|
||||
mapping = dataMapping(mapping, ctx);
|
||||
if (
|
||||
!isEmpty(mapping) &&
|
||||
onBulkChange &&
|
||||
isEffectiveApi(api, ctx) &&
|
||||
this.lastSearchTerm !== term
|
||||
) {
|
||||
const result = await formItem?.loadAutoUpdateData(
|
||||
api,
|
||||
ctx,
|
||||
showToast
|
||||
);
|
||||
if (!result) return;
|
||||
mapping = dataMapping(autoUpdate.mapping, result);
|
||||
this.lastSearchTerm = getVariable(mapping, itemName) ?? term;
|
||||
onBulkChange(mapping);
|
||||
}
|
||||
})(term).catch(e => console.error(e));
|
||||
},
|
||||
250,
|
||||
{
|
||||
trailing: true,
|
||||
leading: false
|
||||
}
|
||||
);
|
||||
|
||||
@autobind
|
||||
async handleOpenDialog(schema: Schema, data: any) {
|
||||
const {formItem: model} = this.props;
|
||||
@ -482,6 +539,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
|
||||
return renderControl({
|
||||
...rest,
|
||||
onOpenDialog: this.handleOpenDialog,
|
||||
|
||||
type,
|
||||
classnames: cx,
|
||||
formItem: model,
|
||||
|
@ -32,12 +32,13 @@ import {flattenTree} from '../utils/helper';
|
||||
import {IRendererStore} from '.';
|
||||
import {normalizeOptions, optionValueCompare} from '../components/Select';
|
||||
import find from 'lodash/find';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
import {SimpleMap} from '../utils/SimpleMap';
|
||||
import memoize from 'lodash/memoize';
|
||||
import {TranslateFn} from '../locale';
|
||||
import {StoreNode} from './node';
|
||||
import {dataMapping} from '../utils/tpl-builtin';
|
||||
import {getStoreById} from './manager';
|
||||
import {toast} from '../components';
|
||||
|
||||
interface IOption {
|
||||
value?: string | number | null;
|
||||
@ -215,6 +216,7 @@ export const FormItemStore = StoreNode.named('FormItemStore')
|
||||
.actions(self => {
|
||||
const form = self.form as IFormStore;
|
||||
const dialogCallbacks = new SimpleMap<(result?: any) => void>();
|
||||
let loadAutoUpdateCancel: Function | null = null;
|
||||
|
||||
function config({
|
||||
required,
|
||||
@ -623,6 +625,39 @@ export const FormItemStore = StoreNode.named('FormItemStore')
|
||||
return json;
|
||||
});
|
||||
|
||||
const loadAutoUpdateData: (
|
||||
api: Api,
|
||||
data?: object,
|
||||
showToast?: boolean
|
||||
) => Promise<Payload> = flow(function* getAutoUpdateData(
|
||||
api: string,
|
||||
data: object,
|
||||
showToast: boolean = false
|
||||
) {
|
||||
if (loadAutoUpdateCancel) {
|
||||
loadAutoUpdateCancel();
|
||||
loadAutoUpdateCancel = null;
|
||||
}
|
||||
|
||||
const json: Payload = yield getEnv(self).fetcher(api, data, {
|
||||
cancelExecutor: (executor: Function) =>
|
||||
(loadAutoUpdateCancel = executor)
|
||||
});
|
||||
loadAutoUpdateCancel = null;
|
||||
if (!json) {
|
||||
return;
|
||||
}
|
||||
const result = json.data?.items || json.data?.rows;
|
||||
// 只处理仅有一个结果的数据
|
||||
if (result?.length === 1) {
|
||||
return result[0];
|
||||
} else if (isPlainObject(json.data)) {
|
||||
return json.data;
|
||||
}
|
||||
showToast && toast.info(self.__('FormItem.autoUpdateloadFaild'));
|
||||
return;
|
||||
});
|
||||
|
||||
const tryDeferLoadLeftOptions: (
|
||||
option: any,
|
||||
leftOptions: any,
|
||||
@ -1166,7 +1201,8 @@ export const FormItemStore = StoreNode.named('FormItemStore')
|
||||
changeTmpValue,
|
||||
changeEmitedValue,
|
||||
addSubFormItem,
|
||||
removeSubFormItem
|
||||
removeSubFormItem,
|
||||
loadAutoUpdateData
|
||||
};
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user