封装一个支持动态表头与权限控制的通用 VxeTable 组件

项目背景 基础需求: 我这边的需求是要把表格的表头设置为动态的情况、并且允许默写列进行显示隐藏、冻结、排序、默认某几项进行禁止取消勾选。 在这个基础需求上需要兼容按钮权限、以及每一个单元格内的点击事件和toolbar的按钮动态禁止功能

动态表头配置
不同角色、不同场景下,用户对字段的关注点不同,需支持运行时动态调整表头结构,包括列的显示/隐藏、顺序调整、宽度记忆等。 精细化列控制 允许部分业务列(如"备注""标签")被用户自由隐藏; 关键列(如首列、操作列、复选框、序号)必须始终可见,禁止在列设置中取消勾选; 支持列冻结(固定左/右)、排序、宽度拖拽等交互能力。 深度交互与权限集成 每个单元格需支持独立点击事件(如跳转详情、编辑内联); 表格工具栏(Toolbar)按钮需按用户权限动态渲染(兼容 v-permission 等指令); 操作列中的按钮同样需支持权限控制与自定义插槽。 配置持久化

用户对表头的个性化设置(如隐藏了哪些列、调整了哪些顺序)应自动保存至服务端,并在下次访问时还原,提升使用体验。

效果图

图片

视频效果

live.csdn.net/v/503221

版本号

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)" />&nbsp; {{ 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>
相关推荐
某只天落1 小时前
Vue2 通用文件在线预览下载组件:一站式解决多类型文件处理需求(支持视频、文档、图片、Office)
前端
AY呀1 小时前
黑马喽大闹天宫与JavaScript的寻亲记:作用域与作用域链全解析
前端·javascript·面试
金融数据出海1 小时前
日本股票市场渲染 KlineCharts K 线图
前端·后端
是Yu欸1 小时前
DevUI MateChat 技术演进:UI 与逻辑解耦的声明式 AI 交互架构
前端·人工智能·ui·ai·前端框架·devui·metachat
梦想CAD控件1 小时前
AI生成CAD图纸(云原生CAD+AI让设计像聊天一样简单)
前端·javascript·vue.js
栀秋6661 小时前
JavaScript 中的 简单数据类型:Symbol——是JavaScript成熟的标志
前端
Nayana1 小时前
前端控制批量请求并发
前端
ssjlincgavw1 小时前
前端高手进阶:从十万到千万,我的性能优化终极指南(实战篇)
前端
比老马还六1 小时前
Bipes项目二次开发/设置功能-1(五)
前端·javascript