• 【摸鱼神器】UI库秒变LowCode工具——列表篇(一)设计与实现


    内容摘要:

    • 需求分析
    • 定义 interface
    • 定义 json 文件
    • 定义列表控件的 props
    • 基于 el-table 封装,实现依赖 json 渲染
    • 实现内置功能:选择行(单选、多选),格式化、锁定等。
    • 使用 slot 实现自定义扩展
    • 做个工具维护 json 文件(下篇介绍)

    管理后台里面,列表是一个常用的功能,UI库提供了列表组件和分页组件实现功能。虽然功能强大,也很灵活,只是还不能称为低代码,不过没关系,我们可以写点代码让UI库变为摸鱼神器!

    本篇介绍列表的设计思路和封装方式。

    需求分析

    如果基于原生HTML来实现显示数据列表的功能的话,那么需考虑如何创建 table,如何设置css等。
    如果直接使用UI库的话,那么可以简单很多,只需要设置各种属性,然后绑定数据即可。
    以 el-table 为例:

      <el-table
        :data="tableData"
        border
        stripe
        style=" 100%"
      >
        <el-table-column prop="date" label="Date" width="180" />
        <el-table-column prop="name" label="Name" width="180" />
        <el-table-column prop="address" label="Address" />
      </el-table>
    

    设置好属性、记录集合,然后设置列(el-table-column)即可。
    这样一个列表就搞定了,再加上 el-pagination 分页组件,编写一些代码即可实现分页的功能。

    如果只是一个列表的话,这种方式没啥问题,但是管理后台项目,往往需要n个列表,而每个列表都大同小异,如果要一个一个手撸出来,那就有点麻烦了。

    那么如何解决呢?我们可以参考低代码,基于 el-talbe 封装一个列表控件,
    实现依赖 json 动态渲染列表,同时支持自定义扩展。

    定义 interface

    最近开始学习 Typescript,发现了一个现象,如果可以先定义好类型,那么代码就可以更清晰的展现出来。

    另外 Vue3 的最新文档,也采用了通过 interface 来介绍API功能的方式,所以我们也可以借鉴一下。

    依据 el-table 的属性,定义列表控件属性的 interface。

    Vue3 的 props 有一套约束方式,这个似乎和TS的方式有点冲突,没想出了更好的方法(option API 和 script setup两种定义props的方式,都有不足 ),所以只好做两个 interface,一个用于定义组件的 props ,一个用于取值。

    • IGridPropsComp:定义组件的 props
    /**
     * 列表控件的属性的描述,基于el-table
     */
    export interface IGridPropsComp {
      /**
       * 模块ID,number | string
       */
      moduleId: IPropsValidation,
      /**
       * 主键字段的名称 String,对应 row-key
       */
      idName: IPropsValidation,
      /**
       * table的高度, Number
       */
      height: IPropsValidation,
      /**
       * 列(字段)显示的顺序 Array<number|string>
       */
      colOrder: IPropsValidation,
      /**
       * 斑马纹,Boolean
       */
      stripe: IPropsValidation,
      /**
       * 纵向边框,Boolean
       */
      border: IPropsValidation,
      /**
       * 列的宽度是否自撑开,Boolean
       */
      fit: IPropsValidation,
      /**
       * 要高亮当前行,Boolean
       */
      highlightCurrentRow: IPropsValidation,
      /**
       * 锁定的列数 Number,设置到 el-table-column 的 fixed
       */
      fixedIndex: IPropsValidation,
      /**
       * table的列的 IGridItem
       * * id: number | string,
       * * colName: string, 字段名称
       * * label: string, 列的标签、标题
       * *  number, 列的宽度
       * * align: EAlign, 内容对齐方式
       * * headerAlign: EAlign 列标题对齐方式
       */
      itemMeta: IPropsValidation, // 
      /**
       * 记录选择的行:IGridSelection
       * * dataId: '', 单选ID number 、string
       * * row: {}, 单选的数据对象 {}
       * * dataIds: [], 多选ID []
       * * rows: [] 多选的数据对象 []
       */
      selection: IPropsValidation, 
        
      /**
       * 绑定的数据 Array, 对应 data
       */
      dataList: IPropsValidation
    
      // 其他扩展属性
      [propName: string]: IPropsValidation
    
    }
    
    • moduleId:模块ID,一个模块菜单只能有一个列表,菜单可以嵌套。
    • itemMeta:列的属性集合,记录列表的列的属性。
    • selection:记录列表的单选、多选的 row。
    • dataList:显示的数据,对应 el-table 的 data
    • 其他:对应 el-table 的属性

    IGridPropsComp 的作用是,约束列表控件需要设置哪些属性,属性的具体类型,就无法在这里约束了。

    • IPropsValidation (不知道vue内部有没有这样的 interface)
    /**
     * vue 的 props 的验证的类型约束
     */
    export interface IPropsValidation {
      /**
       * 属性的类型,比较灵活,可以是 String、Number 等,也可以是数组、class等
       */
      type: Array<any> | any,
      /**
       * 是否必须传递属性
       */
      required?: boolean,
      /**
       * 自定义类型校验函数(箭头函数),value:属性值
       */
      validator?: (value: any) => boolean,
      /**
       * 默认值,可以是值,也可以是函数(箭头函数)
       */
      default?: any
    }
    

    取 props 用的 interface

    IGridPropsComp 无法约束属性的具体类型,所以只好再做一个 interface。

    • IGridProps
    /**
     * 列表控件的属性的类型,基于el-table
     */
     export interface IGridProps {
      /**
       * 模块ID,number | string
       */
      moduleId: number | string,
      /**
       * 主键字段的名称 String,对应 row-key
       */
      idName: String,
      /**
       * table的高度, Number
       */
      height: number,
      /**
       * 列(字段)显示的顺序 Array<number|string>
       */
      colOrder: Array<number|string>,
      /**
       * 斑马纹,Boolean
       */
      stripe: boolean,
      /**
       * 纵向边框,Boolean
       */
      border: boolean,
      /**
       * 列的宽度是否自撑开,Boolean
       */
      fit: boolean,
      /**
       * 要高亮当前行,Boolean
       */
      highlightCurrentRow: boolean,
      /**
       * 锁定的列数 Number,设置到 el-table-column 的 fixed
       */
      fixedIndex: number,
      /**
       * table的列的 Object< IGridItem >
       * * id: number | string,
       * * colName: string, 字段名称
       * * label: string, 列的标签、标题
       * *  number, 列的宽度
       * * align: EAlign, 内容对齐方式
       * * headerAlign: EAlign 列标题对齐方式
       */
      itemMeta: {
        [key:string | number]: IGridItem
      }, // 
      /**
       * 选择行的情况:IGridSelection
       * * dataId: '', 单选ID number 、string
       * * row: {}, 单选的数据对象 {}
       * * dataIds: [], 多选ID []
       * * rows: [] 多选的数据对象 []
       */
      selection: IGridSelection, 
        
      /**
       * 绑定的数据 Array, 对应 data
       */
      dataList: Array<any>
    
      // 其他扩展属性
      [propName: string]: any
    }
    

    对比一下就会发现,属性的类型不一样。因为定义 props 需要使用一套特定的对象格式,而使用 props 的时候需要的是属性自己的类型。

    理想情况下,应该可以在 script setup 里面,引入外部文件 定义的 interface ,然后设置给组件的 props,但是到目前为止还不支持,只能在( script setup方式的)组件内部定义 props。希望早日支持,支持了就不会这么纠结和痛苦了。

    依据 el-table-column 定义列属性的 interface。

    • IGridItem:列表里面列的属性
    /**
     * 列的属性,基于 el-table-column
     */
    export interface IGridItem {
      /**
       * 字段ID、列ID
       */
      id: number | string,
      /**
       * 字段名称
       */
      colName: string,
      /**
       * 列的标签、标题
       */
      label: string,
      /**
       * 列的宽度
       */
       number,
      /**
       * 内容对齐方式 EAlign
       */
      align: EAlign,
      /**
       * 列标题对齐方式
       */
      headerAlign: EAlign,
    
      // 其他扩展属性
      [propName: string]: any
    }
    

    还是需要扩展属性的,因为这里只是列出来目前需要的属性,el-table-column 的其他属性、方法还有很多,而且以后也可能会新增。

    这个属性不是直接设置给组件的 props,所以不用定义两套了。

    对齐方式的枚举

    枚举可以理解为常量,定义之后可以避免低级错误,避免手滑。

    • EAlign
    export const enum EAlign {
      left = 'left',
      center = 'center',
      right = 'right'
    }
    

    选择记录的 interface。

    列表可以单选也可以多选,el-table 在默认情况下似乎是二选一,觉得有点不方便,为啥不能都要?

    • 单选:鼠标单一任意一行就是单选;(清空其他已选项)
    • 多选:单击第一列的(多个)复选框,就是多选;

    这样用户就可以愉快的想单选就单选,想多选就多选了。

    • IGridSelection
    /**
     * 列表里选择的数据
     */
    export interface IGridSelection {
      /**
       * 单选ID number 、string
       */
      dataId: number | string,
      /**
       * 单选的数据对象 {}
       */
      row: any,
      /**
       * 多选ID []
       */
      dataIds: Array<number | string>,
      /**
       * 多选的数据对象 []
       */
      rows: Array<any>
    }
    

    其实我觉得只记录ID即可,不过既然 el-talble 提供的 row,那么还是都记录下来吧。

    定义 json 文件

    接口定义好之后,我们可以依据 interface 编写 json 文件:

    {
      "moduleId": 142,
      "height": 400,
      "idName": "ID",
      "colOrder": [
        90,  100, 101 
      ],
      "stripe": true,
      "border": true,
      "fit": true,
      "highlightCurrentRow": true,
      "highlight-current-row": true,
      "itemMeta": {
        "90": {
          "id": 90,
          "colName": "kind",
          "label": "分类",
          "width": 140,
          "title": "分类",
          "align": "center",
          "header-align": "center"
        },
        "100": {
          "id": 100,
          "colName": "area",
          "label": "多行文本",
          "width": 140,
          "title": "多行文本",
          "align": "center",
          "header-align": "center"
        },
        "101": {
          "id": 101,
          "colName": "text",
          "label": "文本",
          "width": 140,
          "title": "文本",
          "align": "center",
          "header-align": "center"
        } 
      }
    }
    
    
    • 为什么直接设置 json 文件而不是 js 对象呢?
      因为对象会比较长,如果是代码形式的话,那还不如直接使用UI库组件来的方便呢。

    • 你可能又会问了,既然直接用 json文件,为啥还要设计 interface 呢?
      当然是为了明确各种类型,interface 可以当做文档使用,另外封装UI库的组件的时候,也可以用到这些 interface。使用列表控件的时候也可以使用这些 interface。

    其实json文件不用手动编写,而是通过工具来编写和维护。

    定义列表控件的 props

    封装组件之前需要先定义一下组件需要的 props:

    • props-grid.ts
    import type { PropType } from 'vue'
    
    import type {
      IGridProps,
      IGridItem,
      IGridSelection
    } from '../types/50-grid'
    
    /**
     * 表单控件需要的属性propsForm
     */
    export const gridProps: IGridProps = {
      /**
       * 模块ID,number | string
       */
      moduleId: {
        type: Number,
        required: true
      },
      /**
       * 主键字段的名称
       */
      idName: {
        type: String,
        default: 'ID'
      },
      /**
       * 字段显示的顺序
       */
      colOrder: {
        type: Array as PropType<Array<number | string>>,
        default: () => []
      },
      /**
       * 锁定的列数
       */
      fixedIndex: {
        type: Number,
        default: 0
      },
      /**
       * table的列的 meta
       */
      itemMeta: {
        type: Object as PropType<{
          [key:string | number]: IGridItem
        }>
      },
      /**
       * 选择的情况 IGridSelection
       */
      selection: {
        type: Object as PropType<IGridSelection>,
        default: () => {
          return {
            dataId: '', // 单选ID number 、string
            row: {}, // 单选的数据对象 {}
            dataIds: [], // 多选ID []
            rows: [] // 多选的数据对象 []
          }
        }
      },
      /**
       * 绑定的数据
       */
      dataList: {
        type: Array as PropType<Array<any>>,
        default: () => []
      },
      其他略。。。
    }
    
    

    按照 Option API 的方式设置 props 的定义,这样便于共用属性的定义(好吧似乎也没有需要共用的地方,不过我还是喜欢把 props 的定义写在一个单独的文件里)。

    封装列表控件

    定义好 json 、props之后,我们基于 el-table 封装列表控件:

    • template 模板
      <el-table
        ref="gridRef"
        v-bind="$attrs"
        :data="dataList"
        :height="height"
        :stripe="stripe"
        :border="border"
        :fit="fit"
        :highlight-current-row="highlightCurrentRow"
        :row-key="idName"
        @selection-change="selectionChange"
        @current-change="currentChange"
      >
        <!--多选框,实现多选功能-->
        <el-table-column
          type="selection"
          width="55"
          align="center"
          header-align="center"
          @click="clickCheck"
        >
        </el-table-column>
        <!--依据 json 渲染的字段列表-->
        <el-table-column
          v-for="(id, index) in colOrder"
          :key="'grid_list_' + index + '_' + id"
          v-bind="itemMeta[id]"
          :column-key="'col_' + id"
          :fixed="index < fixedIndex"
          :prop="itemMeta[id].colName"
        >
        </el-table-column>
      </el-table>
    

    设置 type="selection"列,实现多选的功能。
    使用 v-for 的方式,遍历出动态列。
    设置 :fixed="index < fixedIndex",实现锁定左面列的功能。

    • js 代码
      import { defineComponent, ref } from 'vue'
      // 列表控件的属性 
      import { gridProps } from '../map'
    
      /**
       * 普通列表控件
       */
      export default defineComponent({
        name: 'nf-elp-grid-list',
        inheritAttrs: false,
        props: {
          ...gridProps // 解构共用属性
        },
        setup (props, ctx) {
          // 获取 el-table 
          const gridRef =  ref(null)
     
          return {
            gridRef
          }
        }
      })
    

    把 props 的定义放在单独的 ts文件 里面,组件内部的代码就可以简洁很多。

    实现内置功能

    可以按照自己的喜好,设置一些内部功能,比如单选/多选的功能,格式化的功能等。

    • 定义控制函数 controller.ts
    import type { ElTable } from 'element-plus'
    
    // 列表控件的属性 
    import type { IGridProps } from '../map'
    
    export interface IRow {
     [key: string | number]: any
    }
    
    /**
    * 列表的单选和多选的事件
    * @param props 列表组件的 props
    * @param gridRef el-table 的 $ref
    */
    export default function choiceManage<T extends IGridProps, V extends typeof ElTable>(props: T, gridRef: V) {
     // 是否单选触发
     let isCurrenting = false
     // 是否多选触发
     let isMoring = false
    
     // 单选
     const currentChange = (row: IRow) => {
       if (isMoring) return // 多选代码触发
       if (!row) return // 清空
    
       if (gridRef.value) {
         isCurrenting = true
         gridRef.value.clearSelection() // 清空多选
         gridRef.value.toggleRowSelection(row) // 设置复选框
         gridRef.value.setCurrentRow(row) // 设置单选
         // 记录
         props.selection.dataId = row[props.idName]
         props.selection.dataIds = [ row[props.idName] ]
         props.selection.row = row
         props.selection.rows = [ row ]
    
         isCurrenting = false
       }
     }
    
     // 多选
     const selectionChange = (rows: Array<IRow>) => {
       if (isCurrenting) return
       // 记录
       if (typeof props.selection.dataIds === 'undefined') {
         props.selection.dataIds = []
       }
       props.selection.dataIds.length = 0 // 清空
       // 设置多选
       rows.forEach((item: IRow) => {
         if (typeof item !== 'undefined' && item !== null) {
           props.selection.dataIds.push(item[props.idName])
         }
       })
       props.selection.rows = rows
       // 设置单选
       switch (rows.length) {
         case 0:
           // 清掉单选
           gridRef.value.setCurrentRow()
           props.selection.dataId = ''
           props.selection.row = {}
           break
         case 1:
           isMoring = true
           // 设置新单选
           gridRef.value.setCurrentRow(rows[0])
           isMoring = false
           props.selection.row = rows[0]
           props.selection.dataId = rows[0][props.idName]
           break
         default:
           // 去掉单选
           gridRef.value.setCurrentRow()
           props.selection.row = rows[rows.length - 1]
           props.selection.dataId = props.selection.row[props.idName]
       }
     }
    
     return {
       currentChange, // 单选
       selectionChange // 多选
     }
    }
    
    • 列表控件的 setup 里调用
    setup (props, ctx) {
      // 获取 el-table 
      const gridRef = ref<InstanceType<typeof ElTable>>()
    
      // 列表选项的事件
      const {
        currentChange, // 单选
        selectionChange // 多选
      } = choiceManage(props, gridRef)
    
      return {
        selectionChange, // 多选
        currentChange, // 单选
        gridRef // table 的 dom
      }
    }
    

    这里有一个“度”的问题:

    • el-table 完全通过 slot 的方式实现各种功能,这种方法的特点是:非常灵活,可以各种组合;缺点是比较繁琐。
      而我们需要寻找到一个适合的“折中点”,显然这个折中点很难统一,这也是过渡封装带来的问题。

    • 不能遇到新的需求,就增加内部功能,这样就陷入了《人月神话》里说的“焦油坑”,进去了就很难出来。

    这也是低代码被诟病的因素。

    支持扩展

    那么如何找到这个折中点呢?可以按照 “开闭原则”,按照不同的需求,设置多个不同功能的列表控件,使用 slot 实现扩展功能。或者干脆改为直接使用 el-table 的方式。(要灵活,不要一刀切)

    比如简单需求,不需要扩展功能的情况,设置一个基础列表控件:nf-grid。
    需要扩展列的情况,设置一个可以扩展的列表控件:nf-grid-slot。

    如果需要多表头、树形数据等需求,可以设置一个新的列表控件,不过需要先想想,是不是直接用 el-table 更方便。

    要不要新增一个控件,不要惯性思维,而要多方面全局考虑。

    这里介绍一下支持 slot 扩展的列表控件的封装方式:

      <el-table
        ref="gridDom"
        v-bind="$attrs"
        size="mini"
        style=" 100%"
        :data="dataList"
        :height="height"
        :stripe="stripe"
        :border="border"
        :fit="fit"
        :highlight-current-row="highlightCurrentRow"
        :current-row-key="idName"
        :row-key="idName"
        @selection-change="selectionChange"
        @current-change="currentChange"
      >
        <!--显示选择框-->
        <el-table-column
          type="selection"
          width="55">
        </el-table-column>
        <!--显示字段列表-->
        <template
          v-for="(id, index) in colOrder"
          :key="'grid_list_' + index + '_' + id"
        >
          <!--检查插槽里是否包含 字段名,作为判断依据-->
          <!--不带插槽的列-->
          <el-table-column
            v-if="!(slotsKey.includes(itemMeta[id].colName))"
            :fixed="index < fixedIndex"
            v-bind="itemMeta[id]"
            :prop="itemMeta[id].colName"
            :min-width="50"
          >
          </el-table-column>
          <!--带插槽的列-->
          <el-table-column  v-else
            v-bind="itemMeta[id]"
          >
            <template #default="scope">
              <!--读取外部插槽内容,并且传递 scope -->
              <slot :name="itemMeta[id].colName" v-bind="scope"></slot>
            </template>
          </el-table-column>
        </template>
      </el-table>
    

    模板部分,首先判断一下是否需要使用 slot,做一个分支。
    需要使用 slot 的列,通过 <template #default="scope"> 设置 slot。

    • 代码部分
      import { defineComponent, ref } from 'vue'
      // 表单控件的属性 
      import { gridProps } from '../map'
      import choiceManage from './controller'
    
      export default defineComponent({
        name: 'nf-elp-grid-slot',
        inheritAttrs: false,
        props: {
          ...gridProps
        },
        setup (props, ctx) {
          // 记录插槽 的 名称
          const slots = ctx.slots
          const slotsKey = Object.keys(slots)
     
          // 列表选项的事件
          const {
            currentChange, // 单选
            selectionChange // 多选
          } = choiceManage(props, gridRef)
    
          return {
            slotsKey,
            selectionChange, // 多选
            currentChange // 单选
          }
        }
      })
    

    一般列表的使用方法

    封装之后,使用起来就很方便了,引入 json文件,设置属性即可。

    • template
      <nf-grid
        v-bind="gridMeta"
        :dataList="dataList"
        :selection="selection"
        size="small"
      />
    

    是不是简单多了。

    • 代码部分
      import { defineComponent, reactive } from 'vue'
      import { nfGrid, createDataList } from '../../../../lib-elp/main'
      import _gridMeta from '../../grid/grid.json'
      import _formMeta from '../../form/form.json'
     
      export default defineComponent({
        name: 'nf-elp-grid-page',
        components: {  nfGrid  },
        setup(props) {
          // 不需要动态改变的话,可以不使用 reactive。
          const gridMeta = reactive(_gridMeta)
          // 设置选择的行
          const selection = reactive({
            dataId: '', // 单选ID number 、string
            row: {}, // 单选的数据对象 {}
            dataIds: [], // 多选ID []
            rows: [] // 多选的数据对象 []
          })
          // 设置记录集。
          const dataList = reactive(_dataList)
    
          return {
            dataList,
            selection,
            gridMeta
          }
        }
      })
    

    控件可以做成全局组件的形式。

    • 看看效果

    一般列表.png

    扩展列表的使用方法

    首先还是依据 json 渲染列表,然后根据需要设置插槽即可,设置插槽后会替换默认的列。

    • template
      可以使用 slot 自定义扩展列 <br>
      <!--表单控件-->
      <nf-grid
        v-grid-drag="gridMeta"
        v-bind="gridMeta"
        :dataList="dataList"
        :selection="selection"
        size="small"
      >
        <!--普通字段,用字段名作为插槽的名称-->
        <template #text="scope">
          <div style="display: flex; align-items: center">
            <el-icon><timer /></el-icon>
            <span style="margin-left: 10px">扩展:{{ scope.row.text }}</span>
          </div>
        </template>
        <!--普通字段-->
        <template #week="scope">
          <span style="margin-left: 10px">{{ scope.row.week.replace('-w','年 第') + '周' }}</span>
        </template>
        <!--操作按钮-->
        <template #option="scope">
          <el-button size="small" @click="handleEdit(scope.$index, scope.row)">修改</el-button>
          <el-button
            size="small"
            type="danger"
            @click="handleDelete(scope.$index, scope.row)">删除</el-button>
        </template>
      </nf-grid>
    

    通过 slot 扩展列,可以按照 Table-column 的匿名插槽的方式进行设置。
    列的先后顺序还是由 colOrder 控制,和插槽的先后顺序无关。

    • 代码部分
      import { defineComponent, reactive } from 'vue'
      // 使用 图标
      import { Timer } from '@element-plus/icons-vue'
      import { nfGridSlot, createDataList } from '../../../../lib-elp/main'
      import _gridMeta from '../../grid/grid.json'
      import _formMeta from '../../form/form.json'
    
      import { EAlign } from '../../../../lib/types/enum'
      import type { IGridSelection, IGridItem } from '../../../../lib/types/50-grid'
     
      export default defineComponent({
        name: 'nf-elp-grid-slot-page',
        components: {
          Timer,
          nfGrid: nfGridSlot
        },
        props: {
          moduleID: { // 模块ID
            type: [Number, String],
            default: 1 
          }
        },
        setup(props) {
          const gridMeta = reactive(_gridMeta)
    
          // 设置列的先后顺序和是否显示
          gridMeta.colOrder = [90, 100, 101, 102, 105, 113, 115, 116, 120,121,150, 2000]
    
          // 设置一个操作按钮列
          const optionCol: IGridItem = {
            id: 2000,
            colName: "option",
            label: "操作",
             180,
            fixed: EAlign.right,
            align: EAlign.center, // 使用枚举
            headerAlign: EAlign.center
          }
    
          gridMeta.itemMeta['2000'] = optionCol // 设置操作列,也可以直接在json文件里设置。
          const dataList = reactive(_dataList)
    
          const handleEdit = (index: number, row: any) => {
            console.log(index, row)
          }
          const handleDelete = (index: number, row: any) => {
            console.log(index, row)
          }
    
          return {
            handleEdit,
            handleDelete,
            dataList,
            gridMeta
          }
        }
      })
    

    使用字段名称作为插槽的名称,可以把任意字段变成插槽的形式。

    如果要添加操作按钮这类的列,可以给 itemMeta 添加对应的列属性。

    • 看看效果:

    扩展列表.png

    管理 json

    其实,前面介绍的那些大家可能都会想到,也许早就实践过了,然后发现虽然看着挺好,但是其实没有解决根本问题!只是把 template 里的问题转移到 json 里面。

    虽然不需要设置模板,但是需要设置 json,还不是一样,有啥本质区别吗?

    其实不一样的,管理 json 的难度明显比管理模板要简单得多。

    比如我们可以做一个维护 json 的小工具:

    • 首先从数据库文档生成基础的 json(毛坯房);
    • 然后使用可视化+拖拽的方式设置格子细节(精装修)。

    这样就可以很方便的维护 json 了。具体实现方式,将在下一篇再介绍。

    源码

    https://gitee.com/naturefw-code/nf-rollup-ui-controller

    https://gitee.com/naturefw-code/nf-rollup-ui-element-plus

  • 相关阅读:
    muduo库源码剖析(一) reactor模式
    POSIX 线程编程(二)线程建立与终止
    visual assist常用快捷键
    Linux下 静态链接库 和 动态链接库
    linux(Ubuntu)下mysql字符集完美解决
    共享内存解读
    hdu2829
    hdu3525
    2013ACM-ICPC亚洲区南京站现场赛G题
    poj1487
  • 原文地址:https://www.cnblogs.com/jyk/p/16347748.html
Copyright © 2020-2023  润新知