vue+element实现自定义表头显示隐藏

vue+element实现自定义表头显示隐藏

效果

组件:
java 复制代码
<template>
  <div class="top-right-btn" :style="style">
    <el-row>
      <el-tooltip v-if="search" class="item" effect="dark" :content="showSearch ? '隐藏搜索表单' : '显示搜索表单'" placement="top">
        <el-button type="text" :icon="showSearch ? 'Hide' : 'View'" class="search-btn" @click="toggleSearch()" />
      </el-tooltip>
      <el-tooltip v-if="showRefresh" class="item" effect="dark" content="刷新" placement="top">
        <el-button type="text" icon="Refresh" @click="refresh()" />
      </el-tooltip>
      <el-tooltip v-if="columns" class="item" effect="dark" content="显示/隐藏列" placement="top">
        <div class="show-btn">
          <el-popover placement="bottom" trigger="click" width="250" @show="handlePopoverShow">
            <div class="popover-content">
              <div class="tree-header">显示/隐藏列</div>

              <!-- 添加滚动条的树容器 -->
              <div class="tree-container">
                <el-tree
                  ref="columnRef"
                  :data="columns"
                  show-checkbox
                  check-on-click-node
                  node-key="key"
                  :props="{ label: 'label', children: 'children' }"
                  @check="columnChange"
                >
                  <template #default="{ node, data }">
                    <span class="custom-tree-node" :title="data.label">
                      {{ node.label }}
                    </span>
                  </template>
                </el-tree>
              </div>

              <!-- 底部按钮区域,包含全选和操作按钮 -->
              <div class="popover-footer">
                <div class="footer-left">
                  <el-checkbox v-model="checkAll" :indeterminate="isIndeterminate" size="small" @change="handleCheckAllChange">全选</el-checkbox>
                </div>
                <div class="footer-right">
                  <el-button size="small" style="font-size: 12px; margin-right: -8px; margin-left: 5px" @click="resetToDefault">恢复默认</el-button>
                  <el-button size="small" style="font-size: 12px" type="primary" :loading="saving" @click="handleSave">保存配置</el-button>
                </div>
              </div>
            </div>
            <template #reference>
              <el-button type="text" icon="Setting" />
            </template>
          </el-popover>
        </div>
      </el-tooltip>
    </el-row>
  </div>
</template>

<script setup lang="ts">
import { propTypes } from '@/utils/propTypes';
import { ElMessage } from 'element-plus';

const props = defineProps({
  showSearch: propTypes.bool.def(true),
  showRefresh: propTypes.bool.def(true),
  columns: propTypes.fieldOption,
  search: propTypes.bool.def(true),
  gutter: propTypes.number.def(10),
  // 表格标识,用于保存到后端时区分不同表格
  tableKey: propTypes.string.def(''),
  // 是否显示保存按钮
  showSave: propTypes.bool.def(true),
  // 树形控件最大高度
  treeMaxHeight: propTypes.string.def('300px')
});

const columnRef = ref<ElTreeInstance>();
const emits = defineEmits(['update:showSearch', 'queryTable', 'update:columns', 'save']);

// 存储默认列配置
const defaultColumns = ref<any[]>([]);
const saving = ref(false);
const checkAll = ref(false);
const isIndeterminate = ref(false);

const style = computed(() => {
  const ret: any = {};
  if (props.gutter) {
    ret.marginRight = `${props.gutter / 2}px`;
  }
  return ret;
});

// 更新全选状态
const updateCheckAllStatus = () => {
  if (!props.columns || props.columns.length === 0) return;

  const checkedCount = props.columns.filter((item) => item.visible).length;
  const totalCount = props.columns.length;

  if (checkedCount === 0) {
    checkAll.value = false;
    isIndeterminate.value = false;
  } else if (checkedCount === totalCount) {
    checkAll.value = true;
    isIndeterminate.value = false;
  } else {
    checkAll.value = false;
    isIndeterminate.value = true;
  }
};

// 全选/取消全选
const handleCheckAllChange = (val: boolean) => {
  // 更新所有列的visible状态
  props.columns?.forEach((item) => {
    item.visible = val;
  });

  // 触发更新事件
  emits('update:columns', props.columns);

  // 更新树形控件的选中状态
  nextTick(() => {
    props.columns?.forEach((item) => {
      if (item.visible) {
        columnRef.value?.setChecked(item.key, true, false);
      } else {
        columnRef.value?.setChecked(item.key, false, false);
      }
    });
  });

  isIndeterminate.value = false;
};

// 搜索
function toggleSearch() {
  emits('update:showSearch', !props.showSearch);
}

// 刷新
function refresh() {
  emits('queryTable');
}

