feat:扩充Scoped根据ID查找组件&补充几个通用动作 (#3380)

* feat:扩充Scoped根据ID查找组件&补充几个通用动作

* feat:扩充Scoped根据ID查找组件&补充几个通用动作
This commit is contained in:
hsm-lv 2022-01-12 14:48:58 +08:00 committed by GitHub
parent afaa9384d2
commit fd3bc997df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 344 additions and 9 deletions

View File

@ -5,6 +5,7 @@ export default {
body: [
{
type: 'button',
id: 'b_001',
label: '发送广播事件1-表单1/2/3都在监听',
actionType: 'reload',
dialog: {
@ -15,9 +16,20 @@ export default {
onEvent: {
click: {
actions: [
{
actionType: 'reload',
args: {
name: 'lvxj',
age: 18
},
preventDefault: true,
stopPropagation: false,
componentId: 'form_001'
// componentId: 'form_001_form_01_text_01'
},
{
actionType: 'broadcast',
eventName: 'broadcast_1',
eventName: 'broadcast_1dddd',
args: {
name: 'lvxj',
age: 18,
@ -40,6 +52,7 @@ export default {
},
{
type: 'button',
id: 'b_002',
label: '发送广播事件2-表单3在监听',
className: 'ml-2',
actionType: 'reload',
@ -65,6 +78,7 @@ export default {
},
{
type: 'form',
id: 'form_001',
title: '表单1(我的权重最低)-刷新',
name: 'form1',
debug: true,
@ -74,12 +88,14 @@ export default {
body: [
{
type: 'form',
id: 'form_001_form_01',
title: '表单1(我的权重最低)-刷新',
name: 'sub-form1',
debug: true,
body: [
{
type: 'input-text',
id: 'form_001_form_01_text_01',
label: '名称',
name: 'name',
disabled: false,
@ -87,6 +103,7 @@ export default {
},
{
type: 'input-text',
id: 'form_001_form_01_text_02',
label: '等级',
name: 'level',
disabled: false,
@ -94,6 +111,7 @@ export default {
},
{
type: 'input-text',
id: 'form_001_form_01_text_03',
label: '昵称',
name: 'myname',
disabled: false,
@ -122,11 +140,13 @@ export default {
{
type: 'form',
name: 'form2',
id: 'form_002',
title: '表单2(权重2)-刷新+发Ajax',
debug: true,
body: [
{
type: 'input-text',
id: 'form_001_text_01',
label: '年龄',
name: 'age',
disabled: false,
@ -176,11 +196,13 @@ export default {
{
type: 'form',
name: 'form3',
id: 'form_003',
title: '表单3(权重3)-逻辑编排',
debug: true,
body: [
{
type: 'input-text',
id: 'form_003_text_01',
label: '职业',
name: 'job',
disabled: false,

View File

@ -13,6 +13,7 @@ import {
} from './factory';
import {asFormItem} from './renderers/Form/Item';
import {renderChild, renderChildren} from './Root';
import {IScopedContext, ScopedContext} from './Scoped';
import {Schema, SchemaNode} from './types';
import {DebugWrapper, enableAMISDebug} from './utils/debug';
import getExprProperties from './utils/filter-schema';
@ -60,8 +61,9 @@ const componentCache: SimpleMap = new SimpleMap();
class BroadcastCmpt extends React.Component<BroadcastCmptProps> {
ref: any;
unbindEvent: (() => void) | undefined = undefined;
static contextType = ScopedContext;
constructor(props: BroadcastCmptProps) {
constructor(props: BroadcastCmptProps, context: IScopedContext) {
super(props);
this.triggerEvent = this.triggerEvent.bind(this);
}
@ -101,6 +103,7 @@ class BroadcastCmpt extends React.Component<BroadcastCmptProps> {
<Component
ref={this.childRef}
{...rest}
scoped={this.context}
dispatchEvent={this.triggerEvent}
/>
);

View File

@ -8,7 +8,15 @@ import find from 'lodash/find';
import hoistNonReactStatic from 'hoist-non-react-statics';
import {dataMapping} from './utils/tpl-builtin';
import {RendererEnv, RendererProps} from './factory';
import {noop, autobind, qsstringify, qsparse} from './utils/helper';
import {
noop,
autobind,
qsstringify,
qsparse,
createObject,
findTree,
TreeItem
} from './utils/helper';
import {RendererData, Action} from './types';
export interface ScopedComponentType extends React.Component<RendererProps> {
@ -29,9 +37,11 @@ export interface ScopedComponentType extends React.Component<RendererProps> {
export interface IScopedContext {
parent?: AliasIScopedContext;
children?: AliasIScopedContext[];
registerComponent: (component: ScopedComponentType) => void;
unRegisterComponent: (component: ScopedComponentType) => void;
getComponentByName: (name: string) => ScopedComponentType;
getComponentById: (id: string) => ScopedComponentType | undefined;
getComponents: () => Array<ScopedComponentType>;
reload: (target: string, ctx: RendererData) => void;
send: (target: string, ctx: RendererData) => void;
@ -46,8 +56,7 @@ function createScopedTools(
env?: RendererEnv
): IScopedContext {
const components: Array<ScopedComponentType> = [];
return {
const self = {
parent,
registerComponent(component: ScopedComponentType) {
// 不要把自己注册在自己的 Scoped 上,自己的 Scoped 是给子节点们注册的。
@ -80,7 +89,7 @@ function createScopedTools(
return paths.reduce((scope, name, idx) => {
if (scope && scope.getComponentByName) {
const result = scope.getComponentByName(name);
const result: ScopedComponentType = scope.getComponentByName(name);
return result && idx < len - 1 ? result.context : result;
}
@ -96,6 +105,27 @@ function createScopedTools(
return resolved || (parent && parent.getComponentByName(name));
},
getComponentById(id: string) {
let root: AliasIScopedContext = this;
// 找到顶端scoped
while (root.parent) {
root = root.parent;
}
// 向下查找
let component = undefined;
findTree([root], (item: TreeItem) =>
item.getComponents().find((cmpt: ScopedComponentType) => {
if (cmpt.props.id === id) {
component = cmpt;
return true;
}
return false;
})
) as ScopedComponentType | undefined;
return component;
},
getComponents() {
return components.concat();
},
@ -208,6 +238,17 @@ function createScopedTools(
}
}
};
if (!parent) {
return self;
}
!parent.children && (parent.children = []);
// 把孩子带上
parent.children!.push(self);
return self;
}
function closeDialog(component: ScopedComponentType) {
@ -257,6 +298,7 @@ export function HocScoped<
context,
this.props.env
);
const scopeRef = props.scopeRef;
scopeRef && scopeRef(this.scoped);
}

View File

@ -18,6 +18,7 @@ export interface ListenerAction {
actionType: 'broadcast' | LogicActionType | 'custom' | string; // 动作类型 逻辑动作|自定义(脚本支撑)|reload|url|ajax|dialog|drawer 其他扩充的组件动作
eventName?: string; // 事件名称actionType: broadcast
description?: string; // 事件描述actionType: broadcast
componentId?: string; // 组件ID用于直接执行指定组件的动作
args?: any; // 参数,可以配置数据映射
preventDefault?: boolean; // 阻止原有组件的动作行为
stopPropagation?: boolean; // 阻止后续的事件处理器执行
@ -70,7 +71,19 @@ export const runActions = async (
for (const actionConfig of actions) {
let actionInstrance = getActionByType(actionConfig.actionType);
// 找不到就通过组件动作完成
// 如果存在指定组件ID说明是组件专有动作
if (actionConfig.componentId) {
actionInstrance = getActionByType('component');
} else if (
actionConfig.actionType === 'url' ||
actionConfig.actionType === 'link' ||
actionConfig.actionType === 'jump'
) {
// 打开页面动作
actionInstrance = getActionByType('openpage');
}
// 找不到就通过组件专有动作完成
if (!actionInstrance) {
actionInstrance = getActionByType('component');
}

58
src/actions/AjaxAction.ts Normal file
View File

@ -0,0 +1,58 @@
import {IRootStore} from '../store/root';
import {isVisible} from '../utils/helper';
import {RendererEvent} from '../utils/renderer-event';
import {filter} from '../utils/tpl';
import {
Action,
ListenerAction,
ListenerContext,
registerAction
} from './Action';
/**
*
*
* @export
* @class AjaxAction
* @implements {Action}
*/
export class AjaxAction implements Action {
async run(
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
const store = renderer.props.store;
store.setCurrentAction(action);
store
.saveRemote(action.api as string, action.args, {
successMessage: action.messages && action.messages.success,
errorMessage: action.messages && action.messages.failed
})
.then(async () => {
if (action.feedback && isVisible(action.feedback, store.data)) {
await this.openFeedback(action.feedback, store);
}
const redirect = action.redirect && filter(action.redirect, store.data);
redirect && renderer.env.jumpTo(redirect, action);
})
.catch(() => {});
}
openFeedback(dialog: any, store: IRootStore) {
return new Promise(resolve => {
store.setCurrentAction({
type: 'button',
actionType: 'dialog',
dialog: dialog
});
store.openDialog(store.data, undefined, confirmed => {
resolve(confirmed);
});
});
}
}
registerAction('ajax', new AjaxAction());

View File

@ -21,8 +21,15 @@ export class CmptAction implements Action {
renderer: ListenerContext,
event: RendererEvent<any>
) {
// 根据唯一ID查找指定组件
const component =
renderer.props.$schema.id !== action.componentId
? renderer.props.scoped?.getComponentById(action.componentId)
: renderer;
// 执行组件动作
await renderer.doAction?.(action, action.args);
(await component.props.onAction?.(event, action, action.args)) ||
component.doAction?.(action, action.args);
}
}

41
src/actions/CopyAction.ts Normal file
View File

@ -0,0 +1,41 @@
import {RendererEvent} from '../utils/renderer-event';
import {dataMapping} from '../utils/tpl-builtin';
import {filter} from '../utils/tpl';
import pick from 'lodash/pick';
import mapValues from 'lodash/mapValues';
import qs from 'qs';
import {
Action,
ListenerAction,
ListenerContext,
LoopStatus,
registerAction
} from './Action';
import {isVisible} from '../utils/helper';
/**
*
*
* @export
* @class CopyAction
* @implements {Action}
*/
export class CopyAction implements Action {
async run(
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
debugger;
if (action.content || action.copy) {
renderer.props.env.copy?.(
filter(action.content || action.copy, action.args, '| raw'),
{
format: action.copyFormat
}
);
}
}
}
registerAction('copy', new CopyAction());

View File

@ -0,0 +1,28 @@
import {RendererEvent} from '../utils/renderer-event';
import {
Action,
ListenerAction,
ListenerContext,
registerAction
} from './Action';
/**
*
*
* @export
* @class DialogAction
* @implements {Action}
*/
export class DialogAction implements Action {
async run(
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
const store = renderer.props.store;
store.setCurrentAction(action);
store.openDialog(action.args);
}
}
registerAction('dialog', new DialogAction());

View File

@ -0,0 +1,28 @@
import {RendererEvent} from '../utils/renderer-event';
import {
Action,
ListenerAction,
ListenerContext,
registerAction
} from './Action';
/**
*
*
* @export
* @class DrawerAction
* @implements {Action}
*/
export class DrawerAction implements Action {
async run(
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
const store = renderer.props.store;
store.setCurrentAction(action);
store.openDrawer(action.args);
}
}
registerAction('drawer', new DrawerAction());

View File

@ -0,0 +1,38 @@
import {RendererEvent} from '../utils/renderer-event';
import {filter} from '../utils/tpl';
import pick from 'lodash/pick';
import mapValues from 'lodash/mapValues';
import qs from 'qs';
import {
Action,
ListenerAction,
ListenerContext,
registerAction
} from './Action';
/**
*
*
* @export
* @class EmailAction
* @implements {Action}
*/
export class EmailAction implements Action {
async run(
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
const mailTo = filter(action.to, action.args);
const mailInfo = mapValues(
pick(action, 'to', 'cc', 'bcc', 'subject', 'body'),
val => filter(val, action.args)
);
const mailStr = qs.stringify(mailInfo);
const mailto = `mailto:${mailTo}?${mailStr}`;
window.open(mailto);
}
}
registerAction('email', new EmailAction());

View File

@ -0,0 +1,39 @@
import {RendererEvent} from '../utils/renderer-event';
import {filter} from '../utils/tpl';
import {
Action,
ListenerAction,
ListenerContext,
registerAction
} from './Action';
/**
*
*
* @export
* @class OpenPageAction
* @implements {Action}
*/
export class OpenPageAction implements Action {
async run(
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
if (!renderer.props.env?.jumpTo) {
throw new Error('env.jumpTo is required!');
}
renderer.props.env.jumpTo(
filter(
(action.to || action.url || action.link) as string,
action.args,
'| raw'
),
action,
action.args
);
}
}
registerAction('openpage', new OpenPageAction());

View File

@ -10,5 +10,11 @@ import './ParallelAction';
import './CustomAction';
import './BroadcastAction';
import './CmptAction';
import './AjaxAction';
import './CopyAction';
import './DialogAction';
import './DrawerAction';
import './EmailAction';
import './OpenPageAction';
export * from './Action';

View File

@ -61,7 +61,17 @@ export interface RendererEnv {
useMobileUI?: boolean;
bindEvent: (context: any) => (() => void) | undefined;
dispatchEvent: (
e: string | React.MouseEvent<any>,
e:
| string
| React.ClipboardEvent<any>
| React.DragEvent<any>
| React.ChangeEvent<any>
| React.KeyboardEvent<any>
| React.TouchEvent<any>
| React.WheelEvent<any>
| React.AnimationEvent<any>
| React.TransitionEvent<any>
| React.MouseEvent<any>,
context: any,
data: any
) => Promise<RendererEvent<any> | undefined>;