【低代码】为客户设计个性化方案:列表篇(客户自己调整排序对齐等)
列表的个性化方案 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
}
}
})
源码
转载自:https://juejin.cn/post/7119102542939684877