业务组件接入指南¶
📄 创建: 刘泽伟 2026-08-04
https://doc.xpku.com/raw/tech/frontend/%E5%B8%83%E5%B1%80%E4%B8%8E%E8%8F%9C%E5%8D%95/%E7%BB%84%E4%BB%B6%E5%8C%85%E8%AF%B4%E6%98%8E.md
BAMS 工作平台的应用框架布局包,基于 Vue 3 + Vite + ant-design-vue 构建,为各业务子应用提供统一的页面骨架与布局能力。
功能特性¶
- 整体布局:顶栏 Header + 侧边菜单 Sider + 工具栏 + 内容区,Sider 宽度随窗口尺寸响应式变化
- 菜单体系:从后端拉取菜单数据,转换为目标 UI 的菜单结构,支持顶层菜单分割线
- 菜单导航:支持三种打开方式
- 普通内部路由跳转
openType == 1:iframe 内嵌打开(query 携带iframe参数)openType == 2:外部链接新窗口打开- 内容区双模式:根据路由 query 中的
iframe参数自动切换「路由视图」与「iframe 视图」,并配合 keep-alive 缓存已打开的页面 - 路由同步:路由变化时自动同步菜单选中状态与标签页(TabsBar)
- 主题体系:样式全部使用
@bams-app/theme提供的 CSS 变量(--bams-page-bg、--bams-surface-bg、--bams-border-color等)
技术栈¶
| 依赖 | 用途 |
|---|---|
| vue ^3.5 / vue-router ^4.6 | 框架与路由 |
| ant-design-vue | PC 布局 UI 基础 |
| @bams-app/store | Pinia store(menu / tabs / app 状态) |
| @bams-app/theme | 主题 CSS 变量与 antd 主题配置 |
| @bams-app/components | BamsIframe 等通用组件 |
| @configurable/header | 编译期通过别名可切换的 Header 组件 |
目录结构¶
src/
├── index.js # 包入口:对外暴露能力统一在此导出
├── composables/
│ ├── createLayoutMenu.js # 惰性单例工厂:从 store 同步菜单,支持 transformMenu 加工转换
│ ├── useAppLayoutInit.js # 初始化菜单 → 监听路由变化 → 同步菜单选中与标签页
│ ├── useMenuNavigation.js # 菜单点击跳转逻辑(内部路由 / iframe / 外部链接)
│ └── useTabsBar.js # 标签页通用能力:tabs 状态与关闭/刷新/跳转逻辑
├── layout/
│ ├── ContentRouteAndIframeView.vue # 路由视图 ⇄ iframe 视图切换(keep-alive 缓存)
│ └── useContentView.js
├── layout-pc/ # PC 端主布局(内置实现)
│ ├── AppLayout.vue # Header + Sider(Menu) + ToolBar + ContentView
│ ├── Menu.vue / useMenu.js # 后端菜单 → antd Menu 节点转换
│ ├── ToolBar.vue / Breadcrumb.vue / TabsBar.vue
│ └── initAppLayout.js # 调用 useAppLayoutInit 做路由同步
├── layout-touch/ # 触屏端布局(基于能力自定义布局的参考实现)
│ ├── AppLayout.vue
│ ├── TouchHeader.vue / Menu.vue / ContentView.vue
│ └── components/ThemeSwitch.vue
├── router/ # 本地 demo 路由
└── views/ # 本地 demo 页面
对外暴露的通用能力¶
包对外暴露的是一组通用能力:菜单数据管理、布局初始化、菜单导航、内容区渲染。它们只负责数据与逻辑,不绑定任何具体 UI,各布局可自由组装。入口 src/index.js 导出:
| 导出 | 类型 | 说明 |
|---|---|---|
AppLayoutContentRouteAndIframeView |
组件 | 内容区视图:路由视图 / iframe 双模式切换,含 keep-alive 缓存 |
createLayoutMenu |
函数 | 创建布局菜单管理实例(惰性单例工厂) |
useAppLayoutInit |
函数 | 布局初始化 composable:拉取菜单 + 路由变化同步菜单与标签页 |
useMenuNavigation |
函数 | 菜单导航 composable:封装菜单点击后的跳转逻辑 |
useTabsBar |
函数 | 标签页 composable:tabs 状态、右键菜单、关闭/刷新/跳转逻辑 |
createLayoutMenu(options)¶
创建菜单管理实例的工厂函数,返回一个工厂:首次调用创建实例,后续调用复用(单例)。
options.transformMenu:菜单转换函数(menuList, { menuStore }) => menuList,用于过滤 / 加工后端菜单数据- 实例属性与方法:
menus:响应式菜单列表(ref)menuStore:Pinia 菜单 storeinitMenuData():先同步本地已有菜单,再调用menuStore.getMenuList()拉取远程菜单并二次同步,返回 PromiseprocessMenu(menuList):对菜单列表执行transformMenu转换syncMenus():从 store 同步菜单到menusref
useAppLayoutInit(options)¶
布局初始化 composable,负责初始化菜单数据并在路由变化时同步菜单选中状态与标签页。
options.useMenu:布局菜单 hook,需提供initMenuData方法(通常由createLayoutMenu生成)options.errorMessage:初始化失败时的错误提示前缀- 返回 Promise:
initMenuData()成功后watch(route.name)开始同步路由状态
useMenuNavigation(options)¶
菜单导航 composable,封装菜单点击后的跳转逻辑。
options.preserveRouteQuery:跳转时是否保留当前路由的 query 参数(默认false)- 返回
{ openMenu }:根据菜单的openType决定跳转方式(普通路由 / iframe 内嵌 / 外部链接新窗口)
useTabsBar(options)¶
标签页通用能力 composable,提供标签页列表、激活态、右键菜单状态,以及关闭 / 刷新 / 跳转等操作,不绑定任何 UI(layout-pc/TabsBar.vue 只是其中一种实现,PC 版 layout-pc/useTabsBar.js 保留不动)。
options.buildBasePath:关闭全部或当前页兜底时,生成基础路由路径的函数(route) => string,默认按 BAMS 平台 URL 约定拼接(/render/或/work/前缀 + channel / projectId 参数)- 返回:
tabs、activeKey、currentActiveTab、contextMenu、hasLeftTabs、hasRightTabs、navigateToBaseRoute、closeContextMenu、openContextMenu、handleTabClick、handleClose、handleCloseAll、handleCloseOthers、handleCloseLeft、handleCloseRight、handleRefresh
菜单数据必填字段¶
通用能力依赖后端菜单数据,菜单项必须包含以下字段,能力才能正常工作:
| 字段 | 说明 |
|---|---|
id |
菜单唯一标识,用作菜单 key 与选中态记录 |
menuName |
菜单显示名称 |
path |
菜单路径,点击后的跳转目标(路由路径或外部链接) |
openType |
打开方式:1 iframe 内嵌、2 外部链接新窗口、其他值普通路由跳转 |
children |
子菜单数组(无子菜单时为空数组) |
以下为可选字段:
| 字段 | 说明 |
|---|---|
iframeSrc |
openType == 1 时的内嵌页面地址,缺省回退为 path |
params |
跳转时附加的路径 / query 参数 |
isCache |
当前页面是否启用 keep-alive 缓存(缺省读路由 meta) |
disabled / popupClassName / popupOffset / theme / menuIcon 等 |
透传给具体 UI 的展示配置 |
为什么对外暴露通用能力(适用场景)¶
各业务端(PC、触屏、后续其他形态)的布局 UI 各不相同,但菜单数据、路由同步、导航跳转、内容区渲染等行为完全一致。因此将公共逻辑下沉为通用能力统一维护,避免每个布局重复实现导致逻辑漂移。
适用场景:
- 新增一套布局:如移动端、平板、新品牌皮肤等。UI 完全自定义,数据与逻辑直接复用
- 自定义菜单展示结构:通过
transformMenu将后端菜单加工为目标 UI 形态(如触屏端转为卡片、PC 端转为 antd Menu 树) - 业务子应用接入布局:只关心 UI 组装,无需了解菜单拉取、路由同步、iframe 处理等内部实现
- 统一行为与升级:菜单选中、标签页同步、导航规则由能力层统一控制,一处升级全端生效
如何基于能力开发新的布局菜单¶
layout-touch 目录即是基于这些能力自定义布局的完整参考实现。核心思路:能力只负责数据与逻辑,UI 由各布局自由组装。按以下步骤共创建 4 个文件:
| 文件 | 职责 |
|---|---|
useMenu.js |
菜单数据 hook:把后端菜单转换为自定义 UI 结构(第 1 步) |
initAppLayout.js |
布局初始化:拉取菜单 + 路由同步(第 2 步) |
Menu.vue |
菜单 UI 组件:渲染菜单并处理点击跳转(第 3 步) |
AppLayout.vue |
布局组件:组装 Header / 菜单 / 内容区(第 4 步) |
1. 创建菜单 hook(useMenu.js)¶
通过 createLayoutMenu({ transformMenu }) 把后端菜单转换为目标 UI 需要的结构:
// useMenu.js
import { createLayoutMenu } from "@bams-app/app-layout";
// 将后端菜单字段映射为自定义菜单结构(参考 layout-touch/useMenu.js)
const transformMenu = menuList => {
return menuList.map(menu => ({
id: menu.id,
name: menu.menuName,
path: menu.path,
icon: menu.menuIcon,
openType: menu.openType,
iframeSrc: menu.iframeSrc,
params: menu.params,
children: menu.children || [],
}));
};
export default createLayoutMenu({ transformMenu });
useMenu() 返回的实例提供 menus(响应式菜单列表)、initMenuData、processMenu、syncMenus 等能力(详见上文 createLayoutMenu 说明)。
2. 封装布局初始化逻辑(initAppLayout.js)¶
// initAppLayout.js
import { useAppLayoutInit } from "@bams-app/app-layout";
import useMenu from "./useMenu.js";
export default function initAppLayout() {
return useAppLayoutInit({
useMenu,
errorMessage: "布局初始化失败:",
});
}
3. 创建菜单 UI 组件(Menu.vue)¶
Menu.vue 是自定义布局自己的菜单组件,包内不内置菜单 UI(PC 端参考 layout-pc/Menu.vue,触屏端参考 layout-touch/Menu.vue)。它基于第 1 步的 useMenu hook 获取菜单数据,并通过 useMenuNavigation 处理点击跳转:
<!-- Menu.vue -->
<template>
<ul class="my-menu">
<li v-for="menu in menus" :key="menu.id" :class="{ active: activeId === menu.id }" @click="handleMenuClick(menu)">
{{ menu.name }}
</li>
</ul>
</template>
<script setup>
import { computed } from "vue";
import { useMenuStore } from "@bams-app/store";
import useMenu from "./useMenu.js";
import { useMenuNavigation } from "@bams-app/app-layout";
const { menus } = useMenu();
const menuStore = useMenuStore();
// 统一处理跳转:自动兼容普通路由、iframe 内嵌、外部链接三种 openType
const { openMenu } = useMenuNavigation();
// 当前激活的菜单 id,从 store 获取(路由变化时由 useAppLayoutInit 自动同步)
const activeId = computed(() => menuStore.selectedKeys[0]);
const handleMenuClick = menu => openMenu(menu);
</script>
4. 组装布局组件(AppLayout.vue)¶
在布局组件中组合 Header / 菜单 / 内容区:Menu 用第 3 步创建的组件,内容区直接复用 AppLayoutContentRouteAndIframeView,并调用 initAppLayout() 完成初始化:
<!-- AppLayout.vue -->
<template>
<div class="my-layout">
<MyHeader />
<Menu />
<AppLayoutContentRouteAndIframeView router-container-class="router-container" iframe-container-class="iframe-container" />
</div>
</template>
<script setup>
import { AppLayoutContentRouteAndIframeView } from "@bams-app/app-layout";
import MyHeader from "./MyHeader.vue"; // 任意自定义头部组件
import Menu from "./Menu.vue";
import initAppLayout from "./initAppLayout.js";
initAppLayout();
</script>
5. 接入标签页(可选)¶
如需标签页,通过 useTabsBar 接入,UI 组件自由实现。若新布局的 URL 约定与 BAMS 平台不同,可定制 buildBasePath:
<!-- MyTabsBar.vue -->
<template>
<div class="my-tabs-bar">
<span v-for="tab in tabs" :key="tab.key" :class="{ active: tab.key === activeKey }" @click="handleTabClick(tab)">
{{ tab.title }}
<button v-if="tabs.length > 1" @click.stop="handleClose(tab.key)">×</button>
</span>
<button @click="handleCloseAll">全部关闭</button>
</div>
</template>
<script setup>
import { useTabsBar } from "@bams-app/app-layout";
// 新布局若 URL 约定不同,可定制基础路由生成规则
const { tabs, activeKey, handleTabClick, handleClose, handleCloseAll } = useTabsBar({
buildBasePath: route => `/${route.params.projectId || "home"}`,
});
</script>