5、页面布局 xnPageLayout
本文基于开源项目 心念后台(xn-admin):Apache License 2.0 可商用,后端 Java 21 + Spring Boot 4 + Spring Cloud(网关 + 系统 / 文件 / 日志 / 任务),前端提供 React 19、Vue3+TS、Vue3+JS、Vue3 Options API 四套管理端,共用同一套 REST API。前文已介绍整体架构、技术栈与启动方式;本文从内部组件切入,讲 xnPageLayout(列表页骨架) 如何把搜索、按钮、表格、左侧树拼成一页,以及用法。演示:React · Vue3 TS。源码:后端 · React · Vue3+TS。
组件作用
xnPageLayout 是后台 CRUD 列表页的整页骨架。它不管业务数据,只规定:左边可以放树、上面放搜索、工具栏一行按钮、下面放表格(或卡片)。xnSearch、xnButton、xnTable、xnTreePanel 四个组件都往它的插槽里塞,各列表页看起来才整齐。
它解决的是:没有统一壳时,每页自己调 padding、滚动、工具栏对齐,时间一长就会乱。有它之后,业务页只填槽,版式由布局组件保证。
功能:
- 有左侧插槽时左右分栏,没有则主区铺满
- 插槽:左侧树、搜索、工具栏、工具栏右侧扩展、表格、卡片、分页、页脚
- 同时提供表格和卡片插槽时,可切换两种视图;偏好按路由写入
localStorage,刷新还在 - 分页可以放在布局层,列表页更常见的是放在 xnTable 内部
- 纯前端,不请求任何接口
用在哪: 用户、角色、字典、日志、文件等绝大多数列表页的最外层包裹。前面四个组件仍然能单独用,但组合出现时都应套本布局。
前端依赖(npm)
| 栈 | 依赖 | 用途 |
|---|---|---|
| Vue3 TS | vue、element-plus | 卡片容器、分页、视图切换按钮 |
| React TS | react、antd | Layout 结构、Pagination、Segmented |
纯布局,无图表、无拖拽库。视图模式只写浏览器 localStorage。
后端依赖(Maven)
无独立后端。 不请求接口,也不对应 Java 类。
运行时会读前端 appConfig(系统配置里的字号、标签栏高度等),那是全局配置,不是本组件的 API。
源码位置
路径均相对 xn-admin/。
| Vue3 TS | React TS | |
|---|---|---|
| 组件 | xn-admin-vue3-ts/src/components/xnPageLayout/ | xn-admin-react-ts/src/components/XnPageLayout/ |
用法
<xnPageLayout>
<template #aside>
<xnTreePanel ... />
</template>
<template #search>
<xnSearch ... />
</template>
<template #toolbar>
<xnButton ... />
</template>
<template #table>
<xnTable ... />
</template>
</xnPageLayout>
截图
用户管理整页就是 xnPageLayout 把左树、搜索、按钮、表格拼在一起:
截图/react-users-list.png/vue3-ts-users-list.png截图/react-dashboard.png:工作台(对照:不是列表壳)
源码(可直接粘贴 CSDN)
本组件无后端。Vue 用插槽,React 用 props 传节点,结构一致。
Vue 插槽骨架
<template>
<div class="page-layout" :class="{ 'is-with-aside': hasAside }">
<aside v-if="hasAside" class="page-layout__aside">
<slot name="aside" />
</aside>
<div class="page-layout__right">
<div v-if="$slots.search" class="page-layout__search">
<slot name="search" />
</div>
<div v-if="hasToolbar" class="page-layout__toolbar">
<slot name="toolbar" />
<!-- 同时有 table + card 插槽才显示切换 -->
<el-radio-group v-if="showViewSwitchActive" v-model="currentViewMode">
<el-radio-button value="table">表格</el-radio-button>
<el-radio-button value="card">卡片</el-radio-button>
</el-radio-group>
</div>
<div class="page-layout__main">
<div v-show="currentViewMode === 'table'"><slot name="table" /></div>
<div v-show="currentViewMode === 'card'"><slot name="card" /></div>
</div>
</div>
</div>
</template>
视图模式写入 localStorage,键为 xn-view-mode: + route.path,刷新不丢。
React 对照
interface XnPageLayoutProps {
aside?: ReactNode
search?: ReactNode
toolbar?: ReactNode
table?: ReactNode
card?: ReactNode
showViewSwitch?: boolean
loading?: boolean
}
export default function XnPageLayout({ aside, search, toolbar, table, card }: XnPageLayoutProps) {
const hasAside = Boolean(aside)
return (
<div className={hasAside ? 'page-layout is-with-aside' : 'page-layout'}>
{aside ? <aside className="page-layout__aside">{aside}</aside> : null}
<div className="page-layout__right">
{search}
{toolbar}
{table}
</div>
</div>
)
}
业务页拼起来
<xnPageLayout>
<template #aside><xnTreePanel :data="tree" @node-click="onNode" /></template>
<template #search><xnSearch :search-item="pageUi.searchItems" @query-form="load" /></template>
<template #toolbar><xnButton :list-item="pageUi.buttonItems" :selected="selected" /></template>
<template #table><xnTable :data="rows" :columns="columns" :total="total" /></template>
</xnPageLayout>