// 更改数据列的显示和隐藏
function columnChange(...args: any[]) {
  props.columns?.forEach((item) => {
    item.visible = args[1].checkedKeys.includes(item.key);
  });

  // 触发更新事件
  emits('update:columns', props.columns);

  // 更新全选状态
  updateCheckAllStatus();
}

// 弹窗显示时的处理
const handlePopoverShow = () => {
  // 确保树形控件的选中状态与columns同步
  nextTick(() => {
    props.columns?.forEach((item) => {
      if (item.visible) {
        columnRef.value?.setChecked(item.key, true, false);
      } else {
        columnRef.value?.setChecked(item.key, false, false);
      }
    });
    updateCheckAllStatus();
  });
};

// 保存默认列配置
const saveDefaultColumns = () => {
  if (props.columns) {
    defaultColumns.value = props.columns.map((item: any) => ({
      key: item.key,
      label: item.label,
      visible: item.visible
    }));
  }
};

// 恢复默认配置
const resetToDefault = () => {
  if (!props.columns || defaultColumns.value.length === 0) return;

  // 恢复默认visible状态
  props.columns.forEach((item: any) => {
    const defaultItem = defaultColumns.value.find((d: any) => d.key === item.key);
    if (defaultItem) {
      item.visible = defaultItem.visible;
    }
  });

  // 触发更新事件
  emits('update:columns', props.columns);

  // 更新树形控件的选中状态
  nextTick(() => {
    props.columns?.forEach((item: any) => {
      if (item.visible) {
        columnRef.value?.setChecked(item.key, true, false);
      } else {
        columnRef.value?.setChecked(item.key, false, false);
      }
    });
    updateCheckAllStatus();
  });

  ElMessage.success('已恢复默认配置');
};

// 处理保存
const handleSave = async () => {
  if (!props.columns || props.columns.length === 0) {
    ElMessage.warning('没有可保存的列配置');
    return;
  }

  // 构建要保存的列配置数据
  const columnConfig = props.columns.map((item: any) => ({
    key: item.key,
    label: item.label,
    visible: item.visible
  }));

  // 触发保存事件,由父组件处理实际的保存逻辑
  emits('save', {
    tableKey: props.tableKey,
    columns: columnConfig
  });
};

// 从后端加载列配置
const loadColumnConfig = (config: any[]) => {
  if (!props.columns || !config) return;

  // 更新列的visible状态
  props.columns.forEach((item: any) => {
    const savedItem = config.find((s: any) => s.key === item.key);
    if (savedItem) {
      item.visible = savedItem.visible;
    }
  });

  // 触发更新事件
  emits('update:columns', props.columns);

  // 更新树形控件的选中状态
  nextTick(() => {
    props.columns?.forEach((item: any) => {
      if (item.visible) {
        columnRef.value?.setChecked(item.key, true, false);
      } else {
        columnRef.value?.setChecked(item.key, false, false);
      }
    });
    updateCheckAllStatus();
  });
};

// 设置保存状态
const setSaving = (status: boolean) => {
  saving.value = status;
};

// 初始化
onMounted(() => {
  saveDefaultColumns();

  // 初始化树形控件的选中状态
  nextTick(() => {
    props.columns?.forEach((item) => {
      if (item.visible) {
        columnRef.value?.setChecked(item.key, true, false);
      }
    });
    updateCheckAllStatus();
  });
});

// 监听columns变化,更新默认配置
watch(
  () => props.columns,
  (newVal) => {
    if (newVal && defaultColumns.value.length === 0) {
      saveDefaultColumns();
    }
  },
  { deep: true }
);

// 暴露方法给父组件调用
defineExpose({
  loadColumnConfig,
  setSaving
});
</script>

<style lang="scss" scoped>
:deep(.el-transfer__button) {
  border-radius: 50%;
  display: block;
  margin-left: 0px;
}
:deep(.el-transfer__button:first-child) {
  margin-bottom: 10px;
}

.my-el-transfer {
  text-align: center;
}

.popover-content {
  padding: 8px 0;
}

.tree-header {
  width: 100%;
  line-height: 24px;
  text-align: center;
  font-weight: bold;
  padding: 0 12px 8px 12px;
  border-bottom: 1px solid #eee;
}

// 树容器,添加滚动条
.tree-container {
  max-height: 300px;
  overflow-y: auto;
  padding: 4px 0;

  // 自定义树节点样式
  :deep(.el-tree-node__content) {
    height: 32px;
  }
}

.custom-tree-node {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding-right: 8px;
  font-size: 13px;
}

.popover-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 12px 0 12px;
  border-top: 1px solid #eee;
  margin-top: 4px;

  .footer-left {
    flex-shrink: 0;
  }

  .footer-right {
    display: flex;
    gap: 8px;
  }
}

