likes
comments
collection
share

【低代码】为客户设计个性化方案:列表篇(客户自己调整排序对齐等)

作者站长头像
站长
· 阅读数 22

列表的个性化方案 for 用户

后台管理项目,需要很多列表,比如公司信息列表、员工信息列表等。一般情况列表做好之后,列的先后顺序以及隐藏等就固定不变了,如果客户想改,那么需要改代码,即使是低代码,一般也需要改JSON。

这样不够灵活嘛,应该可以为客户提供一种“个性化方案”,让客户自己在权限允许的情况下,设置列的(部分)属性,以满足客户灵活多变的需求!

前请提示:

定义各种接口

工欲善其事,必先利其器,我们先设计一下需要的接口

定义拖拽消息的接口

对于用户来说,最方便的当然是拖拽的方式来调整列表,所以我们先来定义一个记录拖拽信息的接口。

/**
 * 拖拽时记录相关信息
 */
export interface IDragInfo {
  /**
   * 拖拽移除的延迟
   */
  timeout: NodeJS.Timeout,
  /**
   * 状态
   */
  state: string,
  /**
   * 拖拽时X坐标
   */
  offsetX: number,
  /**
   * 是否“容器”左面释放鼠标
   */
  isLeft: boolean,
  /**
   * 是否按下 ctrl
   */
  ctrl: boolean,
  /**
   * 开始的“容器”ID
   */
  targetId: string,
  /**
   * 开始的“容器”标题,判断是否拖拽出去
   */
  targetLabel: string,
  /**
   * 开始的 target
   */
  target: any,
  /**
   * 结束的“容器”ID
   */
  sourceId: string,
  /**
   * 开始的序号
   */
  targetIndex: number,
  /**
   * 结束的序号
   */
  sourceIndex: number
}

拖拽的时候记录相关的消息,拖拽结束后,作为相关的操作数据依据。

分析 json 结构

低代码的列表是依赖 JSON 渲染的,所以我们先来看看 JSON 结构:

{
  "gridMeta": { 
    "moduleId": 142,
    "idName": "ID",
    "colOrder": [ 【列的显示依据】
      90,  101, 102, 105,
      110, 111, 114, 112, 113, 115, 116,
      120, 121, 100, 
      150, 151, 152, 153,
      160, 161, 162, 163, 164
    ]
  },
  "height": 400,
  "stripe": 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"
    }
    【其他列】
  }
}

观察JSON结构我们可以找到几个关键元素:

  • colOrder: 列表的列是遍历(v-for)colOrder 渲染出来的,如果想改顺序和隐藏列的话,维护好 colOrder 即可。
  • width:itemMeta 的 width 存放的是列的宽度,想要调整宽度的话,需要修改这个属性。
  • align:itemMeta 的 align 存放的是列的内容的对齐方式。
  • header-align:itemMeta 的 header-align 存放的是列的标题的对齐方式。

定义个性化方案的存储结构

我们依据上面的属性特点,做一个结构来保存变化后的数据:

/**
 * 个性化方案
 */
type ICaseList = {
  caseId: string | number, // 方案编号
  moduleId: string | number, // 模块ID
  label: string, // 方案名称
  meta: ICase // 个性化方案 的 meta
}

/**
 * 个性化方案 的 meta
 */
type ICase = {
  gridMeta: {
    colOrder: Array<number> // 排序用
  },
  itemMeta: {
    [index: string | number]: {
      'header-align': string, // 标题的对齐
      'align': string, // 内容的对齐
      width: number | string // 宽度
    }
  }
}

实现具体功能

准备工作完毕,开始编码。

自定义指令

那么如何实现拖拽呢?我们可以使用 Vue 的自定义指令实现。

const _gridDrag = {
  // 指令的定义
  mounted (el, binding) {
    const className = 'el-table__header'
    // 控件的meta
    const meta = binding.value
    // table 并不会被立即渲染出来
    nextTick(() => {
      setTimeout(() => {
        // 根据 class 找到 table
        const table = el.getElementsByClassName(className)[0]
        // 获取 meta
        const { gridMeta, itemMeta, modCol } = meta
        // 调用拖拽功能
        gridDrag(gridMeta, itemMeta, table, deleteDom, modCol).girdSetup()
      }, 600)
    })
    
  }
}

分析 el-table 渲染出来的 table,我们可以发现 class="el-table__header",这样就找到 table,然后用 gridDrag 实现拖拽即可。

【低代码】为客户设计个性化方案:列表篇(客户自己调整排序对齐等)

