代码生成器模板开发说明

模板引擎采用的是:art-template,修改了解析规则、增加了扩展;

体验demo

目录结构


├── web-project                                          # web项目源码根目录
│   ├── mock                                             # 项目mock 模拟数据
│   │   ├── json
│   │   │   └── createPage.js                            # 生成器对应的expressjs 服务
│   │   └── mock-server.js                               # mock-server加载器
│   ├── src                        
│   │   ├── apis                                         # ajax
│   │   │   └── create-page.js                           # 生成器对应的ajax
│   │   ├── mixins
│   │   │   └── create.js                                # 生成器视图的构建核心方法
│   │   └── views
│   │   │   └── create
│   │   │   │   ├── components
│   │   │   │   │   ├── item-setting                     # 表单元素配置组件存放目录
│   │   │   │   │   │   ├── _com                         # 表单元素配置组件基础组件
│   │   │   │   │   │   ├── element-ui                   # 基于 element-ui 规则的表单配置组件 
│   │   │   │   │   │   │   └── set-select.vue           # select 组件配置功能组件
│   │   │   │   │   │   └── element-plus                 # 基于 element-plus 规则的表单配置组件
│   │   │   │   │   │       └── set-datePicker.vue       # datePicker 组件配置功能组件
│   │   │   │   │   ├── form-item-valid.vue              # 表单元素验证(valid)组件
│   │   │   │   │   └── global-item-setting.js           # 表单元素配置加载器
│   │   │   │   ├── dialog
│   │   │   │   │   └── dialog-form-item-setting.vue     # 表单元素配置组件
│   │   │   │   ├── create-form.vue                      # 表单类视图创建功能组件
│   │   │   │   ├── create-info.vue                      # 详情类视图创建功能组件
│   │   │   │   ├── create-list.vue                      # 列表类视图创建功能组件
│   │   │   │   └── index.vue
├── server                                               # 服务端根目录
│   ├── www                                              # web项目生产环境目录(编译后的代码)
│   ├── create_cfg_tmpl                                  # 生成器模板及配置文件目录
│   │   ├── vue2                                         # vue2 + element-ui 项目模板
│   │   │   ├── config                                   # 生成器配置文件目录
│   │   │   │   ├── config-form-valid-msg.js             # 表单验证(valid)提示信息配置
│   │   │   │   └── config.js                            # 全局配置文件
│   │   │   └── template                                 # 生成器模板目录
│   │   │   │   ├── base                                 # 基础模板
│   │   │   │   │   └── form                             # 表单元素模板
│   │   │   │   ├── dialog                               # 弹窗类模板
│   │   │   │   └── page                                 # 页面视图类模板
│   │   ├── vue3                                         # vue3 + element-plus 项目模板
│   │   │   ├── config                                   # 生成器配置文件目录
│   │   │   └── template                                 # 生成器模板目录
|   ├── import_cfg.js                                    # 导入配置文件
|   ├── web-server.js                                    # web服务
|   └── main.js                                          # 主服务


            

模板语法

模板文件在server/create_cfg_tmpl/**/template/ 目录下,修改后不用重启即可生效;

注意:web-project/public/tmpl_cfg 及 server/www/tmpl_cfg 目录下的配置文件及模板均为临时文件,修改无效,npm run xx 启动时,这部分会被server/create_cfg_tmpl 中与 xx 匹配的文件完全覆盖;

输出
标准输出

${{value}}
${{data.key}}
${{data['key']}}
${{a ? b : c}}
${{a || b}}
${{a + b}}

// 或; 与上面的输出效果一致

<%= value %>
<%= data.key %>
<%= data['key'] %>
<%= a ? b : c %>
<%= a || b %>
<%= a + b %>
              
原文输出

${{@ value }}
// 或
<%- value %>
              
条件判断

${{if value}} ... ${{/if}}
${{if v1}} ... ${{else if v2}} ... ${{/if}}
              
循环

${{each target}}
  ${{$index}} ${{$value}}
${{/each}}
              
变量

{{set temp = data.sub.content}}
// 或;
<% var temp = data.sub.content; %>
              
子模板

