跳转至

业务组件接入指南

📄 创建: 刘泽伟 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 菜单 store
  • initMenuData():先同步本地已有菜单,再调用 menuStore.getMenuList() 拉取远程菜单并二次同步,返回 Promise
  • processMenu(menuList):对菜单列表执行 transformMenu 转换
  • syncMenus():从 store 同步菜单到 menus ref

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 参数)
  • 返回:tabsactiveKeycurrentActiveTabcontextMenuhasLeftTabshasRightTabsnavigateToBaseRoutecloseContextMenuopenContextMenuhandleTabClickhandleClosehandleCloseAllhandleCloseOthershandleCloseLefthandleCloseRighthandleRefresh

菜单数据必填字段

通用能力依赖后端菜单数据,菜单项必须包含以下字段,能力才能正常工作:

字段 说明
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(响应式菜单列表)、initMenuDataprocessMenusyncMenus 等能力(详见上文 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>