自定义指令注册之后,我们就可以在列表控件上面使用了。

  <nf-grid
    v-grid-drag="gridMeta" <!--实现拖拽的自定义指令-->
    v-bind="gridMeta"
    :dataList="dataList"
    :selection="selection"
    size="small"
  />

实现设置列的顺序的功能

拖拽结束后,依据拖拽信息修改 colOrder 。

  /**
   * 交换两个th的位置
   */
  const _swapPlaces = () => {
    // 交换
    colOrder[dragInfo.sourceIndex] = dragInfo.targetId
    colOrder[dragInfo.targetIndex] = dragInfo.sourceId
  }

  /**
   * 拖拽 th 后调整顺序
   */
  const _order = () => {
    // 判断前插、后插。后插:偏移 0;前插:偏移 1
    const offsetTarget = dragInfo.isLeft ? 0 : 1
    // 判断前后顺序。
    const offsetSource = dragInfo.sourceIndex < dragInfo.targetIndex ? 0 : 1

    // 插入源
    colOrder.splice(dragInfo.targetIndex + offsetTarget, 0, dragInfo.sourceId)
    // 删除源
    colOrder.splice(dragInfo.sourceIndex + offsetSource, 1)
  }
  • 动画演示

【低代码】为客户设计个性化方案:列表篇(客户自己调整排序对齐等)

实现设置列的隐藏的功能

隐藏也比较方便,只需要删除 colOrder 里对应的元素即可。我们可以定义一个“手势指令”,向上拖拽移出到 table 外面就表示隐藏这个列。

  /**
   * 移除选择的字段
   * @param dragInfo 拖拽信息
   */
  const _setRemove = (dragInfo: IDragInfo) => {
    const col = itemMeta[dragInfo.sourceId]
    // 调用外部的确认对话框
    deleteDom(col, () => {
      // 确认移除,才会执行回调
      gridMeta.colOrder.splice(dragInfo.sourceIndex, 1)
    }) 
  }
  • 动画演示

【低代码】为客户设计个性化方案:列表篇(客户自己调整排序对齐等)

实现设置设置列的对齐方式的功能

对齐方式分为标题的对齐和内容的对齐。那么如何让客户方便操作呢,我们还是可以定义一个“手势指令”,向左拖动就是左对齐,向右拖动就是右对齐,是不是很自然?

那么如何区分是对齐标题还是内容呢?只好加上 ctrl 作为区分了。

标题一般都是居中的不需要改,所以按住 ctrl 不放,表示对齐标题,直接拖拽是对齐内容。

  /**
   * 设置th、td的对齐方式
   */
  const _setThAlgin = (dragInfo: IDragInfo) => {
    // 判断 th 还是 td
    const alignKind = (dragInfo.ctrl) ? 'header-align' : 'align'
    // 获取列的 meta 
    const col = itemMeta[dragInfo.sourceId]
    // 判断当前对齐方式:左、中、右
    switch (col[alignKind]) {
      case 'left': // -> 变成居中
        col[alignKind] = (dragInfo.isLeft) ? 'left' : 'center'
        break
      case 'center': // -> 变成右对齐; <- 变成左对齐
        col[alignKind] = (dragInfo.isLeft) ? 'left' : 'right'
        break
      case 'right': // <- 变成居中
        col[alignKind] = (dragInfo.isLeft) ? 'center' : 'right'
        break
    }
  }
  • 动画演示

【低代码】为客户设计个性化方案:列表篇(客户自己调整排序对齐等)

实现设置设置列的宽度的功能

el-table 自带调整列宽的功能,所以我们只需要记录下来调整后的 td 的宽度即可,那么什么时候调整完毕呢,是不是需要用户自己按个按钮?

当然不需要,要求用户做的操作,能少一下就少一下。

那么怎么做呢?我们可以监听事件。拖拽结束会触发什么事件呢?对,mouseup!我们监听一下就可以获知用户是否调整了宽度。

  table.removeEventListener("mouseup", _setThWidth)
  table.addEventListener("mouseup", _setThWidth)
    
  // 调整 th 的宽度后,记录新的宽度
  const _setThWidth = (e) => {
    // 等待刷新
    setTimeout(() => {
      // 监听事件,获取调整后的 th 的宽度
      const arr = Array.from(table.rows[0].cells)
      // 遍历 table 的第一行(标题)的 th
      arr.forEach((element, index) => {
        if (index === 0) { // 跳过第一列 (check)
        } else {
          itemMeta[gridMeta.colOrder[index - 1]].width = element.offsetWidth
        }
      })
    }, 600)
  }

