跳转到内容

通过动态列表学习数据思维

刀刀

4/7/2025

0 字

0 分钟

需求效果

效果图

如上图所示,可以同时勾选、取消勾选展示对应列的表格,有一部分的列允许显示隐藏,一部分的列不允许显示隐藏。同时能本地存储上次的配置、表格内容支持通过 slot 自定义的需求。

实现思路

实现该效果有两个方案,方案一是创建一个数组对象,每一项是一个列的配置对象,如下方代码所示:

js
const columnsConfig = [
  {
    label: "姓名",
    prop: "name",
    canHidden: true,
  },
  {
    label: "年龄",
    prop: "age",
    canHidden: true,
  },
  {
    label: "地址",
    prop: "address",
    canHidden: true,
  },
];

通过 v-for 循环数组,渲染每一列。但是这么做有一个缺点,如果到时候不实现某些东西,比如不需要某个配置,则需要直接修改数组对象,后面如果又需要了就找不回来了,会随着操作丢失一部分。因此该方案不适用。

方案二通过两个配置共同决定,columnConfig 是一个不会改变的对象(与方案一直接修改数组配置不同),只用来决定每一列的配置项情况,再新增一个 state 对象,用于定义每一列的显隐。代码如下:

js
const columnsConfig = {
  name: {
    label: "姓名",
    canHidden: true,
  },
  age: {
    label: "年龄",
    canHidden: true,
  },
  address: {
    label: "地址",
    canHidden: true,
  },
};

const state = {
  name: true,
  age: true,
  address: false,
};

实现效果

初步效果

根据上方思路的方案二,通过 v-for 循环 state 对象, v-if 判断 state 对象的值来决定是否显示该列。这样即使不显示该列,该列的配置信息也不会丢失,可以随时恢复显示。

js
const columnsConfig = {
  id: {
    label: "id",
    prop: "id",
    canHidden: false, // 不可取消勾选
  },
  name: {
    label: "姓名",
    prop: "name",
    canHidden: false, // 不可取消勾选
  },
  age: {
    label: "年纪",
    prop: "age",
    canHidden: true,
  },
  mingzu: {
    label: "名族",
    prop: "mingzu",
    canHidden: true,
  },
};

const state = reactive({}); // 是否显示该列
Object.keys(columnsConfig).forEach((key) => {
  state[key] = true;
});

const data = [
  {
    id: 1,
    name: "张三",
    age: 18,
    mingzu: "汉族",
  },
  {
    id: 2,
    name: "李四",
    age: 19,
    mingzu: "回族",
  },
];
vue
<template>
  <!-- 表格列的显隐 -->
  <div v-for="item in Object.keys(state)" :key="item">
    <span>{{ columnsConfig[item].label }}</span>
    <el-checkbox
      :disabled="!columnsConfig[item].canHidden"
      v-model="state[item]"
    ></el-checkbox>
  </div>

  <!-- 表格 -->
  <el-table :data="data" width="100%">
    <template v-for="item in Object.keys(state)" :key="item">
      <el-table-column v-if="state[item]" v-bind="columnsConfig[item]">
        <!-- 表头,自定义label和复选框 -->
        <template #header>
          <div>
            <span>{{ columnsConfig[item].label }}</span>
            <el-checkbox
              :disabled="!columnsConfig[item].canHidden"
              v-model="state[item]"
            />
          </div>
        </template>
      </el-table-column>
    </template>
  </el-table>
</template>

现在初步效果已经实现了。

列动态内容

动态列内容效果比如年龄,小于 18 岁显示红色,反之显示绿色。实现方案有三种,方案一是通过 v-if 判断,是否为年龄,为年龄则设置对应的效果。

vue
<template #default="{ row }">
  <div
    v-if="columnsConfig[item].prop === 'age'"
    :style="{ color: row.age < 18 ? 'red' : 'green' }"
  ></div>
  <div v-if="columnsConfig[item].prop === 'name'"></div>
</template>

