Skip to content

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 优先)booleanfalse
icpICP 备案号string—
copyright版权信息string—
homePath显式首页路径(优先级 2,覆盖自动解析);不传则按菜单 is_home / 第一个可导航项自动解析string—
watermarkContent水印内容(enableWatermark 开启时生效)string应用标题
showUser是否显示 header 用户区(头像 + 退出登录);无用户体系的消费方(如文档站)可关闭booleantrue
showNotify是否显示通知按钮;无通知体系的消费方可关闭booleantrue

Events ​

事件名说明回调参数
logout用户确认退出登录(消费方执行 userStore.logout + 路由重置)()
refreshed刷新完成(refreshing true→false),消费方清缓存(如 useQueryCache)()

数据契约 ​

menus / user 采用 layouts 视图模型(LayoutMenuNode / LayoutUser),仅含渲染所需字段。@fx/layouts 不依赖任何 @fx/api-* 包 —— 传任何结构兼容的对象即可,支持跨应用复用。

LayoutMenuNode ​

字段类型说明
idnumber菜单标识
namestring显示名称
pathstring | null路由路径
iconstring | null图标名(缺省回退默认占位图标 ep:list)
menu_type'directory' | 'menu' | 'button'分类(渲染三分模型)
childrenLayoutMenuNode[]子菜单
is_homeboolean是否为首页标记(命中则作为首页,优先级最高)
badgestring菜单项徽章文本(如 HTTP method 'GET');缺省不渲染,折叠态自动隐藏
badgeColorstring徽章背景色(任意 CSS 色);缺省用 FxBadge 默认类型色

LayoutUser ​

字段类型说明
usernamestring用户名
nicknamestring | 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 > homePath prop > 第一个可导航项)解析首页,调 useLayoutStore.setHome 注入(store 不自行解析,因初始化时 menus 未到)
  • 菜单空状态:无任何可导航项且未传 homePath 时,content 区渲染 FxPageStatus(forbidden)显示"暂无菜单权限"提示
  • 刷新解耦:useLayoutStore.triggerRefresh() 只切 refreshing;完成后 emit refreshed,消费方监听清缓存(@fx/layouts 不依赖 @pinia/colada)

依赖 ​

  • 内部组合 FxLayoutSidebar / FxLayoutHeader / FxLayoutTabs / FxLayoutBreadcrumb / FxLayoutMenuSearch / FxLayoutMenu
  • 状态走 useLayoutStore(pinia)