效果图
因项目采用的是Vue2,所以这个功能目前采用的是Vue2的写法。
Vue3请自行修改扩展代码;或收藏关注帖子,后续Vue3项目如有用到会在本帖子更新修改。
安装vuedraggable(拖拽插件)
cnpm i vuedraggable
先说用法,下方附全源码
引入自定义表头组件
import indicatorTable from "@/components/indicatorTable/index.vue";
使用:(传参说明已在下方标识)
<indicatorTable
ref="rois"
:defaultArr="columns"
:cardDataProp="cardDataProp"
cacheKeyProp="keyROI"
@propData="propsTableHander"
currenKey="ROT"
/>
props参数说明:(均为必传字段)
// ref:用于调用子组件方法。
// columns:表头数据,例如:
[{
prop: "cost_platform",
label: "广告金",
}]
// cardDataProp:可选表头复选框,列如:
cardDataProp: [
{
title: "指标", // 每一项的分类title标题,详见第一张效果图
checkboxes: [...columns], // columns这个就是上面的一样
},
],
// cacheKeyProp:储存的key名,名字自定义来,避免缓存的key一样就行,列如:
cacheKeyProp="keyROI"
// propData:回调方法,用于更新表头,接受函数,直接表头columns数据 = 参数即可
// currenKey:保存的指标key,避免缓存的key一样就行。
页面table使用方法,需用循环:
<el-table
v-loading="loading"
:data="tableList"
border
@sort-change="tableSort"
:height="tableHeight"
ref="tableRef"
>
<el-table-column
v-for="item in columns"
:prop="item.prop"
:label="item.label"
:width="item.width"
align="center"
sortable="custom"
:show-overflow-tooltip="true"
>
</el-table-column>
</el-table>
上面表格的参数不用多说了吧,除非你不会前端!
附源码(拿来直接用!只要参数没问题!)
如遇到报错、不显示等问题,一定是参数不对!已自测 无任何报错或警告信息!
如需要Vue3版本,自行开发或私信,有空定会帮助!
新建组件直接复制:
<template>
<div class="indicator-all-box">
<el-popover placement="bottom" width="300" trigger="click">
<div class="add-custom-indicator-container">
<el-button type="primary" @click="addUserDefinedIndicators">
新增自定义指标
</el-button>
<div class="indicator-list">
<ul>
<li
v-for="(item, index) in pointerArr"
:key="index"
:class="currenPointIndex == index ? 'active-li' : ''"
@click="pointClick(item, index)"
>
<div class="flex-indicator-item">
<span>{{ item.title }}</span>
<div class="right-indicator">
<i
class="el-icon-edit"
@click.stop.prevent="pointIndexHander(item, 'edit')"
></i>
<i
class="el-icon-delete"
@click.stop.prevent="pointIndexHander(item, 'delete')"
></i>
</div>
</div>
</li>
</ul>
</div>
</div>
<el-button slot="reference" type="success">自定义指标</el-button>
</el-popover>
<!-- 弹窗自定义 -->
<el-dialog :title="openTitle" :visible.sync="dialogVisible" width="70%">
<div class="customize-indicator-data-container">
<div class="card-checkbox-content-left">
<el-card
v-for="(item, index) in cardData"
:key="index"
class="box-card"
>
<div slot="header" class="clearfix">
<span>{{ item.title }}</span>
<el-checkbox
v-model="item.selectedAll"
@change="handleSelectAll(item)"
:indeterminate="isIndeterminate(item)"
style="float: right"
>全选</el-checkbox
>
</div>
<div class="check-card-item">
<el-checkbox-group
ref="checkboxGroup"
v-model="selectedCheckboxes"
>
<el-checkbox
v-for="(checkbox, idx) in item.checkboxes"
:key="idx"
:label="checkbox.label"
>{{ checkbox.label }}</el-checkbox
>
</el-checkbox-group>
</div>
</el-card>
</div>
<div class="sort-view-dx">
<el-divider>排序</el-divider>
<div class="sort-row">
<draggable
v-if="selectedCheckboxes.length > 0"
v-model="selectedCheckboxes"
animation="300"
>
<p v-for="(item, index) in selectedCheckboxes" :key="index">
{{ item }}
</p>
</draggable>
<el-empty v-else></el-empty>
</div>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addPointerSubmit">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import draggable from "vuedraggable";
export default {
name: "indicatorTable",
components: {
draggable,
},
props: {
// 默认指标
defaultArr: {
type: Array,
required: true,
},
// 可选指标
cardDataProp: {
type: Array,
required: true,
},
// 存储指标key
cacheKeyProp: {
type: String,
required: true,
},
// 存储的索引key名
currenKey: {
type: String,
required: true,
},
},
data() {
return {
// 弹窗show
dialogVisible: false,
// 全部指标数组
cardData: this.cardDataProp,
// 勾选指标
selectedCheckboxes: [],
// 弹框title
openTitle: "添加",
// 下拉指标列表
pointerArr: [],
// 获取当前编辑item
editItem: null,
// 传出去的prop数组
emitArr: null,
// 当前选择的指标
currenPointIndex: null,
};
},
computed: {
Local() {
return {
get(key) {
const value = localStorage.getItem(key);
if (value == "[]") {
return null;
} else {
return value !== null ? JSON.parse(value) : null;
}
},
set(key, value) {
localStorage.setItem(key, JSON.stringify(value));
},
remove(key) {
localStorage.removeItem(key);
},
};
},
// 指标指定label排序
sortColumns() {
return function (data, sort) {
if (data) {
return data.sort(
(a, b) => sort.indexOf(a.label) - sort.indexOf(b.label)
);
}
};
},
// 获取选指标label
filterCheckbox() {
return function (data, isSort = false, sortData) {
if (data) {
let filteredCheckboxes = [];
this.cardData.forEach((item) => {
item.checkboxes.forEach((checkbox) => {
if (data.arrayCheck.includes(checkbox.label)) {
filteredCheckboxes.push(checkbox);
}
});
});
// 获取后是否排序
if (isSort) {
return this.sortColumns(filteredCheckboxes, sortData);
} else {
return filteredCheckboxes;
}
}
};
},
},
created() {
// this.Local.remove("displayType");
this.getPointData("init");
},
methods: {
// 存储key索引
storeSetCurrentIndex(type = "set") {
if (type === "set") {
const getIndexObj = this.Local.get("pointIndex") || {};
getIndexObj[this.currenKey] = this.currenPointIndex;
this.Local.set("pointIndex", getIndexObj);
} else {
return this.Local.get("pointIndex") || {};
}
},
// 选择当前指标
pointClick(row, index) {
if (this.currenPointIndex != index) {
this.currenPointIndex = index;
// 存储当前点击指标index
this.storeSetCurrentIndex("set");
const checkData = this.filterCheckbox(
row,
true,
this.pointerArr[this.currenPointIndex].arrayCheck
);
this.$emit("propData", checkData);
}
},
// 扩展方法-倍数ROI处理
// roiDisposeFn() {
// const getPonit = this.Local.get(this.cacheKeyProp);
// const displayType = this.Local.get("displayType");
// const prointArrItem = this.pointerArr[this.currenPointIndex];
// const updatedArray = prointArrItem.arrayCheck.map((item) => {
// if (
// displayType == 2 &&
// item.startsWith("ROI") &&
// !item.includes("倍数")
// ) {
// return item + "倍数";
// } else if (displayType != 2 && item.includes("倍数")) {
// return item.replace("倍数", "");
// }
// return item;
// });
// const labelCheckBoxAll = this.filterCheckbox({
// arrayCheck: updatedArray,
// }).map((item) => item.label);
// if (prointArrItem.arrayCheck !== labelCheckBoxAll) {
// getPonit[this.currenPointIndex].arrayCheck = labelCheckBoxAll;
// this.Local.set(this.cacheKeyProp, getPonit);
// this.pointerArr[this.currenPointIndex].arrayCheck = labelCheckBoxAll;
// }
// },
// 获取-更新指标
getPointData(type) {
const getPonit = this.Local.get(this.cacheKeyProp);
if (getPonit) {
this.pointerArr = getPonit;
this.currenIndexNob();
const prointArrItem = this.pointerArr[this.currenPointIndex];
this.roiDisposeFn();
const checkData = this.filterCheckbox(
this.pointerArr[this.currenPointIndex],
true,
prointArrItem.arrayCheck
);
if (checkData) {
this.$emit("propData", checkData);
}
} else if (!getPonit && type !== "init") {
// 如果是空
this.Local.remove(this.cacheKeyProp);
this.$emit("propData", []);
} else {
// 如果默认的
if (this.defaultArr && type === "init" && this.pointerArr.length <= 0) {
const arrs = JSON.parse(JSON.stringify(this.defaultArr));
const labelsArray = arrs.map((item) => item.label);
this.currenIndexNob();
this.pointerArr.push({
title: "默认指标",
arrayCheck: labelsArray,
});
const prointArrItem = this.pointerArr[this.currenPointIndex];
const checkData = this.filterCheckbox(
prointArrItem,
true,
labelsArray
);
this.$emit("propData", checkData);
}
}
},
// 编辑-删除指标
pointIndexHander(item, type) {
if (type === "edit") {
this.openTitle = "编辑";
this.selectedCheckboxes = item.arrayCheck;
this.editItem = item;
this.dialogVisible = true;
} else {
const itemToDelete = this.pointerArr.find(
(ls) => ls.title === item.title
);
if (itemToDelete) {
const indexToDelete = this.pointerArr.indexOf(itemToDelete);
if (indexToDelete > -1) {
this.pointerArr.splice(indexToDelete, 1);
this.Local.set(this.cacheKeyProp, this.pointerArr);
// 删除当前行更新,否则不更新
if (indexToDelete === this.currenPointIndex) {
this.getPointData();
} else {
this.currenIndexNob();
}
}
}
}
},
// 全选当前指标
handleSelectAll(item) {
item.checkboxes.forEach((checkbox) => {
const checkboxIndex = this.selectedCheckboxes.indexOf(checkbox.label);
if (item.selectedAll && checkboxIndex === -1) {
this.selectedCheckboxes.push(checkbox.label);
} else if (!item.selectedAll && checkboxIndex !== -1) {
this.selectedCheckboxes.splice(checkboxIndex, 1);
}
});
},
// 全选状态判断
isIndeterminate(item) {
const selectedLabels = this.selectedCheckboxes;
const allLabels = item.checkboxes.map((checkbox) => checkbox.label);
const selectedCount = selectedLabels.filter((label) =>
allLabels.includes(label)
).length;
item.selectedAll = selectedCount === allLabels.length;
return selectedCount > 0 && selectedCount < allLabels.length;
},
// 指定索引
currenIndexNob() {
const getIndexObj = this.storeSetCurrentIndex("get");
this.currenPointIndex = getIndexObj[this.currenKey];
if (!this.currenPointIndex) {
this.currenPointIndex = 0;
} else {
if (this.pointerArr.length <= 1) {
this.currenPointIndex = 0;
} else {
this.currenPointIndex = getIndexObj[this.currenKey] || 0;
}
}
this.storeSetCurrentIndex("set");
},
// 添加指标
addPointerSubmit() {
this.dialogVisible = false;
this.emitArr = this.filterCheckbox(
{
arrayCheck: this.selectedCheckboxes,
},
true,
this.selectedCheckboxes
);
const dataItem = {
title: "",
arrayCheck: this.selectedCheckboxes,
};
if (this.openTitle === "添加") {
this.$prompt("请输入指标名", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
closeOnClickModal: false,
inputValidator: (value) => {
if (!value) {
return "不能为空!";
}
},
beforeClose: (action, instance, done) => {
if (action === "confirm") {
const isDuplicate = this.pointerArr.some(
(item) => item.title === instance.inputValue
);
if (isDuplicate) {
this.$message.error("已存在相同指标名");
return false;
} else {
done();
}
} else {
done();
}
},
}).then(({ value }) => {
dataItem.title = value;
if (this.pointerArr && Array.isArray(this.pointerArr)) {
const updatedData = [...this.pointerArr, dataItem];
this.Local.set(this.cacheKeyProp, updatedData);
} else {
const newData = [dataItem];
this.Local.set(this.cacheKeyProp, newData);
}
this.$emit("propData", this.emitArr);
this.getPointData();
});
} else {
const editIndex = this.pointerArr.findIndex(
(item) => item.title === this.editItem.title
);
if (editIndex !== -1) {
(dataItem.title = this.editItem.title),
(this.pointerArr[editIndex] = dataItem);
this.Local.set(this.cacheKeyProp, this.pointerArr);
}
this.$emit("propData", this.emitArr);
}
},
// 新增自定义指标
addUserDefinedIndicators() {
this.openTitle = "添加";
this.selectedCheckboxes = [];
this.dialogVisible = true;
},
},
};
</script>
<style lang="scss" scoped>
.indicator-all-box {
float: right;
margin-right: 5px;
}
.indicator-list {
ul {
padding: 0;
li {
padding: 10px 0;
border-bottom: 1px solid #e1e1e1;
}
}
.flex-indicator-item {
display: flex;
justify-content: space-between;
padding: 0 15px;
.right-indicator {
i {
padding-left: 10px;
display: inline-block;
font-size: 16px;
cursor: pointer;
}
}
}
}
.box-card {
margin-bottom: 10px;
}
.el-divider {
margin: 10px 0;
}
.rihgt-all-check {
float: right;
padding: 3px 0;
}
.el-checkbox {
margin-bottom: 10px;
}
.customize-indicator-data-container {
display: flex;
min-height: 60vh;
.card-checkbox-content-left {
max-height: 600px;
overflow-y: scroll;
flex: 1;
}
.sort-view-dx {
width: 300px;
margin-left: 15px;
.sort-row {
height: 60vh;
overflow-y: scroll;
p {
background-color: #fdfdfd;
height: 32px;
line-height: 32px;
border: 1px solid #ebebeb;
padding: 0 10px;
margin: 5px 0;
&:hover {
cursor: move;
}
}
}
}
}
.active-li {
background-color: #efefef;
}
</style>
上方注释扩展方法说明:
比如你上方有筛选条件需要关联切换的,拿我自己的例子,见顶部ROI区域
他筛选条件有一个ROI、ROI倍数的筛选。然后字段展示是ROI123456…等,是循环的数量。切换ROI倍数的时候 表头原有的ROI需要变成ROI倍数 以及prop也一样要变化。
列如顶部ROI附加复选框的方法:
this.cardDataProp[1] = {
title: "ROI指标",
checkboxes: Array.from(
{ length: this.queryParams.displayNum },
(_, i) => ({
prop: `roi${i + 1}${
this.queryParams.displayType == 1 ? "_rate" : ""
}`,
label: `ROI${i + 1}${
this.queryParams.displayType == 2 ? "倍数" : ""
}`,
})
),
};
筛选条件选择切换displayType类型后调用 this.$refs.rois.getPointData("init"); 刷新表头
以上根据了选项displayType变化label和prop 但又是属于同一个label表头 只是字段不一样的 或者要用循环的,可采用这种方式,扩展方法自己研究…估计没有其他人需要用这个扩展的,就注释了,不用的可以删掉!
感谢你的阅读,如对你有帮助请收藏+关注!
只分享干货实战和精品,从不啰嗦!!!
如某处不对请留言评论,欢迎指正~
博主可收徒、常玩QQ飞车,可一起来玩玩鸭~