表格Table实现前端全选所有功能

示例狂
• 阅读 532

前言

最近两家公司都遇到了全选全页+批量操作的功能场景,即点击全选所有的时候需要勾选所有数据包括非当前页的。
表格Table实现前端全选所有功能

方案

如果纯前端分页可以参考 antdv.table,一般主流的组件库都给封装好了。
后端分页一种方案是:
基于组件库的现有能力,全选所有时设置 pageSize 为无穷大并调用列表接口得到全量数据赋值给 selectedRowKeys 即可。但是这套方案最大的问题在于点击全选所有时需要等待后端接口返回,这样的交互延迟是无法忍受的!且全选所有+批量操作两次请求的服务端资源浪费也是巨大的。
因此基于后端分页的前提,提出了另一套合理解决方案:
通过 isAll 判断是否为全选所有,如果是的话配合 excludeIds、否则配合 includeIds 的值完成返显。最后业务中调用批量操作接口的时候还需要传筛选项 searchParams。

实现

框架为 vue3 + antdv,代码如下:

CTable.vue

<!-- CTable -->
<template>
  <a-table
    v-bind="$attrs"
    :columns="columns"
  >
    <template #headerCell="{ column }" v-if="!$slots.headerCell">
      <template v-if="column.dataIndex === '_checkbox_'">
        <CTableHeaderCheckbox
          ref="cTableHeaderCheckboxRef"
          :rowKey="rowKey"
          :dataSource="dataSource"
          :total="total"
          v-model:isAll="isAll"
          v-model:includeIds="includeIds"
          v-model:excludeIds="excludeIds"
          :judgeToggleIsAll="judgeToggleIsAll"
        />
      </template>
    </template>
    <template #bodyCell="{ record, column }" v-if="!$slots.bodyCell">
      <template v-if="column.dataIndex === '_checkbox_'">
        <CTableBodyCheckbox
          :record="record"
          :rowKey="rowKey"
          :isAll="isAll"
          :includeIds="includeIds"
          :excludeIds="excludeIds"
          :judgeToggleIsAll="judgeToggleIsAll"
        />
      </template>
    </template>
    <template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
      <slot :name="name" v-bind="slotProps" v-if="name === 'headerCell'">
        <CTableHeaderCheckbox
          v-if="slotProps.column.dataIndex === '_checkbox_'"
          ref="cTableHeaderCheckboxRef"
          :rowKey="rowKey"
          :dataSource="dataSource"
          :total="total"
          v-model:isAll="isAll"
          v-model:includeIds="includeIds"
          v-model:excludeIds="excludeIds"
          :judgeToggleIsAll="judgeToggleIsAll"
        />
      </slot>
      <slot :name="name" v-bind="slotProps" v-if="name === 'bodyCell'">
        <CTableBodyCheckbox
          v-if="slotProps.column.dataIndex === '_checkbox_'"
          :record="slotProps.record"
          :rowKey="rowKey"
          :isAll="isAll"
          :includeIds="includeIds"
          :excludeIds="excludeIds"
          :judgeToggleIsAll="judgeToggleIsAll"
        />
      </slot>
      <slot :name="name" v-bind="slotProps" v-else></slot>
    </template>
  </a-table>
</template>

<script lang="ts" setup>
import { Table, TableColumnProps } from 'ant-design-vue';
import CTableHeaderCheckbox from './CTableHeaderCheckbox.vue';
import CTableBodyCheckbox from './CTableBodyCheckbox.vue';

const props = withDefaults(
  defineProps<{
    columns: TableColumnProps[],
    allSelection?: {
      onCheckboxChange:(data) => void,
    } | null,
  }>(),
  {
    columns: () => [],
    allSelection: null,
  },
);

const $attrs: any = useAttrs();
const $slots = useSlots();

