feat: 向导组件 事件&动作 配置

This commit is contained in:
liuzedong02 2022-03-02 20:44:08 +08:00
parent 3eed053517
commit 521ad84f50
5 changed files with 336 additions and 97 deletions

View File

@ -32,6 +32,11 @@ order: 73
"label": "邮箱", "label": "邮箱",
"type": "input-email", "type": "input-email",
"required": true "required": true
},
{
"name": "ajax",
"label": "按钮",
"type": "button"
} }
] ]
}, },

View File

@ -0,0 +1,167 @@
const wizardSchema = {
type: 'wizard',
steps: [
{
title: '第一步',
body: [
{
name: 'website',
label: '网址',
type: 'input-url',
required: true
},
{
name: 'email',
label: '邮箱',
type: 'input-email',
required: true
}
],
api: '/api/mock2/form/saveForm?waitSeconds=2'
},
{
title: '第二步',
body: [
{
name: 'defaultEmail',
type: 'input-email',
label: 'Email',
value: 'test@test.com'
},
{
type: 'input-text',
name: 'name'
}
],
initApi: '/api/mock2/form/initForm',
api: '/api/mock2/form/saveForm?waitSeconds=2'
},
{
title: '确定',
body: ['最后一步了,确认要提交吗?'],
api: '/api/mock2/form/saveForm?waitSeconds=2'
}
]
}
function generateEventAcions(events) {
const onEvent = {};
events.forEach(event => {
onEvent[event] = {
actions: [{
actionType: 'toast',
msgType: 'info',
msg: `派发 ${event} 事件`
}]
}
});
console.log('onEvent', onEvent);
return onEvent;
}
function generateActions(actions) {
return actions.map(action => ({
name: `wizard-${action.actionName}`,
type: "button",
label: action.label,
level: actions.level || 'primary',
className: 'mr-3 mb-3',
onEvent: {
click: {
actions: [
{
actionType: action.actionName,
componentId: 'wizard-receiver',
description: action.label,
args: action.actionValue
}
]
}
}
}))
}
export default {
type: 'page',
title: '向导组件事件',
regions: ['body', 'toolbar', 'header'],
body: [
// {
// name: "wizard-next",
// type: "button",
// label: '',
// level: 'primary',
// onEvent: {
// click: {
// actions: [
// {
// actionType: 'next',
// componentId: 'wizard-receiver',
// description: ''
// }
// ]
// }
// }
// },
// {
// name: "wizard-prev",
// type: "button",
// label: '',
// level: 'primary',
// onEvent: {
// click: {
// actions: [
// {
// actionType: 'prev',
// componentId: 'wizard-receiver',
// description: ''
// }
// ]
// }
// }
// },
...generateActions([
{
actionName: 'prev',
label: '跳转上一步'
},
{
actionName: 'next',
label: '跳转下一步'
},
{
actionName: 'goto-step',
label: '跳转第三步',
actionValue: {
step: 3
}
},
{
actionName: 'step-submit',
label: '本步骤提交'
},
{
actionName: 'submit',
label: '全部提交'
}
]),
{
name: 'wizard',
id: 'wizard-receiver',
...wizardSchema,
onEvent: generateEventAcions([
'inited',
'change',
'stepChange',
'finished',
'submitSucc',
'submitFail',
'stepSubmitSucc',
'stepSubmitFail'
])
}
]
};

View File

