<template>
|
<!-- <el-button type="text" @click="reShow">测试重新加载动态表头组件</el-button> -->
|
<el-row ref="searchRef">
|
<FYSearchBar @search="onSearch">
|
<template #options>
|
<slot name="options"></slot>
|
</template>
|
<template #buttons>
|
<slot name="buttons"></slot>
|
</template>
|
</FYSearchBar>
|
</el-row>
|
<el-row ref="expandRef" justify="space-between">
|
<el-col span="22">
|
<slot name="options-expand"></slot>
|
</el-col>
|
<el-col span="2">
|
<el-space :wrap="false">
|
<el-text size="small">字体</el-text>
|
<el-radio-group v-model="fontSize" size="small">
|
<el-radio-button value="small">小</el-radio-button>
|
<el-radio-button value="default">中</el-radio-button>
|
<el-radio-button value="large">大</el-radio-button>
|
</el-radio-group>
|
<el-popover placement="bottom" :width="400" trigger="click">
|
<template #reference>
|
<el-button type="primary" @click="genHeadTreeData">表头修改</el-button>
|
</template>
|
<!-- 列出表头的树状结构 -->
|
<el-tree
|
v-if="isOk"
|
:props="headTreeProps"
|
:data="headTreeData"
|
node-key="label"
|
show-checkbox
|
:default-checked-keys="showHeadLabels"
|
@check-change="handleHeadCheckChange"
|
/>
|
<div class="tree-btus">
|
<el-button size="small" type="primary" @click="deleteSecondLevelNodes"
|
>移除所有二级节点</el-button
|
>
|
<el-button size="small" type="primary" @click="deleteThirdLevelNodes"
|
>移除所有三级节点</el-button
|
>
|
<!-- <el-button size="small" type="primary" @click="resetCols">复原</el-button> -->
|
</div>
|
</el-popover>
|
</el-space>
|
</el-col>
|
</el-row>
|
<div ref="expand2Ref">
|
<slot name="options-expand2"></slot>
|
</div>
|
<el-table
|
:data="tableData"
|
v-loading="loading"
|
:row-class-name="tableRowClassName"
|
table-layout="auto"
|
:height="tableHeight"
|
:size="fontSize"
|
@header-contextmenu="headerContextmenu"
|
border
|
>
|
<el-table-column
|
v-for="(item, index) in tableCols"
|
:key="item.label"
|
:fixed="item.fixed"
|
:label="item.label"
|
:prop="item.name"
|
:ref="item.name"
|
:sortable="item.name ? true : false"
|
>
|
<template v-slot:header v-if="item.needSelect">
|
<div>
|
{{ item.label }}
|
<el-button
|
@click="handleHeaderBacthSelectClick(item, true, $event)"
|
type="primary"
|
size="default"
|
circle
|
>
|
选中
|
</el-button>
|
<el-button
|
@click="handleHeaderBacthSelectClick(item, false, $event)"
|
type="success"
|
size="default"
|
circle
|
>
|
去除
|
</el-button>
|
</div>
|
</template>
|
<template v-slot:default1="scope">
|
<el-checkbox
|
v-if="item.needSelect"
|
v-model="scope.row[`${item.name}Select`]"
|
@change="(checked) => selectChange(checked, scope.row)"
|
>{{ scope.row[item.name] }}</el-checkbox
|
>
|
</template>
|
<el-table-column
|
v-if="item.children && item.children.length > 0"
|
v-for="(item2, index2) in item.children"
|
:label="item2.label"
|
:prop="item2.name"
|
:ref="item2.name"
|
:sortable="item2.name ? true : false"
|
>
|
<template v-slot:header v-if="item2.needSelect">
|
<div>
|
{{ item2.label }}
|
<el-button
|
@click="handleHeaderBacthSelectClick(item2, true, $event)"
|
type="primary"
|
size="default"
|
circle
|
>
|
选中
|
</el-button>
|
<el-button
|
@click="handleHeaderBacthSelectClick(item2, false, $event)"
|
type="success"
|
size="default"
|
circle
|
>
|
去除
|
</el-button>
|
</div>
|
</template>
|
|
<template v-slot:default2="scope2">
|
<el-checkbox
|
v-if="item2.needSelect"
|
v-model="scope2.row[`${item2.name}Select`]"
|
@change="(checked) => selectChange(checked, scope2.row)"
|
>{{ scope2.row[item2.name] }}</el-checkbox
|
>
|
</template>
|
<el-table-column
|
v-if="item2.children && item2.children.length > 0"
|
v-for="(item3, index3) in item2.children"
|
:label="item3.label"
|
:prop="item3.name"
|
:ref="item3.name"
|
:sortable="item3.name ? true : false"
|
>
|
<template v-slot:header v-if="item3.needSelect">
|
<div>
|
{{ item3.label }}
|
<el-button
|
@click="handleHeaderBacthSelectClick(item3, true, $event)"
|
type="primary"
|
size="default"
|
circle
|
>
|
选中
|
</el-button>
|
<el-button
|
@click="handleHeaderBacthSelectClick(item3, false, $event)"
|
type="success"
|
size="default"
|
circle
|
>
|
去除
|
</el-button>
|
</div>
|
</template>
|
<template v-slot="scope3">
|
<div class="flex-div">
|
<!-- <div v-if="scope3.row.loader" class="loader"></div> -->
|
<div v-if="item3.needSelect">
|
<el-checkbox
|
v-model="scope3.row[`${item3.name}Select`]"
|
@change="(checked) => selectChange(checked, scope3.row)"
|
>{{ scope3.row[item3.name] }}</el-checkbox
|
>
|
</div>
|
<div v-else>
|
{{ scope3.row[item3.name] }}
|
</div>
|
</div>
|
</template>
|
</el-table-column>
|
</el-table-column>
|
</el-table-column>
|
</el-table>
|
|
<el-pagination
|
v-if="pagination"
|
ref="paginationRef"
|
class="el-pagination"
|
v-model:current-page="currentPage"
|
v-model:page-size="pageSize"
|
:page-sizes="[10, 20, 50, 100]"
|
:background="true"
|
layout="total, sizes, prev, pager, next, jumper"
|
:total="total"
|
/>
|
</template>
|
|
<script>
|
import { useCloned } from '@vueuse/core';
|
export default {
|
props: {
|
cols: [],
|
rowClassName: undefined,
|
pagination: {
|
type: Boolean,
|
default: true
|
},
|
// '' | 'small' | 'default' | 'large'
|
size: {
|
type: String,
|
default: 'default'
|
}
|
},
|
data() {
|
return {
|
isOk: false,
|
reLoad: true,
|
i: 0,
|
headTreeData: [],
|
headTreeProps: {
|
children: 'children',
|
label: 'label',
|
disabled: 'noncloseable'
|
},
|
tableHeight: '500',
|
tableData: [],
|
total: 0,
|
currentPage: 1,
|
pageSize: 20,
|
loading: false,
|
fontSize: 'default',
|
tableCols: [
|
{
|
name: '',
|
label: '',
|
needSelect: false,
|
children: []
|
}
|
]
|
};
|
},
|
emits: ['search'],
|
watch: {
|
cols: {
|
handler(nValue, oValue) {
|
this.tableCols = nValue;
|
},
|
immediate: true
|
},
|
currentPage(nValue, oValue) {
|
if (nValue != oValue) {
|
this.onSearch();
|
}
|
},
|
pageSize(nValue, oValue) {
|
if (nValue != oValue) {
|
this.onSearch();
|
}
|
},
|
size: {
|
handler(nValue, oValue) {
|
if (nValue != oValue) {
|
this.fontSize = nValue;
|
}
|
},
|
immediate: true
|
}
|
},
|
computed: {
|
showHeadLabels() {
|
var result = this.collectLabels(this.cols);
|
|
return result;
|
},
|
cTableHeight() {
|
if (this.$refs.searchRef) {
|
const h1 = this.$refs.searchRef.$el.offsetHeight;
|
const h2 = this.$refs.paginationRef ? this.$refs.paginationRef.$el.offsetHeight : 0;
|
const h3 = this.$refs.expandRef.$el.offsetHeight;
|
const h4 = this.$refs.expand2Ref.offsetHeight;
|
|
const h = h1 + h2 + h3 + h4;
|
// return `calc(100vh - ${h1}px - ${h2}px - var(--el-main-padding) * 2 - var(--el-header-height))`;
|
return `calc(100vh - ${h}px - 60px - var(--el-main-padding) * 2)`;
|
} else {
|
return '500';
|
}
|
}
|
},
|
methods: {
|
deleteLevelNodes(tree, level) {
|
// 遍历树中的每个节点
|
tree.forEach((node) => {
|
if (node.children && node.children.length > 0) {
|
// 如果当前节点的子节点数组不为空,递归调用
|
this.deleteLevelNodes(node.children, level - 1);
|
}
|
});
|
|
// 如果当前级别达到指定级别,清空子节点数组
|
if (level === 1) {
|
tree.length = 0;
|
}
|
},
|
|
// 删除所有二级节点
|
deleteSecondLevelNodes() {
|
this.loading = true;
|
this.deleteLevelNodes(this.tableCols, 2);
|
|
this.loading = false;
|
this.genHeadTreeData();
|
},
|
|
// 删除所有三级节点
|
deleteThirdLevelNodes() {
|
this.loading = true;
|
this.deleteLevelNodes(this.tableCols, 3);
|
this.loading = false;
|
this.genHeadTreeData();
|
},
|
deepCopyCols() {
|
return this.deepCopy(this.cols);
|
},
|
resetCols() {
|
|
this.tableCols = this.deepCopyCols();
|
},
|
collectLabels(nodes) {
|
let labels = []; // 用于存储 isShow 为 true 的节点的 label
|
|
function traverseNodes(nodes) {
|
nodes.forEach((node) => {
|
// 如果 isShow 为 true,将 label 添加到数组中
|
if (!(`isShow` in node) || node.isShow) {
|
// labels.push(node);
|
labels.push(node.label);
|
}
|
|
// 如果当前节点有子节点,递归调用遍历函数
|
if (node.children && node.children.length > 0) {
|
traverseNodes(node.children);
|
}
|
});
|
}
|
|
traverseNodes(nodes); // 开始递归遍历
|
|
return labels; // 返回收集到的 labels 数组
|
},
|
// 更新isShow状态
|
updateShowTableHead(labelValue, checked) {
|
function traverseNodes(nodes) {
|
for (let index = nodes.length - 1; index >= 0; index--) {
|
const node = nodes[index];
|
if (node.label == labelValue) {
|
|
// node.isShow = !node.isShow;
|
// setTimeout(()=>{
|
// nodes.splice(index, 0, node);
|
// }, 1000)
|
nodes.splice(index, 1);
|
}
|
|
// 如果当前节点有子节点,递归调用遍历函数
|
if (node.children && node.children.length > 0) {
|
traverseNodes(node.children);
|
}
|
}
|
}
|
|
traverseNodes(this.tableCols); // 开始递归遍历
|
},
|
// 生成表头数据
|
genHeadTreeData() {
|
this.headTreeData = this.tableCols;
|
},
|
// 表头状态改变
|
handleHeadCheckChange(data, checked, indeterminate) {
|
this.updateShowTableHead(data.label, checked);
|
},
|
// 生成选中状态属性名
|
genSelectPropName(name) {
|
return `${name}Select`;
|
},
|
// 转中状态转变 传入一行数据和需要转换选中状态的列名
|
setSelectStatus(row, name, status) {
|
if ((!name) in row) {
|
return false;
|
}
|
row[this.genSelectPropName(name)] = status;
|
return true;
|
},
|
reShow() {
|
// setTimeout(() => {
|
// this.isOk = true;
|
// console.log("数据", this.tableData);
|
// }, 1000);
|
},
|
// 表头被右键
|
headerContextmenu(column, event) {
|
event.preventDefault();
|
for (let i = 0; i < this.tableCols.length; i++) {
|
var item = this.tableCols[i];
|
if (item.label == column.label) {
|
this.tableCols.splice(i, 1);
|
break;
|
}
|
}
|
},
|
handleHeaderBacthSelectClick(prop, isSelect, event) {
|
event.stopPropagation();
|
this.tableData.forEach((element) => {
|
this.setSelectStatus(element, prop.name, isSelect);
|
});
|
this.selectBatchChange(isSelect, this.tableData, this.genSelectPropName(prop.name));
|
},
|
initSelectStatus() {
|
for (let index = 0; index < this.tableData.length; index++) {
|
const element = this.tableData[index];
|
}
|
},
|
initTableData() {
|
this.initSelectStatus();
|
},
|
deepCopy(obj, hash = new WeakMap()) {
|
// 如果是null,直接返回null
|
if (obj === null) return null;
|
// 如果是基本数据类型,直接返回
|
if (typeof obj !== 'object') return obj;
|
// 如果已拷贝过,直接返回之前拷贝的结果
|
if (hash.has(obj)) return hash.get(obj);
|
|
// 创建一个新的对象或数组
|
let cloneObj = Array.isArray(obj) ? [] : {};
|
// 缓存当前对象的拷贝
|
hash.set(obj, cloneObj);
|
|
// 递归拷贝对象或数组的每个属性
|
for (let key in obj) {
|
// 如果属性值是数组或对象,则递归拷贝
|
cloneObj[key] = this.deepCopy(obj[key], hash);
|
}
|
return cloneObj;
|
},
|
initCols() {
|
this.i = 0;
|
this.tableCols = this.deepCopyCols();
|
this.isOk = true;
|
for (let index = 0; index < this.tableCols.length; index++) {
|
const element = this.tableCols[index];
|
element.isShow = true;
|
}
|
},
|
// 选中回调
|
selectChange(isSelect, row) {
|
row.loader = true;
|
this.$emit('select-updated', row);
|
setTimeout(() => {
|
row.loader = false;
|
}, 1000);
|
},
|
// 批量选中回调
|
selectBatchChange(isSelect, rows, name) {
|
rows.forEach((element) => {
|
element.loader = true;
|
});
|
this.$emit('select-batch-updated', rows, name);
|
setTimeout(() => {
|
rows.forEach((element) => {
|
element.loader = false;
|
});
|
}, 1000);
|
},
|
/**
|
* 表格数据查询,传递两组参数,分页信息和回调函数
|
* 分页信息包括当前页码currentPage和每页数据量pageSize
|
* 回调函数接收一个对象,包括表格数据数组data和数据总数total
|
*/
|
onSearch() {
|
this.loading = true;
|
this.$emit(
|
'search',
|
{
|
currentPage: this.currentPage,
|
pageSize: this.pageSize
|
},
|
(res) => {
|
this.tableData = res.data;
|
this.total = res.total ? res.total : 0;
|
this.loading = false;
|
}
|
);
|
},
|
calcTableHeight() {
|
const h1 = this.$refs.searchRef.$el.offsetHeight;
|
const h2 = this.$refs.paginationRef ? this.$refs.paginationRef.$el.offsetHeight : 0;
|
const h3 = this.$refs.expandRef.$el.offsetHeight;
|
const h4 = this.$refs.expand2Ref.offsetHeight;
|
|
const h = h1 + h2 + h3 + h4;
|
// return `calc(100vh - ${h1}px - ${h2}px - var(--el-main-padding) * 2 - var(--el-header-height))`;
|
return `calc(100vh - ${h}px - 60px - var(--el-main-padding) * 2)`;
|
},
|
tableRowClassName({ row }) {
|
if (this.rowClassName) {
|
if (typeof this.rowClassName == 'string') {
|
return this.rowClassName;
|
} else if (typeof this.rowClassName == 'function') {
|
return this.rowClassName({ row });
|
}
|
} else {
|
return row.extension1 != '0' ? 'online-row' : 'offline-row';
|
}
|
}
|
},
|
created() {
|
// this.onSearch();
|
},
|
mounted() {
|
this.tableHeight = this.calcTableHeight();
|
this.initCols();
|
this.initTableData();
|
this.isOk = true;
|
}
|
};
|
</script>
|
|
<style>
|
.l-table .online-row {
|
/* background-color: rgb(4, 202, 21); */
|
}
|
|
.el-table .offline-row {
|
background-color: var(--el-disabled-bg-color);
|
color: var(--el-disabled-text-color);
|
}
|
|
.el-table .cell {
|
white-space: nowrap;
|
}
|
|
.el-pagination {
|
background-color: var(--el-color-white);
|
padding-top: 20px;
|
border-top: 1px solid rgba(0, 0, 0, 0.096);
|
/* margin-top: 2px; */
|
}
|
.no-display {
|
display: none;
|
}
|
.btns {
|
margin-left: 10px;
|
}
|
.flex-div {
|
display: flex;
|
}
|
.loader {
|
margin-top: 5px;
|
margin-right: 5px;
|
border: 4px solid rgba(0, 0, 0, 0.1); /* 轻颜色的边框 */
|
border-radius: 50%; /* 圆形 */
|
border-top: 4px solid #3498db; /* 蓝色边框 */
|
width: 20px; /* 加载器的宽度 */
|
height: 20px; /* 加载器的高度 */
|
animation: spin 2s linear infinite; /* 无限循环的旋转动画 */
|
}
|
.tree-btus {
|
margin-left: 24px;
|
}
|
|
@keyframes spin {
|
0% {
|
transform: rotate(0deg);
|
}
|
100% {
|
transform: rotate(360deg);
|
}
|
}
|
</style>
|