<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