项目背景 基础需求: 我这边的需求是要把表格的表头设置为动态的情况、并且允许默写列进行显示隐藏、冻结、排序、默认某几项进行禁止取消勾选。 在这个基础需求上需要兼容按钮权限、以及每一个单元格内的点击事件和toolbar的按钮动态禁止功能
动态表头配置
不同角色、不同场景下,用户对字段的关注点不同,需支持运行时动态调整表头结构,包括列的显示/隐藏、顺序调整、宽度记忆等。 精细化列控制 允许部分业务列(如"备注""标签")被用户自由隐藏; 关键列(如首列、操作列、复选框、序号)必须始终可见,禁止在列设置中取消勾选; 支持列冻结(固定左/右)、排序、宽度拖拽等交互能力。 深度交互与权限集成 每个单元格需支持独立点击事件(如跳转详情、编辑内联); 表格工具栏(Toolbar)按钮需按用户权限动态渲染(兼容 v-permission 等指令); 操作列中的按钮同样需支持权限控制与自定义插槽。 配置持久化
用户对表头的个性化设置(如隐藏了哪些列、调整了哪些顺序)应自动保存至服务端,并在下次访问时还原,提升使用体验。
效果图
图片

视频效果
版本号
javascript
在这里插入代码片
"vxe-pc-ui": "^4.10.30",
"vxe-table": "^4.17.20",
"xe-utils": "^3.7.9",
"vue": "^3.5.18"
Props 说明
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
border |
Boolean | true |
是否显示表格边框 |
stripe |
Boolean | true |
是否显示斑马纹 |
cloumnDrag |
Boolean | false |
是否允许拖拽调整列顺序 |
toolDrag |
Boolean | true |
是否显示右上角"列设置"按钮(齿轮图标) |
height |
String / Number | '500px' |
表格高度,支持 '400px' 或 '80%' |
code |
String | '' |
必填!当前页面唯一标识,用于保存/恢复列配置 |
showCheckbox |
Boolean | true |
是否显示复选框列 |
showIndex |
Boolean | false |
是否显示序号列 |
showAction |
Boolean | false |
是否显示操作列 |
actionWidth |
Number | 100 |
操作列宽度(单位:px) |
slotsFields |
Array<String> | [] |
需要用插槽渲染的字段名,如 ['status', 'name'] |
双向数据绑定
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| data | Array | [] | 表格主体数据,每一项为一行记录 |
| buttons | Array | [] | 左侧工具栏按钮配置,格式如:{ code: 'add', name: '新增' } |
| column | Array | [] | 表头列配置,每列需包含 field(字段名)、title(标题)、visible(是否显示)等属性 |
插槽
js
<!-- 渲染 status 字段 --><template #status="{ row, column, $rowIndex }">
<span>{{ row.statusText }}</span></template>
event方法
| 事件名 | 回调参数 | 说明 |
|---|---|---|
| cellClick | (row, column, value, title) | 点击单元格时触发 |
| checkAll | (selectedRows: Array) | 全选/取消全选时触发 |
| check | (selectedRows: Array) | 单行勾选状态变化时触发 |
| saveSuccess | --- | 用户点击【确定】或【恢复默认】后触发(用于重新加载表格) |
| leftBar | (button: Object) | 点击左侧工具栏按钮时触发 |
完整的使用示例
javascript
// 组件
<Table
ref="tableRef"
v-model:column="columns"
v-model:data="tableData"
v-model:buttons="buttons"
:code="ViewName.CRM_MY_CUSTOMER_LIST"
:height="tableHeight"
:show-action="true"
:stripe="false"
action-width="200"
:slots-fields="slotsFields"
@cell-click="handleCellClick"
@check-all="handleSelectChange"
@check="handleSelectChange"
@save-success="initList"
@left-bar="handleLeftBar">
// 这里的操作栏是action 名称必须固定为action 需配合show-action属性
<template #action="{ row }">
<div v-if="shouldShowActions(row)">
<el-button v-permission="['customer:my:edit']" link type="primary" @click="handleTableUpdate(row)" >编辑</el-button>
</div>
</template>
// 这次插槽totalConsumeAmount必须要在slotsFields内填写 不然不允许使用
<template #phoneNumber="{ row }">
<IconifyIconOnline v-if="row.phoneNumber" icon="ep:copy-document" style="display: inline; cursor: pointer" @click="copy(row.phoneNumber)" /> {{ row.phoneNumber }}
</template>
// 这次插槽totalConsumeAmount必须要在slotsFields内填写 不然不允许使用
<template #followUpCount="{ row }">
<el-button link type="primary" @click="previewTable(row)" >点击查看</el-button>
</template>
// 这次插槽totalConsumeAmount必须要在slotsFields内填写 不然不允许使用
<template #totalConsumeAmount="{ row }">
<span class="custom_high" @click="handleClickConsumptionUserInfo(row)" >{{ row.totalConsumeAmount }</span >
</template>
</Table>
<script setup>
import { computed, ref, onMounted, defineOptions, nextTick } from "vue";
const tableRef = ref(null); // 表格ref
const columns = ref([]); // 列名称list
const tableData = ref([]); // 数据list
// 表格高度
const tableHeight = computed(() => {
// searchHeight 为form的高度
const searchHeight = searchBoxHeight.value || 0;
return window.innerHeight - 280 - searchHeight;
});
// 单元格需要用到的插槽 需要在这里定义才能使用 重点!!!
const slotsFields = ref([
"customerName",
"sourceChannelName",
"phoneNumber",
"followStatus",
"totalConsumeAmount",
"totalRechargeAmount"
]);
const selectListIds = ref([]); // 勾选值id列表
// 勾选事件
const handleSelectChange = val => {
selectListIds.value = val.map(item => item.id);
};
// toolbar上面的按钮
const rawButtons = [
{
code: "addMember", // 必传值
name: "添加客户", // 必传值 文本(页面展示)
icon: "vxe-icon-add", // icon
status: "primary", // 状态
permissionCode: "customer:my:add" // 是否有按钮权限。如果不传或者为值为空字符串 代表所有人可见
},
{
code: "batchImport",
name: "批量导入",
status: "default",
permissionCode: "customer:my:batchImport"
},
{
code: "edit", // 必传值
name: "编辑", // 必传值 文本(页面展示)
status: "default", // 状态
dependsOnSelection: true, // 是否被勾选框控制 如果值为true代表被checkbox勾选有关 不传或者为false则不被checkbox控制
permissionCode: "customer:my:edit" // 是否有按钮权限。如果不传或者为值为空字符串 代表所有人可见
}
];
// usePermissionButtons 函数为控制toolbar按钮权限处理
const buttons = usePermissionButtons(rawButtons, selectListIds);
const handleLeftBar = val => {
switch (val.code) {
case "addMember":
break;
case "batchImport":
break;
default:
break;
}
};
const getHeader = async () => {
const res = await getTableHeader(ViewName.CRM_MY_CUSTOMER_LIST);
columns.value = res.data.map(i => {
return {
...i,
field: i.key, // 必传
title: i.label, // 必传
width: i.width ?? "100px" // 必传
};
});
};
const initList = () => {
getHeader(); // 获取表头数据
getList(); // 获取列表数据
};
</script>
usePermissionButtons文件
javascript
// usePermissionButtons.js文件代码
import { computed } from "vue";
import { useUserStore } from "@/store/modules/user";
// store
const userStore = useUserStore();
/**
* 根据 rawButtons 和选中状态,生成带权限控制和禁用状态的按钮列表
* @param {Array} rawButtons - 原始按钮配置数组
* @param {Ref<number[]> | Ref<any[]>} selectListIds - 选中的 ID 列表(ref)
* @returns {ComputedRef<Array>} 过滤并处理后的按钮列表
*/
export function usePermissionButtons(rawButtons, selectListIds) {
return computed(() => {
const isEmpty = selectListIds.value.length === 0;
const permissionList = getPermissionCodeList(); // 确保这是响应式的或最新值
if (permissionList[0] === "*:*:*") {
// admin
return rawButtons;
} else {
// 非admin
return rawButtons
.filter(
btn =>
!btn.permissionCode || permissionList.includes(btn.permissionCode)
)
.map(btn => ({
...btn,
disabled: btn.dependsOnSelection ? isEmpty : (btn.disabled ?? false)
}));
}
});
}
// 获取登录人所有的按钮权限
export const getPermissionCodeList = () => {
return userStore.permissions || [];
};
Table组件代码
javascript
<template>
<div class="demo-page-wrapper">
<vxe-grid
ref="gridRef"
v-bind="gridOptions"
@toolbar-button-click="handleLeftToolbar"
@checkbox-all="selectAllChangeEvent"
@checkbox-change="selectChangeEvent"
@cell-click="handleCellClick"
@custom="handleColumnCustom"
>
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
</vxe-grid>
</div>
</template>
<script setup>
import {
ref,
defineProps,
defineExpose,
defineModel,
defineEmits,
computed
} from "vue";
import XEUtils from "xe-utils";
import { headCancel, headSave } from "@/api/view";
import { ElMessage } from "element-plus";
const emits = defineEmits([
"cellClick",
"checkAll",
"check",
"saveSuccess",
"leftBar"
]);
// 定义 props
const props = defineProps({
// 边框线
border: {
type: Boolean,
default: true
},
// 斑马线
stripe: {
type: Boolean,
default: true
},
// 列拖拽
cloumnDrag: {
type: Boolean,
default: false
},
// 自定义拖拽icon
toolDrag: {
type: Boolean,
default: true
},
// 表格高度
height: {
type: [String, Number],
default: "500px" // 也支持%
},
// 每个数据的唯一code
code: { type: String, default: "" },
// 是否展示复选框
showCheckbox: { type: Boolean, default: true },
// 是否展示索引号
showIndex: { type: Boolean, default: false },
// 是否展示操作列
showAction: { type: Boolean, default: false },
// 操作列宽度
actionWidth: { type: Number, default: 100 },
// 需要的插槽 例子: ["name", "id", ....]
slotsFields: {
type: Array,
default: () => {
return [];
}
}
});
// 表格数据
const tableData = defineModel("data", {
default: []
});
// 左侧操作栏
const buttonsList = defineModel("buttons", {
default: []
});
// 表头数据
const column = defineModel("column", {
default: []
});
// 将这些值进行禁用
const disabledKeys = computed(() => {
return column.value.length
? ["checkbox", "seq", "action", column.value[0].field]
: ["checkbox", "seq", "action"];
});
// 处理slot插槽
const processedColumns = computed(() => {
return column.value.map(col => {
// 确保是普通数据列且有 field
if (!col.type && col.field != null) {
if (props.slotsFields.includes(col.field)) {
return {
...col,
slots: { default: col.field }
};
}
}
return col; // 原样返回(包括无 field 的列、type 列等)
});
});
// 使用 computed,确保每次都是最新值
const gridOptions = computed(() => {
const cols = [];
// 复选框列
if (props.showCheckbox) {
cols.push({
type: "checkbox",
width: 40,
fixed: "left",
visible: true,
field: "checkbox" // 该值是为了禁用复制的唯一值
});
}
// 序号列
if (props.showIndex) {
cols.push({
type: "seq",
width: 50,
title: "序号",
fixed: "left",
visible: true,
field: "seq" // 该值是为了禁用复制的唯一值
});
}
// 只加处理后的业务列(已自动注入 slots)
cols.push(...processedColumns.value);
// 操作列
if (props.showAction) {
cols.push({
field: "action",
title: "操作",
width: props.actionWidth,
fixed: "right",
align: "center",
visible: true,
slots: { default: "action" }
});
}
return {
border: props.border,
stripe: props.stripe,
showOverflow: true,
height: props.height,
loading: false,
columnConfig: { drag: props.cloumnDrag, resizable: true },
rowConfig: { isCurrent: true, isHover: true },
columnDragConfig: { trigger: "cell", showGuidesStatus: true },
customConfig: {
// 该列是否允许选中
checkMethod({ column }) {
return !disabledKeys.value.includes(column.field);
}
},
toolbarConfig: {
custom: props.toolDrag,
zoom: false,
buttons: buttonsList.value
},
checkboxConfig: { range: true },
columns: cols,
// columns: cols.filter(i => i.field !== "checkbox" && i.field !== "seq"),
data: tableData.value
};
});
const gridRef = ref(null);
// 选中的项
const selectedRows = ref([]);
// 事件处理
const selectAllChangeEvent = ({ checked }) => {
selectedRows.value = gridRef.value?.getCheckboxRecords() || [];
emits("checkAll", selectedRows.value);
};
const selectChangeEvent = ({ checked }) => {
selectedRows.value = gridRef.value?.getCheckboxRecords() || [];
emits("check", selectedRows.value);
};
// 清空选中的数据
const clearSelectEvent = () => {
gridRef.value?.clearCheckboxRow();
};
// 获取选中的数据
const getSelectEvent = () => {
const records = gridRef.value?.getCheckboxRecords() || [];
console.log(`已选中 ${records.length} 条数据`);
};
// 选中所有
const selectAllEvent = () => {
gridRef.value?.setAllCheckboxRow(true);
};
// 设置自定义勾选数据
const setSelectRow = (rows, checked = true) => {
gridRef.value?.setCheckboxRow(rows, checked);
};
// 单元格点击
const handleCellClick = ({
row,
rowIndex,
$rowIndex,
column,
columnIndex,
$columnIndex,
triggerRadio,
triggerCheckbox,
triggerTreeNode,
triggerExpandNode,
$event
}) => {
emits("cellClick", row, column, row[column.property], column.title);
};
// 自定义筛选icon弹窗事件
const handleColumnCustom = params => {
switch (params.type) {
case "open":
break;
case "confirm": {
// 白名单列表 将操作列和复选框、序号列过滤
const whiteList = new Set(["action", null, undefined]);
// 获取勾选的
// const visibleColumn = gridRef.value?.getColumns() || [];
// 获取所有的 visible来区分是否勾选
const visibleColumn = gridRef.value?.getFullColumns() || [];
const result = visibleColumn
.map(i => {
return {
key: i.field,
fixed: i.fixed,
visible: i.visible,
width: i.width,
title: i.title,
label: i.label,
field: i.field
};
})
.filter(k => !whiteList.has(k.key))
.filter(i => i.field !== "checkbox" && i.field !== "seq");
headSave(result, props.code)
.then(() => {
ElMessage.success("保存成功");
emits("saveSuccess");
})
.catch(e => {
console.error(e);
});
break;
}
case "reset": {
// 恢复默认
headCancel(props.code).then(() => {
emits("saveSuccess");
});
break;
}
case "close": {
break;
}
}
};
const handleLeftToolbar = val => {
const { code, button, $event } = val;
emits("leftBar", button);
};
// 暴露方法
defineExpose({
gridRef,
clearSelectEvent,
getSelectEvent,
selectAllEvent,
setSelectRow
});
</script>
<style lang="scss" scoped>
.demo-page-wrapper {
height: 100%;
padding: 0;
background-color: #fff;
}
</style>