{{include './header.art'}}
{{include './header.art' data}}
// 注意模板的根路径为 /template 目录, 例如:include 'header.art' 会自动匹配到 /template/header.art
// 相对路径的模板引入,例如:include './header.art' ,且假设当前模板目录为 /template/base, 会自动匹配到 /template/base/header.art
              

预设模板变量

注意:只有在对应类型的模板中才能访问到对应的预设对象
路由

以VUE的 router 对象模板为例


  { 
    path: '/${{lever1Path}}', 
    component: Layout, 
    name:'${{lever1RouterName}}',
    meta: { 
      title: '${{lever1PageName}}', 
      icon: 'el-icon-orange',
      code: '' // 设置code即可启用权限控制 
    }, 
    redirect: '${{lever2Path}}', 
    children: [ 
      {
        path: '${{lever2Path}}',
        component: () => import('@/views/${{filePath}}'),
        name: '${{lever2RouterName}}',
        meta: { title: '${{lever2PageName}}', code: '' }
      }
    ]
  } 
  
API
注意:内置方法,API、路由与页面生成均依赖此对象

  {
    name, // api名称
    nameHump, // api名称, 大驼峰格式
    nameToPathfilter, // 过滤后的api名称,主要用于生成 view 页面
    nameToPath, // 连接符(-)的 api名称
    desc, // 接口描述
    type, // 请求类型 get|post|del|put, 注意delete统一转换为del
    uri, // 接口地址
    fileName, // API 所属分组 tags,连接符(-)格式,用于生成api文件名
    fileNameHump, // API 所属分组 tags, 大驼峰格式
    fileDesc // API 所属分组的描述
  }
页面/视图

{
  rootPath, // 项目根路径
  formData: [ // 列表页检索项
    {
      column, // 列名 示例:username
      isShow, // 是否显示
      opts: {
        range: range_, // 双日历配置项
        valid: null, // 表单验证配置项
        attr: null // 可选,表单组件配置属性
      }, // 配置项
      needValidateOpts: false, // 是否需要验证配置项
      label,  // 示例:用户名
      labelDesc, // 字段描述 示例:用户名,英文字母
      columnType, // 类型  string|integer|array
      formItemType, // 表单元素类型 input|dataPicker 对应配置文件中 formItemOpts
      range: { // 默认: null , 仅当是双日历表单组件时返回  
        f_: '__DateTime', // 所执行的自动匹配模式 ,注意仅 'startDateTime' 与 'endDateTime' 可自动匹配为 '__DateTime';'startDateTime' 与 'dateTimeEnd' 不会自动匹配
        to_: 'endDateTime', // 自动匹配到的对象列名称
        isDatePickerRange: 'true|false', // 是否是双日历
        isStart: 'true|false' // 是否是start 否则为end
      } 
    }
  ],
  tableData: [ // 列表项
    {
      column,
      label,
      columnType,
      isShow,
      showTips, // 单元格内容溢出后是否显示气泡
      alignType // 列文本对齐方式 ,默认数字居中,文本居左;参照配置文件中 listPage 的配置
    }
  ],
  tableConfig: { // 列表页全局配置
    showBtnCol: false, // 显示按钮列
    showNumCol: true, // 显示序号列
    showFormRightBtns: false // 显示表单右侧按钮区域
  },
  apiConfig, // 参照上方 API 预设变量的说明
}
                

预设模板函数

模板函数可全局访问,且不区分模板类型,任何类型的模板中均可访问;但是要注意其接收的参数需要满足;

获取自定义组件列表
获取需要注册的自定义组件列表(该方法只返回被实际使用到的组件,即按需注入)

// 方法 :$imports.getCusPluginsListImport(formData) ; 其中 formData 为预设页面模板变量
// 示例 :
// 1、声明变量
<% var _CusPluginsList_ = $imports.getCusPluginsListImport(formData) %>

// 2、注入自定义组件
${{each _CusPluginsList_}}
import ${{$value.name}} from '${{$value.path}}'
${{/each}}

// Tips: $value.name 与 $value.path 分别对应配置文件中 formItemOpts 对象的 value 与 path

// 3、注册自定义组件
export default {
  components: {
    ${{each _CusPluginsList_}}${{$value.name}},${{/each}}
  }
}
              
