完成领域模型的搭建,目的就是为了解决项目开发过程中80%的重复任务,
根据一条JSON结构,可以由层级大到层级小动态加载自己封装的组件,大到区分布局,小到区分到按钮, 还可以拓展到前端调用的api接口,查询数据库表明。 针对于前端就是根据数据加载模块
// 因为有重复性的工作 体力活 不用再 c v
// 基于这样 通过模版配置和模版页
// 提供足够的拓展模块, 方便自己拓展
export default{
mode: 'dashboard', // 模版类型, 不同模版类型对应不一样的模版数据结构
name: '', // 名称
desc: '', // 描述
icon: '', // icon
homePage: '', // 首页(项目配置)
// 头部菜单
menu: [{
key:'', // 菜单的唯一描述
name: '', // 菜单名称
menuType: '', // 枚举值,菜单类型 group组 / modulel 模块
// 当 menutype == group 时, 可填
subMenu: [{
// 可递归mentItem
},],
// 当moduleType == module 时, 可填 模块类型
moduleType: '', // 枚举值 slider/iframe/custom/schema
// 当 moduleType == siderConfig 时
siderConfig: { // 成员
menu:[{
// 可递归 menuItem (除 moduleType == siderConfig)
},]
},
// 当 moduleType == iframe 时
iframeConfig: {
path: '', // iframe 路径
},
// 当 moduleType == custom 时
customConfig: { // 自定义模块
path: '', // 自定义路由路径
},
// 当 moduleType == schema 时
schemaConfig: {
api: '/api/user', // 数据源API (遵循 RESTFUL 规范)
schema: { // 板块数据结构
type: 'object',
properties: {
key: {
...schema, // 标准 schema 配置
type: '', // 字段类型
label: '', // 字段的中文名
// 字段在 table 中的相关配置
tableOption :{
...elTableColumnConfig, // 标准 el-table-column 配置
toFixed: 0, // 保留小数点后几位
visiable: true, //默认为 true (false 或 不配置时, 该标识不在表中显示)
},
},
...
}
},
// table 相关配置
tableConfig: {
// 表格外的按钮
headerButtons: [{
label: '', // 按钮名
eventKey: '', // 按钮事件名
eventOption: {}, //按钮具体配置
...elButtonConfig // 标准 el-button 配置
},...],
// 表格内 行按钮
rowButtons: [{
label: '', //按钮中文名
eventKey: '', // 按钮事件名
eventOption: {
// 当 eventKey == 'remove'
params: {
// params = 参数的键值
// rowValueKey = 参数值 (当格式为 schema: tableKey 的时候,到table中招响应的字段)
paramKey: rowValueKey
}
}, // 按钮具体配置
...elButtonConfig // 标准 el-button 配置
}]
},
searchConfig: {}, // search-bar 相关配置
apiConfig: {}, // 接口
dbConfig: {}, /// 数据库
comConfig: {}, // 组件
components: {}, // 模块组件
},
},],
}
例如我们有一个列表页,需要根据表单搜索查询,表格内的内容,我们需要后端的配合,需要将我们前端页面的结构,通过上面的JSON格式返回给前端,
// 这是node实现的方法
const _ = require('lodash')
const glob = require('glob');
const path = require('path');
const { sep } = path;
const projectExtendModel = (model, project) => {
return _.mergeWith({}, model, project, (modelValue, projValue) => {
// 处理数组合并的特殊情况
if (Array.isArray(modelValue) && Array.isArray(projValue)) {
let result = [];
// 因为project 继承model, 所以需要处理修改和新增内容的情况
// project有的键值, model也有 => 修改(重载)
// project有的键值, model没有 => 新增
// model有的键值, project没有 => 保留(继承)
// 处理修改或保留
for (let i = 0; i < modelValue.length; i++) {
let modelItem = modelValue[i];
const projItem = projValue.find(item => item.key === modelItem.key)
// project有的键值, model也有, 则递归调用 projectExtendModel 方法覆盖修改
result.push(projItem ? projectExtendModel(modelItem, projItem) : modelItem)
}
// 处理新增
for (let i = 0; i < projValue.length; i++) {
const projItem = projValue[i];
const modelItem = modelValue.find(item => item.key === projItem.key)
if (!modelItem) {
result.push(projItem)
}
}
return result;
}
})
}
/**
* 解析 module 配置, 并返回组织继承后的数据结构
* [{
* model: ${model}
* project: {
* proj1: ${proj1},
* proj2: ${proj2}
* }
* }, ...]
*/
module.exports = (app) => {
const modelList = [];
// 首先遍历当前文件夹,构造数据结构, 挂载到moduleList上
const modulePath = path.resolve(app.baseDir, `.${sep}model`)
const fileList = glob.sync(path.resolve(modulePath, `.${sep}**${sep}**.js`))
fileList.forEach(file => {
if (file.indexOf('index.js') > -1) {
return
}
//区分配置类型( model / project)
const type = path.resolve(file).indexOf(`${sep}project${sep}`) > -1 ? 'project' : 'model';
if (type === 'project') {
const modelKey = file.match(/\/model\/(.*?)\/project/)?.[1]
const projKey = file.match(/\/project\/(.*?)\.js/)?.[1]
let modelItem = modelList.find(item => item.model?.key === modelKey)
if (!modelItem) { // 初始化 model 数据结构
modelItem = {};
modelList.push(modelItem)
}
if (!modelItem.project) { // 初始化 project 数据结果
modelItem.project = {}
}
modelItem.project[projKey] = require(path.resolve(file))
modelItem.project[projKey].key = projKey; // 注入 projectKey
modelItem.project[projKey].modelKey = modelKey; // 注入 modelKey
}
if (type === 'model') {
const modelKey = file.match(/\/model\/(.*?)\/model\.js/)?.[1]
let modelItem = modelList.find(item => item.model?.key === modelKey)
if (!modelItem) { // 初始化 model 数据结构
modelItem = {};
modelList.push(modelItem)
}
modelItem.model = require(path.resolve(file))
modelItem.model.key = modelKey; // 注入 moedelKey
}
})
// console.log(JSON.stringify(modelList));
// 数据进一步整理: project => 继承model
modelList.forEach((item) => {
const { model, project } = item;
for (const key in project) {
project[key] = projectExtendModel(model, project[key])
}
})
return modelList;
}
如果一个电商系统,系统下面有商品管理、订单管理、客户管理, 拿商品管理举例,我们需要对商品进行CRUD的操作,同样的,订单管理和客户管理也会有CROD的操作,大致,都一样的,一部分是Search模块,一部分是Table展示内容模块,我们就需要封装Search模块和table模块,商品管理、订单管理和客户管理Search搜索的内容是不一样的,调用的api也会是不一样的,搜索的内容是不一样的,Table展示的后面也会有操作的展示,根据权限有的只有查看权力,有的有查看、修改权力,有的增删改权力都有,但是这样我们的单独封装Search和Table模块无法同时满足商品管理】订单管理和客户管理;我们可以利用DSL对Dashboard进行模板解析,根据JSON内容,动态加载各个模块,细分到各个搜索内容,表格的操作按钮,甚至下面我们还可以进行编辑时弹窗的展示内容。
module.exports = {
model: 'dashboard',
name: '电商系统',
menu: [{
key: 'product',
name: ' 商品管理',
menuType: 'module',
moduleType: 'schema',
schemaConfig: {
api: '/api/proj/product',
schema: {
typeof: 'object',
properties: {
product_id: {
type: 'string',
label: '商品ID',
tableOption:{
width: 300,
'show-overflow-tooltip': true
}
},
product_name: {
type: 'string',
label: '商品名称',
tableOption: {
width: 200
},
// searchOption:{
// comType: 'input', // 配置组件类型 input/select/....
// }
searchOption: {
comType: 'dynamicSelect', // 配置组件类型 input/select/....
api: '/api/proj/product_enum/list'
}
},
price: {
type: 'number',
label: '价格',
tableOption: {
width: 200
},
searchOption:{
comType: 'select', // 配置组件类型 input/select/....
enumList:[{
lable: '$39.9',
value: 18888,
},{
lable: '$39.9',
value: 8888,
},]
}
},
inventory: {
type: 'number',
label: '库存',
tableOption: {
width: 200
}
},
create_time: {
type: 'string',
label: '创建时间',
tableOption: {
width: 200
},
searchOption: {
comType: 'dateRange', // 配置组件类型 input/select/dateRange/....
}
},
}
},
tableConfig: {
headerButtons: [{
label: '新增商品',
eventKey: 'showComponent',
type: 'primary',
plain: true,
}],
rowButtons: [{
label: '修改',
eventKey: 'showComponent',
type: 'warning',
}, {
label: '删除',
eventKey: 'remove',
eventOption: {
params: {
product_id: 'schema::product_id'
}
},
type: 'danger',
}]
},
}
}, {
key: 'order',
name: ' 订单管理',
menuType: 'module',
moduleType: 'custom',
customConfig: {
path: '/todo',
},
}, {
key: 'client',
name: ' 客户管理',
menuType: 'module',
moduleType: 'custom',
customConfig: {
path: '/todo',
},
}],
}
这是搜索模块的展示,可以适用到,各个模块
// 统一管理组件展示内容
import input from './complex-view/input/input.vue'
import select from './complex-view/select/select.vue'
import dynamicSelect from './complex-view/dynamic-select/dynamic-select.vue'
import dateRange from './complex-view/date-range/date-range.vue'
const SearchItemConfig = {
input:{
component: input
},
select:{
component: select
},
dynamicSelect:{
component: dynamicSelect
},
dateRange:{
component: dateRange
},
}
export default SearchItemConfig
将各个组件的内容的获取和展现统一交给父级组件来进行操作,不用再将input、select...逐个引入
<template>
<el-form
v-if="schema && schema.properties"
:inline="true"
class="schema-search-bar"
>
<el-form-item v-for="(schemaItem, key) in schema.properties"
:key="key"
:label="schemaItem.label"
>
<component
:ref="handleSearchComList"
:is="SearchItemConfig[schemaItem.option?.comType]?.component"
:key="key"
:schema="schemaItem"
:schemaKey="key"
@load="handleChildLoaded"
>
</component>
<!-- 动态组件 -->
</el-form-item>
<el-form-item>
<!-- 操作区域 -->
<el-button type="primary" plain class="searche-btn" @click="search" >
搜索
</el-button>
<el-button type="primary" plain class="reset-btn" @click="reset">
重置
</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { ref, toRefs } from 'vue';
import SearchItemConfig from './schema-item-config.js';
const props = defineProps({
/**
* schema配置 结构如下
* {
type: 'object',
properties: {
key: {
...schema, // 标准 schema 配置
type: '', // 字段类型
label: '', // 字段的中文名
searchOption: {
...eleComponentConfig, // 标准 el-component-column 配置
comType: '', // 配置组件类型 input/select/....
default: '', // 默认值
},
},
...
}
},
*/
schema: Object,
})
const { schema } = toRefs(props);
const emit = defineEmits(['load', 'search', 'reset']);
const searchComList = ref([]);
const handleSearchComList = (el) => {
searchComList.value.push(el)
}
const getValue = () =>{
let dtoObj = {};
searchComList.value.forEach(component => {
dtoObj = {
...dtoObj,
...component?.getValue()
}
})
return dtoObj;
}
let childComLoadedCount = 0;
const handleChildLoaded = () =>{
childComLoadedCount++
if(childComLoadedCount >= Object.keys(schema?.value?.properties).length){
emit('load', getValue())
}
}
const search = () => {
searchComList.value.forEach((component) =>{
component?.reset()
})
emit('search', getValue())
}
const reset = () => {
emit('reset')
}
defineExpose({
reset,
getValue
})
</script>
例如菜单导航,一级菜单栏下还由二级菜单栏,二级菜单栏下有三级...,我们可以用递归来实现布局这一功能
// 父级组件
<template>
<sider-container>
<template #menu-content>
<el-menu
:default-active="activeKey"
:ellipsis="false"
@select="onMenuSelect"
>
<template v-for="item in menuList">
<!-- group -->
<sub-menu v-if="item.subMenu && item.subMenu.length >0"
:menu-item="item">
</sub-menu>
<el-menu-item v-else :index="item.key">{{ item.name }}</el-menu-item>
<!-- module -->
</template>
</el-menu>
</template>
<template #main-content>
<router-view></router-view>
</template>
</sider-container>
</template>
// 子级组件 sider-container
<template>
<el-container class="sider-container">
<el-aside width="200px" class="aside">
<slot name="menu-content"></slot>
</el-aside>
<el-main>
<slot name="main-content"></slot>
</el-main>
</el-container>
</template>