本文将讲解如何通过自定义,实现多页面的步骤条组件。其中每个步骤的元素里都对应界面设计器的一个页面。以下是代码实现和原理分析。
代码实现
NextStepWidget 多页面步骤条 ts 组件
import {
CallChaining,
createRuntimeContextByView,
customMutation,
customQuery,
RuntimeView,
SPI,
ViewCache,
Widget,
DefaultTabsWidget,
BasePackWidget
} from '@oinone/kunlun-dependencies';
import { isEmpty } from 'lodash-es';
import { MyMetadataViewWidget } from './MyMetadataViewWidget';
import NextStep from './NextStep.vue';
import { IStepConfig, StepDirection } from './type';
@SPI.ClassFactory(BasePackWidget.Token({ widget: 'NextStep' }))
export class NextStepWidget extends DefaultTabsWidget {
public initialize(props) {
this.titles = props.template?.widgets?.map((item) => item.title) || [];
props.template && (props.template.widgets = []);
super.initialize(props);
this.setComponent(NextStep);
return this;
}
@Widget.Reactive()
public get invisible() {
return false;
}
// 配置的每一步名称
@Widget.Reactive()
public titles = [];
// region 上一步下一步配置
// 步骤配置,切换的顺序就是数组的顺序,模型没有限制
@Widget.Reactive()
public get stepJsonConfig() {
let data = JSON.parse(
this.getDsl().stepJsonConfig ||
'[{"model":"resource.ResourceCountry","viewName":"国家form"},{"model":"resource.ResourceProvince","viewName":"省form"},{"model":"resource.ResourceCity","viewName":"市form"}]'
);
return data as IStepConfig[];
}
// 切换上一步下一步
@Widget.Method()
public async onStepChange(stepDirection: StepDirection) {
// 没有激活的,说明是初始化,取第一步
if (!this.activeStepKey) {
const step = this.stepJsonConfig[0];
if (step) {
this.activeStepKey = `${step.model}_${step.viewName}`;
await this.initStepView(step);
}
return;
}
// 获取当前步骤的索引
if (this.currentActiveKeyIndex > -1) {
await this.onSave();
// 获取下一步索引
const nextStepIndex =
stepDirection === StepDirection.NEXT ? this.currentActiveKeyIndex + 1 : this.currentActiveKeyIndex - 1;
// 在索引范围内,则渲染视图
if (nextStepIndex >= 0 && nextStepIndex < this.stepJsonConfig.length) {
const nextStep = this.stepJsonConfig[nextStepIndex];
if (nextStep) {
this.activeStepKey = `${nextStep.model}_${nextStep.viewName}`;
await this.initStepView(nextStep);
}
}
}
}
// region 创建上一步下一步视图
// 每个步骤视图组件
@Widget.Reactive()
public stepViewWidget: Record<string, MyMetadataViewWidget> = {};
// 当前激活的步骤,用 model 和 viewName 联合作为 key
@Widget.Reactive()
public activeStepKey: string = '';
// 当前激活的步骤索引,即当前是第几步,从 0 开始
@Widget.Reactive()
public get currentActiveKeyIndex() {
return this.stepJsonConfig.findIndex((step) => `${step.model}_${step.viewName}` === this.activeStepKey);
}
// 传入 step,动态创建一个步骤视图,并初始化数据
public async initStepView(step: IStepConfig) {
const widget = this.stepViewWidget[`${step.model}_${step.viewName}`];
if (widget) {
// 命中缓存
this.initStepViewData(step);
return;
}
// 根据 stepJsonConfig 里的 model 和 viewName 获取页面配置
const resView = await ViewCache.get(step.model, step.viewName);
if (resView) {
// 创建一个元数据隔离的视图组件
const widget = this.createDynamicWidget(resView, step);
if (!widget) {
throw new Error('Invalid widget.');
}
this.stepViewWidget[`${step.model}_${step.viewName}`] = widget;
this.initStepViewData(step);
}
}
// 根据取的中间协议视图 view,动态构建视图
public createDynamicWidget(view: RuntimeView, step: IStepConfig) {
if (view) {
// 中间协议构建上下文
const runtimeContext = createRuntimeContextByView(view, true, `Form_${Math.random()}`, this.currentHandle);
runtimeContext.parentContext = this.rootRuntimeContext;
// 取得上下文唯一标识
const runtimeContextHandle = runtimeContext.handle;
const widget = this.createWidget(
new MyMetadataViewWidget(runtimeContextHandle),
`Form_${step.model}_${step.viewName}`,
{
metadataHandle: runtimeContextHandle,
rootHandle: runtimeContextHandle,
mountedCallChaining: new CallChaining(),
submitCallChaining: new CallChaining(),
refreshCallChaining: new CallChaining(),
dataSource: [{}],
activeRecords: [{}],
inline: false
}
);
widget.initContext(runtimeContext);
return widget;
}
}
// 每个 step 的请求数据逻辑
private async initStepViewData(step: IStepConfig) {
// 当前步骤的 widget
const widget = this.stepViewWidget[`${step.model}_${step.viewName}`];
if (!widget) {
return;
}
if (this.currentActiveKeyIndex > 0) {
// 根据上一步的数据,构造数据回填
const lastWidget =
this.stepViewWidget[
`${this.stepJsonConfig[this.currentActiveKeyIndex - 1].model}_${
this.stepJsonConfig[this.currentActiveKeyIndex - 1].viewName
}`
];
const lastWidgetData = (await lastWidget.getData()) || {};
const data = (await customQuery(step.model, 'construct', lastWidgetData)) as Record<string, unknown>;
widget.setData(data);
// await widget.refreshCallChaining?.syncCall();
} else {
if (isEmpty(await widget.getData())) {
const data = (await customQuery(step.model, 'construct')) as Record<string, unknown>;
widget.setData(data);
// await widget.refreshCallChaining?.syncCall();
}
}
}
// region 初始化上一步下一步视图
protected async $$beforeCreated() {
await super.$$beforeCreated();
let steps = this.stepJsonConfig;
if (steps && steps.length) {
await this.onStepChange(StepDirection.NEXT);
}
// 浏览器空闲就先把剩下的视图初始化掉
window.requestIdleCallback(async () => {
(steps || []).map(async (step) => {
if (!this.stepViewWidget[`${step.model}_${step.viewName}`]) {
this.initStepView(step);
}
});
});
}
// region 提交数据
// 保存
@Widget.Method()
public async onSave() {
this.loading = true;
const step = this.stepJsonConfig[this.currentActiveKeyIndex];
const widget = this.stepViewWidget[`${step.model}_${step.viewName}`];
if (!widget) {
return;
}
const validatorRes = await widget.validator?.();
if (validatorRes) {
const submitData = (await widget.getData()) as any;
await customMutation(step.model, 'create', submitData || {});
this.loading = false;
}
}
}
NextStep.vue 多页面步骤条 vue 组件
<template>
<div class="next-step">
<a-steps :current="current">
<a-step v-for="title in titles" :title="title" />
</a-steps>
<OioSpin :loading="loading" class="oio-spin">
<div class="setp__main">
<template v-for="(step, index) in stepJsonConfig">
<div class="setp__item" v-show="currentActiveKeyIndex === index">
<slot :name="`Form_${step.model}_${step.viewName}`" />
</div>
</template>
</div>
<div class="setp__footer" v-if="stepJsonConfig.length">
<a-button v-if="current > 0" class="oio-button" type="primary" @click="previous"> 上一步 </a-button>
<a-button v-if="current < stepJsonConfig.length - 1" class="oio-button" type="primary" @click="next">
下一步
</a-button>
<a-button v-if="current === stepJsonConfig.length - 1" class="oio-button" type="primary" @click="finish">
完成
</a-button>
</div>
</OioSpin>
</div>
</template>
<script setup lang="ts">
import { PropType, ref } from 'vue';
import { OioSpin } from '@oinone/kunlun-vue-ui-antd';
import { IStepConfig, StepDirection } from './type';
const props = defineProps({
loading: {
type: Boolean,
default: false
},
titles: {
type: Array as PropType<string[]>,
default: []
},
stepJsonConfig: {
type: Array as PropType<IStepConfig[]>,
default: []
},
currentActiveKeyIndex: {
type: Number,
default: 0
},
onStepChange: {
type: Function
},
onSave: {
type: Function
}
});
const current = ref<number>(0);
const next = () => {
current.value++;
props.onStepChange?.(StepDirection.NEXT);
};
const previous = () => {
current.value--;
props.onStepChange?.(StepDirection.PREVIOUS);
};
const finish = async () => {
await props.onSave?.();
window.history.back();
};
</script>
<style lang="scss" scoped>
.next-step {
height: 100%;
background-color: #fff;
padding: 16px;
.setp__main {
display: flex;
justify-content: flex-start;
.setp__item {
width: 100%;
}
}
.setp__footer {
display: flex;
justify-content: flex-start;
align-items: center;
gap: 16px;
margin-top: 18px;
}
}
</style>
MyMetadataViewWidget 数据隔离组件
import {
ActiveRecord,
ActiveRecords,
CallChaining,
FormWidget,
MetadataViewWidget,
queryDslWidget,
Widget
} from '@oinone/kunlun-dependencies';
/**
* 通过视图handle查找搜索组件
* @param viewHandle
*/
const queryFormWidgetByViewHandle = (viewHandle: string): FormWidget | null => {
const baseViewWidget = Widget.select(viewHandle);
const formWidget = queryDslWidget(baseViewWidget?.getChildrenInstance(), FormWidget);
if (formWidget) {
return formWidget as unknown as FormWidget;
}
return null;
};
export class MyMetadataViewWidget extends MetadataViewWidget {
@Widget.Provide()
public mountedCallChaining: CallChaining | undefined;
@Widget.Provide()
public submitCallChaining: CallChaining | undefined;
@Widget.Provide()
public refreshCallChaining: CallChaining | undefined;
@Widget.Provide()
@Widget.Reactive()
public dataSource: ActiveRecord[] = [];
@Widget.Method()
@Widget.Provide()
public reloadDataSource(records: ActiveRecords | undefined) {
if (Array.isArray(records)) {
this.dataSource = records;
} else {
this.dataSource = [records || {}];
}
}
@Widget.Provide()
@Widget.Reactive()
public activeRecords: ActiveRecord[] = [];
@Widget.Method()
@Widget.Provide()
public reloadActiveRecords(records: ActiveRecords | undefined) {
if (Array.isArray(records)) {
this.activeRecords = records;
} else {
this.activeRecords = [records || {}];
}
}
public initialize(props): this {
this.mountedCallChaining = props.mountedCallChaining;
this.submitCallChaining = props.submitCallChaining;
this.refreshCallChaining = props.refreshCallChaining;
this.dataSource = props.dataSource;
this.activeRecords = props.activeRecords;
super.initialize(props);
return this;
}
protected mounted() {
this.mountedCallChaining?.syncCall();
}
public async validator() {
const formWidget = queryFormWidgetByViewHandle(this.currentHandle);
const res = await formWidget?.validator();
return res;
}
public async getData() {
const formWidget = queryFormWidgetByViewHandle(this.currentHandle);
const callResult = await formWidget?.submit();
return callResult?.records as Record<string, unknown>;
}
protected getModelFields() {
const formWidget = queryFormWidgetByViewHandle(this.currentHandle);
return formWidget?.rootRuntimeContext.getRequestModelFields();
}
public setData(data: Record<string, unknown>) {
if (data) {
this.dataSource = [data];
this.activeRecords = [data];
}
}
public getRuntimeModel() {
return this.runtimeContext?.model;
}
}
原理分析
参考 https://doc.oinone.top/frontend/components/21426.html
Oinone社区 作者:银时原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/components/21432.html
访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验