SOURCE

<template>
  <div class="base-table-warp" v-bind="$attrs">
    <el-table
      v-bind="props"
      v-loading="tableLoading"
      element-loading-text="加载中..."
      element-loading-background="rgba(255, 255, 255, 0.5)"
      ref="tableRef"
      border
      stripe
      :header-cell-class-name="headerCellClassName"
      @sortChange="sortChange"
      @filterChange="filterChange"
    >
      <CustomTableColumn v-for="column in columns" :key="column.field" :column="column">
        <template #header="scope">
          <div class="el-table__header__cell__custom">
            <slot name="header" :scope="scope">
              <el-checkbox v-if="getColumnCheckbox(column)" v-model="checkboxAllCheckd" :indeterminate="indeterminate"> </el-checkbox>
              <BaseToolTip :rows="1" placement="top" v-else>
                {{ getHeaderRender(scope, column) }}
              </BaseToolTip>
            </slot>
          </div>
        </template>
        <template #default="scope">
          <slot name="default" :scope="scope">
            <div class="el-table__body__cell__custom">
              <el-radio
                v-if="[column.field, column.type].includes('radio')"
                :label="getRowLabel(scope)"
                v-model="radioCheckd"
                :disabled="getSelectDisabled(scope, column)"
                @change="val => radioChange(val, scope, column)"
              >
              </el-radio>
              <el-checkbox
                v-else-if="getColumnCheckbox(column)"
                :label="getRowLabel(scope)"
                v-model="checkboxCheckd"
                :disabled="getSelectDisabled(scope, column)"
                @change="val => checkboxChange(val, scope, column)"
              >
              </el-checkbox>
              <component
                v-else-if="column.componentIs"
                :is="column.componentIs"
                v-bind="column.getComponentPorpsCallback ? column.getComponentPorpsCallback(scope, column) : column.componentPorps"
              ></component>
              <BaseToolTip v-else :rows="1" placement="top"> {{ getDefaultRender(scope, column) }}</BaseToolTip>
            </div>
          </slot>
        </template>
      </CustomTableColumn>
      <template #empty><slot name="empty"></slot></template>
      <template #append><slot name="append"></slot></template>
    </el-table>
    <BasePagination
      v-if="isPagination"
      v-bind="props"
      :disabled="tableLoading"
      v-model:currentPage="modelCurrentPage"
      v-model:pageSize="modelPageSize"
      @paginationChange="paginationChange"
    ></BasePagination>
  </div>
</template>

<script lang="jsx" setup>
import {ElTable} from 'element-plus';
import BasePagination from './BasePagination';
import BaseToolTip from './BaseToolTip.vue';
import {ref, toRefs, computed, useSlots, nextTick, onMounted} from 'vue';
import useGlobalProperties from '_@/hooks/useGlobalProperties.js';
const tableSlots = useSlots();
const {$utils} = useGlobalProperties();
const CustomTableColumn = {
  name: 'CustomTableColumn',
  props: ['column'],
  inheritAttrs: false,
  setup: (props, {attrs, slots}) => {
    const columnSlots = {
      header: scope => {
        const headerSlotsName = [props.column.field, 'header'].join('-');
        if (tableSlots?.[headerSlotsName]) {
          return tableSlots[headerSlotsName](scope);
        }
        return slots?.header ? slots.header(scope) : '';
      },
      default: scope => {
        if (tableSlots?.[props.column.field]) {
          return tableSlots[props.column.field](scope);
        }
        if (!props.column.field && props.column?.type === 'expand' && tableSlots?.expand) {
          return tableSlots.expand(scope);
        }
        if (tableSlots?.default) {
          return tableSlots.default(scope);
        }
        return slots?.default ? slots.default(scope) : '';
      }
    };
    if (props?.column?.children?.length) {
      return () => (
        <el-table-column {...props.column}>
          {props.column.children.map(item => {
            return (
              <CustomTableColumn column={item} key={item.field}>
                {columnSlots}
              </CustomTableColumn>
            );
          })}
        </el-table-column>
      );
    }
    return () => (
      <el-table-column {...props.column} prop={props.column.field}>
        {columnSlots}
      </el-table-column>
    );
  }
};

defineOptions({
  inheritAttrs: false
});