话说,el-table 提供了调整宽度的功能,然后有没有考虑过刷新的问题?或者,是我没有认真看文档漏掉了什么。

  • 动画演示

【低代码】为客户设计个性化方案:列表篇(客户自己调整排序对齐等)

保存、加载和共享

用户设置好个性化方案之后,还需要保存一下,否则一刷新就没了,容易被打。

那么保存到哪里呢?有多个容器可以选择:

  • localStorage —— 特点:同步操作,非常方便;缺点:容量有限。
  • indexedDB —— 特点:不用担心容量问题;缺点:异步操作有点麻烦。
  • 云端 —— 特点:支持多设备同步;缺点:需要后端配合,提供存放用户的个性化方案的功能。

综合考虑,前端采用 indexedDB 保存个性化方案。

  • 动画演示

【低代码】为客户设计个性化方案:列表篇(客户自己调整排序对齐等)

定义 indexedDB 的结构

// 创建help
import { dbCreateHelp } from '@naturefw/storage'

// 设置数据库名称和版本
const db = {
  dbName: 'nf-customer-setting',
  ver: 1
}

/**
 * 用户的个性化方案
 */
export default async function createDBHelp (callback) {
  const help = dbCreateHelp({
    dbFlag: 'nf-customer-setting',
    dbConfig: db,
    stores: {
      /**
       * * caseId:'方案编号',
       * * moduleId: '模块ID',
       * * label: '方案名称',
       * * meta: { // meta内容
       * * * header-align: '标题对齐方式'
       * * * align: '内容对齐方式'
       * * * width: '宽度'
       * * }
       */
      cus_grid: { // 列表的个性化方案
        id: 'caseId',
        index: {
          moduleId: false
        },
        isClear: false
      }
    },
    // 设置初始数据
    async init (help) {
    }
  })
  return help
}

【低代码】为客户设计个性化方案:列表篇(客户自己调整排序对齐等)

使用方法

  • 引入列表控件
  • 加载 meta
  • 加载个性化方案
  • 设置指令
  • 监听 meta 的变化,保存
  维护 json 的工具
  <el-button type="" @click="myclear">清空数据</el-button>
  <el-button type="" @click="reset"> 重置</el-button>
  <br>
  <!--个性化方案-->
  <cus-grid :meta="gridMeta" kind="grid"></cus-grid>
  <br>
  <hr>
  <!--列表控件-->
  <nf-grid
    v-grid-drag="gridMeta"
    v-bind="gridMeta"
    :dataList="dataList"
    :selection="selection"
  >
  </nf-grid>
  <hr>
  <!--维护工具-->
  <help-grid-card :gridMeta="gridMeta"></help-grid-card>
  import { defineComponent, reactive } from 'vue'
  import { nfGrid, nfGridSlot, createDataList, _gridDrag } from '@naturefw/ui-elp'
  import _gridMeta from './grid.json'
  import _formMeta from '../form/form.json'

  import cusGrid from './cus-grid.vue'

  // help
  import { helpGridCard } from '../../../lib/main'

  export default defineComponent({
    name: 'nf-helps-grid-card',
    directives: {
      gridDrag: _gridDrag
    },
    components: {
      nfGrid: nfGrid,
      helpGridCard,
      cusGrid
    },
    props: {
      moduleID: { // 模块ID
        type: [Number, String],
        default: 1 
      }
    },
    setup(props) {
      const gridMeta = reactive(_gridMeta)
    
      // 设置抽屉
      const drawerInfo = reactive({
        isShow: false
      })
    
      const selection = reactive({
        dataId: '', // 单选ID number 、string
        row: {}, // 单选的数据对象 {}
        dataIds: [], // 多选ID []
        rows: [] // 多选的数据对象 []
      })

      // 根据 meta 创建表单的 model
      const _dataList = createDataList(_formMeta.itemMeta, 10).reverse()

      const dataList = reactive<Array<any>>(_dataList)
      
      // 清空数据,演示 “没有数据”
      const myclear = () => {
        dataList.length = 0
      }
      
      // 重新设置数据
      const reset = () => {
        dataList.length = 0
        dataList.push(...createDataList(_formMeta.itemMeta, 30).reverse())
      }

      return {
        // 重置
        reset,
        myclear,
        // 数据
        dataList,
        selection,
        gridMeta
      }
    }
  })

源码