获取表单验证规则

// 方法 :$imports.getValidationListImport(formData) ; 其中 formData 为预设页面模板变量
// 示例 :
// 1、声明变量
<% var _ValidationList_ = $imports.getValidationListImport(formData) %>

// 2、绑定到form组件
<el-form ref="claFrom" :model="claForm" 
  ${{if _ValidationList_.length}} :rules="rules" ${{/if}}
  label-width="auto" />
  
// 3、在data()中声明 rules
${{if _ValidationList_.length}}
  rules:{
    ${{include 'base/base-form-rule.art' _ValidationList_}}
  },
${{/if}}

              
获取表单元素枚举属性

方法 :$imports.getFormItemAttr(itemOpts) ; 其中 itemOpts 为预设页面模板变量: opts.attr
描述:返回属性的键值对字符串,注意此方法不会返回以'_'开头并且以 '_'结尾 (/^_[\w-]+_$/)的私有属性或临时变量
示例 :
<el-input
  v-model="claForm.${{column}}"
  placeholder="${{label}}"
  ${{@ opts.attr | getFormItemAttr}}
/>
              
获取组件数据源
用于获取组件的数据源,例如:select、radio(分组)、checkbox(分组);
目前暂不支持远端数据,暂时数据只能来源自:
缺省值(优先级最低):config/config.js -> formItemOpts -> dataSource -> default
手动配置的值:生成器 -》 检索条件 -》 组件配置 -》 数据源
手动配置的值会全量覆盖缺省值;

// 详见模板 template/base/base-plugins-data-source.art

// 用法:
<% var _pluginsDataSourceList_ = $imports.getPluginsDataSourceImport(formData) %>
${{each _pluginsDataSourceList_}}
// ${{$value.label}}
${{$value.name}}:${{@ $value.data}}${{if _pluginsDataSourceList_.length-1 != $index}},${{/if}}
${{/each}}

// 输出示例:
userStatusOpts:[
  {
    value: '1',
    label: '正常'
  },
  {
    value: '2',
    label: '禁用'
  }
]
              
获取表单元素的值
根据字段类型返回匹配类型的数据定义,例如:[], '', 0 等

// 用法:
${{each formData}} ${{$value.column}}: ${{@ $value.columnType | getFormItemValue }} ${{if formData.length-1 != $index}},${{/if}} // ${{@ $value.label}}
${{/each}}

// 输出示例:
name: '', // 学生姓名
city: '', // 城市
phone: '', // 手机号
edu: [] // 教育经历
              

form元素属性配置组件

用于可视化配置form表单元素的属性,例如:文本框类型,时间格式等

注意:元素的属性的名称和可选值,不同的UI框架是不一样的,所以在web-project/src/views/create/components/item-setting目录下根据UI框架名称进行了分组; 建议把常用的UI框架的配置放在这里; 目前预设了 element-ui 与 element-plus 两个UI框架的配置组件;
如果不想使用form元素属性配置组件,可以直接在config.js 中,修改 _UI_TEMP_PATH_ 的值为 '_' 即可;否则默认应用的是element-ui的配置属性;
新增UI框架分组

如果目前预设的UI组件配置属性,与您使用的UI组件属性不匹配,可通过新建UI组件分组的方式解决;例如新建 Ant Design Vue ;


1、在 components/item-setting 目录下新建一个 ant 文件夹;
2、在 ant 文件夹下新建一个组件配置文件,例如:set-input.vue;文件名格式为set-xxx.vue; 其中xxx是form组件名称,且需在config.js -> formItemOpts 中声明(常用的都已声明);
3、【可选】在 set-input.vue 中编写组件配置模板,具体参照 element-ui 下的配置文件;例如:
<template>
<el-form
    ref="form"
    :model="formData"
    label-width="120px"
  >
  <el-form-item label="前置元素">
    <el-input v-model="formData._prependVal_" :disabled="formData._prependType_ === ''">
      <el-select
          slot="prepend"
          v-model="formData._prependType_"
          class="cus-allowed"
          style="width: 120px;"
        >
         <el-option label="不使用" value="" />
         <el-option label="文字" value="text" />
         <el-option label="图标按钮" value="btnIcon" />
      </el-select>
        <el-button
          v-if="formData._prependType_ === 'btnIcon'"
          slot="append"
          icon="el-icon-search"
          @click="$message('暂未支持图标库选取,请输入icon的class')"
        />
      </el-input>
    </el-form-item>
    <!-- 后置元素 -->
  </el-form>
