398 lines
9.6 KiB
Vue
398 lines
9.6 KiB
Vue
<script lang="ts" setup>
|
|
import {
|
|
computed,
|
|
defineComponent,
|
|
nextTick,
|
|
onBeforeUnmount,
|
|
onMounted,
|
|
onUpdated,
|
|
ref,
|
|
} from 'vue';
|
|
|
|
import { Pagination, Table } from 'antdv-next';
|
|
|
|
defineOptions({ name: 'AdminConfigTable' });
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
autoHeight?: boolean;
|
|
columns: Array<Record<string, any>>;
|
|
current: number;
|
|
dataSource: Array<Record<string, any>>;
|
|
hasMore?: boolean;
|
|
infinite?: boolean;
|
|
loading?: boolean;
|
|
loadMoreText?: string;
|
|
pageSize: number;
|
|
plain?: boolean;
|
|
rowKey?: string;
|
|
rowSelection?: Record<string, any>;
|
|
scrollX?: number | string;
|
|
showPager?: boolean;
|
|
showSizeChanger?: boolean;
|
|
summaryText?: string;
|
|
total: number;
|
|
}>(),
|
|
{
|
|
autoHeight: false,
|
|
hasMore: false,
|
|
infinite: false,
|
|
loadMoreText: '下滑加载下一页',
|
|
loading: false,
|
|
plain: false,
|
|
rowKey: 'id',
|
|
scrollX: 1000,
|
|
showPager: true,
|
|
showSizeChanger: true,
|
|
},
|
|
);
|
|
|
|
const emit = defineEmits<{
|
|
loadMore: [];
|
|
pageChange: [page: number, pageSize: number];
|
|
}>();
|
|
|
|
const RenderNode = defineComponent({
|
|
name: 'AdminConfigTableRenderNode',
|
|
props: {
|
|
node: {
|
|
default: null,
|
|
required: false,
|
|
},
|
|
},
|
|
setup(props) {
|
|
return () => props.node as any;
|
|
},
|
|
});
|
|
|
|
const tableAreaRef = ref<HTMLDivElement | null>(null);
|
|
const pagerRef = ref<HTMLDivElement | null>(null);
|
|
const summaryRef = ref<HTMLDivElement | null>(null);
|
|
const tableScrollY = ref(320);
|
|
let tableResizeObserver: null | ResizeObserver = null;
|
|
let tableBodyElement: HTMLElement | null = null;
|
|
let loadMoreLocked = false;
|
|
|
|
const tableScroll = computed(() => {
|
|
const scroll: Record<string, any> = { x: props.scrollX };
|
|
if (props.autoHeight) {
|
|
scroll.y = tableScrollY.value;
|
|
}
|
|
return scroll;
|
|
});
|
|
|
|
const infiniteStatusText = computed(() => {
|
|
if (!props.infinite) {
|
|
return '';
|
|
}
|
|
if (props.loading && props.dataSource.length > 0) {
|
|
return '加载中...';
|
|
}
|
|
if (props.hasMore) {
|
|
return props.loadMoreText;
|
|
}
|
|
return props.dataSource.length > 0 ? '已加载全部' : '';
|
|
});
|
|
|
|
function stringifyCellValue(value: any) {
|
|
if (value === undefined || value === null || value === '') {
|
|
return '-';
|
|
}
|
|
return String(value);
|
|
}
|
|
|
|
function getColumnValue(record: Record<string, any>, column: Record<string, any>) {
|
|
const dataIndex = column?.dataIndex;
|
|
if (Array.isArray(dataIndex)) {
|
|
let value = record;
|
|
for (const key of dataIndex) {
|
|
value = value?.[String(key)];
|
|
}
|
|
return value;
|
|
}
|
|
if (dataIndex === undefined || dataIndex === null) {
|
|
return undefined;
|
|
}
|
|
return record?.[String(dataIndex)];
|
|
}
|
|
|
|
function isVNodeLike(value: any) {
|
|
return Boolean(value && typeof value === 'object' && value.__v_isVNode);
|
|
}
|
|
|
|
function updateTableScrollY() {
|
|
if (!props.autoHeight) {
|
|
return;
|
|
}
|
|
const area = tableAreaRef.value;
|
|
if (!area) {
|
|
return;
|
|
}
|
|
const headerHeight =
|
|
area.querySelector('.ant-table-thead')?.getBoundingClientRect().height || 48;
|
|
const scrollbarReserve = 8;
|
|
tableScrollY.value = Math.max(
|
|
160,
|
|
Math.floor(area.clientHeight - headerHeight - scrollbarReserve),
|
|
);
|
|
}
|
|
|
|
function emitLoadMore() {
|
|
if (loadMoreLocked) {
|
|
return;
|
|
}
|
|
loadMoreLocked = true;
|
|
emit('loadMore');
|
|
window.setTimeout(() => {
|
|
loadMoreLocked = false;
|
|
}, 300);
|
|
}
|
|
|
|
function handleTableBodyScroll(event: Event) {
|
|
if (!props.infinite || props.loading || !props.hasMore) {
|
|
return;
|
|
}
|
|
const target = event.currentTarget as HTMLElement;
|
|
const distanceToBottom =
|
|
target.scrollHeight - target.scrollTop - target.clientHeight;
|
|
if (distanceToBottom <= 80) {
|
|
emitLoadMore();
|
|
}
|
|
}
|
|
|
|
function bindTableBodyScroll() {
|
|
const nextBody = props.infinite
|
|
? (tableAreaRef.value?.querySelector('.ant-table-body') as HTMLElement | null)
|
|
: null;
|
|
if (nextBody === tableBodyElement) {
|
|
return;
|
|
}
|
|
tableBodyElement?.removeEventListener('scroll', handleTableBodyScroll);
|
|
tableBodyElement = nextBody;
|
|
tableBodyElement?.addEventListener('scroll', handleTableBodyScroll);
|
|
}
|
|
|
|
async function refreshTableScroll() {
|
|
await nextTick();
|
|
updateTableScrollY();
|
|
bindTableBodyScroll();
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await refreshTableScroll();
|
|
if (!props.autoHeight || typeof ResizeObserver === 'undefined') {
|
|
return;
|
|
}
|
|
tableResizeObserver = new ResizeObserver(() => {
|
|
void refreshTableScroll();
|
|
});
|
|
if (tableAreaRef.value) {
|
|
tableResizeObserver.observe(tableAreaRef.value);
|
|
}
|
|
if (pagerRef.value) {
|
|
tableResizeObserver.observe(pagerRef.value);
|
|
}
|
|
if (summaryRef.value) {
|
|
tableResizeObserver.observe(summaryRef.value);
|
|
}
|
|
window.addEventListener('resize', updateTableScrollY);
|
|
});
|
|
|
|
onUpdated(() => {
|
|
void refreshTableScroll();
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
tableBodyElement?.removeEventListener('scroll', handleTableBodyScroll);
|
|
tableResizeObserver?.disconnect();
|
|
window.removeEventListener('resize', updateTableScrollY);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
class="admin-config-table-card"
|
|
:class="{ 'admin-config-table-card--plain': plain }"
|
|
>
|
|
<div v-if="summaryText" ref="summaryRef" class="admin-config-table-summary">
|
|
{{ summaryText }}
|
|
</div>
|
|
|
|
<div ref="tableAreaRef" class="admin-config-table-area">
|
|
<Table
|
|
class="admin-config-table"
|
|
:columns="columns"
|
|
:data-source="dataSource"
|
|
:loading="loading"
|
|
:pagination="false"
|
|
:row-key="rowKey"
|
|
:row-selection="rowSelection"
|
|
:scroll="tableScroll"
|
|
>
|
|
<template #headerCell="slotProps">
|
|
<RenderNode
|
|
v-if="isVNodeLike(slotProps.column?.title)"
|
|
:node="slotProps.column.title"
|
|
/>
|
|
<slot v-else name="headerCell" v-bind="slotProps">
|
|
{{ slotProps.column.title }}
|
|
</slot>
|
|
</template>
|
|
<template #bodyCell="slotProps">
|
|
<RenderNode v-if="isVNodeLike(slotProps.text)" :node="slotProps.text" />
|
|
<slot v-else name="bodyCell" v-bind="slotProps">
|
|
{{ stringifyCellValue(getColumnValue(slotProps.record, slotProps.column)) }}
|
|
</slot>
|
|
</template>
|
|
</Table>
|
|
</div>
|
|
|
|
<div v-if="infiniteStatusText" class="admin-config-table-load-status">
|
|
{{ infiniteStatusText }}
|
|
</div>
|
|
|
|
<div v-if="showPager" ref="pagerRef" class="admin-config-table-pager">
|
|
<Pagination
|
|
:current="current"
|
|
:page-size="pageSize"
|
|
:total="total"
|
|
:show-size-changer="showSizeChanger"
|
|
@change="(page: number, size: number) => emit('pageChange', page, size)"
|
|
@show-size-change="(page: number, size: number) => emit('pageChange', page, size)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.admin-config-table-card {
|
|
background: #fff;
|
|
border: 1px solid rgb(229 231 235);
|
|
border-radius: 8px;
|
|
box-shadow: none;
|
|
display: flex;
|
|
flex: 1 1 auto;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.admin-config-table-card--plain {
|
|
border: 0;
|
|
border-radius: 0;
|
|
}
|
|
|
|
.admin-config-table-area {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.admin-config-table-summary {
|
|
border-bottom: 1px solid rgb(237 240 244);
|
|
color: rgb(71 84 103);
|
|
flex: 0 0 auto;
|
|
font-size: 12px;
|
|
line-height: 18px;
|
|
padding: 8px 12px;
|
|
}
|
|
|
|
.admin-config-table {
|
|
height: 100%;
|
|
width: 100%;
|
|
}
|
|
|
|
.admin-config-table :deep(.ant-table) {
|
|
color: rgb(52 64 84);
|
|
font-size: var(--density-table-body-font-size);
|
|
}
|
|
|
|
.admin-config-table :deep(.ant-spin-nested-loading),
|
|
.admin-config-table :deep(.ant-spin-container) {
|
|
height: 100%;
|
|
min-height: 0;
|
|
}
|
|
|
|
.admin-config-table :deep(.ant-spin-container) {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.admin-config-table :deep(.ant-table) {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
}
|
|
|
|
.admin-config-table :deep(.ant-table-container) {
|
|
min-height: 0;
|
|
}
|
|
|
|
.admin-config-table :deep(.ant-table-thead > tr > th) {
|
|
background: #fafafa;
|
|
border-bottom-color: rgb(237 240 244);
|
|
color: rgb(71 84 103);
|
|
font-size: var(--density-table-header-font-size);
|
|
font-weight: 500;
|
|
height: var(--density-table-header-height);
|
|
line-height: 1.35;
|
|
padding: 8px var(--density-table-cell-padding-x);
|
|
}
|
|
|
|
.admin-config-table :deep(.ant-table-thead > tr > th::before) {
|
|
background-color: rgb(237 240 244) !important;
|
|
}
|
|
|
|
.admin-config-table :deep(.ant-table-tbody > tr > td) {
|
|
border-bottom-color: rgb(237 240 244);
|
|
height: var(--density-table-row-height);
|
|
line-height: 1.35;
|
|
padding: 3px var(--density-table-cell-padding-x);
|
|
}
|
|
|
|
.admin-config-table :deep(.ant-table-body) {
|
|
overflow-y: auto !important;
|
|
}
|
|
|
|
.admin-config-table :deep(.switch-column) {
|
|
padding-left: var(--density-table-fixed-cell-padding-x) !important;
|
|
padding-right: var(--density-table-fixed-cell-padding-x) !important;
|
|
}
|
|
|
|
.admin-config-table :deep(.action-column) {
|
|
padding-left: var(--density-table-fixed-cell-padding-x) !important;
|
|
padding-right: var(--density-table-fixed-cell-padding-x) !important;
|
|
}
|
|
|
|
.admin-config-table :deep(.ant-table-tbody > tr:hover > td) {
|
|
background: rgb(250 252 255);
|
|
}
|
|
|
|
.admin-config-table :deep(.ant-table-cell-fix-left),
|
|
.admin-config-table :deep(.ant-table-cell-fix-right) {
|
|
background: #fff;
|
|
}
|
|
|
|
.admin-config-table :deep(.ant-table-tbody > tr:hover > .ant-table-cell-fix-left),
|
|
.admin-config-table :deep(.ant-table-tbody > tr:hover > .ant-table-cell-fix-right) {
|
|
background: rgb(250 252 255);
|
|
}
|
|
|
|
.admin-config-table-load-status {
|
|
border-top: 1px solid rgb(237 240 244);
|
|
color: rgb(100 116 139);
|
|
flex: 0 0 auto;
|
|
font-size: 12px;
|
|
line-height: 18px;
|
|
padding: 8px 12px;
|
|
text-align: center;
|
|
}
|
|
|
|
.admin-config-table-pager {
|
|
border-top: 1px solid rgb(237 240 244);
|
|
display: flex;
|
|
flex: 0 0 auto;
|
|
justify-content: flex-end;
|
|
padding: 10px 12px;
|
|
}
|
|
</style>
|