BasicLayout 业务布局
@fx/layouts 业务层根布局组件。封装 FxLayout + slot 填业务组件(Sidebar/Header/Tabs/content/Footer)+ provide 业务上下文(统一菜单源)+ route 变化加 tab + 首页解析 + 刷新完成 emit refreshed。多模式编排:读 setting 驱动 7 种布局模式,双列 / 混合状态机(useExtraMenu / useMixedMenu)自动接线,内置设置按钮(fixed 形态)+ 设置抽屉 + 页面切换动画 + 水印 + 移动端断点。消费方一行接入完整后台 layout。
基础用法
vue
<script setup lang="ts">
import { computed } from "vue"
import { BasicLayout, type LayoutMenuNode } from "@fx/layouts"
import { useUserStore } from "@/stores/user"
import { staticRoutes } from "@/router/static-routes"
import { layoutStaticRoutes } from "@/router/routes"
import { staticRoutesToMenuNodes } from "@/utils/menu"
import { resetDynamicRoutes } from "@/router"
import { APP_TITLE, SHOW_FOOTER, ICP, COPYRIGHT } from "@/utils"
import { useQueryCache } from "@pinia/colada"
const userStore = useUserStore()
/** 统一菜单源:后端菜单 + 静态路由(转成菜单节点拼接在后) */
const mergedMenus = computed<LayoutMenuNode[]>(() => [
...userStore.menus,
...staticRoutesToMenuNodes(layoutStaticRoutes),
])
async function handleLogout() {
await userStore.logout()
resetDynamicRoutes()
}
function onRefreshed() {
useQueryCache().invalidateQueries()
}
</script>
<template>
<BasicLayout
:title="APP_TITLE"
:menus="mergedMenus"
:user="userStore.user"
:show-footer="SHOW_FOOTER"
:icp="ICP"
:copyright="COPYRIGHT"
@logout="handleLogout"
@refreshed="onRefreshed"
/>
</template>首页默认按菜单
is_home标记 / 第一个可导航项自动解析。若需显式指定(如固定用/dashboard),加:home-path="'/dashboard'"。
Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| title | 应用标题(Logo) | string | —(必填) |
| menus | 统一菜单源:后端菜单树 + 业务侧静态路由菜单(staticRoutesToMenuNodes 转换后拼接) | LayoutMenuNode[] | —(必填) |
| user | 当前用户 | LayoutUser | null | —(必填) |
| showFooter | 是否显示 footer(初值语义:无持久化 setting 时以此初始化,setting 优先) | boolean | false |
| icp | ICP 备案号 | string | — |
| copyright | 版权信息 | string | — |
| homePath | 显式首页路径(优先级 2,覆盖自动解析);不传则按菜单 is_home / 第一个可导航项自动解析 | string | — |
| watermarkContent | 水印内容(enableWatermark 开启时生效) | string | 应用标题 |
| showUser | 是否显示 header 用户区(头像 + 退出登录);无用户体系的消费方(如文档站)可关闭 | boolean | true |
| showNotify | 是否显示通知按钮;无通知体系的消费方可关闭 | boolean | true |
Events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| logout | 用户确认退出登录(消费方执行 userStore.logout + 路由重置) | () |
| refreshed | 刷新完成(refreshing true→false),消费方清缓存(如 useQueryCache) | () |
数据契约
menus / user 采用 layouts 视图模型(LayoutMenuNode / LayoutUser),仅含渲染所需字段。@fx/layouts 不依赖任何 @fx/api-* 包 —— 传任何结构兼容的对象即可,支持跨应用复用。
LayoutMenuNode
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 菜单标识 |
| name | string | 显示名称 |
| path | string | null | 路由路径 |
| icon | string | null | 图标名(缺省回退默认占位图标 ep:list) |
| menu_type | 'directory' | 'menu' | 'button' | 分类(渲染三分模型) |
| children | LayoutMenuNode[] | 子菜单 |
| is_home | boolean | 是否为首页标记(命中则作为首页,优先级最高) |
| badge | string | 菜单项徽章文本(如 HTTP method 'GET');缺省不渲染,折叠态自动隐藏 |
| badgeColor | string | 徽章背景色(任意 CSS 色);缺省用 FxBadge 默认类型色 |
LayoutUser
| 字段 | 类型 | 说明 |
|---|---|---|
| username | string | 用户名 |
| nickname | string | null | 昵称 |
@fx/api-admin的MenuNode/SystemUser是上述视图模型的超集,结构子类型天然兼容,直接传入即可。
静态路由并入菜单源
前端固定页面(开发演示页等)无需在后台配菜单:业务侧把静态路由转成菜单节点拼进 menus 即可,侧边栏 / 面包屑 / 菜单搜索自动全覆盖。
@fx/layouts 不内置该转换函数——静态路由(RouteRecordRaw)是业务侧路由定义,转换逻辑由业务侧自行实现。以下为参考实现(frontend/web utils/menu):
ts
// utils/menu:RouteRecordRaw 树 → LayoutMenuNode 树
import type { RouteRecordRaw } from "vue-router"
import type { LayoutMenuNode } from "@fx/layouts"
let staticMenuNodeSeq = 0
export function staticRoutesToMenuNodes(
routes: RouteRecordRaw[],
parentPath = "",
): LayoutMenuNode[] {
return routes.map((route) => {
const relative = parentPath ? `${parentPath}/${route.path}` : route.path
const path = `/${relative.replace(/^\/+/, "")}`
const children = route.children?.length
? staticRoutesToMenuNodes(route.children, path.slice(1))
: []
return {
id: ++staticMenuNodeSeq,
name: String(route.meta?.title ?? ""),
path,
icon: (route.meta?.icon as string) ?? null,
menu_type: children.length ? "directory" : "menu",
children,
}
})
}要点:
path按路由层级累积为绝对路径(与路由注册一致),子路由自动拼接父段- 有
children→menu_type: "directory"(侧边栏渲染为可展开分组);叶子 →"menu" id为递增序号,仅渲染期使用(静态菜单节点不落库)meta.title→name、meta.icon→icon,与后端菜单字段同构,合并后无差别渲染
ts
// router/routes.ts:注册与菜单同源(dev 全量注册;prod 静态路由整体不注册,
// 侧边栏以后端下发菜单为准,无可注册项时即为空数组)
export const layoutStaticRoutes: RouteRecordRaw[] = IS_DEV ? staticRoutes : []ts
// layout/index.vue:并入统一菜单源
const mergedMenus = computed<LayoutMenuNode[]>(() => [
...userStore.menus,
...staticRoutesToMenuNodes(layoutStaticRoutes),
])内部行为
- provide 业务上下文:统一菜单源 menus 经
provideLayoutBusiness注入,业务子组件(FxLayoutBreadcrumb / FxLayoutMenu / FxLayoutMenuSearch)inject 使用 - route 变化加 tab:watch route.path →
useLayoutStore.addTab(route) - 首页解析:watch menus / homePath,按优先级(菜单
is_home>homePathprop > 第一个可导航项)解析首页,调useLayoutStore.setHome注入(store 不自行解析,因初始化时 menus 未到) - 菜单空状态:无任何可导航项且未传
homePath时,content 区渲染FxPageStatus(forbidden)显示"暂无菜单权限"提示 - 刷新解耦:
useLayoutStore.triggerRefresh()只切 refreshing;完成后 emitrefreshed,消费方监听清缓存(@fx/layouts不依赖@pinia/colada)
依赖
- 内部组合
FxLayoutSidebar/FxLayoutHeader/FxLayoutTabs/FxLayoutBreadcrumb/FxLayoutMenuSearch/FxLayoutMenu - 状态走
useLayoutStore(pinia)