const props = defineProps({
  ...ElTable.props,
  ...BasePagination.props,
  columns: {
    type: Array,
    required: true,
    validator(value) {
      // field: 字段属性名
      // label:字体名称
      // disabled: 禁用回调(单选,复选列)
      // type:类型(单选,复选,index)
      // empty: 空占位字符
      // componentIs:组件
      // componentPorps:组件Porps
      // getComponentPorpsCallback:组件props回调
      // + El-Table-Column 属性
      return value.every(item => {
        return ['field', 'label'].every(key => Object.keys(item).includes(key));
      });
    }
  },
  radio: {
    type: String
  },
  checkbox: {
    type: Array
  },
  rowKey: {
    type: [Function, String],
    default: 'id'
  },
  defaultPageSize: {
    type: Number,
    default: 10
  },
  isPagination: {
    type: Boolean,
    default: true
  },
  paramsPropKey: {
    type: Object,
    default: () => ({
      order: 'sortType', // 排序中order映射key,
      prop: 'sortField', // 排序中prop映射key,
      descending: 'desc', // descending映射key,
      ascending: 'asc', // ascending映射key
      currentPage: 'pageIndex', // currentPage映射key
      pageSize: 'pageSize' // pageSize映射key
    })
  }
});

const {isPagination = true, defaultSort, currentPage, pageSize, defaultPageSize, paramsPropKey, columns, radio, rowKey, checkbox, data} = toRefs(props);

const tableRef = ref(null);
const tableLoading = ref(true);
const propSort = ref(null);
const propFilter = ref(null);

const emits = defineEmits([
  ...Object.keys(ElTable.emits),
  ...Object.keys(BasePagination.emits),
  'update:radio',
  'update:checkbox',
  'sortChange',
  'selectAll',
  'selectionChange',
  'radioChange'
]);

const getHeaderRender = (scope, column) => scope.column.label;

const indexSlot = (scope, column) => {
  const no = scope.$index + 1;
  if ('index' in column) {
    return typeof column.index === 'function' ? column.index(no) : column.index;
  } else {
    return no + modelPageSize.value * (modelCurrentPage.value - 1);
  }
};

const getSelectDisabled = (scope, column) => (typeof column?.disabled === 'function' ? column.disabled(scope, column) : false);

const radioChange = (val, scope, column) => {
  if (!getSelectDisabled(scope, column)) {
    emits('radioChange', {value: val, ...scope});
  }
};

const getColumnCheckbox = column => ['checkbox', 'selection'].some(key => [column.field, column.type].includes(key));

const checkboxChange = (val, scope, column) => {
  if (!getSelectDisabled(scope, column)) {
    emits('checkboxChange', {value: val, ...scope});
    emits('selectionChange', {value: val, ...scope});
  }
};

const getRowLabel = ({row}) => (typeof rowKey.value === 'function' ? rowKey.value(row) : row[rowKey.value]);

const getDefaultRender = (scope, column) => {
  if ([column.field, column.type].includes('index')) {
    return indexSlot(scope, column);
  }
  if (column?.formatter) {
    const {$index, cellIndex, column, row} = scope;
    return column?.formatter(row, column, cellIndex, $index);
  }
  if (scope?.column?.property) {
    return $utils.getTextPlaceholder(scope.row[scope.column.property], column.empty);
  }
  return '';
};

const getListRowLabel = list => list.map(row => getRowLabel({row}));

const getAllRowLabel = computed(() => getListRowLabel(data.value));

//  排除disabled选项为禁用的全部可选复选框值
const getAllEnableValue = computed(() => {
  return getListRowLabel(data.value.filter(row => !getSelectDisabled({row}, {...getCheckboxColumn.value, row})));
});

//  获取disabled选项为禁用且被选中的可选复选框值
const getAllDisabledCheckboxValue = computed(() => {
  return getListRowLabel(
    data.value.filter(row => getSelectDisabled({row}, {...getCheckboxColumn.value, row}) && checkboxCheckd.value.includes(getRowLabel({row})))
  );
});

const getAllCheckboxValue = computed(() => getAllEnableValue.value.concat(...getAllDisabledCheckboxValue.value));

const getCheckboxColumn = computed(() => columns.value.find(({type}) => ['selection', 'checkbox'].includes(type)) ?? {});

const checkboxAllCheckd = computed({
  get() {
    return checkboxCheckd.value?.length && getAllRowLabel.value.every(item => checkboxCheckd.value.includes(item));
  },
  set(newVal) {
    if (getAllCheckboxValue.value.every(val => checkboxCheckd.value.includes(val))) {
      checkboxCheckd.value = getAllDisabledCheckboxValue.value;
    } else {
      checkboxCheckd.value = getAllCheckboxValue.value;
    }
    checkboxCheckd.value = result;
    nextTick(() => {
      emits('select-all', result);
    });
  }
});

const indeterminate = computed(() => Boolean(checkboxCheckd.value?.length && getAllRowLabel.value.some(item => !checkboxCheckd.value.includes(item))));

const getParamsPropKey = computed(() => ({
  ...paramsPropKey.value
}));

