DSL领域模型设计及dashboard模版实现

0 阅读6分钟

完成领域模型的搭建,目的就是为了解决项目开发过程中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>