这个方法前期还好,后续维护如果需要很多种效果,则会有很多 v-if 判断,代码会显得很冗余。

方案二是把每一个需要自定义内容的 DOM 单独抽离出来封装为一个子组件,修改 columnConfig 对象,新增一个 render 属性,绑定对应的子组件。这样只需要 v-if 判断当前对象有没有 render 属性,有则渲染对应的子组件。

js
import AgeComponent from "./AgeComponent.vue"; 

const columnsConfig = {
  id: {
    label: "id",
    prop: "id",
    canHidden: false, // 不可取消勾选
  },
  name: {
    label: "姓名",
    prop: "name",
    canHidden: false, // 不可取消勾选
  },
  age: {
    label: "年纪",
    prop: "age",
    canHidden: true,
    render: AgeComponent, // 自定义渲染组件
  },
  mingzu: {
    label: "名族",
    prop: "mingzu",
    canHidden: true,
  },
};
vue
<template>
  <!-- 表格列的显隐 -->
  <div v-for="item in Object.keys(state)" :key="item">
    <span>{{ columnsConfig[item].label }}</span>
    <el-checkbox :disabled="!columnsConfig[item].canHidden" v-model="state[item]"></el-checkbox>
  </div>

  <!-- 表格 -->
  <el-table :data="data" width="100%">
    <template v-for="item in Object.keys(state)" :key="item">
      <el-table-column v-if="state[item]" v-bind="columnsConfig[item]">
        <!-- 表头,自定义label和复选框 -->
        <template #header>
          <div>
            <span>{{ columnsConfig[item].label }}</span>
            <el-checkbox :disabled="!columnsConfig[item].canHidden" v-model="state[item]" />
          </div>
        </template>
        <template v-if="columnsConfig[item].render"" #default="{row}"> 
          <component :is="columnsConfig[item].render" :row="row"></component> 
        </template> 
      </el-table-column>
    </template>
  </el-table>
</template>

这个方案比起第一个方案,虽然需要新增很多子组件维护,但是父组件不再有过多的 v-if 判断,减少代码的冗余量。

第三种方法是通过 renderJSX 的方法来实现,这里采取 render 的方法。首先与方法二一样,需要创建一个子组件,接收父组件的参数和 render 函数,返回的是 render 方法和 h 方法创建的 DOM。父组件的 columnConfig 对象新增 render 属性,值是一个函数,传递三个参数,参数一是该 DOM 的标签,参数二是一个对象,该 DOM 的元素(如类名、id 等),参数三是标签的文本。父组件的 el-table-column 组件的 #default 插槽引入子组件后,传递 row 和对应属性的 render 参数。

js
import TableSlot from "./TableSlot.vue"; 

const columnsConfig = {
  id: {
    label: "id",
    prop: "id",
    canHidden: false, // 不可取消勾选
  },
  name: {
    label: "姓名",
    prop: "name",
    canHidden: false, // 不可取消勾选
  },
  age: {
    label: "年纪",
    prop: "age",
    canHidden: true,
    render: (h, row) => {
      return h(
        "div",
        {
          style: {
            color: row.age < 18 ? "red" : "green", 
          }, 
        },
        row.age
      ); 
    }, 
  },
  mingzu: {
    label: "名族",
    prop: "mingzu",
    canHidden: true,
  },
};
vue
<template>
  <!-- 表格列的显隐 -->
  <div v-for="item in Object.keys(state)" :key="item">
    <span>{{ columnsConfig[item].label }}</span>
    <el-checkbox
      :disabled="!columnsConfig[item].canHidden"
      v-model="state[item]"
    ></el-checkbox>
  </div>

  <!-- 表格 -->
  <el-table :data="data" width="100%">
    <template v-for="item in Object.keys(state)" :key="item">
      <el-table-column v-if="state[item]" v-bind="columnsConfig[item]">
        <!-- 表头,自定义label和复选框 -->
        <template #header>
          <div>
            <span>{{ columnsConfig[item].label }}</span>
            <el-checkbox
              :disabled="!columnsConfig[item].canHidden"
              v-model="state[item]"
            />
          </div>
        </template>
        <template v-if="columnsConfig[item].render" #default="{ row }">
          <TableSlot
            :row="row"
            :render="columnsConfig[item].render"
          ></TableSlot>
        </template>
      </el-table-column>
    </template>
  </el-table>
