基于Avue Crud组件 二次封装为公共表格组件以及动态配置列显隐
对 Avue Crud 组件的深度二次封装及动态列配置功能详解
在我们的前端项目中,表格是一个非常重要的组件,用于展示各种业务数据。为了提升开发效率和用户体验,我们对 Avue 的 Crud 组件进行了深度的二次封装,并实现了表格列的动态配置功能。本文将详细介绍这一技术实现过程。
为什么需要二次封装
Avue Crud 是一个功能强大的表格组件,提供了丰富的功能,如分页、排序、筛选等。但在实际项目开发中,我们发现直接使用原生组件存在以下问题:
- 重复代码多:每个页面都需要配置相似的选项
- 功能扩展困难:如需要添加全屏、列设置等通用功能
- 样式统一困难:不同页面的表格样式难以保持一致
- 业务逻辑耦合:数据请求、分页处理等逻辑分散在各个页面
因此,我们决定对 Avue Crud 进行二次封装,创建一个名为 TnTable 的组件。
TnTable 组件架构设计
我们的 TnTable 组件主要由以下几个部分组成:
1. 主组件文件 (index.vue)
主组件文件是整个封装的核心,它接收外部传入的配置参数,并处理表格的各种交互逻辑。
2. 列设置组件 (tableColumnSet.vue)
这是一个独立的抽屉组件,用于实现表格列的动态配置功能,包括:
- 列的显示/隐藏
- 列的固定位置设置(左固定、右固定、不固定)
- 列的顺序调整(通过拖拽)
3. 分隔线组件 (tnDivider.vue)
用于在列设置抽屉中分隔不同区域的固定列。
核心功能实现
1. 动态列配置功能详解
动态列配置是本次封装的核心功能之一。用户可以根据自己的需求自定义表格列的显示、隐藏和位置。
实现思路
- 列状态管理:我们将表格列分为三类:左固定列、右固定列和普通列(中间列)
- 可视化配置界面:通过拖拽和按钮操作来调整列的显示状态和位置
- 实时预览:用户操作时实时显示调整效果
- 配置持久化:将用户配置保存到服务器,下次访问时自动加载
详细实现过程
首先,我们在 tableColumnSet.vue 组件中定义了三个数组来分别存储三类列:
const leftFixed = ref([]); // 左固定列 const centerFixed = ref([]); // 普通列(中间) const rightFixed = ref([]); // 右固定列
当用户打开列设置抽屉时,会调用 initDrawer 方法初始化这些数组:
// 初始化 const initDrawer = (columns, tName) => { leftFixed.value = []; centerFixed.value = []; rightFixed.value = []; tableName.value = tName; cloneDeep(columns || []).forEach(item => { if (item.fixed === 'left') { leftFixed.value.push(item); } else if (item.fixed === 'right') { rightFixed.value.push(item); } else { centerFixed.value.push(item); } }); visibleDrawer.value = true; };
用户可以通过以下操作来调整列的配置:
- 添加左侧固定列:
// 添加左侧固定列
const addLeftHandle = item => {
  item.fixed = 'left';
  leftFixed.value.push(item);
  const index = centerFixed.value.findIndex(el => {
    return el.prop === item.prop;
  });
  if (index > -1) {
    centerFixed.value.splice(index, 1);
  }
};
- 取消左侧固定列:
// 取消左侧固定列
const cancelLeftHandle = item => {
  item.fixed = undefined;
  centerFixed.value.push(item);
  const index = leftFixed.value.findIndex(el => {
    return el.prop === item.prop;
  });
  if (index > -1) {
    leftFixed.value.splice(index, 1);
  }
};
- 拖拽排序:通过 vuedraggable组件实现列的拖拽排序功能
列配置持久化
用户对列的配置会被保存到服务器,下次访问时会自动加载用户的个性化设置:
// 保存列设置 const saveColumn = (columns, tableName) => { let colInfo = columns.map(item => ({ prop: item.prop, visible: item.visible, fixed: item.fixed, hide: !item.visible, width: item.width, minWidth: item.minWidth, })); // 参数校验 if (!tableName?.length) return ElMessage.warning('请联系系统管理员补充表格配置参数'); if (colInfo.filter(item => item.visible).length === 0) return ElMessage.warning('表格配置展示列不能低于一列'); // 根据操作类型调用不同API let operateMethod, params; if (props.displayColParams.method === 'save') { operateMethod = displayColumnDetailSave; params = { bizCode: tableName, displayColumn: JSON.stringify(colInfo), }; } else if (props.displayColParams.method === 'update') { operateMethod = displayColumnDetailUpdate; params = { id: props.displayColParams.id, bizCode: tableName, displayColumn: JSON.stringify(colInfo), version: props.displayColParams.version, }; } // 发送请求并处理结果 operateMethod && operateMethod(params).then(res => { if (res.data.success) { ElMessage.success('操作成功'); handleCancel(); emits('updateColumn', columns); emits('getDisplayCols'); } }); };
2. 主组件中的列配置处理
在主组件 index.vue 中,我们需要处理列配置的加载和应用:
// 获取显示列配置 const getDisplayCols = () => { displayColumnDetail({ bizCode: tableOption.value.name }).then(({ data }) => { if (data.success) { if (data.data && Object.keys(data.data).length) { // 已有配置,更新为修改模式 displayColParams.value.method = 'update'; displayColParams.value.version = data?.data?.version; displayColParams.value.id = data?.data?.id; let displayColumn = JSON.parse(data?.data?.displayColumn || '[]'); const columns = props.tableColumns; const newColumns = []; // 根据配置更新列属性 displayColumn.forEach(el => { const ele = columns?.find(item => item.prop === el.prop); if (ele) { ele.fixed = el.fixed || null; ele.hide = el.hide || false; ele.visible = el.visible || !el.hide; ele.width = el.width || null; ele.minWidth = el.minWidth || null; newColumns.push(ele); } }); tableOption.value.column = newColumns; updateColumn(); } else { // 无配置,使用默认配置并切换到保存模式 tableOption.value.column = props.tableColumns; updateColumn(); displayColParams.value.method = 'save'; } } }); };
3. 响应式设计和用户体验优化
我们还添加了多项用户体验优化功能:
<template> <div class="tn-table-operate-btns" v-if="tableOption.needToolbar"> <el-tooltip effect="dark" :content="getIconText(2)" v-if="tableOption.needShowFullscreenBtn"> <svg-icon :name="isFullscreen ? 'tuichuquanping' : 'quanping'" size="20" @click="handleFullscreenChange" color="#444444"></svg-icon> </el-tooltip> <el-tooltip effect="dark" content="表格刷新" v-if="tableOption.needShowRefreshBtn"> <svg-icon name="lieshuaxin" size="20" @click="handleRefreshTable" color="#444444"></svg-icon> </el-tooltip> <el-tooltip effect="dark" :content="getIconText(1)" v-if="tableOption.needShowTableModeBtn && !isFullscreen"> <svg-icon name="newCard" size="20" @click="handleChangeTableMode" color="#444444"></svg-icon> </el-tooltip> <el-tooltip effect="dark" content="表格列设置" v-if="tableOption.needShowColumnSettingBtn && !isFullscreen"> <svg-icon name="liepeizhi" size="20" @click="handleTableColumnSetting" color="#444444"></svg-icon> </el-tooltip> </div> </template>
包括:
- 全屏显示/退出
- 表格刷新
- 卡片/列表模式切换
- 列设置
4. CRUD 二次封装的其他功能
除了列配置功能,我们的二次封装还包括以下特性:
- 统一的数据请求处理:
// 获取表格数据 const getTableList = async () => { // 执行加载表格数据之前处理的自定义函数 if (props.beforeLoadTable) await props.beforeLoadTable(); tableLoading.value = true; try { const { currentPage, pageSize } = pageInfo.value; // 外部查询条件表单 const _params = props.customParams; const { data: { data: reqData, success }, } = tableOption.value.needPagination && props.isGetFn ? await props.tableFn(currentPage, pageSize, _params) : await props.tableFn(_params); if (success) { // customHandleTableData 表示部分场景需要业务层对接口返回数据做数据二次处理 tableData.value = props.customHandleTableData ? props.customHandleTableData(reqData.records) : reqData.records; pageInfo.value.total = tableOption.value.needPagination ? reqData.total : 0; } } catch (e) { tableData.value = []; pageInfo.value.total = 0; } finally { tableLoading.value = false; } };
- 分页处理:
// 分页事件 const currentChange = (currentPage: number) => { pageInfo.value.currentPage = currentPage; // 部分业务场景需要这个配置,列表只展示数据,但是需要允许分页 if (!props.initRequest && props.tableDataTotal > 0) { emits('currentChange', currentPage); } }; // 每页条数事件 const sizeChange = (pageSize: number) => { pageInfo.value.pageSize = pageSize; // 部分业务场景需要这个配置,列表只展示数据,但是需要允许分页 if (!props.initRequest && props.tableDataTotal > 0) { emits('sizeChange', pageSize); } };
- 列宽拖拽调整:
// 表头拖拽事件(改变列宽度) const handleDragendHeader = (newWidth: number, oldWidth: number, column: object) => { tableOption.value.column = tableOption.value.column.map((item: any) => { if (item.prop === column.property) item.width = newWidth; return item; }); // 保存调整后的列宽 let _newColumnList = cloneDeep(tableOption.value.column); // ... 添加序号列和操作列 _newColumnList = _newColumnList.map(item => { return { ...item, visible: !item.hide, }; }); tableColumnSetDrawer.value?.saveColumn(_newColumnList, tableOption.value.name); };
总结
通过对 Avue Crud 组件的深度二次封装,我们实现了以下目标:
- 提高开发效率:减少了重复代码,统一了表格使用方式
- 增强用户体验:提供了列动态配置、全屏显示等实用功能
- 便于维护:将表格相关逻辑集中管理,便于后续功能扩展和bug修复
- 个性化定制:用户可以根据自己的需求自定义表格列的显示方式
特别是动态列配置功能,让用户能够根据自己的使用习惯调整表格显示,大大提升了用户体验。通过将配置持久化到服务器,用户在不同设备和浏览器中都能获得一致的个性化体验。
这一封装在我们的项目中得到了广泛应用,显著提升了开发效率和用户体验。未来我们还计划进一步优化,比如增加更多的表格交互功能、支持更多类型的列格式化等。
表格组件源码(index.vue)
 
