赞
踩
el-table树形表格实现 父子联动勾选 并过滤时进行勾选缓存
el-table,支持树形表格,但是多选框并没有自动支持父子联动勾选;
解释一下,一般树形表格,很难做分页,但是一般我们都是使用el-table做二次封装,然后封装后的table组件,可能不仅支持数据表格,还支持普通一级数据的分页,甚至还支持所有结构数据的 查询、前端过滤等高级功能。
树形表格,只有一个祖先元素,如何进行性能优化?
涉及的属性
@Prop({ default: false }) multiple: boolean; //? 是否开启多选
/** 树形数据全部展开标识 */
@Prop({ default: false }) expandAllFlag: boolean
@Prop({ default: false }) needRelativeSelect: boolean; //? 树形表格,是否需要联动勾选
涉及的状态
list = []; //? 页面展示的临时数据
treeData = null; //? 树形结构的总结构
cachedSelection = []; //? 缓存的总数据
currentRecord = null; //? 当前选中的记录 —— 服务于单选
当前只有标题头的勾选框有半勾选状态,行上没有,需要手动实现
首先是半勾选的样式,需要从标题头勾选框的半勾选状态的样式复制;当父级处于半勾选状态时,去使用这个类的样式;当行的某一数据标识为true时,勾选框的列 的类名添加indeterminate,否则勾选框的列 的类名为空;
可以使用cell-class-name属性,进行实现< el-table :row-key=“valueKey”>< /el-table>
tableCellClassName ({row, column}) {
let cellClassName = ''
if (row.indeterminate && column.type === 'selection') {
cellClassName = 'indeterminate'
}
return cellClassName
}
<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>
获取到数据后,根据上次的cachedSelection,修改树形总结构treeData;
同时给树形结构的数据初始化setTreeMetaData,使得每一个子节点都有parentId,方便后续的联动操作
async getList(type?) {
if (type !== 'first' && !this.treeData) { //* 第一次,必须获取到树形结构,才能继续执行
this.$alert('树形表格,第一次接口异常,不允许后续操作').finally(() => {
this.closeModal()
})
return
}
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')
if (type === 'first') { //* 第一次,必须获取到树形结构,才能继续执行
if (!this.treeData) this.treeData = result;
}
this.list = result
this.totalRows = res.data.total
if (this.needRelativeSelect) this.batchUpdateTreeData(this.cachedSelection); //* 给树形结构设置初始的勾选状态
}
} catch (err) {
this.$message.error(err.message)
} finally {
this.loading = false
}
}
@Watch('treeData', { deep: true, immediate: true })
onTableConfigChange(newValue) {
if (newValue) {
this.$nextTick(() => {
this.afterTreeDataChanged()
})
}
}
afterTreeDataChanged() { //todo 树形结构的数据发生变化时的后续
if (this.needRelativeSelect) {
this.getListFormTreeData();
this.getCachedSelectionFromTreeData();
this.selectRowFromList();
}
}
getListFormTreeData() { //todo 从树形结构,获取当前页面的临时数据
const expandedTreeData = getExpandedTreeData(this.treeData, 'children')
const updateList = (tree) => {
tree.forEach(record => {
const currentRecord = expandedTreeData.find(item => item[this.valueKey] == record[this.valueKey]); //? 树形结构中该行 对应的数据
this.$set(record, 'selected', currentRecord.selected)
this.$set(record, 'selectedString', currentRecord.selected ? 'Y' : 'N')
this.$set(record, 'indeterminate', currentRecord.indeterminate)
if (Array.isArray(record.children) && record.children.length) {
updateList(record.children)
}
})
}
updateList(this.list)
}
getCachedSelectionFromTreeData() { //todo 从树形结构,获取总的勾选数据
const expandedTreeData = getExpandedTreeData(this.treeData, 'children')
this.cachedSelection = expandedTreeData.filter(item => item.selected)
}
selectRowFromList() { //todo 根据当前页面的临时数据,勾选表格的行
const expandedList = getExpandedTreeData(this.list, 'children')
const modalTableRef: any = this.$refs.modalTableRef
modalTableRef.clearSelection(); //* 打扫干净屋子再请客
expandedList.forEach(record => {
modalTableRef.toggleRowSelection(record, !!record.selected); //* 调用el-table内置方法,进行勾选
})
}
batchUpdateTreeData(list, isSelected: boolean = true) { //todo 批量的根据勾选的数据,动态计算树形结构的数据联动
this.treeData = batchUpdateTreeData({
treeData: this.treeData,
list,
treeKey: 'id',
childrenKey: 'children',
isSelected
})
}
updateTreeData(row, isSelected: boolean = true) { //todo 根据勾选的数据,动态计算树形结构的数据联动
this.treeData = updateTreeData({
treeData: this.treeData,
row: row,
treeKey: 'id',
childrenKey: 'children',
isSelected
})
}
使用selection-change事件(当选择项发生变化时会触发该事件)监听的话,只有一个selection参数,并不知道当前勾选的是哪一行,于是使用select(当用户手动勾选数据行的 Checkbox 时触发的事件)和select-all(当用户手动勾选全选 Checkbox 时触发的事件)
/** 勾选全选操作(多选模式) */
selectAllOnClick() {
const modalTableRef: any = this.$refs.modalTableRef
const isAllSelected = modalTableRef?.store.states.isAllSelected; //? 是否全部勾选
if (this.needRelativeSelect) {
this.batchUpdateTreeData(this.list, isAllSelected)
return
}
if (isAllSelected) {
this.list.forEach((cur) => {
const isUnselect = !this.cachedSelection.map((sel) => sel[this.valueKey]).includes(cur[this.valueKey])
if (isUnselect) {
this.cachedSelection.push(cur)
}
})
} else {
this.list.forEach((item) => {
const cacheIndex = this.cachedSelection.findIndex(
(cache) => cache[this.valueKey] === item[this.valueKey]
)
if (cacheIndex !== -1) {
this.cachedSelection.splice(cacheIndex, 1)
}
})
}
}
/** 勾选单条操作(多选模式) */
selectOnClick(selection, row) {
const modalTableRef: any = this.$refs.modalTableRef
const curSelIndex = selection.findIndex((cache) => cache[this.valueKey] === row[this.valueKey])
if (this.needRelativeSelect) {
this.updateTreeData(row, curSelIndex > -1)
//todo 通过勾选的数据selection,去总树结构 treeData,设置相关联动勾选状态,并将当前表格的相关数据进行勾选
return
}
if (curSelIndex > -1) { //* 被勾选
this.cachedSelection.push(row)
} else {
this.cachedSelection.splice(curSelIndex, 1)
}
}
util.js
//! zhy start 树形表格 父子联动 + 勾选缓存
/**
* 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 统一设置的 勾选的状态
*/
/**
* todo 将当前行下的所有子级切换勾选状态
* @param record 单前行
* @param toggleRowSelection 切换行的勾选状态的内置方法
* @param childrenKey 树形结构子级的字段
* @param selected 统一设置的 勾选的状态
*/
const toggleSubAll = ({record, childrenKey = 'children', selected = true}) => {
if (Array.isArray(record[childrenKey]) && record[childrenKey].length) { //* 有子级
record[childrenKey].forEach(subRecord => {
subRecord.selected = selected;
if (Array.isArray(subRecord[childrenKey]) && subRecord[childrenKey].length) { //* 子级还有下级
toggleSubAll({record: subRecord, childrenKey, selected})
}
})
}
}
/**
* todo 设置树形表格父级的勾选状态
* @param parentValue 父级的id
* @param expandedList 树形表格展开后的数据
* @param userSelection 用户勾选的所有数据
* @param tableRef 表格的ref
* @param treeKey 树形结构的id字段
*/
const setTableTreeParentSelection = ({parentValue, expandedList, treeKey = 'id'}) => {
const parentRecord = expandedList.find(item => item[treeKey] === parentValue); //? 当前的父级
const subList = expandedList.filter(item => item.parentValue === parentValue); //? 当前父级下的所有的子级
const selectedList = subList.filter(subRecord => subRecord.selected); //? 所有子级中,被勾选的数据
const halfSelectedList = subList.filter(subRecord => subRecord.indeterminate === 'Y'); //? 所有子级中,半勾选状态的子级
parentRecord.indeterminate = undefined;
if (subList.length === selectedList.length) { //* 所有子级全部勾选,父级勾选
parentRecord.selected = true;
} else if (!selectedList.length && !halfSelectedList.length) { //* 所有子级 全部没有勾选也没有半勾选
parentRecord.selected = false;
} else {
//* 子级部分勾选,
parentRecord.selected = false;
parentRecord.indeterminate = 'Y'
}
if (parentRecord.parentValue) setTableTreeParentSelection({parentValue: parentRecord.parentValue, expandedList, treeKey})
}
//todo 根据用户勾选的行,去修改树形结构的数据
const updateTreeData = ({treeData, row, treeKey = 'id', childrenKey = 'children', isSelected = true}) => {
const treeDataClone = clone(treeData)
const expandedTreeData = getExpandedTreeData(treeDataClone, childrenKey);
const record = expandedTreeData.find(item => item[treeKey] === row[treeKey])
record.indeterminate = undefined;
record.selected = isSelected;
toggleSubAll({record, childrenKey, selected: isSelected})
if (record.parentValue) setTableTreeParentSelection({parentValue: record.parentValue, expandedList: expandedTreeData, treeKey})
return treeDataClone
}
//todo 根据勾选的数据,动态计算树形结构的数据联动
const batchUpdateTreeData = ({treeData, list, treeKey = 'id', childrenKey = 'children', isSelected = true}) => {
let treeDataClone = clone(treeData)
list.forEach(item => {
treeDataClone = updateTreeData({
treeData: treeDataClone,
row: item,
treeKey, childrenKey, isSelected
})
})
return treeDataClone
}
//! zhy end 树形表格 RelativeSelect
<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
:default-expand-all="expandAllFlag"
: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, Watch } 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 { batchUpdateTreeData, getExpandedTreeData, setTreeMetaData, updateTreeData } 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 = []; //? 页面展示的临时数据
treeData = null; //? 树形结构的总结构
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 selectedIds() {
return (this.cachedSelection || []).map((sel) => sel[this.valueKey])
}
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()
}
@Watch('treeData', { deep: true, immediate: true })
onTableConfigChange(newValue) {
if (newValue) {
this.$nextTick(() => {
this.afterTreeDataChanged()
})
}
}
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]) {
this.multiple
? modalTableRef.toggleRowSelection(item, true)
: modalTableRef.setCurrentRow(item)
}
})
if (getType(item.children) === 'Array' && item.children.length) {
checkSelect(item.children)
}
})
}
// 递归选中勾选缓存数据
checkSelect(this.list)
}
}
afterTreeDataChanged() { //todo 树形结构的数据发生变化时的后续
if (this.needRelativeSelect) {
this.getListFormTreeData();
this.getCachedSelectionFromTreeData();
this.selectRowFromList();
}
}
getListFormTreeData() { //todo 从树形结构,获取当前页面的临时数据
const expandedTreeData = getExpandedTreeData(this.treeData, 'children')
const updateList = (tree) => {
tree.forEach(record => {
const currentRecord = expandedTreeData.find(item => item[this.valueKey] == record[this.valueKey]); //? 树形结构中该行 对应的数据
this.$set(record, 'selected', currentRecord.selected)
this.$set(record, 'selectedString', currentRecord.selected ? 'Y' : 'N')
this.$set(record, 'indeterminate', currentRecord.indeterminate)
if (Array.isArray(record.children) && record.children.length) {
updateList(record.children)
}
})
}
updateList(this.list)
}
getCachedSelectionFromTreeData() { //todo 从树形结构,获取总的勾选数据
const expandedTreeData = getExpandedTreeData(this.treeData, 'children')
this.cachedSelection = expandedTreeData.filter(item => item.selected)
}
selectRowFromList() { //todo 根据当前页面的临时数据,勾选表格的行
const expandedList = getExpandedTreeData(this.list, 'children')
const modalTableRef: any = this.$refs.modalTableRef
modalTableRef.clearSelection(); //* 打扫干净屋子再请客
expandedList.forEach(record => {
modalTableRef.toggleRowSelection(record, !!record.selected); //* 调用el-table内置方法,进行勾选
})
}
batchUpdateTreeData(list, isSelected: boolean = true) { //todo 批量的根据勾选的数据,动态计算树形结构的数据联动
this.treeData = batchUpdateTreeData({
treeData: this.treeData,
list,
treeKey: 'id',
childrenKey: 'children',
isSelected
})
}
updateTreeData(row, isSelected: boolean = true) { //todo 根据勾选的数据,动态计算树形结构的数据联动
this.treeData = updateTreeData({
treeData: this.treeData,
row: row,
treeKey: 'id',
childrenKey: 'children',
isSelected
})
}
async getList(type?) {
if (type !== 'first' && !this.treeData) { //* 第一次,必须获取到树形结构,才能继续执行
this.$alert('树形表格,第一次接口异常,不允许后续操作').finally(() => {
this.closeModal()
})
return
}
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')
if (type === 'first') { //* 第一次,必须获取到树形结构,才能继续执行
if (!this.treeData) this.treeData = result;
}
this.list = result
this.totalRows = res.data.total
this.$nextTick(() => {
this.initSelection()
})
if (this.needRelativeSelect) this.batchUpdateTreeData(this.cachedSelection); //* 给树形结构设置初始的勾选状态
}
} 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('first')
// this.expandAllFlag && this.toggleRowExpanAll(this.expandAllFlag)
}
closeModal() {
this.pageIndex = 1
this.pageSize = 10
this.totalRows = 0
this.visible = false
this.treeData = null
this.list = []
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.batchUpdateTreeData(this.list, isAllSelected)
return
}
if (isAllSelected) {
this.list.forEach((cur) => {
const isUnselect = !this.cachedSelection.map((sel) => sel[this.valueKey]).includes(cur[this.valueKey])
if (isUnselect) {
this.cachedSelection.push(cur)
}
})
} else {
this.list.forEach((item) => {
const cacheIndex = this.cachedSelection.findIndex(
(cache) => cache[this.valueKey] === item[this.valueKey]
)
if (cacheIndex !== -1) {
this.cachedSelection.splice(cacheIndex, 1)
}
})
}
}
/** 勾选单条操作(多选模式) */
selectOnClick(selection, row) {
const curSelIndex = selection.findIndex((cache) => cache[this.valueKey] === row[this.valueKey])
if (this.needRelativeSelect) {
this.updateTreeData(row, curSelIndex > -1)
//todo 通过勾选的数据selection,去总树结构 treeData,设置相关联动勾选状态,并将当前表格的相关数据进行勾选
return
}
if (curSelIndex > -1) { //* 被勾选
this.cachedSelection.push(row)
} else {
this.cachedSelection = this.cachedSelection.filter(item => item[this.valueKey] !== row[this.valueKey])
}
}
/** 双击行 */
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>
.show-box {
height: 100%;
}
.fold-box {
height: 58px;
}
.table_pagination {
margin-top: 10px;
text-align: right;
}
::v-deep {
.el-card {
border: none;
.el-card__body {
padding: 0;
}
}
}
::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>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。