const cTableHeaderCheckboxRef = ref();
const columns = computed(() => {
  if (props.allSelection) {
    return [
      {
        title: '多选',
        dataIndex: '_checkbox_',
        fixed: 'left',
        width: 48,
        customHeaderCell: () => ({ class: 'ant-table-checkbox-column' }),
      },
      ...props.columns,
    ];
  }
  return props.columns;
});
// 是否全选所有
const isAll = ref(false);
// 未全选所有时勾选数据
const includeIds = ref<string[]>([]);
// 全选所有时反选数据
const excludeIds = ref<string[]>([]);
const rowKey = computed(() => $attrs.rowKey || $attrs['row-key']);
const dataSource = computed(() => $attrs.dataSource || $attrs['data-source']);
// 表单数据可能存在disabled不可选择状态,此时需要后端返回enabledTotal帮助判断
const total = computed(() => $attrs.pagination?.enabledTotal || $attrs.pagination?.total || $attrs.enabledTotal || $attrs.total);
// 已勾选总数,帮助业务展示
const checkedTotal = computed(() => (isAll.value ? total.value - excludeIds.value.length : includeIds.value.length));
// 当选择数据发生改变时,需要判断是否切换全选状态
const judgeToggleIsAll = () => {
  if (isAll.value && excludeIds.value.length && excludeIds.value.length === total.value) {
    isAll.value = false;
    includeIds.value = [];
    excludeIds.value = [];
  }
  if (!isAll.value && includeIds.value.length && includeIds.value.length === total.value) {
    isAll.value = true;
    includeIds.value = [];
    excludeIds.value = [];
  }
};
// 当源数据发生改变时,手动重置选择框状态
const onResetCheckbox = () => {
  cTableHeaderCheckboxRef.value.handleMenu({ key: Table.SELECTION_NONE });
};

// 有任何选择变化时,同步回传给父组件
watch(
  [isAll, includeIds, excludeIds],
  () => {
    props.allSelection?.onCheckboxChange?.({
      isAll: isAll.value,
      includeIds: includeIds.value,
      excludeIds: excludeIds.value,
      checkedTotal: checkedTotal.value,
    });
  },
  { deep: true },
);

defineExpose({
  onResetCheckbox,
});
</script>

<style lang="less" scoped>
.ant-table-wrapper {
  .ant-table {
    &-thead {
      .ant-table-checkbox-column {
        padding-right: 4px;
      }
    }
  }
}
</style>

vue 模版里需要额外判断 slots 是否存在 headerCell 和 bodyCell,如果存在的话透传动态插槽,否则通过具名插槽传入。CTableHeaderCheckbox 使用了 v-model 而 CTableBodyCheckbox 没有使用的原因是 CTableBodyCheckbox 里的操作比较简单,巧妙的利用了数组 splicepush 特性触发响应式对象更新。

CTableHeaderCheckbox.vue

<!-- CTableHeaderCheckbox -->
<template>
  <a-checkbox
    :checked="isCurrentChecked"
    :indeterminate="isCurrentIndeterminate"
    :disabled="isCurrentDisabled"
    @change="onCheckboxChange"
  />
  <a-dropdown
    :disabled="!total"
  >
    <CIcon
      class="ml-2 cursor-pointer"
      icon="triangle-down-o"
      :size="12"
      color="#C9CCD0"
    />
    <template #overlay>
      <a-menu @click="handleMenu">
        <a-menu-item :key="Table.SELECTION_ALL">全选所有</a-menu-item>
        <a-menu-item :key="Table.SELECTION_INVERT">反选当页</a-menu-item>
        <a-menu-item :key="Table.SELECTION_NONE">清空所有</a-menu-item>
      </a-menu>
    </template>
  </a-dropdown>
</template>

<script lang="ts" setup>
import { Table } from 'ant-design-vue';

const props = withDefaults(
  defineProps<{
    rowKey: string,
    dataSource: any[],
    isAll: boolean,
    total: number,
    includeIds: string[],
    excludeIds: string[],
    judgeToggleIsAll:() => void,
  }>(),
  {},
);
const emit = defineEmits(['update:isAll', 'update:includeIds', 'update:excludeIds']);

