mapping 添加 source 接口 (#1752)

This commit is contained in:
liaoxuezhi 2021-04-06 20:06:13 +08:00 committed by GitHub
parent 2ec81de736
commit 1dd386c089
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 255 additions and 45 deletions

View File

@ -119,11 +119,65 @@ List 的内容、Card 卡片的内容配置同上
}
```
### 远程拉取字典
> since 1.1.6
通过配置 `source` 接口来实现,接口返回字典对象即可,数据格式参考 map 配置。
```schema: scope="body"
{
"type": "form",
"data": {
"type": "2"
},
"controls": [
{
"type": "mapping",
"name": "type",
"label": "映射",
"source": "https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/mapping"
}
]
}
```
> 默认 source 是有 30s 缓存的,通常字典数据不长变更。如果想修改,请参考 [API](../../../docs/types/api) 文档配置缓存。
### 关联上下文变量
> since 1.1.6
同样通过配置 `source` 来实现,只是格式是取变量。
```schema: scope="body"
{
"type": "form",
"initApi": {
"url": "https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/mapping",
"method": "get",
"responseData": {
"zidian": "$$$$",
"type": "2"
}
},
"controls": [
{
"type": "mapping",
"name": "type",
"label": "映射",
"source": "$${zidian}"
}
]
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| ----------- | -------- | ------ | -------------------------------------------------------------------------------------- |
| type | `string` | | 如果在 Table、Card 和 List 中,为`"color"`;在 Form 中用作静态展示,为`"static-color"` |
| className | `string` | | 外层 CSS 类名 |
| placeholder | `string` | | 占位文本 |
| map | `object` | | 映射配置 |
| 属性名 | 类型 | 默认值 | 说明 |
| ----------- | ----------------- | ------ | -------------------------------------------------------------------------------------- |
| type | `string` | | 如果在 Table、Card 和 List 中,为`"color"`;在 Form 中用作静态展示,为`"static-color"` |
| className | `string` | | 外层 CSS 类名 |
| placeholder | `string` | | 占位文本 |
| map | `object` | | 映射配置 |
| source | `string` or `API` | | [API](../../../docs/types/api) 或 [数据映射](../../../docs/concepts/data-mapping) |

11
mock/mapping.json Normal file
View File

@ -0,0 +1,11 @@
{
"status": 0,
"msg": "",
"data": {
"1": "<span class='label label-info'>漂亮</span>",
"2": "<span class='label label-success'>开心</span>",
"3": "<span class='label label-danger'>惊吓</span>",
"4": "<span class='label label-warning'>紧张</span>",
"*": "其他:${type}"
}
}

View File

@ -1,10 +1,27 @@
import React from 'react';
import {Renderer, RendererProps} from '../factory';
import {Renderer, RendererEnv, RendererProps} from '../factory';
import {ServiceStore, IServiceStore} from '../store/service';
import {Api, SchemaNode, PlainObject} from '../types';
import {Api, SchemaNode, PlainObject, Payload} from '../types';
import {filter} from '../utils/tpl';
import cx from 'classnames';
import {BaseSchema, SchemaTpl} from '../Schema';
import {
BaseSchema,
SchemaApi,
SchemaTokenizeableString,
SchemaTpl
} from '../Schema';
import {withStore} from '../components/WithStore';
import {flow, Instance, types} from 'mobx-state-tree';
import {getVariable, guid, isObject} from '../utils/helper';
import {StoreNode} from '../store/node';
import isPlainObject from 'lodash/isPlainObject';
import {isPureVariable, resolveVariableAndFilter} from '../utils/tpl-builtin';
import {
buildApi,
isApiOutdated,
isEffectiveApi,
normalizeApi
} from '../utils/api';
/**
* Mapping
@ -28,54 +45,182 @@ export interface MappingSchema extends BaseSchema {
[propName: string]: SchemaTpl;
};
/**
* source
*/
source?: SchemaApi | SchemaTokenizeableString;
/**
*
*/
placeholder?: string;
}
export interface MappingProps
extends RendererProps,
Omit<MappingSchema, 'type' | 'className'> {}
export const Store = StoreNode.named('MappingStore')
.props({
fetching: false,
errorMsg: '',
map: types.frozen<{
[propName: string]: any;
}>({})
})
.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);
export class MappingField extends React.Component<MappingProps, object> {
static defaultProps: Partial<MappingProps> = {
placeholder: '-',
map: {
'*': '通配值'
}
};
render() {
const {className, placeholder, map, render, classnames: cx} = this.props;
let key = this.props.value;
let viewValue: React.ReactNode = (
<span className="text-muted">{placeholder}</span>
if (ret.ok) {
const data = ret.data || {};
(self as any).setMap(data);
} else {
throw new Error(ret.msg || 'fetch error');
}
} catch (e) {
self.errorMsg = e.message;
} finally {
self.fetching = false;
}
}
);
key =
typeof key === 'string'
? key.trim()
: key === true
? '1'
: key === false
? '0'
: key; // trim 一下,干掉一些空白字符。
return {
load,
setMap(options: any) {
if (isObject(options)) {
self.map = {
...options
};
}
}
};
});
if (typeof key !== 'undefined' && map && (map[key] ?? map['*'])) {
viewValue = render(
'tpl',
map[key] ?? map['*'] // 兼容平台旧用法:即 value 为 true 时映射 1 ,为 false 时映射 0
);
export type IStore = Instance<typeof Store>;
export interface MappingProps
extends Omit<RendererProps, 'store'>,
Omit<MappingSchema, 'type' | 'className'> {
store: IStore;
}
export const MappingField = withStore(props =>
Store.create(
{
id: guid(),
storeType: Store.name
},
props.env
)
)(
class extends React.Component<MappingProps, object> {
static defaultProps: Partial<MappingProps> = {
placeholder: '-',
map: {
'*': '通配值'
}
};
constructor(props: MappingProps) {
super(props);
props.store.syncProps(props, undefined, ['map']);
}
return <span className={cx('MappingField', className)}>{viewValue}</span>;
componentDidMount() {
const {store, source, data} = this.props;
this.reload();
}
componentDidUpdate(prevProps: MappingProps) {
const props = this.props;
const {store, source, data} = this.props;
store.syncProps(props, prevProps, ['map']);
if (isPureVariable(source)) {
const prev = resolveVariableAndFilter(
prevProps.source as string,
prevProps.data,
'| raw'
);
const curr = resolveVariableAndFilter(source as string, data, '| raw');
if (prev !== curr) {
store.setMap(curr);
}
} else if (
isApiOutdated(
prevProps.source,
props.source,
prevProps.data,
props.data
)
) {
this.reload();
}
}
reload() {
const {source, data, env} = this.props;
const store = this.props.store;
if (isPureVariable(source)) {
store.setMap(resolveVariableAndFilter(source, data, '| raw'));
} else if (isEffectiveApi(source, data)) {
const api = normalizeApi(source, 'get');
api.cache = api.cache ?? 30 * 1000;
store.load(env, api, data);
}
}
render() {
const {
className,
placeholder,
render,
classnames: cx,
name,
data,
store
} = this.props;
const map = store.map;
let key =
this.props.value ?? (name ? getVariable(data, name) : undefined);
let viewValue: React.ReactNode = (
<span className="text-muted">{placeholder}</span>
);
key =
typeof key === 'string'
? key.trim()
: key === true
? '1'
: key === false
? '0'
: key; // trim 一下,干掉一些空白字符。
if (typeof key !== 'undefined' && map && (map[key] ?? map['*'])) {
viewValue = render(
'tpl',
map[key] ?? map['*'] // 兼容平台旧用法:即 value 为 true 时映射 1 ,为 false 时映射 0
);
}
return <span className={cx('MappingField', className)}>{viewValue}</span>;
}
}
}
);
@Renderer({
test: /(^|\/)(?:map|mapping)$/,
name: 'mapping'
})
export class MappingFieldRenderer extends MappingField {}
export class MappingFieldRenderer extends React.Component<RendererProps> {
render() {
return <MappingField {...this.props} />;
}
}

View File

@ -628,11 +628,11 @@ export const resolveVariable = (path?: string, data: any = {}): any => {
}, data);
};
export const isPureVariable = (path?: any) =>
typeof path === 'string'
export function isPureVariable(path?: any): path is string {
return typeof path === 'string'
? /^\$(?:([a-z0-9_.]+)|{[^}{]+})$/.test(path)
: false;
}
export const resolveVariableAndFilter = (
path?: string,
data: object = {},