<script setup lang="ts"> import { ref, watch, useAttrs, nextTick } from 'vue'; import TableColumnSet from '@/components/tn-table/components/tableColumnSet.vue'; import { cloneDeep } from 'lodash-es'; import { displayColumnDetail } from '@/api/system/displayColumn'; const props = defineProps({ // 表格列配置 tableColumns: { type: Array, default: () => [], }, // 表格额外配置 tableOption: { type: Object, default: () => { return { name: null, }; }, }, // 表格数据请求 tableFn: { type: Function, default: () => {}, }, // 外部搜索条件 customParams: { type: Object, default: () => {}, }, // 是否初始化请求 initRequest: { type: Boolean, default: true, }, // 表格绑定数据 tableData: { type: Array, default: () => [], }, // 表格数据处理 customHandleTableData: { type: Function, default: null, }, // 加载表格数据之前处理的自定义函数 beforeLoadTable: { type: Function, default: null, }, // 表格列显隐组件宽度 drawerSize: { type: String, default: '20%', }, // 表格数据总数(针对手动绑定数据且需要展示分页场景) tableDataTotal: { type: Number, default: 0, }, // 是否GET请求,部分业务场景使用POST请求 isGetFn: { type: Boolean, default: true, }, }); interface PageInstance { currentPage: number; pageSize: number; total?: number; } const emits = defineEmits(['currentChange', 'sizeChange', 'refreshChange', 'expandChange']); const attrs = useAttrs(); const tableColumnSetDrawer = ref(null); //表格列配置组件 const crudTable = ref(null); // 表格组件 const tableLoading = ref<boolean>(false); // 表格加载 const tableData = ref<any>([]); // 表格数据 const pageInfo = ref<PageInstance>({ currentPage: 1, pageSize: 10, total: 0, }); // 分页信息 const tableOption = ref({ tip: false, search: false, showHeader: true, header: false, border: true, index: true, indexLabel: '序号', indexWidth: 60, addBtn: false, editBtn: false, delBtn: false, menu: true, menuWidth: 150, menuAlign: 'left', height: 'auto', calcHeight: 60, dialogClickModal: false, grid: false, needPagination: true, // 是否需要分页 needToolbar: true, // 是否需要工具栏 needShowRefreshBtn: true, // 是否显示刷新按钮 needShowColumnSettingBtn: true, // 是否显示列设置 needShowFullscreenBtn: true, // 是否显示全屏设置按钮 needShowTableModeBtn: true, // 是否显示表格模式切换按钮 ...props.tableOption, column: props.tableColumns, }); // 表格配置 const isFullscreen = ref(false); // 是否全屏 const displayColParams = ref({}); // 列显隐参数 const originalTableColumns = ref([]); watch( () => props.tableColumns, () => { // 监听列配置,自动补充最小宽度属性以适应展示 tableOption.value.column = (props.tableColumns || []).map(item => { let _newItemInfo = item; if (!item.width && !item.minWidth) _newItemInfo.minWidth = 150; return _newItemInfo; }); // 记录页面配置基础固定表格列配置 originalTableColumns.value = cloneDeep(tableOption.value.column); }, { immediate: true, deep: true } ); watch( () => props.tableOption?.name, newVal => { if (newVal) { tableOption.value.name = newVal; // 如果有其他需要重新执行的逻辑 nextTick(() => { getDisplayCols(); }); } }, { immediate: true } ); // 表头拖拽事件(改变列宽度) const handleDragendHeader = (newWidth: number, oldWidth: number, column: object) => { tableOption.value.column = tableOption.value.column.map((item: any) => { if (item.prop === column.property) item.width = newWidth; return item; }); let _newColumnList = cloneDeep(tableOption.value.column); if (tableOption.value.index) { _newColumnList.unshift({ width: tableOption.value.indexWidth, label: tableOption.value.indexLabel, prop: 'rowNo', fixed: 'left', }); } if (tableOption.value.menu) { _newColumnList.push({ prop: 'edit', label: '操作', width: tableOption.value.menuWidth, fixed: 'right', }); } _newColumnList = _newColumnList.map(item => { return { ...item, visible: !item.hide, }; }); tableColumnSetDrawer.value?.saveColumn(_newColumnList, tableOption.value.name); }; // 分页事件 const currentChange = (currentPage: number) => { pageInfo.value.currentPage = currentPage; // 部分业务场景需要这个配置,列表只展示数据,但是需要允许分页 if (!props.initRequest && props.tableDataTotal > 0) { emits('currentChange', currentPage); } }; // 每页条数事件 const sizeChange = (pageSize: number) => { pageInfo.value.pageSize = pageSize; // 部分业务场景需要这个配置,列表只展示数据,但是需要允许分页 if (!props.initRequest && props.tableDataTotal > 0) { emits('sizeChange', pageSize); } }; // 获取表格数据 const getTableList = async () => { // 执行加载表格数据之前处理的自定义函数 if (props.beforeLoadTable) await props.beforeLoadTable(); tableLoading.value = true; try { const { currentPage, pageSize } = pageInfo.value; // 外部查询条件表单 const _params = props.customParams; const { data: { data: reqData, success }, } = tableOption.value.needPagination && props.isGetFn ? await props.tableFn(currentPage, pageSize, _params) : await props.tableFn(_params); if (success) { // customHandleTableData 表示部分场景需要业务层对接口返回数据做数据二次处理 tableData.value = props.customHandleTableData ? props.customHandleTableData(reqData.records) : reqData.records; pageInfo.value.total = tableOption.value.needPagination ? reqData.total : 0; } } catch (e) { tableData.value = []; pageInfo.value.total = 0; } finally { tableLoading.value = false; } }; // 表格加载事件 const tableOnload = async () => { if (props.initRequest) { await getTableList(); } else { updateTableData(); } }; // 手动赋值修改表格绑定数据 const updateTableData = () => { tableData.value = props.tableData; // 部分业务场景需要这个配置 if (props.tableDataTotal > 0) pageInfo.value.total = props.tableDataTotal; }; // 表格刷新 const handleRefreshTable = (isResetPageInfo = true) => { if (props.initRequest) { if (isResetPageInfo) pageInfo.value.currentPage = 1; getTableList(); } else { if (props.tableDataTotal > 0) emits('refreshChange'); } }; // 获取图标文字 const getIconText = type => { if (type === 1) return `${tableOption.value.grid ? '列表' : '卡片'}模式`; if (type === 2) return `${isFullscreen.value ? '退出全屏' : '全屏'}模式`; }; // 切换表格模式 const handleChangeTableMode = () => { tableOption.value.grid = !tableOption.value.grid; }; // 全屏切换 const handleFullscreenChange = async () => { if (!isFullscreen.value) { isFullscreen.value = true; tableOption.value.height = '100%'; } else { isFullscreen.value = false; tableOption.value.height = 'auto'; } }; // 表格列设置 const handleTableColumnSetting = () => { let _newColumnList = cloneDeep(tableOption.value.column); _newColumnList = _newColumnList.map(item => { return { ...item, visible: !item.hide, }; }); tableColumnSetDrawer.value?.initDrawer(_newColumnList, tableOption.value.name); }; // 列显隐保存之后页面重新加载 const updateColumn = () => { crudTable.value?.refreshTable(); }; // 重置列 恢复默认 const restDefault = () => { let _newColumnList = cloneDeep(originalTableColumns.value); if (tableOption.value.index) { _newColumnList.unshift({ width: tableOption.value.indexWidth, label: tableOption.value.indexLabel, prop: 'rowNo', fixed: 'left', }); } if (tableOption.value.menu) { _newColumnList.push({ prop: 'edit', label: '操作', width: tableOption.value.menuWidth, fixed: 'right', }); } _newColumnList = _newColumnList.map(item => { return { ...item, visible: !item.hide, }; }); tableColumnSetDrawer.value?.saveColumn(_newColumnList, tableOption.value.name); tableColumnSetDrawer.value?.handleCancel(); }; const getDisplayCols = () => { displayColumnDetail({ bizCode: tableOption.value.name }).then(({ data }) => { if (data.success) { if (data.data && Object.keys(data.data).length) { displayColParams.value.method = 'update'; displayColParams.value.version = data?.data?.version; displayColParams.value.id = data?.data?.id; let displayColumn = JSON.parse(data?.data?.displayColumn || '[]'); const columns = props.tableColumns; const newColumns: any[] = []; displayColumn.forEach(el => { const ele = columns?.find(item => item.prop === el.prop); if (ele) { ele.fixed = el.fixed || null; ele.hide = el.hide || false; ele.visible = el.visible || !el.hide; ele.width = el.width || null; ele.minWidth = el.minWidth || null; newColumns.push(ele); } }); tableOption.value.column = newColumns; updateColumn(); } else { tableOption.value.column = props.tableColumns; updateColumn(); displayColParams.value.method = 'save'; } } }); }; // 获取表格配置列展示 const getTableColumns = () => { return crudTable.value?.columnOption; }; // 清除选中 const handleClearSelection = () => { crudTable.value?.clearSelection(); }; // 修改表格配置属性 const updateTableOption = (updateOption: any) => { const _newOption = cloneDeep(updateOption); if (_newOption.hasOwnProperty('column')) delete _newOption.column; tableOption.value = { ...tableOption.value, ..._newOption }; }; const getValueByDic = (dicData: any[], value: string, props: { label: 'dictValue'; value: 'dictKey' }, prop) => { if (value !== null && value !== undefined) { return dicData.find(item => value == item[props.value])?.[props.label]; } }; const handleExpandChange = (row, expendList) => { emits('expandChange', { row, expendList }); }; defineExpose({ tableData, pageInfo, crudTable, getTableList, getTableColumns, handleRefreshTable, handleClearSelection, tableOnload, updateColumn, updateTableData, updateTableOption, }); </script> <template> <avue-crud class="reset-table-style" :class="{ 'fullscreen-table': isFullscreen }" :option="tableOption" :table-loading="tableLoading" :data="tableData" ref="crudTable" v-model:page="pageInfo" v-bind="attrs" @current-change="currentChange" @size-change="sizeChange" @on-load="tableOnload" @header-dragend="handleDragendHeader" @expand-change="handleExpandChange" > <template #expand="{ row, index }"> <slot name="expand" v-bind="{ row, index }"></slot> </template> <template #page="{ row }"> <div class="tn-table-operate-btns" v-if="tableOption.needToolbar"> <el-tooltip effect="dark" :content="getIconText(2)" v-if="tableOption.needShowFullscreenBtn"> <svg-icon :name="isFullscreen ? 'tuichuquanping' : 'quanping'" size="20" @click="handleFullscreenChange" color="#444444"></svg-icon> </el-tooltip> <el-tooltip effect="dark" content="表格刷新" v-if="tableOption.needShowRefreshBtn"> <svg-icon name="lieshuaxin" size="20" @click="handleRefreshTable" color="#444444"></svg-icon> </el-tooltip> <el-tooltip effect="dark" :content="getIconText(1)" v-if="tableOption.needShowTableModeBtn && !isFullscreen"> <svg-icon name="newCard" size="20" @click="handleChangeTableMode" color="#444444"></svg-icon> </el-tooltip> <el-tooltip effect="dark" content="表格列设置" v-if="tableOption.needShowColumnSettingBtn && !isFullscreen"> <svg-icon name="liepeizhi" size="20" @click="handleTableColumnSetting" color="#444444"></svg-icon> </el-tooltip> </div> </template> <template #menu="{ row, index, size }"> <slot name="menu" v-bind="{ row, index, size }"></slot> </template> <template v-for="col in tableOption.column" #[col.prop]="{ row, index }"> <slot :name="col.prop" v-bind="{ row, index }"> <span v-if="col.dicData?.length">{{ getValueByDic(col.dicData, row[col.prop], col.props, col.prop) }}</span> <span v-else>{{ row[col.prop] }}</span> </slot> </template> <template v-for="col in tableOption.column" #[`${col.prop}-header`]="{ column }"> <slot :name="`${col.prop}-header`" v-bind="{ column }"> <span>{{ column?.label || '' }}</span> </slot> </template> </avue-crud> <table-column-set ref="tableColumnSetDrawer" :drawer-size="drawerSize" :displayColParams="displayColParams" @updateColumn="updateColumn" @restDefault="restDefault" @getDisplayCols="getDisplayCols" /> </template> <style scoped lang="scss"> @import 'css/tableStyle'; </style>
表格列设置源码(table-column-set.vue)
 
<script setup lang="ts"> import { ref } from 'vue'; import Draggable from 'vuedraggable'; import TnDivider from '@/components/tn-table/components/tnDivider.vue'; import { cloneDeep } from 'lodash-es'; import { ElMessage } from 'element-plus'; import { displayColumnDetailSave, displayColumnDetailUpdate } from '@/api/system/displayColumn'; const tableName = ref(''); const visibleDrawer = ref(false); const leftFixed = ref<any[]>([]); const centerFixed = ref<any[]>([]); const rightFixed = ref<any[]>([]); const hoverItem = ref(); const emits = defineEmits(['restDefault', 'updateColumn', 'getDisplayCols']); const props = defineProps({ displayColParams: { type: Object, default: () => {} }, drawerSize: { type: String, default: '20%', }, }); // 鼠标悬停 const mouseOverHandle = item => { hoverItem.value = item; }; // 添加左侧固定列 const addLeftHandle = item => { item.fixed = 'left'; leftFixed.value.push(item); const index = centerFixed.value.findIndex(el => { return el.prop === item.prop; }); if (index > -1) { centerFixed.value.splice(index, 1); } }; // 取消左侧固定列 const cancelLeftHandle = item => { item.fixed = undefined; centerFixed.value.push(item); const index = leftFixed.value.findIndex(el => { return el.prop === item.prop; }); if (index > -1) { leftFixed.value.splice(index, 1); } }; // 添加右侧固定列 const addRightHandle = item => { item.fixed = 'right'; rightFixed.value.splice(0, 0, item); const index = centerFixed.value.findIndex(el => { return el.prop === item.prop; }); if (index > -1) { centerFixed.value.splice(index, 1); } }; // 取消右侧固定列 const cancelRightHandle = item => { item.fixed = undefined; centerFixed.value.push(item); const index = rightFixed.value.findIndex(el => { return el.prop === item.prop; }); if (index > -1) { rightFixed.value.splice(index, 1); } }; // 初始化 const initDrawer = (columns, tName) => { leftFixed.value = []; centerFixed.value = []; rightFixed.value = []; tableName.value = tName; cloneDeep(columns || []).forEach(item => { if (item.fixed === 'left') { leftFixed.value.push(item); } else if (item.fixed === 'right') { rightFixed.value.push(item); } else { centerFixed.value.push(item); } }); visibleDrawer.value = true; }; // 取消 const handleCancel = () => { visibleDrawer.value = false; }; // 恢复默认 const handleSetDefault = () => { emits('restDefault'); }; // 保存 const handleUpdate = () => { const columns = [...leftFixed.value, ...centerFixed.value, ...rightFixed.value]; saveColumn(columns, tableName.value); }; // 保存列设置 const saveColumn = (columns, tableName) => { let colInfo = columns.map(item => ({ prop: item.prop, visible: item.visible, fixed: item.fixed, hide: !item.visible, width: item.width, minWidth: item.minWidth, })); if (!tableName?.length) return ElMessage.warning('请联系系统管理员补充表格配置参数'); if (colInfo.filter(item => item.visible).length === 0) return ElMessage.warning('表格配置展示列不能低于一列'); let operateMethod, params; if (props.displayColParams.method === 'save') { operateMethod = displayColumnDetailSave; params = { bizCode: tableName, displayColumn: JSON.stringify(colInfo), }; } else if (props.displayColParams.method === 'update') { operateMethod = displayColumnDetailUpdate; params = { id: props.displayColParams.id, bizCode: tableName, displayColumn: JSON.stringify(colInfo), version: props.displayColParams.version, }; } operateMethod && operateMethod(params).then(res => { if (res.data.success) { ElMessage.success('操作成功'); handleCancel(); emits('updateColumn', columns); emits('getDisplayCols'); } }); }; defineExpose({ initDrawer, saveColumn, handleCancel, }); </script> <template> <el-drawer v-model="visibleDrawer" :show-close="false" :size="drawerSize" class="column-drawer-box"> <template #header> <div class="drawer-title"> <span>列设置</span> <el-tooltip content="“序号”及“操作”首尾标题列默认固定且无法取消及排序"> <el-icon class="column-info-icon"><QuestionFilled /></el-icon> </el-tooltip> </div> </template> <div class="drawer-content"> <div class="left-fixed-content"> <Draggable item-key="key" ghost-class="ghost" handle=".drag-widget" :animation="200" :group="{ name: 'center' }" :list="leftFixed"> <template #item="{ element }"> <div v-if="element.label" class="item-content" :class="{ actived: hoverItem?.prop === element.prop }" @mouseover="mouseOverHandle(element)"> <div class="columns-left"> <i v-if="!['rowNo', 'edit'].includes(element.prop)" class="iconfont icon-tuozhuaipaixu drag-widget"></i> <span v-else class="empty-span"></span> <el-checkbox v-model="element.visible" :disabled="['rowNo', 'edit'].includes(element.prop)"></el-checkbox> <p>{{ element.label }}</p> </div> <div class="columns-right"> <div :class="{ disabled: element.prop === 'rowNo' }"> <el-tooltip content="取消左侧固定" effect="dark" placement="top-start"> <i class="iconfont icon-zuoceguding" @click="cancelLeftHandle(element)"></i> </el-tooltip> </div> </div> </div> </template> </Draggable> </div> <tn-divider>以上为左固定标题列</tn-divider> <div class="center-fixed-content"> <Draggable item-key="key" ghost-class="ghost" handle=".drag-widget" :animation="200" :group="{ name: 'center' }" :list="centerFixed"> <template #item="{ element }"> <div class="item-content" :class="{ actived: hoverItem?.prop === element.prop }" @mouseover="mouseOverHandle(element)"> <div class="columns-left"> <i v-if="!['rowNo', 'edit'].includes(element.prop)" class="iconfont icon-tuozhuaipaixu drag-widget"></i> <span v-else class="empty-span"></span> <el-checkbox v-model="element.visible" :disabled="['rowNo', 'edit'].includes(element.prop)"></el-checkbox> <p>{{ element.label }}</p> </div> <div v-if="hoverItem?.prop === element.prop" class="columns-right"> <el-tooltip content="左侧固定" effect="dark" placement="top-start"> <i class="iconfont icon-youceguding" @click="addLeftHandle(element)"></i> </el-tooltip> <el-tooltip content="右侧固定" effect="dark" placement="bottom-start"> <i class="iconfont icon-zuoceguding" @click="addRightHandle(element)"></i> </el-tooltip> </div> </div> </template> </Draggable> </div> <tn-divider>以下为右固定标题列</tn-divider> <div class="right-fixed-content"> <Draggable item-key="key" ghost-class="ghost" handle=".drag-widget" :animation="200" :group="{ name: 'center' }" :list="rightFixed"> <template #item="{ element }"> <div class="item-content" :class="{ actived: hoverItem?.prop === element.prop }" @mouseover="mouseOverHandle(element)"> <div class="columns-left"> <i v-if="!['rowNo', 'edit'].includes(element.prop)" class="iconfont icon-tuozhuaipaixu drag-widget"></i> <span v-else class="empty-span"></span> <el-checkbox v-model="element.visible" :disabled="['rowNo', 'edit'].includes(element.prop)"></el-checkbox> <p>{{ element.label }}</p> </div> <div class="columns-right"> <div :class="{ disabled: element.prop === 'edit' }"> <el-tooltip content="取消右侧固定" effect="dark" placement="top-start"> <i class="iconfont icon-zuoceguding" @click="cancelRightHandle(element)"></i> </el-tooltip> </div> </div> </div> </template> </Draggable> </div> </div> <template #footer> <div class="drawer-footer"> <el-button @click="handleCancel">取消</el-button> <el-button type="primary" @click="handleSetDefault">恢复默认</el-button> <el-button type="primary" @click="handleUpdate">保存</el-button> </div> </template> </el-drawer> </template> <style lang="scss"> .column-drawer-box { width: 350px !important; .el-drawer__header, .el-drawer__footer { padding: 0; margin: 0; } .el-drawer__body { padding: 20px 0 20px 20px; overflow-y: hidden; } .drawer-title { font-size: 16px; font-weight: 700; color: #000000; text-align: left; border-bottom: 1px solid #ebebeb; padding: 13px 15px !important; span { margin: 0 5px 0 0; } .column-info-icon { color: #aeafb2; cursor: pointer; font-size: 14px; &:hover { color: #1472ff; } } } .drawer-footer { height: 48px; display: flex; flex-direction: row; align-items: center; justify-content: flex-end; padding: 0 10px; border-top: 1px solid #ebebeb; .el-button + .el-button { margin-left: 8px; } } .drawer-content { height: calc(100vh - 130px); overflow-y: auto; .left-fixed-content, .right-fixed-content { .columns-right { .iconfont { color: #1472ff; } } } .center-fixed-content { .columns-right { .iconfont { color: #888888; } } } .item-content { display: flex; justify-content: space-between; border-radius: 4px; align-items: center; --vxe-primary-color: #1472ff; .columns-left { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; p { color: #444; margin-left: 5px; font-size: 14px; } .drag-widget { color: #aeafb2; cursor: move; } .empty-span { display: inline-block; width: 16px; } } .columns-right { line-height: 36px; .iconfont { cursor: pointer; } .iconfont:hover { background-color: #cadfff; color: var(--vxe-primary-color); } .disabled { cursor: not-allowed; .iconfont { pointer-events: none; opacity: 0.7; } } } } } .actived { background-color: #eef5ff; } } </style>
表格列配置区域分割线源码 (tn-divider.vue)
 
<template> <div :class="[ `m-divider ${orientation}`, { dashed: dashed, margin24: !showText, marginLeft: orientationMargin !== '' && orientation === 'left', marginRight: orientationMargin !== '' && orientation === 'right', }, ]" :style="`--border-width: ${borderWidth}px;`" > <span v-if="orientation === 'left'" v-show="showText" ref="text" class="u-text" :style="`margin-left: ${margin};`"> <slot></slot> </span> <span v-else-if="orientation === 'right'" v-show="showText" ref="text" class="u-text" :style="`margin-right: ${margin};`"> <slot></slot> </span> <span v-else v-show="showText" ref="text" class="u-text"> <slot></slot> </span> </div> </template> <script setup lang="ts"> import { ref, onMounted, computed } from 'vue'; interface Props { dashed?: boolean; // 是否为虚线 orientation?: string; // 分割线标题的位置 orientationMargin?: string | number; // 标题和最近 left/right 边框之间的距离,去除了分割线,同时 orientation 必须为 left 或 right borderWidth?: number; // 分割线宽度 } const props = withDefaults(defineProps<Props>(), { dashed: false, orientation: 'center', // 可选 left center right orientationMargin: '', borderWidth: 1, }); const text = ref(); const margin = computed(() => { if (props.orientationMargin !== '') { if (typeof props.orientationMargin === 'number') { return props.orientationMargin + 'px'; } return props.orientationMargin; } return 0; }); const showText = computed(() => { if (text.value) { const flag = text.value.offsetHeight; if (!flag) return false; } return true; }); </script> <style lang="scss" scoped> * { box-sizing: border-box; margin: 0; padding: 0; } .m-divider { display: flex; align-items: center; margin: 12px 0; width: 100%; min-width: 100%; &:before, &:after { position: relative; width: 50%; border-top-width: 1px; border-top-style: solid; border-top-color: #ededed; transform: translateY(50%); content: ''; } .u-text { display: inline-block; font-size: 12px; color: #96989b; font-weight: 500; line-height: 12px; white-space: nowrap; text-align: center; padding: 0 12px; } } .dashed { &:before { border-top-style: dashed; } &:after { border-top-style: dashed; } } .left { &:before { width: 5%; } &:after { width: 95%; } } .right { &:before { width: 95%; } &:after { width: 5%; } } .margin24 { margin: 24px 0; } .marginLeft { &:before { width: 0; } &:after { width: 100%; } } .marginRight { &:before { width: 100%; } &:after { width: 0; } } </style>

 
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号