refactor(useTableScroll): 重构表格滚动高度计算逻辑,适配表头高度扣除

1.  优化滚动高度计算方式,改为容器总高减去表头和分页器高度,计算更准确
2.  新增nextTick等待DOM渲染完成后再进行尺寸测量
3.  调整默认最小高度为100px,优化变量命名和注释
4.  简化菜单页面的模板代码格式,统一代码风格
5.  更新useTableScroll的调用参数,移除旧的自定义paginationHeight传参
This commit is contained in:
andyesfly committed 2026-08-21 19:59:22 +08:00
1 parent 4048aeb6fb
commit 026ec34471
2 files changed
+66 -90

No files matched your search

+23 -16
View File
@@ -1,49 +1,56 @@
import { ref, onMounted, onBeforeUnmount } from 'vue'; import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
/** /**
* 监听表格父容器高度,动态计算 a-table 的 scroll.y。 * 正确计算 AntDV a-table 的 scroll.y 高度。
* 自动测量 a-table 内部的分页器高度,从而得到准确的 tbody 高度。 * 核心逻辑:scroll.y = 外层容器总高 - 表头真实高度 - 分页器真实高度
*/ */
export function useTableScroll(minHeight = 120) { export function useTableScroll(minHeight = 100) {
const wrapperRef = ref<HTMLElement | null>(null); const wrapperRef = ref<HTMLElement | null>(null);
const tableHeight = ref<number>(minHeight); const tableHeight = ref<number>(minHeight);
let observer: ResizeObserver | null = null; let observer: ResizeObserver | null = null;
function update() { function update() {
if (!wrapperRef.value) return; if (!wrapperRef.value) return;
const wrapperHeight = wrapperRef.value.clientHeight; const wrapperHeight = wrapperRef.value.clientHeight;
// 动态获取分页器高度(如果存在) // 动态获取表头和分页器的真实高度
const headerEl = wrapperRef.value.querySelector('.ant-table-thead') as HTMLElement | null;
const paginationEl = wrapperRef.value.querySelector('.ant-table-pagination') as HTMLElement | null; const paginationEl = wrapperRef.value.querySelector('.ant-table-pagination') as HTMLElement | null;
const headerHeight = headerEl ? headerEl.offsetHeight : 55; // 55 为兜底
const paginationHeight = paginationEl ? paginationEl.offsetHeight : 0; const paginationHeight = paginationEl ? paginationEl.offsetHeight : 0;
// scroll.y = 容器总高 - 分页器高度(表头由 a-table 自动固定,无需扣除) // 计算 tbody 的高度
const tbodyHeight = wrapperHeight - paginationHeight; const tbodyHeight = wrapperHeight - headerHeight - paginationHeight;
tableHeight.value = Math.max(tbodyHeight, minHeight); tableHeight.value = Math.max(tbodyHeight, minHeight);
} }
onMounted(() => { onMounted(async () => {
if (!wrapperRef.value) return; if (!wrapperRef.value) return;
// 等待 DOM 渲染完成后再测量
await nextTick();
observer = new ResizeObserver(update); observer = new ResizeObserver(update);
observer.observe(wrapperRef.value); observer.observe(wrapperRef.value);
// 初始测量 // 初始计算
update(); update();
// MutationObserver 用于监听分页器的动态添加/移除 // 使用 MutationObserver 监听 DOM 变化(比如数据加载后表头/分页器出现)
const mutationObserver = new MutationObserver(() => { const mutationObserver = new MutationObserver(() => {
update(); update();
}); });
mutationObserver.observe(wrapperRef.value, { childList: true, subtree: true }); mutationObserver.observe(wrapperRef.value, { childList: true, subtree: true });
// 保存以便卸载时使用 (wrapperRef.value as any).__mo = mutationObserver;
(wrapperRef.value as any).__mutationObserver = mutationObserver;
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
observer?.disconnect(); observer?.disconnect();
observer = null; observer = null;
if (wrapperRef.value && (wrapperRef.value as any).__mutationObserver) { if (wrapperRef.value && (wrapperRef.value as any).__mo) {
(wrapperRef.value as any).__mutationObserver.disconnect(); (wrapperRef.value as any).__mo.disconnect();
} }
}); });
+43 -74
View File
@@ -3,86 +3,59 @@
<a-card title="菜单管理"> <a-card title="菜单管理">
<template #extra> <template #extra>
<a-button type="primary" @click="handleAdd()"> <a-button type="primary" @click="handleAdd()">
<template #icon><PlusOutlined /></template> <template #icon>
<PlusOutlined />
</template>
新增菜单 新增菜单
</a-button> </a-button>
</template> </template>
<div class="table-wrapper" ref="wrapperRef"> <div class="table-wrapper" ref="wrapperRef">
<a-table <a-table :columns="columns" :data-source="menuList" :loading="loading" :pagination="false" row-key="id"
:columns="columns" :scroll="{ x: 'max-content', y: tableHeight }" :expandable="{
:data-source="menuList" defaultExpandAllRows: false,
:loading="loading" showExpandColumn: true,
:pagination="false" }">
row-key="id" <template #bodyCell="{ column, record }">
:scroll="{ x: 'max-content', y: tableHeight }" <template v-if="column.dataIndex === 'menuType'">
:expandable="{ <a-tag :color="record.menuType === 1 ? 'blue' : 'green'">
defaultExpandAllRows: false, {{ record.menuType === 1 ? '目录' : '菜单' }}
showExpandColumn: true, </a-tag>
}" </template>
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'menuType'">
<a-tag :color="record.menuType === 1 ? 'blue' : 'green'">
{{ record.menuType === 1 ? '目录' : '菜单' }}
</a-tag>
</template>
<template v-else-if="column.dataIndex === 'visible'"> <template v-else-if="column.dataIndex === 'visible'">
<a-tag :color="record.visible === 1 ? 'success' : 'default'"> <a-tag :color="record.visible === 1 ? 'success' : 'default'">
{{ record.visible === 1 ? '显示' : '隐藏' }} {{ record.visible === 1 ? '显示' : '隐藏' }}
</a-tag> </a-tag>
</template> </template>
<template v-else-if="column.key === 'action'"> <template v-else-if="column.key === 'action'">
<a-space> <a-space>
<a-button type="link" size="small" @click="handleAdd(record)"> <a-button type="link" size="small" @click="handleAdd(record)">
<PlusOutlined />新增子菜单 <PlusOutlined />新增子菜单
</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">
<EditOutlined />编辑
</a-button>
<a-popconfirm
title="确定删除该菜单?"
ok-text="确定"
cancel-text="取消"
@confirm="handleDelete(record)"
>
<a-button type="link" size="small" danger>
<DeleteOutlined />删除
</a-button> </a-button>
</a-popconfirm> <a-button type="link" size="small" @click="handleEdit(record)">
</a-space> <EditOutlined />编辑
</a-button>
<a-popconfirm title="确定删除该菜单?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record)">
<a-button type="link" size="small" danger>
<DeleteOutlined />删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</template> </template>
</template> </a-table>
</a-table>
</div> </div>
</a-card> </a-card>
<a-modal <a-modal v-model:open="modalVisible" :title="modalTitle" :confirm-loading="modalLoading" width="640px"
v-model:open="modalVisible" @ok="handleSubmit" @cancel="handleCancel">
:title="modalTitle" <a-form ref="formRef" :model="formData" :rules="formRules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
:confirm-loading="modalLoading"
width="640px"
@ok="handleSubmit"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="上级菜单" name="parentId"> <a-form-item label="上级菜单" name="parentId">
<a-tree-select <a-tree-select v-model:value="formData.parentId" :tree-data="parentOptions"
v-model:value="formData.parentId" :field-names="{ label: 'menuName', value: 'id', children: 'children' }" allow-clear tree-default-expand-all
:tree-data="parentOptions" placeholder="请选择上级菜单" />
:field-names="{ label: 'menuName', value: 'id', children: 'children' }"
allow-clear
tree-default-expand-all
placeholder="请选择上级菜单"
/>
</a-form-item> </a-form-item>
<a-form-item label="菜单类型" name="menuType"> <a-form-item label="菜单类型" name="menuType">
@@ -117,12 +90,8 @@
</a-form-item> </a-form-item>
<a-form-item label="可见性" name="visible"> <a-form-item label="可见性" name="visible">
<a-switch <a-switch :checked="formData.visible === 1" checked-children="显示" un-checked-children="隐藏"
:checked="formData.visible === 1" @change="(checked: boolean) => (formData.visible = checked ? 1 : 0)" />
checked-children="显示"
un-checked-children="隐藏"
@change="(checked: boolean) => (formData.visible = checked ? 1 : 0)"
/>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
@@ -136,7 +105,7 @@ import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vu
import { menuApi } from '@/api/menu'; import { menuApi } from '@/api/menu';
import { useTableScroll } from '@/composables/useTableScroll'; import { useTableScroll } from '@/composables/useTableScroll';
const { wrapperRef, tableHeight } = useTableScroll({ paginationHeight: 0 }); const { wrapperRef, tableHeight } = useTableScroll();
interface MenuItem { interface MenuItem {
id: number; id: number;