@ -82,6 +82,7 @@ import SelectEventActionSchema from './EventAction/SelectEvent';
import ButtonEventActionSchema from './EventAction/ButtonEvent'; import ButtonEventActionSchema from './EventAction/ButtonEvent';
import InputRatingEventSchema from './EventAction/InputRatingEvent'; import InputRatingEventSchema from './EventAction/InputRatingEvent';
import ExcelEventSchema from './EventAction/ExcelEvent'; import ExcelEventSchema from './EventAction/ExcelEvent';
import WizardEventSchema from './EventAction/WizardEvent';
import WizardSchema from './Wizard'; import WizardSchema from './Wizard';
import ChartSchema from './Chart'; import ChartSchema from './Chart';
import EChartsEditorSchema from './ECharts'; import EChartsEditorSchema from './ECharts';
@ -580,7 +581,12 @@ export const examples = [
label: 'excel', label: 'excel',
path: 'examples/event/excel', path: 'examples/event/excel',
component: makeSchemaRenderer(ExcelEventSchema) component: makeSchemaRenderer(ExcelEventSchema)
} },
{
label: '向导组件',
path: 'examples/event/wizard',
component: makeSchemaRenderer(WizardEventSchema)
},
] ]
}, },
{ {

View File

@ -229,7 +229,7 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
initFinishedField, initFinishedField,
store, store,
messages: {fetchSuccess, fetchFailed}, messages: {fetchSuccess, fetchFailed},
onInit onInit // c
} = this.props; } = this.props;
if (isEffectiveApi(initApi, store.data, initFetch)) { if (isEffectiveApi(initApi, store.data, initFetch)) {
@ -253,7 +253,6 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
} }
}) })
.then(value => { .then(value => {
onInit && onInit(store.data);
const state = { const state = {
currentStep: currentStep:
typeof this.props.startStep === 'string' typeof this.props.startStep === 'string'
@ -291,7 +290,7 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
? parseInt(tokenize(this.props.startStep, this.props.data)) ? parseInt(tokenize(this.props.startStep, this.props.data))
: 1 : 1
}, },
() => onInit && onInit(store.data) () => this.handleInitEvent(store.data)
); );
} }
@ -336,6 +335,19 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
this.unSensor && this.unSensor(); this.unSensor && this.unSensor();
} }
async dispatchEvent(action: string, value?: object) {
const {dispatchEvent, data} = this.props;
const rendererEvent = await dispatchEvent(action, createObject(data, value ? {value} : {}));
return rendererEvent?.prevented ?? false;
}
async handleInitEvent(data: any) {
const {onInit} = this.props;
(await this.dispatchEvent('inited', data)) && onInit && onInit(data);
}
@autobind @autobind
affixDetect() { affixDetect() {
if ( if (
@ -364,11 +376,15 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
affixed ? affixDom.classList.add('in') : affixDom.classList.remove('in'); affixed ? affixDom.classList.add('in') : affixDom.classList.remove('in');
} }
gotoStep(index: number) { async gotoStep(index: number) {
const steps = this.props.steps || []; const steps = this.props.steps || [];
index = Math.max(Math.min(steps.length, index), 1); index = Math.max(Math.min(steps.length, index), 1);
if (index != this.state.currentStep) { if (index != this.state.currentStep) {
if (await this.dispatchEvent('stepChange')) {
return
}
this.setState({ this.setState({
currentStep: index, currentStep: index,
completeStep: Math.max(this.state.completeStep, index - 1) completeStep: Math.max(this.state.completeStep, index - 1)
@ -517,9 +533,9 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
throwErrors: boolean = false, throwErrors: boolean = false,
delegate?: IScopedContext delegate?: IScopedContext
) { ) {
const {onAction, store, env} = this.props; const {onAction, store, env, steps} = this.props;
if (action.actionType === 'next' || action.type === 'submit') { if (action.actionType === 'next' || action.type === 'submit' || action.actionType === 'step-submit') {
this.form.doAction( this.form.doAction(
{ {
...action, ...action,
@ -572,6 +588,14 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
}); });
} else if (action.actionType === 'reload') { } else if (action.actionType === 'reload') {
action.target && this.reloadTarget(action.target, data); action.target && this.reloadTarget(action.target, data);
} else if (action.actionType === 'goto-step') {
const targetStep = (data as any).step;
if (targetStep !== undefined && targetStep <= steps.length && targetStep >= 0) {
this.gotoStep((data as any).step);
}
} else if (action.actionType === 'submit') {
this.finalSubmit()
} else if (onAction) { } else if (onAction) {
onAction(e, action, data, throwErrors, delegate || this.context); onAction(e, action, data, throwErrors, delegate || this.context);
} }
@ -601,9 +625,17 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
} }
@autobind @autobind
handleChange(values: object) { async handleChange(values: object) {
const {store} = this.props; const {store} = this.props;
const previous = store.data;
const final = {...previous, ...values};
if (await this.dispatchEvent('change', final)) {
return;
}
store.updateData(values); store.updateData(values);
} }
@ -630,9 +662,7 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
store.updateData(reseted); store.updateData(reseted);
} }
// 接管里面 form 的提交,不能直接让 form 提交,因为 wizard 自己需要知道进度。 async finalSubmit(values: object = {}, action: Action = {type: 'submit'}) {
@autobind
handleSubmit(values: object, action: Action) {
const { const {
store, store,
steps, steps,
@ -643,13 +673,121 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
redirect, redirect,
reload, reload,
env, env,
onFinished onFinished // c
} = this.props; } = this.props;
if (await this.dispatchEvent('finished', store.data)) {
return;
}
const step = steps[this.state.currentStep - 1]; const step = steps[this.state.currentStep - 1];
store.updateData(values); store.updateData(values);
// 最后一步
if (target) {
this.submitToTarget(target, store.data);
this.setState({completeStep: steps.length});
} else if (action.api || step.api || api) {
let finnalAsyncApi = action.asyncApi || step.asyncApi || asyncApi;
isEffectiveApi(finnalAsyncApi, store.data) &&
store.updateData({
[finishedField || 'finished']: false
});
const formStore = this.form
? (this.form.props.store as IFormStore)
: store;
store.markSaving(true);
formStore
.saveRemote(action.api || step.api || api!, store.data, {
onSuccess: () => {
this.dispatchEvent('submitSucc', store.data);
if (
!isEffectiveApi(finnalAsyncApi, store.data) ||
store.data[finishedField || 'finished']
) {
return;
}
return until(
() => store.checkRemote(finnalAsyncApi as Api, store.data),
(ret: any) => ret && ret[finishedField || 'finished'],
cancel => (this.asyncCancel = cancel)
);
},
onFailed: json => this.dispatchEvent('submitFail', json)
})
.then(async value => {
const feedback = action.feedback;
if (feedback && isVisible(feedback, value)) {
const confirmed = await this.openFeedback(feedback, value);
// 如果 feedback 配置了,取消就跳过原有逻辑。
if (feedback.skipRestOnCancel && !confirmed) {
throw new SkipOperation();
} else if (feedback.skipRestOnConfirm && confirmed) {
throw new SkipOperation();
}
}
this.setState({completeStep: steps.length});
store.updateData({
...store.data,
...value
});
store.markSaving(false);
if (value && typeof value.step === 'number') {
this.gotoStep(value.step);
} else if (onFinished && onFinished(value, action) === false) {
// 如果是 false 后面的操作就不执行
return value;
}
const finalRedirect =
(action.redirect || step.redirect || redirect) &&
filter(action.redirect || step.redirect || redirect, store.data);
if (finalRedirect) {
env.jumpTo(finalRedirect, action);
} else if (action.reload || step.reload || reload) {
this.reloadTarget(
action.reload || step.reload || reload!,
store.data
);
}
return value;
})
.catch(error => {
this.dispatchEvent('submitFail', error)
store.markSaving(false);
console.error(error);
});
} else {
onFinished && onFinished(store.data, action);
this.setState({completeStep: steps.length});
}
}
// 接管里面 form 的提交,不能直接让 form 提交,因为 wizard 自己需要知道进度。
@autobind
async handleSubmit(values: object, action: Action) {
const {
store,
steps,
finishedField
} = this.props;
if (this.state.currentStep < steps.length) { if (this.state.currentStep < steps.length) {
const step = steps[this.state.currentStep - 1];
store.updateData(values);
let finnalAsyncApi = action.asyncApi || step.asyncApi; let finnalAsyncApi = action.asyncApi || step.asyncApi;
isEffectiveApi(finnalAsyncApi, store.data) && isEffectiveApi(finnalAsyncApi, store.data) &&
@ -661,6 +799,8 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
store store
.saveRemote(action.api || step.api!, store.data, { .saveRemote(action.api || step.api!, store.data, {
onSuccess: () => { onSuccess: () => {
this.dispatchEvent('stepSubmitSucc', store.data);
if ( if (
!isEffectiveApi(finnalAsyncApi, store.data) || !isEffectiveApi(finnalAsyncApi, store.data) ||
store.data[finishedField || 'finished'] store.data[finishedField || 'finished']
@ -675,6 +815,7 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
); );
}, },
onFailed: json => { onFailed: json => {
this.dispatchEvent('stepSubmitFail', json);
if (json.status === 422 && json.errors && this.form) { if (json.status === 422 && json.errors && this.form) {
this.form.props.store.setFormItemErrors(json.errors); this.form.props.store.setFormItemErrors(json.errors);
} }
@ -700,6 +841,7 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
); );
}) })
.catch(reason => { .catch(reason => {
this.dispatchEvent('stepSubmitFail', reason);
if (reason instanceof SkipOperation) { if (reason instanceof SkipOperation) {
return; return;
} }
@ -709,90 +851,7 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
this.gotoStep(this.state.currentStep + 1); this.gotoStep(this.state.currentStep + 1);
} }
} else { } else {
// 最后一步 this.finalSubmit(values, action);
if (target) {
this.submitToTarget(target, store.data);
this.setState({completeStep: steps.length});
} else if (action.api || step.api || api) {
let finnalAsyncApi = action.asyncApi || step.asyncApi || asyncApi;
isEffectiveApi(finnalAsyncApi, store.data) &&
store.updateData({
[finishedField || 'finished']: false
});
const formStore = this.form
? (this.form.props.store as IFormStore)
: store;
store.markSaving(true);
formStore
.saveRemote(action.api || step.api || api!, store.data, {
onSuccess: () => {
if (
!isEffectiveApi(finnalAsyncApi, store.data) ||
store.data[finishedField || 'finished']
) {
return;
}
return until(
() => store.checkRemote(finnalAsyncApi as Api, store.data),
(ret: any) => ret && ret[finishedField || 'finished'],
cancel => (this.asyncCancel = cancel)
);
}
})
.then(async value => {
const feedback = action.feedback;
if (feedback && isVisible(feedback, value)) {
const confirmed = await this.openFeedback(feedback, value);
// 如果 feedback 配置了,取消就跳过原有逻辑。
if (feedback.skipRestOnCancel && !confirmed) {
throw new SkipOperation();
} else if (feedback.skipRestOnConfirm && confirmed) {
throw new SkipOperation();
}
}
this.setState({completeStep: steps.length});
store.updateData({
...store.data,
...value
});
store.markSaving(false);
if (value && typeof value.step === 'number') {
this.gotoStep(value.step);
} else if (onFinished && onFinished(value, action) === false) {
// 如果是 false 后面的操作就不执行
return value;
}
const finalRedirect =
(action.redirect || step.redirect || redirect) &&
filter(action.redirect || step.redirect || redirect, store.data);
if (finalRedirect) {
env.jumpTo(finalRedirect, action);
} else if (action.reload || step.reload || reload) {
this.reloadTarget(
action.reload || step.reload || reload!,
store.data
);
}
return value;
})
.catch(e => {
store.markSaving(false);
console.error(e);
});
} else {
onFinished && onFinished(store.data, action);
this.setState({completeStep: steps.length});
}
} }
return false; return false;

View File

@ -102,7 +102,9 @@ export interface Action extends Button {
| 'reset-and-submit' | 'reset-and-submit'
| 'clear' | 'clear'
| 'clear-and-submit' | 'clear-and-submit'
| 'toast'; | 'toast'
| 'goto-step'
| 'step-submit';
api?: Api; api?: Api;
asyncApi?: Api; asyncApi?: Api;
payload?: any; payload?: any;