</template>

<script>

  export default {
    name: 'SetInput',
    inject: ['itemSetIns'],
    data() {
      return {
        rowData: this.itemSetIns.rowData, // 【原型链对象】,当前操作行的数据,直接修改原型链
        tmpRowData: this.itemSetIns.tmpRowData,
        /* 注意以 '_'开头并且以 '_'结尾 (/^_[\w-]+_$/)的属性为私有属性,或临时变量,不会出现在模板属性过滤器 getFormItemAttr 中 */
        formData: {
          _prependType_: '', // 前置对象的类型
          _prependVal_: '', // 前置对象的值
          _appendType_: '',
          _appendVal_: ''
        },
        tmpFormData: {},
        /* 需要覆写的属性名称, 即在formDate中修改了组件默认属性值的属性的名称, 例如 'type';注意:具有非空值的私有属性(_xx_)会自动应用,无需维护在这里 */
        rewriteAttr: []
      }
    },
    mounted() {
      this.init()
    },
    methods: {
      init() {
        if (this.tmpRowData.opts.attr) {
          Object.assign(this.formData, this.tmpRowData.opts.attr || {})
        }
      },
      /**
       * 配置组件提交方法,此方法中修改数据原型链
       * @return Boolean 返回 true 则关闭弹窗,其它则无;
       */
      toSubmitFn() { // 直接修改原型链
        if (this.formData._prependType_ === '' && this.formData._appendType_ === '') {
          this.rowData.opts.attr = null
        } else {
          const tmp = {}
          const oldData = this.$options.data.call(this).formData
          // 只保留当前最新的属性,忽略已移除的历史属性和组件默认值,以减少冗余输出。
          const tempData = JSON.parse(JSON.stringify(this.formData))
          Object.keys(oldData).forEach(k => {
            if (this.rewriteAttr.includes(k) || /^_[\w-]+_$/.test(k) || tempData[k] !== oldData[k]) {
              tmp[k] = tempData[k]
            }
          })
          this.rowData.opts.attr = { ...tmp }
        }
        return true
      }
    }
  }
</script>

4、在 server/create_cfg_tmpl 下创建文件夹 ant , 并将VUE2(js语法) 或 VUE3(TS语法) 目录下的文件拷贝过来;
5、【可选】修改 config/config.js 中的配置信息,主要是修改文件路径和后缀名;
6、在 server/create_cfg_tmpl 下找到input.art 文件,修改如下:

// 原 vue2 模板的源码
<el-input
  ${{if columnType == 'integer'}} v-model.number="claForm.${{column}}" ${{else}} v-model.trim="claForm.${{column}}" ${{/if}}
  placeholder="${{label}}"
  style="width:120px;"
  ${{@ opts.attr | getFormItemAttr}}
>
${{if opts.attr?._prependType_ == 'text'}}
<template slot="prepend">${{opts.attr._prependVal_ || 'text'}}</template>
${{/if}}
${{if opts.attr?._prependType_ == 'btnIcon'}}
<el-button slot="prepend" icon="${{opts.attr._prependVal_ || 'el-icon-search'}}"></el-button>
${{/if}}


// 修改后的匹配 Ant Design Vue 模板的源码
<a-input v-model:value="claForm.${{column}}" 
  placeholder="${{label}}"
  style="width:120px;"
  ${{@ opts.attr | getFormItemAttr}}
>
${{if opts.attr?._prependType_ == 'text'}}
<template #addonBefore>
  ${{opts.attr._prependVal_ || 'text'}}
</template>
${{/if}}
${{if opts.attr?._prependType_ == 'btnIcon'}}
<template #addonBefore>
  <${{opts.attr._prependVal_ || 'setting-outlined'}} />