const modelCurrentPage = computed({
  get() {
    return currentPage.value;
  },
  set(newVal) {
    emits('update:currentPage', newVal);
    emits('currentPage', newVal);
  }
});

const modelPageSize = computed({
  get() {
    return pageSize.value || defaultPageSize.value;
  },
  set(newVal) {
    emits('update:pageSize', newVal);
    emits('pageSizePage', newVal);
  }
});

const radioCheckd = computed({
  get() {
    return radio.value;
  },
  set(newVal) {
    emits('update:radio', newVal);
  }
});

const checkboxCheckd = computed({
  get() {
    return checkbox.value.filter(item => getAllRowLabel.value.includes(item));
  },
  set(newVal) {
    emits('update:checkbox', newVal);
  }
});

const getSort = computed(() => updatePropSort(propSort.value || defaultSort.value));

const getPagination = computed(() => ({
  [getParamsPropKey.value.currentPage]: modelCurrentPage.value,
  [getParamsPropKey.value.pageSize]: modelPageSize.value
}));

const getFilter = computed(() => propFilter.value);

const updatePropSort = val => {
  const {order, prop} = val ?? {};
  if (!prop) {
    return {};
  }
  return {
    [getParamsPropKey.value.order]: getParamsPropKey.value[order] || order || null,
    [getParamsPropKey.value.prop]: prop
  };
};

const sortChange = val => {
  propSort.value = val;
  nextTick(() => {
    emits('sortChange', val);
  });
};

const filterChange = val => {
  const [[key, value]] = Object.entries(val);
  const {filterMultiple = true} = columns.value.find(({columnKey}) => columnKey === key);
  if (!propFilter.value) {
    propFilter.value = {};
  }
  if (value.length) {
    propFilter.value[key] = filterMultiple ? value : value[0];
  } else {
    delete propFilter.value[key];
  }
  emits('filterChange', propFilter.value);
};

const headerCellClassName = ({row, column}) => {
  let str = '';
  if (column.columnKey && column.filterable) {
    str += ' is-filter';
  }
  return str;
};

const paginationChange = val => {
  emits('paginationChange', val);
};

const updateCurrentPage = (current = 1) => {
  modelCurrentPage.value = current;
};

const updatePageSize = (size = defaultPageSize.value) => {
  modelPageSize.value = size;
};

const updatePagination = (current = 1, size = defaultPageSize.value) => {
  updateCurrentPage(current);
  updatePageSize(size);
};

const updateRadio = (val = null) => {
  radioCheckd.value = val;
};

const updateCheckbox = (val = []) => {
  checkboxCheckd.value = val;
};

const clearSort = () => {
  propSort.value = null;
  tableRef.value.clearSort();
};

const clearFilter = () => {
  propFilter.value = null;
  tableRef.value.clearFilter();
};

const doLayout = () => {
  tableRef.value.doLayout();
};

defineExpose({
  tableRef,
  tableLoading,
  isPagination,
  updateCurrentPage,
  updatePageSize,
  updatePagination,
  updateRadio,
  updateCheckbox,
  getPagination,
  getSort,
  getFilter,
  clearSort,
  clearFilter,
  doLayout
});
</script>

<style lang="scss">
.el-table .caret-wrapper {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  height: 14px;
  width: 24px;
  vertical-align: middle;
  cursor: pointer;
  overflow: initial;
  position: relative;
}
.el-table col {
  min-width: 50px !important;
}

.el-table td.el-table__cell div {
  .el-checkbox__label,
  .el-radio__label {
    display: none;
  }
  & > .el-checkbox,
  & > .el-radio {
    display: flex;
  }
}
.el-table th {
  .cell {
    display: flex;
    align-items: center;
  }
  vertical-align: middle;
  .el-table__header__cell__custom {
    display: inline-flex;
  }
  &.is-right > .cell {
    justify-content: flex-end;
  }
  &.is-center > .cell {
    justify-content: center;
  }
  &.is-filter .el-table__header__cell__custom {
    max-width: calc(100% - 14px);
  }
  &.is-sortable .el-table__header__cell__custom {
    max-width: calc(100% - 24px);
  }
  &.is-sortable.is-filter .el-table__header__cell__custom {
    max-width: calc(100% - 24px - 14px);
  }
}
.el-table .cell {
  display: flex;
  align-items: center;
}
.el-table td {
  .el-table__body__cell__custom {
    flex: 1;
  }
}
.el-table--border th.el-table__cell::after {
  top: 50% !important;
  transform: translateY(-50%);
}
.el-table.el-loading-parent--relative {
  .el-table__empty-text {
    opacity: 0;
    pointer-events: none;
  }
}
</style>
console 命令行工具 X clear

                    
>
console