From fd3bc997df722555f1747a3295c9a132c83b1882 Mon Sep 17 00:00:00 2001 From: hsm-lv <80095014+hsm-lv@users.noreply.github.com> Date: Wed, 12 Jan 2022 14:48:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=89=A9=E5=85=85Scoped=E6=A0=B9=E6=8D=AE?= =?UTF-8?q?ID=E6=9F=A5=E6=89=BE=E7=BB=84=E4=BB=B6&=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E5=87=A0=E4=B8=AA=E9=80=9A=E7=94=A8=E5=8A=A8=E4=BD=9C=20(#3380?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat:扩充Scoped根据ID查找组件&补充几个通用动作 * feat:扩充Scoped根据ID查找组件&补充几个通用动作 --- examples/components/Linkage/Event.jsx | 24 ++++++++++- src/SchemaRenderer.tsx | 5 ++- src/Scoped.tsx | 50 +++++++++++++++++++++-- src/actions/Action.ts | 15 ++++++- src/actions/AjaxAction.ts | 58 +++++++++++++++++++++++++++ src/actions/CmptAction.ts | 9 ++++- src/actions/CopyAction.ts | 41 +++++++++++++++++++ src/actions/DialogAction.ts | 28 +++++++++++++ src/actions/DrawerAction.ts | 28 +++++++++++++ src/actions/EmailAction.ts | 38 ++++++++++++++++++ src/actions/OpenPageAction.ts | 39 ++++++++++++++++++ src/actions/index.ts | 6 +++ src/env.tsx | 12 +++++- 13 files changed, 344 insertions(+), 9 deletions(-) create mode 100644 src/actions/AjaxAction.ts create mode 100644 src/actions/CopyAction.ts create mode 100644 src/actions/DialogAction.ts create mode 100644 src/actions/DrawerAction.ts create mode 100644 src/actions/EmailAction.ts create mode 100644 src/actions/OpenPageAction.ts diff --git a/examples/components/Linkage/Event.jsx b/examples/components/Linkage/Event.jsx index b48bbf798..1026389d5 100644 --- a/examples/components/Linkage/Event.jsx +++ b/examples/components/Linkage/Event.jsx @@ -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, diff --git a/src/SchemaRenderer.tsx b/src/SchemaRenderer.tsx index 086cd4624..fb5d36df3 100644 --- a/src/SchemaRenderer.tsx +++ b/src/SchemaRenderer.tsx @@ -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 { 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 { ); diff --git a/src/Scoped.tsx b/src/Scoped.tsx index c18c3d066..03ab3e7c1 100644 --- a/src/Scoped.tsx +++ b/src/Scoped.tsx @@ -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 { @@ -29,9 +37,11 @@ export interface ScopedComponentType extends React.Component { 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; reload: (target: string, ctx: RendererData) => void; send: (target: string, ctx: RendererData) => void; @@ -46,8 +56,7 @@ function createScopedTools( env?: RendererEnv ): IScopedContext { const components: Array = []; - - 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); } diff --git a/src/actions/Action.ts b/src/actions/Action.ts index 3909ae1ae..8df516fb4 100644 --- a/src/actions/Action.ts +++ b/src/actions/Action.ts @@ -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'); } diff --git a/src/actions/AjaxAction.ts b/src/actions/AjaxAction.ts new file mode 100644 index 000000000..4d9f71107 --- /dev/null +++ b/src/actions/AjaxAction.ts @@ -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 + ) { + 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()); diff --git a/src/actions/CmptAction.ts b/src/actions/CmptAction.ts index 91dce37a2..42740a957 100644 --- a/src/actions/CmptAction.ts +++ b/src/actions/CmptAction.ts @@ -21,8 +21,15 @@ export class CmptAction implements Action { renderer: ListenerContext, event: RendererEvent ) { + // 根据唯一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); } } diff --git a/src/actions/CopyAction.ts b/src/actions/CopyAction.ts new file mode 100644 index 000000000..067029c0f --- /dev/null +++ b/src/actions/CopyAction.ts @@ -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 + ) { + 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()); diff --git a/src/actions/DialogAction.ts b/src/actions/DialogAction.ts new file mode 100644 index 000000000..fba425eb2 --- /dev/null +++ b/src/actions/DialogAction.ts @@ -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 + ) { + const store = renderer.props.store; + store.setCurrentAction(action); + store.openDialog(action.args); + } +} + +registerAction('dialog', new DialogAction()); diff --git a/src/actions/DrawerAction.ts b/src/actions/DrawerAction.ts new file mode 100644 index 000000000..7102ba9b4 --- /dev/null +++ b/src/actions/DrawerAction.ts @@ -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 + ) { + const store = renderer.props.store; + store.setCurrentAction(action); + store.openDrawer(action.args); + } +} + +registerAction('drawer', new DrawerAction()); diff --git a/src/actions/EmailAction.ts b/src/actions/EmailAction.ts new file mode 100644 index 000000000..ec69b94ae --- /dev/null +++ b/src/actions/EmailAction.ts @@ -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 + ) { + 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()); diff --git a/src/actions/OpenPageAction.ts b/src/actions/OpenPageAction.ts new file mode 100644 index 000000000..a1504384f --- /dev/null +++ b/src/actions/OpenPageAction.ts @@ -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 + ) { + 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()); diff --git a/src/actions/index.ts b/src/actions/index.ts index b9aa30e86..050131a16 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -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'; diff --git a/src/env.tsx b/src/env.tsx index 7a0643a65..6535289b5 100644 --- a/src/env.tsx +++ b/src/env.tsx @@ -61,7 +61,17 @@ export interface RendererEnv { useMobileUI?: boolean; bindEvent: (context: any) => (() => void) | undefined; dispatchEvent: ( - e: string | React.MouseEvent, + e: + | string + | React.ClipboardEvent + | React.DragEvent + | React.ChangeEvent + | React.KeyboardEvent + | React.TouchEvent + | React.WheelEvent + | React.AnimationEvent + | React.TransitionEvent + | React.MouseEvent, context: any, data: any ) => Promise | undefined>;