el-table树形表格实现 父子联动勾选,子部分勾选时,父处于半勾选的状态
需求背景
el-table,支持树形表格,但是多选框并没有自动支持父子联动勾选;
- 勾选全选,只有最外层的行被勾选;
- 勾选父,子级不会自动勾选;
- 勾选部分子,父级不会自动处于半勾选状态;
- 依次勾选全部的子级,父级不会自动勾选;
具体要求:
- 勾选全选按钮,表格所有层级 均被勾选,取消勾选全选按钮,表格所有层级 均被取消勾选;
- 父级勾选,子级(以及更深层级的子级)全部勾选,父级取消勾选,子级(以及更深层级的子级)全部取消勾选;
- 子级部分勾选 或 处于 半勾选的状态,则父级处于半勾选的状态
- 依次勾选全部的子级,父级会自动勾选;
开发分析
1、半勾选状态的实现
当前只有标题头的勾选框有半勾选状态,行上没有,需要手动实现
首先是半勾选的样式,需要从标题头勾选框的半勾选状态的样式复制;当父级处于半勾选状态时,去使用这个类的样式;当行的某一数据标识为true时,勾选框的列 的类名添加indeterminate,否则勾选框的列 的类名为空;
可以使用cell-class-name属性,进行实现< el-table :row-key="valueKey">< /el-table>
js
tableCellClassName ({row, column}) {
let cellClassName = ''
if (row.indeterminate && column.type === 'selection') {
cellClassName = 'indeterminate'
}
return cellClassName
}
css
<style lang="scss" scoped>
::v-deep .lov_table {
.indeterminate {
.el-checkbox__input {
.el-checkbox__inner {
background-color: #5f4efd;
border-color: #5f4efd;
&::before {
content: '';
position: absolute;
display: block;
background-color: #fff;
height: 2px;
-webkit-transform: scale(.5);
transform: scale(.5);
left: 0;
right: 0;
top: 5px;
}
}
}
}
}
</style>
2、编辑初始化时,要给保存前的数据自动勾选上
获取到数据后,使用modalTableRef.toggleRowSelection(row, true) 进行勾选
同时给树形结构的数据初始化setTreeMetaData,使得每一个子节点都有parentId,方便后续的联动操作
js
async getList() {
this.loading = true
try {
const res = await this.lovRemote.request({
...this.remoteParams,
...this.queryForm,
pageIndex: this.pageIndex,
pageSize: this.pageSize
})
if (res?.data) {
const remoteResult: any = this.$attrs.remoteResult
const result = remoteResult ? remoteResult(res) : res.data.list || res.data
setTreeMetaData(result, this.valueKey, 'children')
this.list = result
this.totalRows = res.data.total
this.$nextTick(() => {
this.initSelection()
})
}
} catch (err) {
this.$message.error(err.message)
} finally {
this.loading = false
}
}
initSelection() {
const modalTableRef: any = this.$refs.modalTableRef
modalTableRef.clearSelection(); //* 打扫干净屋子再请客
if (this.cachedSelection.length) {
const checkSelect = (data) => {
data.forEach((item) => {
this.cachedSelection.forEach((sel) => {
if (item[this.valueKey] === sel[this.valueKey]) {
if (this.multiple) {
modalTableRef.toggleRowSelection(item, true)
resetTableTreeSelection({record: item, userSelection: this.cachedSelection, list: this.list, tableRef: modalTableRef, toggleSub: false})
} else {
modalTableRef.setCurrentRow(item)
}
}
})
if (getType(item.children) === 'Array' && item.children.length) {
checkSelect(item.children)
}
})
}
// 递归选中勾选缓存数据
checkSelect(this.list)
}
}
3、父子联动勾选
使用selection-change事件(当选择项发生变化时会触发该事件)监听的话,只有一个selection参数,并不知道当前勾选的是哪一行,于是使用select(当用户手动勾选数据行的 Checkbox 时触发的事件)和select-all(当用户手动勾选全选 Checkbox 时触发的事件)
js
/** 勾选全选操作(多选模式) */
selectAllOnClick() {
const modalTableRef: any = this.$refs.modalTableRef
const isAllSelected = modalTableRef?.store.states.isAllSelected; //? 是否全部勾选
if (this.needRelativeSelect) {
this.list.forEach(record => {
toggleSubAll({record, toggleRowSelection: modalTableRef.toggleRowSelection, selected: isAllSelected})
})
const finalSelection = modalTableRef?.store.states.selection;
this.cachedSelection = finalSelection;
return
}
}
/** 勾选单条操作(多选模式) */
selectOnClick(selection, row) {
const modalTableRef: any = this.$refs.modalTableRef
if (this.needRelativeSelect) {
resetTableTreeSelection({record: row, userSelection: selection, list: this.list, tableRef: modalTableRef})
const finalSelection = modalTableRef?.store.states.selection;
this.cachedSelection = finalSelection;
return
}
}
util.js
js
/**
* todo 对树形结构的数据进行加工,直接子节设置parentValue
* @param data 树形结构的数据
* @param treeKey 树形结构的id字段
* @param childrenKey 树形结构子级的字段
* @param parentNode 上级节点
*/
const setTreeMetaData = (data, treeKey: string = 'id', childrenKey: string = 'children' ,parentNode?: any) => {
data.forEach(item => {
if (parentNode) item.parentValue = parentNode[treeKey];
if (Array.isArray(item[childrenKey]) && item[childrenKey].length) {
item[childrenKey].forEach(child => {
child.parentValue = item[treeKey];
if (Array.isArray(child[childrenKey]) && child[childrenKey].length) {
setTreeMetaData(child[childrenKey], treeKey, childrenKey, child)
}
});
}
})
}
/**
* todo 获取展开后的树形表格数据 深层 变成 一层
* @param treeData 树形结构的数据
* @param childrenKey 树形结构子级的字段
* @returns 展开后的树形表格数据
*/
const getExpandedTreeData = (treeData, childrenKey: string = 'children') => {
return treeData.reduce((accumulator, curItem) => {
if (Array.isArray(curItem[childrenKey]) && curItem[childrenKey].length) {
return accumulator.concat(curItem).concat(getExpandedTreeData(curItem[childrenKey]))
}
return accumulator.concat(curItem)
}, [])
}
/**
* todo 将当前行下的所有子级切换勾选状态
* @param record 单前行
* @param toggleRowSelection 切换行的勾选状态的内置方法
* @param childrenKey 树形结构子级的字段
* @param selected 统一设置的 勾选的状态
*/
const toggleSubAll = ({record, toggleRowSelection, childrenKey = 'children', selected = true}) => {
if (Array.isArray(record[childrenKey]) && record[childrenKey].length) { //* 有子级
record[childrenKey].forEach(subRecord => {
toggleRowSelection(subRecord, selected); //* 调用el-table内置方法,进行勾选
if (Array.isArray(subRecord[childrenKey]) && subRecord[childrenKey].length) { //* 子级还有下级
toggleSubAll({record: subRecord, toggleRowSelection, childrenKey, selected})
}
})
}
}
/**
* todo 设置树形表格父级的勾选状态
* @param parentValue 父级的id
* @param expandedList 树形表格展开后的数据
* @param userSelection 用户勾选的所有数据
* @param tableRef 表格的ref
* @param treeKey 树形结构的id字段
*/
const setTableTreeParentSelection = ({parentValue, expandedList, userSelection, tableRef, treeKey = 'id'}) => {
const toggleRowSelection = tableRef.toggleRowSelection;
const subList = expandedList.filter(item => item.parentValue === parentValue);
const parentRecord = expandedList.find(item => item[treeKey] === parentValue)
const selectedList = subList.filter(subRecord => userSelection.some(selectedRecord => selectedRecord[treeKey] === subRecord[treeKey]))
const halfSelectedList = subList.filter(subRecord => subRecord.indeterminate === 'Y')
parentRecord.indeterminate = undefined;
if (subList.length === selectedList.length) { //* 所有子级全部勾选,父级勾选
toggleRowSelection(parentRecord, true)
} else if (!selectedList.length && !halfSelectedList.length) { //* 所有子级 全部没有勾选也没有半勾选
toggleRowSelection(parentRecord, false)
} else {
//* 子级部分勾选,
toggleRowSelection(parentRecord, false)
parentRecord.indeterminate = 'Y'
}
const currentSelection = tableRef?.store.states.selection;
if (parentRecord.parentValue) setTableTreeParentSelection({parentValue: parentRecord.parentValue, expandedList, userSelection: currentSelection, tableRef})
}
/**
* todo 重置树形表格的勾选状态
* @param record 用户勾选的单前行
* @param userSelection 用户勾选的所有数据
* @param list 当前页面的所有数据(树形结构)
* @param childrenKey 树形结构子级的字段
* @param tableRef 表格的ref
*/
const resetTableTreeSelection = ({record, userSelection, list, tableRef, treeKey = 'id', childrenKey = 'children', toggleSub = true}) => {
const toggleRowSelection = tableRef.toggleRowSelection; //? 切换行的勾选状态的内置方法
const isSelected = userSelection.some(item => item[treeKey] === record[treeKey]); //* 当前项被勾选
record.indeterminate = undefined;
toggleSub && toggleSubAll({record, toggleRowSelection, childrenKey, selected: isSelected})
const expandedTreeData = getExpandedTreeData(list, childrenKey);
if (record.parentValue) setTableTreeParentSelection({parentValue: record.parentValue, expandedList: expandedTreeData, userSelection, tableRef, treeKey})
}
设计缺点
不支持表格勾选状态还没有缓存就再次查询的场景,比如前端手动过滤,点击【查询】按钮进行后端精确查询,翻页等,也就是说,勾选状态没有缓存
这个设计方案只是为了简单的场景,只有新增-编辑-查询,没有查询,分页等复杂的场景;
有查询分页的,请看二次设计的方案 el-table树形表格实现 父子联动勾选 并查询时进行勾选缓存
完整代码
js
<template>
<el-dialog
class="lov_modal"
:width="`${width || '60vw'}`"
:title="title"
:visible="visible"
@close="closeModal"
destroy-on-close
append-to-body
:close-on-click-modal="false"
>
<base-query-form
:defaultQuery="remoteParams"
:fieldProps="queryItems"
:loading="loading"
@query="getQuery"
/>
<el-table
border
:height="500"
ref="modalTableRef"
:data="list"
class="lov_table"
v-loading="loading"
:row-key="valueKey"
highlight-current-row
:cell-class-name="tableCellClassName"
@current-change="currentChange"
@select="selectOnClick"
@select-all="selectAllOnClick"
@row-dblclick="rowOnDoubleClick"
>
<el-table-column
v-if="multiple"
type="selection"
width="55"
reserve-selection
align="center"
/>
<el-table-column
:label="colLabel(column)"
:prop="column.descKey || column.key"
show-overflow-tooltip
v-for="column in tableColumns"
:key="column.key"
:formatter="column.format"
v-bind="column"
>
</el-table-column>
</el-table>
<el-pagination
v-if="!$attrs.remoteResult"
class="table_pagination"
:disabled="loading"
@size-change="listSizeOnChange"
@current-change="listCurrentOnChange"
:current-page="pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="totalRows"
>
</el-pagination>
<span slot="footer" class="dialog-footer">
<el-button @click="closeModal">取 消</el-button>
<el-button type="primary" @click="confirmOnClick">确 定</el-button>
</span>
</el-dialog>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
import BaseQueryForm from '@/components/BaseQueryForm/index.vue'
import { ColumnProp } from '@/utils/interface'
import * as LOV_CONFIG from './lovModalConfig'
import { getType, isNil } from '@/utils/util'
import { resetTableTreeSelection, setTreeMetaData, toggleSubAll } from '../BaseSearchTable/utils'
@Component({
components: {
BaseQueryForm
},
name: 'lovModal'
})
export default class extends Vue {
@Prop({ required: true }) lovCode: string
@Prop() remoteParams: any
@Prop() valueKey: string
@Prop() cached: any[]
@Prop() width: string
@Prop({ default: '弹窗' }) title: string
@Prop({ default: false }) multiple: boolean; //? 是否开启多选
/** 树形数据全部展开标识 */
@Prop({ default: false }) expandAllFlag: boolean
@Prop({ default: false }) needRelativeSelect: boolean; //? 树形表格,是否需要联动勾选
list = []
// selection = []
cachedSelection = []
currentRecord = null
totalRows: number = 0
loading: boolean = false
queryForm: any = {}
pageIndex = 1
pageSize = 10
visible = false
get lovRemote() {
return LOV_CONFIG[this.lovCode].lovRemote
}
get lovModalColumns(): ColumnProp[] {
return LOV_CONFIG[this.lovCode].columns
}
get colLabel() {
return (col) => {
return col.i18nKey ? this.$t(`table.${col.i18nKey}`) : col.name
}
}
get tableColumns() {
return this.lovModalColumns.filter((item) => item.showInTable)
}
get queryItems() {
return this.lovModalColumns.filter((item) => item.showInQuery)
}
getQuery(params) {
this.queryForm = params
this.initList()
}
initList() {
this.pageIndex = 1
this.totalRows = 0
this.list = []
this.getList()
}
initSelection() {
const modalTableRef: any = this.$refs.modalTableRef
modalTableRef.clearSelection(); //* 打扫干净屋子再请客
if (this.cachedSelection.length) {
const checkSelect = (data) => {
data.forEach((item) => {
this.cachedSelection.forEach((sel) => {
if (item[this.valueKey] === sel[this.valueKey]) {
if (this.multiple) {
modalTableRef.toggleRowSelection(item, true)
resetTableTreeSelection({record: item, userSelection: this.cachedSelection, list: this.list, tableRef: modalTableRef, toggleSub: false})
} else {
modalTableRef.setCurrentRow(item)
}
}
})
if (getType(item.children) === 'Array' && item.children.length) {
checkSelect(item.children)
}
})
}
// 递归选中勾选缓存数据
checkSelect(this.list)
}
}
async getList() {
this.loading = true
try {
const res = await this.lovRemote.request({
...this.remoteParams,
...this.queryForm,
pageIndex: this.pageIndex,
pageSize: this.pageSize
})
if (res?.data) {
const remoteResult: any = this.$attrs.remoteResult
const result = remoteResult ? remoteResult(res) : res.data.list || res.data
setTreeMetaData(result, this.valueKey, 'children')
this.list = result
this.totalRows = res.data.total
this.$nextTick(() => {
this.initSelection()
})
}
} catch (err) {
this.$message.error(err.message)
} finally {
this.loading = false
}
}
listSizeOnChange(val) {
this.pageIndex = 1
this.pageSize = val
this.$nextTick(() => {
this.initList()
})
}
listCurrentOnChange(val) {
this.pageIndex = val
this.$nextTick(() => {
this.getList()
})
}
toggleRowExpanAll(isExpan) {
this.toggleRowExpan(this.list, isExpan)
}
toggleRowExpan(data, isExpan) {
const tree: any = this.$refs.modalTableRef
data.forEach((item) => {
tree.toggleRowExpansion(item, isExpan)
if (!isNil(item.children) && item.children.length) {
this.toggleRowExpan(item.children, isExpan)
}
})
}
async showModal() {
this.visible = true
this.cachedSelection = JSON.parse(JSON.stringify(this.cached))
this.queryForm = { ...this.queryForm, ...this.remoteParams }
await this.getList()
this.expandAllFlag && this.toggleRowExpanAll(this.expandAllFlag)
}
closeModal() {
this.pageIndex = 1
this.pageSize = 10
this.totalRows = 0
this.visible = false
this.cachedSelection = []
this.queryForm = {}
this.currentRecord = null
}
/** 点击单行操作 */
currentChange(val) {
this.currentRecord = val
}
tableCellClassName ({row, column}) {
let cellClassName = ''
if (row.indeterminate && column.type === 'selection') {
cellClassName = 'indeterminate'
}
return cellClassName
}
/** 勾选全选操作(多选模式) */
selectAllOnClick() {
const modalTableRef: any = this.$refs.modalTableRef
const isAllSelected = modalTableRef?.store.states.isAllSelected; //? 是否全部勾选
if (this.needRelativeSelect) {
this.list.forEach(record => {
toggleSubAll({record, toggleRowSelection: modalTableRef.toggleRowSelection, selected: isAllSelected})
})
const finalSelection = modalTableRef?.store.states.selection;
this.cachedSelection = finalSelection;
return
}
}
/** 勾选单条操作(多选模式) */
selectOnClick(selection, row) {
const modalTableRef: any = this.$refs.modalTableRef
if (this.needRelativeSelect) {
resetTableTreeSelection({record: row, userSelection: selection, list: this.list, tableRef: modalTableRef})
const finalSelection = modalTableRef?.store.states.selection;
this.cachedSelection = finalSelection;
return
}
}
/** 双击行 */
rowOnDoubleClick(row) {
this.currentChange(row)
if (!this.multiple) { //* 单选时,双击行时执行【确认】操作
this.$nextTick(() => {
this.confirmOnClick()
})
}
}
/** 点击确认按钮 */
confirmOnClick() {
if (this.multiple) {
if(this.cachedSelection.length == 0){
let message=(this.$t('documentation.pleaseSelect') as any)+this.title
this.$message({
type:'warning',
message
})
}
this.$emit('onOk', this.cachedSelection)
} else {
if(!this.currentRecord) {
let message=(this.$t('documentation.pleaseSelect') as any)+this.title
this.$message({
type:'error',
message
})
return
}
this.$emit('onOk', this.currentRecord)
}
}
}
</script>
<style lang="scss" scoped>
::v-deep .lov_table {
.indeterminate {
.el-checkbox__input {
.el-checkbox__inner {
background-color: #5f4efd;
border-color: #5f4efd;
&::before {
content: '';
position: absolute;
display: block;
background-color: #fff;
height: 2px;
-webkit-transform: scale(.5);
transform: scale(.5);
left: 0;
right: 0;
top: 5px;
}
}
}
}
}
</style>