.show-btn {
  margin-left: 12px;
}

.el-button {
  color: #333;
  font-size: 20px;
  margin-right: 5px;
}
</style>

<style>
/* 全局样式,确保弹窗不会影响表格布局 */
.el-popover.column-popover {
  padding: 0 !important;
}
</style>

用法

java 复制代码
     <right-toolbar
            v-model:showSearch="showSearch"
            :columns="columns"
            table-key="SQMS_MATERIALS_BASE_INFO"
            @update:columns="columns = $event"
            @query-table="getList"
            @save="handleSaveColumnConfig"
          ></right-toolbar>
<el-table-column v-if="columns[0].visible" label="xxx" align="center" prop="xxx" width="120"/>

// 列显隐信息
const columns = ref<>(
[
  { key: 0, label: `xxx`, visible: true, children: [] }
]


import { saveColumnConfig, getColumnConfig } from '@/api/system/user/index';

const rightToolbarRef = ref();
// 保存列配置到后端
const handleSaveColumnConfig = async (data) => {
  try {
    // 这里调用您的保存接口
    const saveData = {
      tableName: data.tableKey,
      columnConfig: JSON.stringify(data.columns)
    };

    await saveColumnConfig(saveData);
    ElMessage.success('列配置保存成功');
  } catch (error) {
    console.error('保存失败:', error);
    ElMessage.error('保存失败');
  }
};

// 加载列配置
const loadColumnConfig = async () => {
  try {
    // 从后端获取配置
    const res = await getColumnConfig({
      tableName: 'SQMS_MATERIALS_BASE_INFO'
    });

    if (res.data && res.data.length > 0) {
      columns.value = res.data;
      const config = JSON.parse(res.data);
      // 调用组件暴露的方法加载配置
      rightToolbarRef.value?.loadColumnConfig(config);
    }
  } catch (error) {
    console.error('加载配置失败:', error);
  }
};
后端接口
java 复制代码
    @Override
    public R saveColumnConfig(Map<String, Object> map) {
        if (ObjectUtil.isEmpty(map.get("tableName"))) {
            throw new ServiceException("列配置保存失败:表名称不可为空");
        }
        if (ObjectUtil.isEmpty(map.get("columnConfig"))) {
            throw new ServiceException("列配置保存失败:配置项不可为空");
        }
        List<ColumnConfig> columnConfig = JSON.parseObject(map.get("columnConfig").toString(), new TypeReference<List<ColumnConfig>>() {
        });

        String key = String.format("column-config:%s_%s", LoginHelper.getUsername(), map.get("tableName"));
        RedisUtils.setCacheObject(key, JSON.toJSONString(columnConfig));

        return R.ok();
    }

    @Data
    public static class ColumnConfig {
        private int key;
        private String label;
        private Boolean visible;
    }

    @Override
    public R getColumnConfig(Map<String, Object> map) {
        if (ObjectUtil.isEmpty(map.get("tableName"))) {
            throw new ServiceException("列配置查询失败:表名称不可为空");
        }
        String key = String.format("column-config:%s_%s", LoginHelper.getUsername(), map.get("tableName"));
        Object cacheObject = RedisUtils.getCacheObject(key);
        if (ObjectUtil.isEmpty(cacheObject)) {
            return R.ok();
        }
        List<ColumnConfig> columnConfigs = JSON.parseObject(cacheObject.toString(), new TypeReference<List<ColumnConfig>>() {
        });
        return R.ok(columnConfigs);
    }
相关推荐
亓才孓2 小时前
【反射机制】
java·javascript·jvm
Cherry的跨界思维2 小时前
【AI测试全栈:质量】47、Vue+Prometheus+Grafana实战:打造全方位AI监控面板开发指南
vue.js·人工智能·ci/cd·grafana·prometheus·ai测试·ai全栈
wing982 小时前
通往“全干”之路一:前端部署
前端·vue.js·全栈
阿珊和她的猫2 小时前
探究浏览器最大请求并发数:提升网页加载性能的关键
前端·javascript·vue.js
D_C_tyu2 小时前
Vue3 + Element Plus | el-table 多级表头表格导出 Excel(含合并单元格、单元格居中)第二版
vue.js·elementui·excel
wuhen_n3 小时前
Vue3 组件生命周期详解
前端·javascript·vue.js
wuhen_n3 小时前
渲染器核心:mount挂载过程
前端·javascript·vue.js
不想秃头的程序员3 小时前
vue3 Pinia 全解析:从入门到实战。
前端·javascript·vue.js
wuhen_n3 小时前
组件渲染:从组件到DOM
前端·javascript·vue.js