</template>
${{/if}}
</a-input>


              
注意,set-input.vue 不是必须的;因为只要你使用UI组件的属性名称及其可选值,与目前预设的UI组件属性名称、属性值一致,就可以直接使用;不常用的属性的差异可以忽略,可手动修改生成后的源码; 其次:你也可以只处理差异的部分,例如:element-plus 的属性规则大部分与 element-ui 一致,在常用属性里只有日期组件的参数格式不一致,那么只需要在 components/item-setting/element-plus 下新建一个日期组件的配置文件,并且设置 _UI_TEMP_PATH_: [ 'element-ui', 'element-plus' ] 即可;这样 element-plus 下的重名组件会覆盖 element-ui 下的组件;
编辑/扩展组件模板
添加自定义form组件,并创建组件配置模板(可选)

1、在 config/config.js -> formItemOpts 中声明需要新增的自定义form组件, 例如:cusDatePicker


/**
  * 表单元素类型
  * disabled : 在构造模板中,不可使用(暂不支持)的元素
  *   [undefined] : 不禁用, 默认值;
  *   [boolean] true : 全部禁用;
  *   [string] 只有完全匹配的才禁用,例如:'list|form' 则表示为 list 或 form 禁用;
  * path : 可选,自定义组件的路径 例如:'@/components/cusDatePicker/index.vue'
  * valid : 必须,表单验证配置
  *  -- trigger: 必须,触发方式 'blur' 、'change' 等
  *  -- type: 可选,数据类型 'string'(默认)、'date'、'array' 等
  * dataSource : 可选,数据源配置,例如:下拉选择框的选项
  *  -- dataType: 必须,数据格式,默认array,可选:array、object
  *  -- default: 可选,数据源默认值,例如:下拉选择框的默认值 [{value: null, label: '全部'}] ; 如果没有对组件进行配置,则使用默认值
  */
  formItemOpts: [
    {
      value: 'cusDatePicker',
      label: '日期选择器(双)',
      path: '@/components/cusDatePicker/index.vue',
      valid: {
        trigger: 'change',
        type: 'date'
      }
    }
  ]
                
注意:修改配置文件后,必须重启服务才能生效

2、【可选】创建组件配置模板,如果不需要可视化配置,则跳过此步骤

在目录 /template/base/form 下创建名为 cusDatePicker.art 的文件,注意文件名称必须与上面声明的 formItemOpt中的名称一致;


