refactor(system): 实现表格自适应高度布局,抽离通用滚动工具hooks
1. 新增useTableScroll组合式函数,动态计算表格滚动高度 2. 改造多个系统页面布局为弹性伸缩容器,解决页面滚动问题 3. 统一表格滚动配置,优化页面视觉体验
This commit is contained in:
1 parent
cbc0a936b7
commit
4048aeb6fb
7 files changed
+232
-2
No files matched your search
@@ -0,0 +1,51 @@
|
|||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听表格父容器高度,动态计算 a-table 的 scroll.y。
|
||||||
|
* 自动测量 a-table 内部的分页器高度,从而得到准确的 tbody 高度。
|
||||||
|
*/
|
||||||
|
export function useTableScroll(minHeight = 120) {
|
||||||
|
const wrapperRef = ref<HTMLElement | null>(null);
|
||||||
|
const tableHeight = ref<number>(minHeight);
|
||||||
|
let observer: ResizeObserver | null = null;
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
if (!wrapperRef.value) return;
|
||||||
|
const wrapperHeight = wrapperRef.value.clientHeight;
|
||||||
|
|
||||||
|
// 动态获取分页器高度(如果存在)
|
||||||
|
const paginationEl = wrapperRef.value.querySelector('.ant-table-pagination') as HTMLElement | null;
|
||||||
|
const paginationHeight = paginationEl ? paginationEl.offsetHeight : 0;
|
||||||
|
|
||||||
|
// scroll.y = 容器总高 - 分页器高度(表头由 a-table 自动固定,无需扣除)
|
||||||
|
const tbodyHeight = wrapperHeight - paginationHeight;
|
||||||
|
tableHeight.value = Math.max(tbodyHeight, minHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!wrapperRef.value) return;
|
||||||
|
observer = new ResizeObserver(update);
|
||||||
|
observer.observe(wrapperRef.value);
|
||||||
|
|
||||||
|
// 初始测量
|
||||||
|
update();
|
||||||
|
|
||||||
|
// MutationObserver 用于监听分页器的动态添加/移除
|
||||||
|
const mutationObserver = new MutationObserver(() => {
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
mutationObserver.observe(wrapperRef.value, { childList: true, subtree: true });
|
||||||
|
// 保存以便卸载时使用
|
||||||
|
(wrapperRef.value as any).__mutationObserver = mutationObserver;
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
observer?.disconnect();
|
||||||
|
observer = null;
|
||||||
|
if (wrapperRef.value && (wrapperRef.value as any).__mutationObserver) {
|
||||||
|
(wrapperRef.value as any).__mutationObserver.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { wrapperRef, tableHeight };
|
||||||
|
}
|
||||||
@@ -409,13 +409,17 @@ function handleUserMenuClick({ key }: { key: string }) {
|
|||||||
|
|
||||||
.content {
|
.content {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: calc(100vh - 64px);
|
height: calc(100vh - 64px);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
.view-wrapper {
|
.view-wrapper {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
min-height: calc(100vh - 64px);
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,12 +63,14 @@
|
|||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
<a-card :bordered="false" class="table-card">
|
<a-card :bordered="false" class="table-card">
|
||||||
|
<div class="table-wrapper" ref="wrapperRef">
|
||||||
<a-table
|
<a-table
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data-source="logList"
|
:data-source="logList"
|
||||||
:loading="tableLoading"
|
:loading="tableLoading"
|
||||||
:pagination="paginationConfig"
|
:pagination="paginationConfig"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
|
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||||
@change="handleTableChange"
|
@change="handleTableChange"
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
@@ -82,6 +84,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -92,6 +95,9 @@ import { message } from 'ant-design-vue'
|
|||||||
import { SearchOutlined, ReloadOutlined, DownloadOutlined } from '@ant-design/icons-vue'
|
import { SearchOutlined, ReloadOutlined, DownloadOutlined } from '@ant-design/icons-vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { logApi } from '@/api/log'
|
import { logApi } from '@/api/log'
|
||||||
|
import { useTableScroll } from '@/composables/useTableScroll'
|
||||||
|
|
||||||
|
const { wrapperRef, tableHeight } = useTableScroll()
|
||||||
|
|
||||||
const tableLoading = ref(false)
|
const tableLoading = ref(false)
|
||||||
const exportLoading = ref(false)
|
const exportLoading = ref(false)
|
||||||
@@ -271,9 +277,15 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.operation-log {
|
.operation-log {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
.search-card {
|
.search-card {
|
||||||
|
flex-shrink: 0;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
|
||||||
:deep(.ant-card-body) {
|
:deep(.ant-card-body) {
|
||||||
@@ -282,8 +294,29 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-card {
|
.table-card {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.ant-table) {
|
:deep(.ant-table) {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,12 +40,14 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="table-wrapper" ref="wrapperRef">
|
||||||
<a-table
|
<a-table
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data-source="dataSource"
|
:data-source="dataSource"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:pagination="pagination"
|
:pagination="pagination"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
|
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||||
@change="handleTableChange"
|
@change="handleTableChange"
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
@@ -80,6 +82,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
<a-modal
|
<a-modal
|
||||||
@@ -131,8 +134,11 @@ import {
|
|||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import { dictApi } from '@/api/dict';
|
import { dictApi } from '@/api/dict';
|
||||||
|
import { useTableScroll } from '@/composables/useTableScroll';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const { wrapperRef, tableHeight } = useTableScroll();
|
||||||
|
|
||||||
interface DictItem {
|
interface DictItem {
|
||||||
id: number;
|
id: number;
|
||||||
dictType: string;
|
dictType: string;
|
||||||
@@ -346,18 +352,47 @@ async function handleDelete(record: DictItem) {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.dict-management {
|
.dict-management {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
|
flex-shrink: 0;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.ant-table) {
|
:deep(.ant-table) {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.ant-btn-link) {
|
:deep(.ant-btn-link) {
|
||||||
|
|||||||
@@ -8,12 +8,14 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<div class="table-wrapper" ref="wrapperRef">
|
||||||
<a-table
|
<a-table
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data-source="menuList"
|
:data-source="menuList"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
|
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||||
:expandable="{
|
:expandable="{
|
||||||
defaultExpandAllRows: false,
|
defaultExpandAllRows: false,
|
||||||
showExpandColumn: true,
|
showExpandColumn: true,
|
||||||
@@ -54,6 +56,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
<a-modal
|
<a-modal
|
||||||
@@ -131,6 +134,9 @@ import { ref, reactive, onMounted, computed } from 'vue';
|
|||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
||||||
import { menuApi } from '@/api/menu';
|
import { menuApi } from '@/api/menu';
|
||||||
|
import { useTableScroll } from '@/composables/useTableScroll';
|
||||||
|
|
||||||
|
const { wrapperRef, tableHeight } = useTableScroll({ paginationHeight: 0 });
|
||||||
|
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -329,12 +335,43 @@ async function handleDelete(record: MenuItem) {
|
|||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.menu-management {
|
.menu-management {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
:deep(.ant-card) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.ant-btn-link) {
|
:deep(.ant-btn-link) {
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.ant-table-row-expand-icon) {
|
:deep(.ant-table-row-expand-icon) {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -21,12 +21,14 @@
|
|||||||
</a-input-search>
|
</a-input-search>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="table-wrapper" ref="wrapperRef">
|
||||||
<a-table
|
<a-table
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data-source="roleList"
|
:data-source="roleList"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:pagination="pagination"
|
:pagination="pagination"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
|
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||||
@change="handleTableChange"
|
@change="handleTableChange"
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
@@ -68,6 +70,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
<a-modal
|
<a-modal
|
||||||
@@ -144,8 +147,11 @@ import {
|
|||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import { roleApi, type RoleInfo } from '@/api/role';
|
import { roleApi, type RoleInfo } from '@/api/role';
|
||||||
|
import { useTableScroll } from '@/composables/useTableScroll';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const { wrapperRef, tableHeight } = useTableScroll();
|
||||||
|
|
||||||
interface MenuTreeNode {
|
interface MenuTreeNode {
|
||||||
id: number;
|
id: number;
|
||||||
menuName: string;
|
menuName: string;
|
||||||
@@ -400,17 +406,46 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.role-management {
|
.role-management {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
|
flex-shrink: 0;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.ant-table) {
|
:deep(.ant-table) {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.ant-btn-link) {
|
:deep(.ant-btn-link) {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="table-wrapper" ref="wrapperRef">
|
||||||
<a-table
|
<a-table
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data-source="dataSource"
|
:data-source="dataSource"
|
||||||
@@ -45,6 +46,7 @@
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
:pagination="pagination"
|
:pagination="pagination"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
|
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||||
@change="handleTableChange"
|
@change="handleTableChange"
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
@@ -89,6 +91,7 @@
|
|||||||
<a-empty description="暂无用户数据" />
|
<a-empty description="暂无用户数据" />
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
<a-modal
|
<a-modal
|
||||||
@@ -165,8 +168,11 @@ import {
|
|||||||
EditOutlined,
|
EditOutlined,
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import { userApi, type UserInfo, type UserQuery, type RoleInfo } from '@/api/user';
|
import { userApi, type UserInfo, type UserQuery, type RoleInfo } from '@/api/user';
|
||||||
|
import { useTableScroll } from '@/composables/useTableScroll';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const { wrapperRef, tableHeight } = useTableScroll();
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const dataSource = ref<UserInfo[]>([]);
|
const dataSource = ref<UserInfo[]>([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
@@ -469,18 +475,47 @@ function formatDateTime(dateStr: string) {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.user-management {
|
.user-management {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
|
flex-shrink: 0;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.ant-table) {
|
:deep(.ant-table) {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.ant-btn-link) {
|
:deep(.ant-btn-link) {
|
||||||
|
|||||||
Reference in new issue
Block a user