前面完成了基本的框架建设,能够通过 DSL 渲染出一个常用的列表页。但是主要还是偏展示性,其中的查询、表格翻页点击事件基本算是表格必备功能,具体的实现代码也都分别跟搜索、表格组件写在一起。
实际的需求肯定不止这些,我们还需要一些其他的交互,例如:
- 在表格头部,添加新增按钮,点击新增按钮可以弹窗显示一个表单组件,允许用户填写表格并保存,保存后关闭弹窗并且刷新表格。
- 表格最后一列是各种操作按钮,如查看详情、编辑、删除等,其中编辑的交互更新增交互类似。
因此需要增加自定义的 动态组件,根据不同的操作展示不同组件,并完成一系列功能。因为最终的目的是通过一套数据生成整个项目,因此这里还是从 DSL 设计开始说起。
这里先回顾一下DSL的基本结构
项目是以菜单(menu)为顶级维度,即页面是某个菜单页面。
module.exports = {
model: "dashboard",
name: "电商系统",
menu: [
{
key: "product",
name: "商品管理",
menuType: "module",
moduleType: "schema",
schemaConfig: {
api: "/api/proj/product",
schema: {},
tableConfig: {},
componentConfig: {},
}
}
]
}
schema:一个标准的 JSON-schema 格式的数据,描述了这个页面用到的原子数据以及在不同模版组件下的配置,如 tableOption、xxxOption、yyyOption。
tableConfig:表格整体配置,如表格头部按钮,表格最后一列的操作等。
componentConfig:使用到的动态组件以及动态组件的具体配置,如 createForm 会和schema的createFormOption 对应上。
schema
以动态组件createForm为例。
type: "object",
properties: {
product_id: {
type: "string",
label: "商品ID",
},
product_name: {
type: "string",
label: "商品名称",
createFormOption: {
comType: "input",
},
},
price: {
type: "number",
label: "价格",
createFormOption: {
comType: "inputNumber",
},
},
inventory: {
type: "number",
label: "库存",
createFormOption: {
comType: "select",
enumList: [
{
label: "100",
value: 100,
},
{
label: "500",
value: 500,
},
{
label: "1000",
value: 1000,
},
],
},
},
},
componentConfig的配置是createForm的整体配置,而不是具体的每个字段。
componentConfig: {
createForm: {
title: "新增商品",
saveBtnText: "确定",
},
}
先说说为什么要这个设计数据结构。有些人可能不是在schema里面使用 createOption标记 ,而是会把 createForm 动态组件要渲染的字段配置都写在 componentConfig.createForm 里面,这貌似比较符合正常的思维,维护的时候一眼就可以看到有哪些字段。
当组件只有一两个的时候,确实没有太大问题,但是如果组件很多(实际上确实会有很多,而且可以不断扩充),那么将会出现很多重复的配置。
{
createForm: {
title: "新增商品",
saveBtnText: "确定",
product_name: {
comType: "input",
defaultValue: "",
// ...
}
price: {
comType: "inputNumber",
defaultValue: null
},
},
editForm: {
title: "新增商品",
saveBtnText: "确定",
// 比 createForm 多了商品id,保存的时候要回传
product_id: {
disabled: true,
comType: "input",
},
product_name: {
comType: "input",
defaultValue: "",
// ...
}
price: {
comType: "inputNumber",
defaultValue: null,
// ...
},
},
}
// 其他动态组件
如果按照以上写法,越往后会越臃肿,而且相同内容重复多次,维护的时候不好分辨,容易出错。有个软件开发原则叫 DRY(Don’t Repeat Yourself),不要重复你自己,即减少信息的重复。所以最终选择在schema.properties的字段里插入createFormOption,表示这个字段将会出现先 createForm表单里面。其他组件同理,例如编辑表单的配置是editFormOption和componentConfig.editForm,一个是具体字段,一个是表单整体的配置。
schema.properties这个字段可以称得上核心的核心,表格、新建表单、编辑表单、详情等组件都是围绕着这个份数据进行处理。
既然createOption分散在各个字段,那组件渲染的时候肯定要把他们提取出来后,处理一番,再进行渲染。
最先拿到数据的是 app/pages/dashboard/complex-view/schema-view/hook.js,部分处理逻辑如下:
if (componentConfig && Object.keys(componentConfig).length > 0) {
const dtoComponents = {};
for (const comName in componentConfig) {
dtoComponents[comName] = {
// 例如:product_name 里面的 comAOption 进行转换
schema: buildDtoSchema(configSchema, comName),
// 这是动态组件的配置
config: componentConfig[comName],
};
}
components.value = dtoComponents;
}
最终得出 components,格式为 { compName: { schema, config }, ... } ,通过 provide ,在 schema-view.vue 这个顶层组件注入,后续要使用的时候直接 inject 就可以了。
在 schema-view.vue 文件,和表格容器平级的地方,遍历 components,把动态组件组件都添加上。
注意要绑定 ref="comListRef",需要通过ref拿到组件实例,调用内部的方法。
components 数据格式大致如下:
{
createForm: {
schema: {
type: "object",
properties: {
product_name: {
type: "string",
label: "商品名称",
maxLength: 8,
minLength: 4,
option: {
comType: "input",
required: true,
},
},
price: {
type: "number",
label: "价格",
minimum: 1,
maximum: 1000,
option: {
comType: "inputNumber",
},
},
inventory: {
type: "number",
label: "库存",
option: {
comType: "select",
enumList: [
{
label: "100",
value: 100,
},
{
label: "500",
value: 500,
},
{
label: "1000",
value: 1000,
},
],
},
},
},
},
config: {
title: "新增商品",
saveBtnText: "确定",
},
};
}
接下来就是动态组件的开发,结构大致如下:
FormItemConfig 是表单要用到的组件二次封装,统一加了getValue、validate方法。
import input from "./complex-view/input/input.vue";
import inputNumber from './complex-view/input-number/input-number.vue'
import select from './complex-view/select/select.vue'
const FormItemConfig = {
input: {
component: input,
},
inputNumber: {
component: inputNumber,
},
select: {
component: select,
},
};
export default FormItemConfig;
schema-form.vue
<template>
<el-row v-if="schema && schema.properties" class="schema-form">
<template v-for="(itemSchema, key) in schema.properties">
<component
:is="FormItemConfig[itemSchema.option?.comType]?.component"
v-show="itemSchema.option.visible !== false"
ref="formComList"
:schema-key="key"
:schema="itemSchema"
:model="model ? model[key] : undefined"
/>
</template>
</el-row>
</template>
<script setup>
import { ref, toRefs, provide } from "vue";
import FormItemConfig from "./form-item-config";
const Ajv = require("ajv");
const ajv = new Ajv();
provide("ajv", ajv);
const formComList = ref([]);
const props = defineProps({
schema: Object,
// editForm 表单数据回填
model: Object,
});
const { schema, model } = toRefs(props);
const validate = () => {
return formComList.value.every((item) => {
const res = item.validate();
return res;
});
};
// 获取表单数据
const getValue = () => {
return formComList.value.reduce(
(dtoObj, item) => ({ ...dtoObj, ...item.getValue() }),
{}
);
};
defineExpose({
validate,
getValue,
});
</script>
schema-form 的 getValue 和 validate ,会遍历每个表单子组件的字段值和校验,因此子组件内部也必须实现 getValue和validate方法。
接下来是点击新建按钮,触发弹窗显示,填写表单后,点保存提交数据并关闭弹窗,刷新表格,完成整个流程的串联。
DSL 的 tableConfig 配置
tableConfig: {
headerButtons: [
{
label: "新增",
// showComponent 是自己定义的,这里怎么定义,组件内部也要用相同的名字
eventKey: "showComponent",
type: "primary",
plain: true,
eventOption: {
comName: "createForm",
// 其他配置,如 params 等
},
},
],
}
点击按钮的时候,把一行的数据(编辑才有,新建没有)和按钮的配置传给处理函数,因此可能拿到eventKey和eventOption.comName。如果是 showComponent,就会调起组件内写好的 showComponent 方法。因为所有的动态组件都已经挂载了,eventOption.comName,找到对应的组件(这里是CreateForm),调用组件内部写好的 show 方法显示弹窗和表单。点保存按钮时,
调用 validate和 getValue 方法,通用校验后,提交数据,成功则使用 notification 进行提示,并且通知父组件更新表格 emit("command", { event: "loadTableData" })。
至此,通过一份配置,生成一个项目的列表页基本完成。
出处:《哲玄课堂-大前端全栈实践》