</template>
vue
<script>
import { ref, h } from "vue";

export default {
  props: ["render", "row"],
  setup(props) {
    return () => {
      return props.render(h, props.row);
    };
  },
};
</script>

题外话

这里的 render 返回的是一个方法,查看官方文档关于 h 方法的代码是 return h('div', 123) ,实际上相当于以下代码的缩写:

js
return () => {
  const vnode = h("div", 123);
  return vnode;
};

本地存储

本地存储效果实现可以在最开始获取本地存储的数据。后续 state 每次更新时,都要调用方法更新本地存储。

js
function setStateStroe(state) {
  localStorage.setItem("table_state", JSON.stringify(state)); 
} 

const state = reactive({}); // 是否显示该列
if (localStorage.getItem("table_state")) {
  state = reactive(JSON.parse(localStorage.getItem("table_state"))); 
} 
else {
  Object.keys(columnsConfig).forEach((key) => {
    state[key] = true; 
  }); 
} 

watchEffect(() => {
  setStateStroe(state); 
}); 

完整代码

vue
<script setup>
import TableSlot from "./TableSlot.vue";

const columnsConfig = {
  id: {
    label: "id",
    prop: "id",
    canHidden: false, // 不可取消勾选
  },
  name: {
    label: "姓名",
    prop: "name",
    canHidden: false, // 不可取消勾选
  },
  age: {
    label: "年纪",
    prop: "age",
    canHidden: true,
    render: (h, row) => {
      return h(
        "div",
        {
          style: {
            color: row.age < 18 ? "red" : "green",
          },
        },
        row.age
      );
    },
  },
  mingzu: {
    label: "名族",
    prop: "mingzu",
    canHidden: true,
  },
};

function setStateStroe(state) {
  localStorage.setItem("table_state", JSON.stringify(state));
}

const state = reactive({}); // 是否显示该列
if (localStorage.getItem("table_state")) {
  state = reactive(JSON.parse(localStorage.getItem("table_state")));
} else {
  Object.keys(columnsConfig).forEach((key) => {
    state[key] = true;
  });
}

watchEffect(() => {
  setStateStroe(state);
});

const data = [
  {
    id: 1,
    name: "张三",
    age: 18,
    mingzu: "汉族",
  },
  {
    id: 2,
    name: "李四",
    age: 19,
    mingzu: "回族",
  },
];
</script>

<template>
  <!-- 表格列的显隐 -->
  <div v-for="item in Object.keys(state)" :key="item">
    <span>{{ columnsConfig[item].label }}</span>
    <el-checkbox
      :disabled="!columnsConfig[item].canHidden"
      v-model="state[item]"
    ></el-checkbox>
  </div>

  <!-- 表格 -->
  <el-table :data="data" width="100%">
    <template v-for="item in Object.keys(state)" :key="item">
      <el-table-column v-if="state[item]" v-bind="columnsConfig[item]">
        <!-- 表头,自定义label和复选框 -->
        <template #header>
          <div>
            <span>{{ columnsConfig[item].label }}</span>
            <el-checkbox
              :disabled="!columnsConfig[item].canHidden"
              v-model="state[item]"
            />
          </div>
        </template>
        <template v-if="columnsConfig[item].render" #default="{ row }">
          <TableSlot
            :row="row"
            :render="columnsConfig[item].render"
          ></TableSlot>
        </template>
      </el-table-column>
    </template>
  </el-table>
</template>
vue
<script>
import { ref, h } from "vue";

export default {
  props: ["render", "row"],
  setup(props) {
    return () => {
      return props.render(h, props.row);
    };
  },
};
</script>