SOURCE

<template>
  <div class="base-table-warp" v-bind="$attrs">
    <el-table
      v-bind="props"
      :class="{
        'el-loading-parent--relative': tableLoading,
      }"
      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, scope}
                "
              ></component>
              <slot :name="column.field" v-else-if="$slots?.[column.field]"></slot>
              <BaseToolTip v-else :rows="1" placement="top"> {{ getDefaultRender(scope, column) }}</BaseToolTip>
            </div>
          </slot>
        </template>
      </CustomTableColumn>
      <template #empty><slot name="empty" v-if="!tableLoading"></slot></template>
      <template #append><slot name="append"></slot></template>
    </el-table>
    <BasePagination
      v-if="isPagination && total > 0 && columns.length"
      v-bind="props"
      :disabled="tableLoading"
      :beforeCallback="fetchBeforeCallback"
      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回调
      // desc 列头描述
      // sorterable 列头排序
      // filterable 列头过滤
      // + 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,
  },
  fetchBeforeCallback: {
    type: Function,
  },
  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
    }),
  },
  loading: {
    type: Boolean,
    default: false,
  },
});

const {
  isPagination = true,
  defaultSort,
  currentPage,
  pageSize,
  defaultPageSize,
  paramsPropKey,
  columns,
  radio,
  rowKey,
  checkbox,
  data,
  fetchBeforeCallback,
  loading,
} = 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',
  'update:loading',
]);

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 getTextPlaceholder = (str, column) => {
  const { empty: defaultStr = '--', isThousandsSeparator } = column;
  if (!str && str !== 0) {
    return defaultStr;
  }
  // 匹配纯数字(正、负数,小数),则转千分位
  const regex = /^-?\d+\.?\d*$/;
  if (isThousandsSeparator && regex.test(str)) {
    return str.toString().replace(/(?<!\.\d*)\B(?=(\d{3})+(?!\d))/g, ',');
  }
  return str;
};

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: columnv, row } = scope;
    return columnv?.formatter(row, columnv, cellIndex, $index);
  }
  if (scope?.column?.property) {
    return getTextPlaceholder(scope.row[scope.column.property], column);
  }
  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 tableLoading = computed({
  get() {
    return loading.value;
  },
  set(newVal) {
    emits('update:loading', 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 autoScrollFirstError = () => {
  nextTick(() => {
    const firstErrorEl = tableRef.value?.$el?.querySelectorAll(
      '.el-table__body .el-table__body__cell__custom .el-input.is-error'
    )?.[0];
    firstErrorEl?.scrollIntoView({
      behavior: 'smooth',
      inline: 'center',
    });
  });
};

// 找到第一个body中td元素,重置滚动到开始位置
const resetScrollError = () => {
  nextTick(() => {
    const firstTdEl = tableRef.value?.$el?.querySelectorAll('.el-table__body .el-table__cell')?.[0];
    firstTdEl?.scrollIntoView({
      behavior: 'smooth',
    });
  });
};

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

const paginationChange = (val) => {
  // 重置单元格编辑对象
  resetValidateCellEditItem();
  // 存在前置拦截回调时
  if (
    fetchBeforeCallback.value &&
    !fetchBeforeCallback.value(() => {
      emits('paginationChange', val);
    })
  ) {
    return;
  }
  emits('paginationChange', val);
};

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

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

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

onBeforeUnmount(() => {
  updatePagination();
  tableRef.value?.$el?.querySelector('.el-scrollbar__wrap')?.removeEventListener('scroll', hidePopper);
});

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();
};


const hidePopper = () => {
  (document.body.querySelectorAll('.el-popover.popper-is-filter') ?? []).forEach((el) => {
    el.style.display = 'none';
  });
};


const keydownEvent = (e, index) => {
  if (![8, 46].includes(e.keyCode) && !/[0-9.-]/.test(e.key)) {
    e.preventDefault();
    if (e.target.selectionEnd !== e.target.selectionStart) {
      filterOption.value.value[index] =
        filterOption.value.value[index].substring(0, e.target.selectionStart) +
        filterOption.value.value[index].substring(e.target.selectionEnd);
    }
  }
};

const init = () => {
  nextTick(() => {
    tableRef.value?.$el?.querySelector('.el-scrollbar__wrap')?.addEventListener('scroll', hidePopper);
  });
};

onMounted(() => {
  init();
});

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

<style lang="scss">

  .el-table--border:before,
  .el-table--border:after,
  .el-table__border-left-patch,
  .el-table--border .el-table__inner-wrapper:after {
    opacity: 0;
  }

  .el-table--border .el-table__inner-wrapper {
    .el-table__body-wrapper {
      .el-scrollbar {
        .el-scrollbar__bar {
          &.is-horizontal {
            bottom: 0;
            height: 8px;
          }

          &.is-vertical {
            width: 8px;
          }
        }
      }
    }
  }

  .el-table .cell {
    padding: 0 16px;    
  }
  .el-table thead th {
    font-weight: normal;
    &:not(:first-child) .cell {
      position: relative;
      &::before {
        content: '';
        position: absolute;
        left: 0;
        top: 50%;
        transform: translateY(-50%);
        width: 1px;
        height: 16px;
        background: #dedede;
      }
    }
  }
  .el-table th.el-table__cell.is-leaf {
    border-bottom-color: transparent;
  }
  .el-table {
    --el-table-border-color: #d9dee5;
    --el-table-text-color: #1d2129;
    --el-table-header-bg-color: #f2f4f8;
    --el-table-header-text-color: #1d2129;
  }
  .el-table .el-table__cell {
    padding: 12px 0;
    &.is-cell-editing {
      padding: 8px 0 7px;
    }
  }
  .el-table--border .el-table__cell {
    border-right-color: transparent;
  }

  

.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;
    max-width: 100%;
  }
  &.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;
  word-break: break-all;
}
.el-table td {
  .el-table__body__cell__custom {
    flex: 1;
    max-width: 100%;
  }
}
.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