// 继承 inject: ['itemSetIns'] ,其指向 dialog-form-item-setting.vue 的实例(this);
inject: ['itemSetIns'],
data () {
  return {
    rowData: this.itemSetIns.rowData, // 【原型链对象】,当前操作行的数据,直接修改原型链
    tmpRowData: this.itemSetIns.tmpRowData, // rowData的克隆对象,submit 后才修改数据
    rowList: this.itemSetIns.rowList, // 【原型链对象】,当前操作行的数据,直接修改原型链
    /* 注意以 '_'开头并且以 '_'结尾 (/^_[\w-]+_$/)的属性为私有属性,或临时变量,不会出现在模板属性过滤器 getFormItemAttr 中 */
    formData: {
      _start_: '',
      _end_: '',
      type: 'daterange',
      format: 'yyyy-MM-dd',
      'value-format': 'yyyy-MM-dd'
      //。。。 可配置的属性
    },
    tmpFormData: {}
    。。。
  }
},
methods: {
  init () {
    Object.assign(this.formData, this.tmpRowData.opts.attr || {})
    // 。。。 组件的业务逻辑代码
  },
  /**
   * 配置组件提交方法,此方法中修改数据原型链
   * @return Boolean 返回 true 则关闭弹窗,其它则无;
   */
  toSubmitFn () {
    // 。。。 组件的业务逻辑代码
    return true
  },
  /**
    * 自动初始化配置项
    * 【内置钩子,静态工具类】【原型链修改】
    * 组件在被初始化前,会调用此方法,用于初始化配置项,此方法会在组件实例化前调用,所以无法访问组件实例,并且其中的 this 指向的是 methods 对象;
    * 尽量不要在此方法中使用this,或不在当前实例中使用此方法,以免造成this指向错误
    * 可访问的全局变量:
    *  _$cusConfig$_ 配置项,对应config.js中的配置项
    *  _$cusComponents$_ 组件列表,对应item-setting 目录下的组件列表;_$cusComponents$_['SetInput'] 为 SetInput 组件,注意不是组件实例
    *  访问组件内的方法: _$cusComponents$_['SetInput'].methods.__autoInitConfig, 注意:被访问的方法中不能使用this
    *  访问组件内的data: _$cusComponents$_['SetInput'].data.call({}), 注意:不能获取data中包含this的引用,均为undefined 或 异常
    * @param {Object} row 当前操作行的数据 【原型链修改】 其中 row.formItemType 始终为修改后的新值,而 __toResetFn 方法中的 row.formItemType 始终为修改前的旧值
    * @param {Array | false} tableDataSearch 当前视图的数据 【原型链修改】,如果是false则表示只操作当前行的数据,不操作关联行的数据
    * @param {Object} other 其它参数
    */
  __autoInitConfig (row, tableDataSearch = [], other = {}) {
    // 示例,注意示例功能为判断下拉框是否是多选模式,与组件 cusDatePicker 无关; 因为cusDatePicker 的这部分代码比较多,不方便作为示例展示;
    if (row.formItemType === 'select' && row.columnType === 'array') { // 数组类型的select,默认多选
      row.opts.attr = Object.assign({}, (row.opts.attr || {}), {
        multiple: true // 是否多选
      })
    }
  },
  /**
   * 重置关联数据的配置项
   * 注意当前操作行的数据,或默认自动重置不需要在这里处理(组件私有数据除外),这里只处理关联数据的配置项
   * 例如 开始时间与结束时间的关联,开始时间的配置项修改后,同时需要在这里重置结束时间的配置项
   */
  __toResetFn (row, tableDataSearch = [], newFormItemType) {},
  /**
    * 选择组件类型
    * 【内置钩子,静态工具类】注意不能修改 row 的值
    * @param {object} row 当前操作行的数据
    * @param {object} other 其它参数
    * @return {number | null} 返回值为null则不做任何操作,返回值为数字则代表匹配优先级,数字越大优先级越高
    */
  __formItemTypeChoice (row, other = {}) {
    if (!config_.formFieldDetection.findDate) {
      return null
    }
    const name_ = row.name || ''
    if (/(date|time)/i.test(name_) && other.rangeObj[name_] && other.rangeObj[name_].isDatePickerRange) { // 日期范围
      return 5
    }
    return null
  },
  /**
    * 验证方法
    * 验证组件配置信息是否有效
    * @param {Object} row 当前操作行的数据, 禁止修改
    * @param {Array} tableDataSearch 当前视图的数据, 禁止修改
    * @return {Boolean} 返回 false 则验证不通过
    */
  __validationFn (row, tableDataSearch = []) {
    if (!row.opts.range) {
      this.$alert(`字段【${row.column}】未指定【开始时间或结束时间】`, '错误')
      return false
    }
  }
}
                
注意:这里热更新即可生效,如未生效且尝试重启服务后扔未生效,请检查文件名是否与声明的一致,区分大小写;

其它

访问配置文件
对应 config/config.js 文件
方法 :$imports.comConfig
示例 :输出作者 ${{$imports.comConfig.author}}
生成 mock 文件
有接口的请忽略,默认也未开启; 配置路径:config.js -> makeFile -> isMakeMock (是否生成mock数据文件)
参照 /template/base/base-mock-item.art

{
  url: '${{apiConfig.uri | getMockRouterUrl}}',  // getMockRouterUrl 返回带参数匹配规则的mock路由地址, 详见express.js官网
  type: '${{apiConfig.type}}',
  response: config => {
    if (!demo.__cacheKey__['${{apiConfig.name}}']) {
      const tobj = {
        ${{@ tableData | getMockRouterResData}}  // getMockRouterResData 返回mock数据
      }
      demo.demoFnGetTheListKey(tobj) // mock 数据字段猜想匹配关系,返回字段对应类型的mock数据,例如 数字、字符串、省份、城市、手机号等
      demo.__cacheKey__['${{apiConfig.name}}'] = { ...tobj }
    }

    return demo.demoFnMakeListPageData('${{apiConfig.name}}', config.query) // 处理分页、检索
  }
}