const dataSourceIds = computed(() => props.dataSource.filter((record) => !record.disabled).map((item) => item[props.rowKey]));
const isAll = computed({
  get: () => props.isAll,
  set: (val) => {
    emit('update:isAll', val);
  },
});
const includeIds = computed({
  get: () => props.includeIds,
  set: (val) => {
    emit('update:includeIds', val);
  },
});
const excludeIds = computed({
  get: () => props.excludeIds,
  set: (val) => {
    emit('update:excludeIds', val);
  },
});
const isCurrentChecked = computed(() => {
  if (!dataSourceIds.value.length) return false;
  return isAll.value
    ? !dataSourceIds.value.some((id) => excludeIds.value.includes(id))
    : dataSourceIds.value.every((id) => includeIds.value.includes(id));
});
const isCurrentIndeterminate = computed(() => {
  if (!dataSourceIds.value.length) return false;
  if (isAll.value) {
    return !dataSourceIds.value.every((id) => excludeIds.value.includes(id)) && !isCurrentChecked.value;
  } else {
    return dataSourceIds.value.some((id) => includeIds.value.includes(id)) && !isCurrentChecked.value;
  }
});
const isCurrentDisabled = computed(() => !props.total || props.dataSource.every((record) => record.disabled));
const handleMenu = ({ key }) => {
  if (key === Table.SELECTION_INVERT) {
    // 数学意义的补集
    if (isAll.value) {
      excludeIds.value = [
        ...excludeIds.value.filter((id) => !dataSourceIds.value.includes(id)),
        ...dataSourceIds.value.filter((id) => !excludeIds.value.includes(id)),
      ];
    } else {
      includeIds.value = [
        ...includeIds.value.filter((id) => !dataSourceIds.value.includes(id)),
        ...dataSourceIds.value.filter((id) => !includeIds.value.includes(id)),
      ];
    }
    props.judgeToggleIsAll();
  } else {
    isAll.value = key === Table.SELECTION_ALL;
    includeIds.value = [];
    excludeIds.value = [];
  }
};
const onCheckboxChange = (e) => {
  const { checked } = e.target;
  if (isAll.value) {
    excludeIds.value = checked
      ? excludeIds.value.filter((id) => !dataSourceIds.value.includes(id))
      : Array.from(new Set([...excludeIds.value, ...dataSourceIds.value]));
  } else {
    includeIds.value = checked
      ? Array.from(new Set([...includeIds.value, ...dataSourceIds.value]))
      : includeIds.value.filter((id) => !dataSourceIds.value.includes(id));
  }
  props.judgeToggleIsAll();
};

defineExpose({
  handleMenu,
});
</script>

代码里可以看到 enabledTotal、record.disabled 等字段,这些都是考虑到列表项中有禁用态的数据做的兼容,disabled 是与后端定义好的保留字段,实际封装过程中也可以通过传参 Function(record) => boolean 保持灵活性。

CTableBodyCheckbox.vue

<!-- CTableBodyCheckbox -->
<template>
  <a-checkbox
    :checked="isAll ? !excludeIds.includes(record[rowKey]) : includeIds.includes(record[rowKey])"
    :disabled="record.disabled"
    @change="onCheckboxChange(record[rowKey])"
  />
</template>

<script lang="ts" setup>
const props = withDefaults(
  defineProps<{
    record: any,
    rowKey: string,
    isAll: boolean,
    includeIds: string[],
    excludeIds: string[],
    judgeToggleIsAll:() => void,
  }>(),
  {},
);

const onCheckboxChange = (id) => {
  const ids = props.isAll ? props.excludeIds : props.includeIds;
  const index = ids.indexOf(id);
  if (~index) {
    ids.splice(index, 1);
  } else {
    ids.push(id);
  }
  props.judgeToggleIsAll();
};
</script>

使用

<CTable
  ref="cTableRef"
  :columns
  :dataSource="tableData.dataSource"
  :allSelection
  @change="onTableChange"
/>

const cTableRef = ref();
const allSelection = reactive({
  isAll: false,
  includeIds: [],
  excludeIds: [],
  checkedTotal: 0,
  onCheckboxChange: ({
    isAll,
    includeIds,
    excludeIds,
    checkedTotal
  }) => {
    allSelection.isAll = isAll;
    allSelection.includeIds = includeIds;
    allSelection.excludeIds = excludeIds;
    allSelection.checkedTotal = checkedTotal;
  },
});

const batchDelete = () => {
  api({
    isAll: allSelection.isAll,
    includeList: allSelection.includeIds,
    excludeList: allSelection.excludeIds,
    ...searchParams,
  }).then(() => {
    cTableRef.value.onResetCheckbox();
  });
};

结论

如此一来,展示和交互逻辑就全部收拢在前端了,对于交互体验和服务端负载都是极大的改善。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这