yumi-admin/apps/src/views/_shared/admin-config-table.vue
2026-05-19 22:23:14 +08:00

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>