20260720215810

This commit is contained in:
oneao committed 2026-07-20 21:58:10 +08:00
1 parent 94207c77ff
commit fd161dd302
9 files changed
+1242 -1102

No files matched your search

@@ -0,0 +1,197 @@
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
defineOptions({ inheritAttrs: false });
const props = defineProps({
bordered: { type: Boolean, default: true },
scroll: { type: Object, default: () => ({}) },
scrollX: { type: [Boolean, Number, String], default: "max-content" },
});
const tableAreaRef = ref(null);
const bodyHeight = ref(240);
const mergedScroll = computed(() => ({
...props.scroll,
x: props.scrollX,
y: bodyHeight.value,
}));
let resizeObserver;
let mutationObserver;
let frameId = 0;
const observedElements = new Set();
function getOuterHeight(element) {
if (!element) return 0;
const style = getComputedStyle(element);
const marginTop = Number.parseFloat(style.marginTop) || 0;
const marginBottom = Number.parseFloat(style.marginBottom) || 0;
return element.getBoundingClientRect().height + marginTop + marginBottom;
}
function clipsOverflowY(element) {
const { overflow, overflowY } = getComputedStyle(element);
return [overflow, overflowY].some((value) => /^(auto|clip|hidden|scroll)$/.test(value));
}
function getAvailableHeight(area) {
const areaTop = area.getBoundingClientRect().top;
let boundaryBottom = Number.POSITIVE_INFINITY;
let ancestor = area.parentElement;
while (ancestor && ancestor !== document.documentElement) {
if (clipsOverflowY(ancestor)) {
const rect = ancestor.getBoundingClientRect();
boundaryBottom = Math.min(boundaryBottom, rect.top + ancestor.clientTop + ancestor.clientHeight);
}
ancestor = ancestor.parentElement;
}
if (Number.isFinite(boundaryBottom) && boundaryBottom > areaTop) {
return Math.floor(boundaryBottom - areaTop);
}
return area.clientHeight;
}
function updateBodyHeight() {
const area = tableAreaRef.value;
if (!area?.clientHeight) return;
const headerHeight = getOuterHeight(
area.querySelector(".ant-table-header") || area.querySelector(".ant-table-thead"),
);
const paginationHeight = getOuterHeight(area.querySelector(".ant-table-pagination"));
const titleHeight = getOuterHeight(area.querySelector(".ant-table-title"));
const footerHeight = getOuterHeight(area.querySelector(".ant-table-footer"));
const nextHeight = Math.max(
1,
getAvailableHeight(area) - headerHeight - paginationHeight - titleHeight - footerHeight,
);
if (nextHeight !== bodyHeight.value) bodyHeight.value = nextHeight;
}
function scheduleHeightUpdate() {
cancelAnimationFrame(frameId);
frameId = requestAnimationFrame(updateBodyHeight);
}
function observe(element) {
if (!element || observedElements.has(element)) return;
resizeObserver?.observe(element);
observedElements.add(element);
}
function syncObservedElements() {
const area = tableAreaRef.value;
if (!area) return;
let ancestor = area.parentElement;
while (ancestor && ancestor !== document.documentElement) {
if (ancestor === area.parentElement || clipsOverflowY(ancestor)) observe(ancestor);
ancestor = ancestor.parentElement;
}
observe(area.querySelector(".ant-table-header"));
observe(area.querySelector(".ant-table-pagination"));
observe(area.querySelector(".ant-table-title"));
observe(area.querySelector(".ant-table-footer"));
scheduleHeightUpdate();
}
onMounted(async () => {
resizeObserver = new ResizeObserver(scheduleHeightUpdate);
mutationObserver = new MutationObserver(() => {
nextTick(syncObservedElements);
});
await nextTick();
syncObservedElements();
mutationObserver.observe(tableAreaRef.value, { childList: true, subtree: true });
});
onBeforeUnmount(() => {
cancelAnimationFrame(frameId);
resizeObserver?.disconnect();
mutationObserver?.disconnect();
observedElements.clear();
});
</script>
<template>
<div
ref="tableAreaRef"
class="auto-height-table"
:style="{ '--auto-table-body-height': bodyHeight + 'px' }"
>
<a-table v-bind="$attrs" :bordered="bordered" :scroll="mergedScroll" size="small">
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
<slot :name="name" v-bind="slotProps || {}" />
</template>
</a-table>
</div>
</template>
<style scoped>
.auto-height-table {
display: flex;
width: 100%;
height: 100%;
min-height: 0;
flex: 1 1 auto;
flex-direction: column;
overflow: hidden;
}
:deep(.ant-table-wrapper),
:deep(.ant-spin-nested-loading),
:deep(.ant-spin-container),
:deep(.ant-table),
:deep(.ant-table-container) {
min-height: 0;
}
:deep(.ant-table-wrapper) {
display: flex;
height: 100%;
flex: 1 1 auto;
flex-direction: column;
}
:deep(.ant-spin-nested-loading),
:deep(.ant-spin-container),
:deep(.ant-table),
:deep(.ant-table-container) {
display: flex;
flex: 1 1 auto;
flex-direction: column;
}
:deep(.ant-table-header),
:deep(.ant-table-pagination) {
flex: 0 0 auto;
}
:deep(.ant-table-body) {
height: var(--auto-table-body-height);
min-height: 0;
max-height: var(--auto-table-body-height) !important;
flex: 0 0 var(--auto-table-body-height);
overflow-y: auto !important;
}
/* scroll.y 模式下 antdv 不会渲染 body 底部外边框;且直接加在滚动容器 .ant-table-body
上时,边框会被内部滚动内容的最后一行盖住、或被祖先 overflow:hidden 裁掉,表现为
“看不到/被遮挡”。所以把底边框挂在不滚动的 .ant-table-container 上,并强制 border-box
让边框计入盒子高度,避免溢出裁切。 */
::deep(.ant-table-container),
::deep(.ant-table-body) {
box-sizing: border-box;
}
::deep(.ant-table-bordered .ant-table-container) {
border-bottom: 1px solid var(--ant-color-border-secondary, #f0f0f0);
}
</style>