Skip to content

About

一个基于Vue3+TypeScript开发的Admin框架!

Topics

Resources

Stars

73 stars

Watchers

1 watching

Forks

Repository files navigation

Grow Admin

基于 Vue 3 / Vite / TypeScript 的 Monorepo 管理后台框架,采用 IOC 模块化架构,并通过组件驱动桥接层支持多套 UI 组件库切换。

快速开始

# 安装依赖
pnpm install

# 启动示例应用
pnpm serve

默认访问地址:http://localhost:3000

项目结构

grow_admin/
├── DesignRock/              # 框架核心层
│   ├── rock-components/     # 契约组件(Grow* 前缀)
│   ├── rock-layouts/        # 布局壳(主题设置抽屉、菜单、标签页等)
│   ├── rock-state/          # 应用状态(主题模式、配置持久化)
│   ├── rock-styles/         # 全局样式与 CSS 变量
│   ├── rock-component-driver/              # 驱动桥接基础包
│   ├── rock-component-driver-element-plus/   # Element Plus 驱动
│   ├── rock-component-driver-naive/          # Naive UI 驱动
│   ├── rock-component-driver-antdv/          # Ant Design Vue 驱动
│   ├── rock-ioc/            # 依赖注入
│   └── ...
├── DesignCornerstone/       # 业务模块层
│   ├── cornerstone-apps-login/     # 登录模块
│   ├── cornerstone-apps-home/      # 登录后首页(布局壳 + 动态路由注册)
│   └── cornerstone-apps-workspace/ # 工作区业务页(路由配置 + 页面组件)
├── configs/                 # 共享构建配置(含 UnoCSS 主题色映射)
└── sample/                  # 宿主示例应用

路由与菜单

框架采用 静态基础路由 + 接口驱动动态路由 的模式。登录后进入 Home 布局,业务页面作为 Home 的子路由渲染在 home.vue 的 <router-view /> 中;侧边菜单与路由共用同一份接口数据,但职责分离:目录节点只负责菜单展示,叶子节点才注册为可访问路由。

架构概览

业务包路由配置(apps-workspace/route-config)
        ↓  Mock / 真实接口  GET /api/menu/list
apps-home/registerDynamicRoutes.ts
        ├─ flatten → router.addRoute('Home', route)   ← 仅叶子节点
        └─ tree    → authStore(按 permissionMode 写入) ← 保留树形结构
        ↓
rock-layouts/menu(MenuTreeNode 递归渲染)
        ↓ 点击叶子菜单
router.push('/home/xxx')  ← 通过 IoC 获取 router 实例
        ↓
home.vue <router-view /> 渲染业务页面
层级 路由路径 说明
根 / Login(静态,whiteRoute: true)
布局 /home Home 布局壳(静态,isBasic: true)
业务 /home/workspace、/home/settings 动态注册的 Home 子路由

路由实例的获取方式

业务代码与布局组件不直接 import { useRouter } from 'vue-router',统一通过 @grow-admin-rock/middleware-router + IoC 获取:

import { Lib as routeLib } from '@grow-admin-rock/middleware-router'
import { resolveByKeyOrThrow } from '@grow-admin-rock/ioc'

// 获取 router 实例(与 registerDynamicRoutes.ts 用法一致)
const router = resolveByKeyOrThrow(routeLib.types.RouteTable).router

router.push('/home/workspace')
router.addRoute('Home', childRoute)

宿主应用在 sample/src/plugin/initIoc.ts 末尾挂载路由:

const router = diKT(routeLib.types.RouteTable).router
app.use(router)
await router.isReady()

静态路由注册

各业务模块通过 Lib.routes 在 IOC 加载时注册到 AppContext:

// cornerstone-apps-home/src/routes/index.ts
const HOME_ROUTE: RouteRecordItem = {
  path: '/home',
  name: 'Home',
  component: () => import('../pages/home.vue'),
  meta: { title: '首页', isBasic: true },
  // 注意:业务子路由不在此静态声明,由接口动态注入
}

export const RouteList: RouteRecordItem[] = [HOME_ROUTE]
meta 字段 含义
isBasic: true 基础路由,应用启动时写入 router,重置路由时不会被移除
whiteRoute: true 白名单路由(如 Login),未登录可访问

动态路由注册

动态路由在用户已登录且首次进入受保护页面时完成,核心逻辑位于 cornerstone-apps-home/src/routes/registerDynamicRoutes.ts:

export async function registerDynamicRoutes() {
  // 1. 请求菜单/路由配置
  const { menuList } = await getMenuList()

  // 2. 展平树形配置,仅叶子节点注册为 Vue 路由
  flattenWorkspaceRouteConfigs(menuList).forEach((config) => {
    const route = resolveWorkspaceRoute(config)  // 合并 API 配置与本地 component 映射
    router.addRoute('Home', route)               // 挂到 Home 下
  })

  // 3. 完整树形结构写入 state,供侧边菜单渲染
  authStore.setBackMenuList(toMenuList(menuList))
}

路由守卫(cornerstone-apps-home/src/routes/guard.ts)保证注册时机正确——必须在导航完成前注册,否则直接访问 /home/workspace 会因路由不存在而无法匹配:

if (!authStore.getIsDynamicAddedRoute) {
  await registerDynamicRoutes()
  authStore.setDynamicAddedRoute(true)
  next({ path: to.fullPath, query: to.query, hash: to.hash, replace: true })
  return
}

不可仅在 home.vue 的 onMounted 中注册路由:若用户直接访问子路由 URL,Home 组件尚未挂载,动态路由永远不会被添加。

业务包路由配置(apps-workspace)

业务模块维护两份配置,职责分离:

文件 职责 是否含 .vue 组件
src/routes/config.ts 可序列化的树形菜单/路由元数据,供 Mock 与接口返回 ❌
src/routes/index.ts 本地 component 映射 + resolveWorkspaceRoute() ✅

树形配置示例(config.ts):

export const WORKSPACE_ROUTE_CONFIGS: WorkspaceRouteConfig[] = [
  {
    path: 'workspace-catalog',       // 目录标识,不注册为路由
    name: 'WorkspaceCatalog',
    icon: 'ant-design:folder-outlined',
    meta: { title: '工作区' },        // 父级:菜单目录
    children: [
      {
        path: 'workspace',
        name: 'Workspace',
        icon: 'ant-design:appstore-outlined',
        meta: { title: '工作台' },    // 叶子:可访问页面
      },
      {
        path: 'settings',
        name: 'WorkspaceSettings',
        meta: { title: '设置中心' },
      },
    ],
  },
]

客户端组件绑定(routes/index.ts)——API 只返回可序列化数据,客户端结构直接持有组件:

const WORKSPACE_ROUTE_COMPONENTS: Record<string, GrowRouteComponent> = {
  Workspace: () => import('../pages/workspace.vue'),
  WorkspaceSettings: () => import('../pages/settings.vue'),
}

export const WORKSPACE_CLIENT_ROUTE_STRUCTURES = bindWorkspaceRouteComponents(
  WORKSPACE_ROUTE_STRUCTURES,
)

Mock 通过子路径导出引用纯配置,避免 vite-plugin-mock 打包 .vue 文件:

// sample/mock/routers.ts
import { WORKSPACE_ROUTE_CONFIGS } from '@grow-admin-cornerstone/apps-workspace/route-config'

// GET /api/menu/list → { menuList: WORKSPACE_ROUTE_CONFIGS }

路由与菜单的关系

同一份接口数据,registerDynamicRoutes 处理后产生两种结构:

用途 数据结构 处理方式
Vue Router 扁平叶子路由 flattenWorkspaceRouteConfigs() → addRoute('Home', route)
侧边菜单 树形 Menu[] toMenuList() → backMenuList / frontMenuList(按模式)

字段映射规则(toMenuItem):

节点类型 Menu.path 是否注册路由 点击行为
menuType = directory name 字符串(如 WorkspaceCatalog) ❌ 展开/收起,不跳转
menuType = menu 完整路径(如 /home/workspace) ✅ router.push(path);即使存在子节点也注册自身路由

菜单状态按 permissionMode 写入 authStore.backMenuList / frontMenuList,侧边栏通过 useAuthMenuList() / getMenuList 取当前模式生效的菜单树(详见下方「权限模式」)。

低代码、沙箱和报表等特殊页面通过 pageType(lowcode / sandbox / report)交给 cornerstone-apps-dispark 的固定承载组件;iframe 同样由该模块处理。普通页面不设置 pageType,直接使用客户端路由结构中的 component。

菜单渲染(rock-layouts)

@grow-admin-rock/layouts 的 Menu 组件从 useAuthMenuList() 读取数据,通过 MenuTreeNode 递归组件渲染树形菜单:

Menu(menu.vue)
  └─ MenuTreeNode(递归)
       ├─ 有 children → GrowSubMenu(目录)
       └─ 无 children → GrowMenuItem(可点击菜单项)

Element Plus 的 ElMenu 要求 SubMenu / MenuItem 作为直接子节点,因此不可使用 <template v-for> 包裹,必须通过递归组件保证每个节点只有一个根元素。

菜单点击跳转同样通过 IoC 获取 router(不依赖 vue-router 作为 layouts 的直接依赖):

// rock-layouts/src/menu/menu.vue
function handleMenuSelect(path: string) {
  if (!path.startsWith('/')) return  // 目录节点 path 不以 / 开头,忽略
  resolveByKeyOrThrow(routeLib.types.RouteTable).router.push(path)
}

Home 页面通过 Teleport 将 Menu 挂载到布局插槽:

<!-- cornerstone-apps-home/src/pages/home.vue -->
<template #view>
  <router-view />   <!-- 子路由页面渲染位置 -->
</template>

<Teleport to="#grow-menu">
  <Menu />          <!-- 侧边菜单 -->
</Teleport>

新增业务页面流程

以在 apps-workspace 中新增页面为例:

  1. 新建页面组件 — src/pages/xxx.vue
  2. 更新树形配置 — 在 src/routes/config.ts 的 children 中追加节点(或新增目录)
  3. 绑定客户端组件 — 在 src/routes/index.ts 的 WORKSPACE_ROUTE_COMPONENTS 中添加页面组件,由客户端结构直接携带 component
  4. Mock 自动生效 — sample/mock/routers.ts 引用 route-config,无需额外修改
  5. 重启/刷新 — 重新登录或清除 isDynamicAddedRoute 状态后验证

关键文件索引

文件 职责
DesignCornerstone/cornerstone-apps-workspace/src/routes/config.ts 树形路由/菜单元数据(Mock 安全导出)
DesignCornerstone/cornerstone-apps-workspace/src/routes/index.ts 客户端组件绑定、resolveWorkspaceRoute()
DesignCornerstone/cornerstone-apps-home/src/routes/index.ts Home 静态路由
DesignCornerstone/cornerstone-apps-home/src/routes/guard.ts 登录守卫 + 动态路由注册触发
DesignCornerstone/cornerstone-apps-home/src/routes/registerDynamicRoutes.ts 拉取菜单、注册路由、写入 state
DesignCornerstone/cornerstone-apps-home/src/api/routers.ts getMenuList() 接口定义
sample/mock/routers.ts 开发环境 Mock 菜单接口
DesignRock/rock-layouts/src/menu/menu.vue 侧边菜单容器
DesignRock/rock-layouts/src/menu/MenuTreeNode.vue 菜单树递归节点
DesignRock/rock-state/src/modules/authStore.ts backMenuList / frontMenuList / getMenuList
DesignRock/rock-middleware-router/ 路由表 IoC 注册、RouteOperator、MenuState

开发自检清单

  1. 登录后直接访问 /home/workspace,页面正常渲染(非空白、非跳转 Login)。
  2. 侧边栏显示树形目录,目录节点点击不跳转,叶子节点点击切换路由。
  3. 当前路由对应的菜单项高亮。
  4. Mock 接口 /api/menu/list 返回的数据结构与 config.ts 一致。
  5. 新增页面后,WORKSPACE_COMPONENTS 中存在对应 name 映射,否则 resolveWorkspaceRoute 会抛错。

权限模式

框架通过 projectSetting.permissionMode 控制动态菜单与路由的来源。角色信息只存在于 UserInfo.roles(role.value),没有单独的 ROLE 权限模式。

三种模式

枚举 值 菜单 / 路由来源
PermissionModeEnum.BACK BACK 仅接口 GET /api/menu/list → 写入 backMenuList
PermissionModeEnum.FRONT FRONT 仅前端 toFeatRouteConfigs(),再按角色过滤 → 写入 frontMenuList
PermissionModeEnum.MIXTURE MIXTURE 前端(先角色过滤)与后端按 name 合集;同名节点整条采用后端,children 递归合集

当前 sample 默认:

// sample/src/projectSetting.ts
import { PermissionModeEnum } from '@grow-admin-rock/constants'

export const projectSetting: ProjectSetting = {
  permissionMode: PermissionModeEnum.MIXTURE,
  // ...
}

initAppConfig.ts 每次启动都会把 permissionMode 从 projectSetting 同步进 useAppConfig()(即改配置文件即可切换模式,不必依赖设置抽屉)。

模式行为详解

BACK     → getMenuList() ──────────────────────────→ backMenuList + 注册路由
FRONT    → toFeatRouteConfigs → filterByRoles ─────→ frontMenuList + 注册路由
MIXTURE  → front(过滤) ∪ back → mergeTreesByName ─→ 合集注册路由
           (同时仍分别写入 frontMenuList / backMenuList,侧栏取合集)

MIXTURE 合并规则(mergeTreesByName):

  1. 先铺前端树,再叠后端树
  2. 同名节点:整条元数据用后端;children 再按 name 递归合集
  3. 合并结果按 sort 升序排序(缺省视为 0)

侧栏实际渲染:

// useAuthMenuList() / authStore.getMenuList
BACK     → sortTreesBySort(backMenuList)
FRONT    → sortTreesBySort(frontMenuList)
MIXTURE  → mergeTreesByName(frontMenuList, backMenuList)

配置与切换

步骤 说明
1. 改 permissionMode sample/src/projectSetting.ts
2. 刷新 / 重启 bootstrapAppConfig 同步模式;若与本地 LAST_PERMISSION_MODE 不一致会清菜单/标签缓存
3. 重新登录或刷新受保护页 守卫触发 registerDynamicRoutes(),按新模式拉取并注册

本地缓存 key(前缀来自 createStorageName):

Key 作用
__LAST_PERMISSION_MODE 上次成功应用的模式;与配置不一致时清缓存并 resetRouter
__TAB 标签页持久化;模式变更时清除,避免旧 URL 白屏
__APP_CONFIG 应用配置;permissionMode 每次启动仍以 projectSetting 为准覆盖

守卫在模式变更、或目标 path 在新菜单下不可达时,会 next({ name: 'Home' }),由默认菜单 redirect,避免继续访问失效 URL 导致白屏。

FRONT / MIXTURE:角色过滤

  1. 登录后 /user/info(或已有 userStore.userInfo)提供 roles: [{ name, value }]
  2. Mock 示例:admin → roles: [{ value: 'super' }](sample/mock/auth.ts)
  3. 白名单:apps-feat/src/routes/authority.ts 的 FEAT_ROUTE_AUTHORITY(route name → 允许的 role.value[])
  4. 未配置的 name 视为无权限;有 children 时先过滤子级,子非空则保留父级
// DesignCornerstone/cornerstone-apps-feat/src/routes/authority.ts
export const FEAT_ROUTE_AUTHORITY: Record<string, string[]> = {
  FeatCatalog: ['super', 'minor'],
  OpenSubpage: ['super'],
  MixtureFrontDemo: ['super', 'minor'],
  // ...
}

BACK 模式不按该表过滤接口菜单;隐藏路由 FEAT_HIDDEN_ROUTES 在 FRONT/MIXTURE 下也会按角色过滤。

FRONT 菜单数据从哪来

前端完整配置 = 展示信息 + 结构,经 toFeatRouteConfigs() 合并:

文件 职责
apps-feat/.../menuList.ts FEAT_MENU_LIST:title / icon / sort 等(可与 BACK Mock 共用)
apps-feat/.../menuList.ts FEAT_FRONT_ONLY_MENU_LIST:仅前端项(勿放进 /menu/list Mock)
apps-feat/.../config.ts FEAT_ROUTE_STRUCTURES / FEAT_FRONT_ONLY_STRUCTURES:可序列化 path 与行为配置
apps-feat/.../mergeMenu.ts toFeatRouteConfigs()
apps-feat/.../index.ts 客户端组件绑定、resolveFeatRoute

如何新增带权限的页面

仅 FRONT / MIXTURE 前端侧:

  1. 页面组件 → apps-feat/src/pages/...
  2. config.ts 增加可序列化 structure(name / path)
  3. menuList.ts 增加 title、icon、sort;若只要前端有,放 FEAT_FRONT_ONLY_*
  4. index.ts 将组件绑定到客户端路由结构
  5. authority.ts 配置该 name 允许的角色
  6. 切换到 FRONT 或 MIXTURE 验证

仅 BACK / MIXTURE 后端侧:

  1. 业务包 config + 客户端组件绑定(如 apps-workspace)
  2. Mock / 真实接口 /menu/list 返回对应节点
  3. MIXTURE 下与前端同名时,整条展示以后端为准

MIXTURE 演示约定(sample):

name 来源 说明
MixtureDemoCatalog 两端均可有 目录「权限演示」
MixtureFrontDemo 仅前端(FEAT_FRONT_ONLY_*) 合集后出现
MixtureBackDemo 仅后端(workspace + mock) 合集后出现

建议 sort:Dashboard 10、功能示例 20、权限演示 30、外部页 40。

默认首页

注册完成后写入 HomeIndexRedirect:

  1. 优先 defaultShow: true 的可导航菜单
  2. 否则取第一个目录下第一个可导航叶子
  3. 再否则整树回退第一个可导航菜单

实现:resolveDefaultMenuRedirect(rock-state/tabStore.ts)。

关键文件索引

文件 职责
rock-constants/.../appEnum.ts PermissionModeEnum
sample/src/projectSetting.ts 配置 permissionMode
sample/src/initAppConfig.ts 启动同步模式、模式变更清缓存
apps-home/.../registerDynamicRoutes.ts 按模式注册路由与菜单
apps-home/.../guard.ts 触发注册;模式变更 / 不可达时回 Home
apps-feat/.../authority.ts 前端角色白名单
rock-state/.../authStore.ts 菜单状态与 useAuthMenuList
rock-state/.../mergeTreesByName.ts 合集与 sort
rock-state/.../permissionModeCache.ts LAST_PERMISSION_MODE 与清缓存
rock-middleware-router/.../MenuState.ts isBackMode / isFrontMode / isMixtureMode

开发自检清单

  1. 改 permissionMode 后刷新,侧栏与可访问路由符合该模式预期。
  2. FRONT:无权限角色看不到对应菜单;未在 authority 配置的 name 不可见。
  3. MIXTURE:前端独有 / 后端独有 / 同名以后端为准,三项都能验证。
  4. 模式切换后旧书签 URL 不应白屏,应回到默认首页。
  5. /user/info(或登录信息)必须带回 roles[].value,否则 FRONT/MIXTURE 前端树会被滤空。

主题与颜色

框架通过 CSS 变量 + Pinia 状态 + 三库 Config 驱动 统一管理主题。开发时修改颜色,通常只需动下面几处;运行时用户在「项目配置」抽屉中选色会写入 localStorage,可能覆盖你改过的默认值。

架构概览

sample/src/projectSetting.ts          ← 宿主静态默认配置(themeColor 等)
        ↓ 首次启动 merge
@grow-admin-rock/state (useAppConfig) ← 运行时状态 + localStorage 持久化
        ↓ useTheme()
:root / :root.dark CSS 变量            ← @grow-admin-rock/styles
        ↓ GrowConfig 驱动
Element Plus / Naive UI / Ant Design Vue 主色与 hover/active
包 职责
@grow-admin-rock/styles :root 变量、亮/暗 token、主题切换过渡动画
@grow-admin-rock/state themeMode(亮/暗/跟随系统)、themeColor、动态写入 DOM
@grow-admin-rock/layouts SettingDrawer 等项目配置 UI
@grow-admin-rock/constants 预设色板 APP_THEME_COLOR_LIST
configs/vite UnoCSS 语义类名 bg-layout、text-text、bg-primary 等

修改默认主题色(新项目 / 首次加载)

推荐只改宿主应用的 projectSetting.ts:

// sample/src/projectSetting.ts
export const projectSetting: ProjectSetting = {
  themeColor: '#8b5cf6', // ← 改这里
  // ...
};

首次访问(localStorage 尚无 APP_CONFIG)时,sample/src/initAppConfig.ts 会把该值 merge 进 useAppConfig。
若本地已有持久化配置,需清除站点 localStorage 或在设置抽屉点击「重置配置」才能看到新默认值。

Pinia 内置默认值位于 DesignRock/rock-state/src/modules/appConfig.ts 的 themeColor,一般不必改;以宿主 projectSetting.ts 为准即可。

增加 / 修改设置抽屉中的可选主题色

设置抽屉的色块来自常量 APP_THEME_COLOR_LIST:

// DesignRock/rock-constants/src/designSetting.ts
export const APP_THEME_COLOR_LIST: string[] = [
  '#8b5cf6', // 第一项建议与默认 themeColor 一致
  '#0084f4',
  // 追加新颜色…
];

修改后重启 pnpm serve 即可;无需改 SettingDrawer 组件逻辑。

修改 CSS 变量(布局、文字、边框等)

全局 design token 在 DesignRock/rock-styles/src/variables.css:

:root {
  --primary-color: #8b5cf6;
  --primary-color-hover: #a78bfa;   /* 静态回退值 */
  --primary-color-active: #7c3aed;
  --text-color: rgba(0, 0, 0, 0.85);
  --layout-container-background-color: #f0f2f5;
  /* … */
}

:root.dark {
  --text-color: rgba(255, 255, 255, 0.85);
  --layout-container-background-color: rgb(16, 16, 20);
  /* … */
}

注意:

  • 运行时切换 themeColor 时,useTheme 会按主色自动计算 hover / active 及 Element Plus 的 --el-color-primary-light-*,并写入 :root 行内样式。
  • variables.css 里的 --primary-color-hover 等主要作首屏回退;动态主色以 JS 计算结果为准。
  • 新增语义变量时,建议同时在 :root 与 :root.dark 各写一套。

在页面中使用颜色(UnoCSS)

UnoCSS 已映射到 CSS 变量(configs/vite/src/plugins/unocss.ts),无需写 dark: 前缀,亮/暗随 :root.dark 自动切换:

UnoCSS 类 含义
bg-primary / text-primary 主题主色
bg-layout 页面背景
bg-component 卡片 / 面板背景
text-text 主文字
text-muted / text-text-secondary 次要文字
border-border 边框
shadow-card 卡片阴影(随主题变化)
surface-panel shortcut:bg-component border border-border rounded-lg

示例(参考 cornerstone-apps-login/src/pages/login.vue):

<template>
  <div class="min-h-screen bg-layout text-text">
    <div class="surface-panel shadow-card p-8">
      <h1 class="text-2xl font-semibold text-primary">标题</h1>
      <p class="text-muted">说明文字</p>
    </div>
  </div>
</template>

新增 UnoCSS 语义色: 在 configs/vite/src/plugins/unocss.ts 的 theme.colors 增加映射,并在 variables.css 定义对应变量:

// configs/vite/src/plugins/unocss.ts
theme: {
  colors: {
    accent: 'var(--accent-color)', // 新增
  },
},
/* rock-styles/src/variables.css */
:root { --accent-color: #f59e0b; }
:root.dark { --accent-color: #fbbf24; }

修改 UnoCSS 配置后需重启 dev server。

三库主色同步(一般无需手动改)

GrowConfig 驱动会在运行时注入当前主题色及 hover/active:

组件库 实现位置
Element Plus html.dark + --el-color-primary* CSS 变量
Naive UI rock-component-driver-naive/src/components/Config.vue
Ant Design Vue rock-component-driver-antdv/src/components/Config.vue

业务代码使用 GrowButton type="primary" 等即可,不要在业务里单独写各库主色。

Ant Design Vue 构建时 Less 变量在 configs/vite/src/presets/antd.ts 的 primary-color;若默认主色与线上一致,可同步修改该文件(主要影响构建期 antd 基础样式)。

主题模式(亮 / 暗 / 跟随系统)

配置项 位置 说明
默认模式 initAppConfig → themeMode: ThemeModeEnum.SYSTEM 跟随系统
运行时切换 登录页 LoginThemeSwitch / 设置抽屉 SettingTheme 写入 useAppConfig
暗色 class html.dark UnoCSS dark: 与 EP 暗色变量均依赖此类
切换动画 rock-styles/src/theme-transition.css 约 0.35s,可在 variables.css 调整 --theme-transition-duration

开发自检清单

  1. 改完 projectSetting.themeColor 后,清除 localStorage 或使用无痕窗口验证默认值。
  2. 在设置抽屉切换色块,确认按钮 hover、主色、UnoCSS text-primary 同步变化。
  3. 切换亮/暗模式,确认 :root.dark 下布局背景、文字、卡片阴影正常。
  4. 切换 componentLibrary 后,主色在三库下表现一致。

主题选择与语言选择

主题与语言分为两套 UI:登录页顶部工具栏(cornerstone-apps-login 内专用组件)与 项目配置抽屉(@grow-admin-rock/layouts)。二者共用同一套运行时状态,切换会同步。

前置条件

宿主应用(sample/src/plugin/initIoc.ts)需注册:

import { Lib as localeLib } from '@grow-admin-rock/locale'
import { Lib as stateLib } from '@grow-admin-rock/state'
import { Lib as componentsLib } from '@grow-admin-rock/components'

app
  .use(stateLib, appContext)
  .use(localeLib, appContext)      // 多语言必需
  .use(componentsLib, appContext) // GrowSwitch / GrowSelect 等

根组件需挂载 GrowMessageProvider 等 Provider(参考 sample/src/App.vue)。

登录页:主题 / 语言

登录页使用本包专用组件,不要直接使用项目配置里的 SwitchLanguage。

组件 路径 说明
LoginThemeSwitch cornerstone-apps-login/src/components/LoginThemeSwitch 暗色模式开关(亮色 ↔ 暗色)
LoginLanguageSwitch cornerstone-apps-login/src/components/LoginLanguageSwitch 语言下拉(简体中文 / English)
<script setup lang="ts">
import { useLocale } from '@grow-admin-rock/locale'
import LoginThemeSwitch from '#/components/LoginThemeSwitch/index.vue'
import LoginLanguageSwitch from '#/components/LoginLanguageSwitch/index.vue'

const { getLocale } = useLocale()
</script>

<template>
  <div :key="getLocale">
    <LoginThemeSwitch />
    <LoginLanguageSwitch />
  </div>
</template>

参考:DesignCornerstone/cornerstone-apps-login/src/pages/login.vue。

项目配置抽屉:主题 / 语言

完整主题(模式 + 主题色)与语言表单项在 @grow-admin-rock/layouts:

组件 导出 说明
SettingDrawer @grow-admin-rock/layouts 项目配置抽屉(默认宽度 400px)
SettingTheme @grow-admin-rock/layouts 主题模式 + 主题色(GrowForm)
SwitchLanguage @grow-admin-rock/layouts 语言下拉(GrowForm)
<script setup lang="ts">
import { ref } from 'vue'
import { SettingDrawer, SettingTheme, SwitchLanguage } from '@grow-admin-rock/layouts'

const settingVisible = ref(false)
</script>

<template>
  <GrowButton @click="settingVisible = true">项目配置</GrowButton>
  <SettingDrawer v-model="settingVisible" />

  <!-- 也可单独拼装 -->
  <SettingTheme />
  <SwitchLanguage />
</template>

SwitchLanguage 可选 Props:

Prop 默认值 说明
showLabel true 是否显示表单项标签
labelKey layout.setting.language i18n 标签 key
selectClass w-full 下拉框 class

设置抽屉内点击「重置配置」会恢复主题默认值,并将语言重置为 简体中文(zh_CN)。

编程式调用

不渲染组件时,可直接操作状态 API:

import { ThemeModeEnum } from '@grow-admin-rock/constants'
import { LOCALE, useLocale } from '@grow-admin-rock/locale'
import { useAppConfig } from '@grow-admin-rock/state'

// 主题
const appConfig = useAppConfig()
appConfig.setThemeMode(ThemeModeEnum.DARK)   // 暗色
appConfig.setThemeMode(ThemeModeEnum.LIGHT)  // 亮色
appConfig.setThemeMode(ThemeModeEnum.SYSTEM) // 跟随系统
appConfig.setThemeColor('#8b5cf6')

// 语言
const { changeLocale } = useLocale()
await changeLocale(LOCALE.zh) // 简体中文(默认)
await changeLocale(LOCALE.en) // English
能力 包 API
主题模式 / 主题色 @grow-admin-rock/state useAppConfig()
语言切换 / 持久化 @grow-admin-rock/locale useLocale().changeLocale()
文案 @grow-admin-rock/locale useI18n().t('layout.login.*') / layout.setting.*

语言偏好保存在 localStorage(key:LOCALE__),登录页与项目配置抽屉共用。

文案扩展

在 DesignRock/rock-locale/src/lang/ 下维护:

  • 登录页:zh-CN/layout/login.ts、en/layout/login.ts
  • 项目配置:zh-CN/layout/setting.ts、en/layout/setting.ts

新增语言时,同步修改 rock-locale/src/config.ts 的 localeList 与 availableLocales。

组件驱动架构

业务代码与具体 UI 库解耦,通过三层结构协作:

业务代码 / 业务模块(apps-login 等)
        ↓
Grow* 契约组件(@grow-admin-rock/components)
        ↓
组件驱动桥接层(@grow-admin-rock/component-driver)
        ↓
具体驱动包(element-plus / naive-ui / ant-design-vue)

设计原则:

  • 业务模块只使用 Grow* 契约组件,禁止直接 import element-plus / naive-ui / ant-design-vue
  • 组件库切换在宿主应用(sample)统一配置,业务模块无需关心底层实现
  • 支持全局一种组件库 + 局部子树覆盖另一种

切换组件库

当前支持三种组件库,默认使用 Element Plus。

切换时需要同时修改两处配置,分别控制运行时驱动和构建时自动导入:

配置位置 作用 影响范围
sample/src/projectSetting.ts 运行时加载哪个驱动包 Grow* 组件实际渲染的 UI 库
sample/vite.config.ts 的 preset 构建时 unplugin-vue-components 的 resolver 三方组件按需自动导入、样式预处理

两处必须保持一致,否则可能出现样式缺失或组件行为异常。

配置对照表

组件库 componentLibrary vite preset 驱动包
Element Plus(默认) ComponentLibraryType.ElementPlus 'ele' @grow-admin-rock/component-driver-element-plus
Naive UI ComponentLibraryType.NaiveUI 'naive' @grow-admin-rock/component-driver-naive
Ant Design Vue ComponentLibraryType.AntDesignVue 'antd' @grow-admin-rock/component-driver-antdv

切换流程

projectSetting.componentLibrary
        ↓
sample/src/init-components-driver.ts   ← 动态 import 对应驱动包
        ↓
driver.builder().enableAll()           ← 注册全部组件映射
        ↓
AppContext.DriverComponentDictionary
        ↓
componentsLib.onSetup → registerGrowComponent()  ← 全局注册 Grow* 组件

切换到 Element Plus(默认)

第一步 — sample/src/projectSetting.ts:

import { ComponentLibraryType } from '@grow-admin-rock/types';

export const projectSetting: ProjectSetting = {
  componentLibrary: ComponentLibraryType.ElementPlus,
  // ...
};

第二步 — sample/vite.config.ts:

export default defineConfig(async ({ command, mode }) => {
  return await createViteConfig(command, mode, process.cwd(), { preset: 'ele' });
});

第三步 — 重启开发服务器:

pnpm serve

控制台应输出:[ComponentDriver] 已加载组件库驱动: element-plus

Element Plus 的 Message / Notification / Dialog 同样会在 init-components-driver.ts 中自动绑定(ElMessage / ElNotification / ElMessageBox),无需额外配置。

切换到 Naive UI

第一步 — sample/src/projectSetting.ts:

componentLibrary: ComponentLibraryType.NaiveUI,

第二步 — sample/vite.config.ts:

{ preset: 'naive' }

第三步 — 重启开发服务器,确认控制台输出:naive-ui

Naive UI 的 Message / Notification / Dialog 会在 init-components-driver.ts 中自动绑定,无需额外配置。

切换到 Ant Design Vue

第一步 — sample/src/projectSetting.ts:

componentLibrary: ComponentLibraryType.AntDesignVue,

第二步 — sample/vite.config.ts:

{ preset: 'antd' }

第三步 — 重启开发服务器,确认控制台输出:ant-design-vue

开发环境与生产环境

projectSetting.ts 在开发和生产构建中均生效,切换方式相同。无论 pnpm serve 还是 pnpm build,都需要保证 componentLibrary 与 preset 一致。

若需按环境区分(例如开发用 Element Plus、生产用 Naive UI),可在 projectSetting.ts 中根据 import.meta.env 分支:

export const projectSetting: ProjectSetting = {
  componentLibrary: import.meta.env.PROD
    ? ComponentLibraryType.NaiveUI
    : ComponentLibraryType.ElementPlus,
  // ...
};

对应的 vite.config.ts 也需按 mode 传入不同 preset,或通过 .env.development / .env.production 配合脚本动态选择。

验证切换是否成功

  1. 控制台出现 [ComponentDriver] 已加载组件库驱动: xxx
  2. 页面中 <GrowButton>、<GrowInput> 渲染为对应 UI 库风格
  3. 浏览器开发者工具中,对应 UI 库的 CSS 已加载(驱动包在入口自动引入样式)

常见问题

现象 原因 解决
组件无样式 preset 与 componentLibrary 不一致 对照配置表同步修改两处
控制台报「缺少驱动」 未重启 dev server 修改配置后重新 pnpm serve
切换后组件行为异常 只改了 projectSetting 没改 preset 两处必须同时切换
ComponentMap is not defined 驱动包构建缓存问题 清除缓存后重启:rm -rf sample/node_modules/.vite

在页面中使用组件

契约组件以 Grow 前缀全局注册,可直接在模板中使用:

<template>
  <GrowButton type="primary">提交</GrowButton>
  <GrowInput v-model="value" placeholder="请输入" />
  <GrowSelect v-model="selected" :options="options" />
</template>

在 <script> 中需要获取底层驱动组件时:

import { useDriverComponent, RockComponent } from '@grow-admin-rock/components';

const Button = useDriverComponent(RockComponent.Button);

命令式 API 统一调用(Message / Notification / Dialog)

除模板中的 Grow* 组件外,消息提示、通知、对话框等命令式 API 也通过桥接层统一暴露,切换组件库时业务代码无需 import element-plus / naive-ui / ant-design-vue。

绑定机制

宿主应用在 sample/src/init-components-driver.ts 安装驱动时,调用 setMessage / setNotice / setDialog 注入当前组件库的实现:

组件库 Message Notification Dialog
Element Plus ElMessage ElNotification ElMessageBox
Naive UI useMessage() useNotification() useDialog()
Ant Design Vue message notification Modal
init-components-driver.ts
        ↓ setMessage / setNotice / setDialog
@grow-admin-rock/components
        ↓ useMessage() / useNotice() / useDialog()
业务代码(统一 import,无需感知底层库)

Provider 包裹(必须)

Naive UI 的 useMessage() 等必须在 GrowMessageProvider 子树的组件 setup 中调用;Element Plus / Ant Design Vue 也建议保持相同结构,便于三库切换时代码一致。

推荐在宿主应用根组件(参考 sample/src/App.vue)中包裹:

<template>
  <GrowConfig>
    <GrowNotificationProvider>
      <GrowMessageProvider>
        <GrowDialogProvider>
          <router-view />
          <!-- 业务页面作为 Provider 的子组件 -->
        </GrowDialogProvider>
      </GrowMessageProvider>
    </GrowNotificationProvider>
  </GrowConfig>
</template>

useMessage() 必须在 Provider 子组件 的 setup 中调用,不可在根组件 setup 中直接调用(此时 Provider 尚未挂载)。可将业务逻辑放在子页面或独立子组件中。

统一入口

import { useMessage, useNotice, useDialog } from '@grow-admin-rock/components';

// useMessage 是 useMsg 的别名,两者等价
const message = useMessage();
const notice = useNotice();
const dialog = useDialog();

Message(消息提示)

三库均支持相同调用方式,可直接统一编写:

message.success('操作成功');
message.error('操作失败');
message.warning('请注意');
message.info('提示信息');

Notification(通知)

各库参数名略有差异,建议同时传入兼容字段:

notice.success({
  title: '通知标题',
  content: '通知内容',      // Naive UI
  message: '通知内容',      // Element Plus
  description: '通知内容',  // Ant Design Vue
});

Dialog(对话框)

三库 API 差异较大,需按库分支处理:

import { ComponentLibraryType } from '@grow-admin-rock/types';
import { projectSetting } from '@/projectSetting';

function showConfirm() {
  const dialog = useDialog();
  if (!dialog) return;

  // Naive UI
  if (typeof dialog.warning === 'function') {
    dialog.warning({
      title: '确认操作',
      content: '确定要执行此操作吗?',
      positiveText: '确定',
      negativeText: '取消',
    });
    return;
  }

  // Ant Design Vue
  if (projectSetting.componentLibrary === ComponentLibraryType.AntDesignVue) {
    dialog.confirm({
      title: '确认操作',
      content: '确定要执行此操作吗?',
    });
    return;
  }

  // Element Plus
  if (typeof dialog.confirm === 'function') {
    dialog.confirm('确定要执行此操作吗?', '确认操作');
  }
}

完整示例

sample/src/components/DriverDemo.vue 提供了可运行的演示,启动 pnpm serve 后点击页面按钮即可验证:

<script setup lang="ts">
import { useMessage, useNotice, useDialog } from '@grow-admin-rock/components';

const message = useMessage();
const notice = useNotice();
const dialog = useDialog();

function handleMessage(type: 'success' | 'error' | 'warning' | 'info') {
  message?.[type]?.('这是一条 Message 提示');
}

function handleNotice() {
  notice?.success?.({
    title: '通知标题',
    content: '这是一条 Notification 通知',
    message: '这是一条 Notification 通知',
    description: '这是一条 Notification 通知',
  });
}
</script>

<template>
  <GrowButton type="primary" @click="handleMessage('success')">Message Success</GrowButton>
  <GrowButton @click="handleNotice">Notification</GrowButton>
</template>

API 对照速查

能力 统一入口 三库一致性 备注
消息提示 useMessage() / useMsg() ✅ 完全一致 .success() .error() .warning() .info()
通知 useNotice() ⚠️ 参数名不同 建议同时传 title + content/message/description
对话框 useDialog() ⚠️ 方法不同 Naive .warning(),EP confirm(msg, title),Antdv confirm({ title, content })

开发规范

✅ 推荐 ❌ 禁止
import { useMessage } from '@grow-admin-rock/components' import { ElMessage } from 'element-plus'
在 Provider 子组件中调用 useMessage() 在根组件 setup 中直接调用(Naive 会失效)
通过 projectSetting 切换库后自动切换底层实现 业务模块内手动绑定各库 Message API

在业务模块中使用(以 apps-login 为例)

业务模块(DesignCornerstone/*)不需要自行安装驱动,依赖宿主应用完成初始化。

宿主应用启动顺序

sample/src/plugin/initIoc.ts 中的顺序必须保持:

await installComponentDriver(app, appContext);  // 1. 安装驱动
app
  .use(IocPlugin, iocOptions)
  .use(infrastructureLib, appContext)
  .use(routeLib, appContext)
  .use(appsLoginLib, appContext)
  .use(componentsLib, appContext);               // 2. 注册 Grow 组件
await appContext.load(app);

业务模块声明依赖

在业务模块 package.json 中添加:

{
  "peerDependencies": {
    "vue": "~3.3.4",
    "@grow-admin-rock/components": "workspace:*"
  },
  "devDependencies": {
    "@grow-admin-rock/components": "workspace:*"
  }
}

业务页面编写

<!-- DesignCornerstone/cornerstone-apps-login/src/pages/login.vue -->
<script lang="ts" setup>
import { ref } from 'vue';

const username = ref('');
const password = ref('');
</script>

<template>
  <div class="flex flex-col items-center gap-4 p-8">
    <GrowInput v-model="username" placeholder="用户名" />
    <GrowInput v-model="password" type="password" placeholder="密码" />
    <GrowButton type="primary">登录</GrowButton>
  </div>
</template>

局部覆盖组件库

当某个页面需要使用与全局不同的组件库时,用 ComponentDriverProvider 包裹子树:

<script setup>
import { ComponentDriverProvider } from '@grow-admin-rock/component-driver';
import { NaiveComponentDriver } from '@grow-admin-rock/component-driver-naive';

const driver = NaiveComponentDriver.builder()
  .enableButton()
  .enableInput()
  .finish();
</script>

<template>
  <ComponentDriverProvider :driver="driver">
    <!-- 此区域内的 Grow* 组件走 Naive UI 驱动 -->
    <GrowButton>局部 Naive 按钮</GrowButton>
  </ComponentDriverProvider>
</template>

按需启用组件:

EPComponentDriver.builder()
  .enableButton()
  .enableDatePicker()
  .finish();

常用命令

命令 说明
pnpm serve 启动 sample 开发服务器
pnpm build 生产构建
pnpm stub 预构建 Vite 配置包

开发规范

✅ 推荐 ❌ 禁止
模板中使用 <GrowButton> 等契约组件 直接 import { ElButton } from 'element-plus'
通过 projectSetting.componentLibrary 切换库 在业务模块内安装/切换驱动
业务模块 peer 依赖 @grow-admin-rock/components 业务模块直接依赖三方 UI 库
特殊场景使用 ComponentDriverProvider 绕过桥接层直接使用三方组件
useMessage() / useNotice() / useDialog() 统一调用 直接 import { ElMessage } 等三方 API

相关包说明

包名 职责
@grow-admin-rock/components RockComponent 枚举、Grow* 契约组件、ComponentMap
@grow-admin-rock/layouts 布局壳:SettingDrawer、SettingTheme、SwitchLanguage 等
@grow-admin-rock/locale useI18n、useLocale、语言包加载与持久化
@grow-admin-rock/state useAppConfig、useTheme、useAuthStore / useAuthMenuList(按权限模式取菜单)、配置持久化
@grow-admin-rock/middleware-router 路由表 IoC 注册、RouteTable、RouteOperator
@grow-admin-rock/styles 全局 CSS 变量、UnoCSS 入口、主题过渡
@grow-admin-rock/constants APP_THEME_COLOR_LIST 等设计常量
@grow-admin-rock/component-driver 抽象驱动、ComponentDriverProvider、Builder API
@grow-admin-rock/component-driver-element-plus Element Plus 组件映射(84 个共有组件)
@grow-admin-rock/component-driver-naive Naive UI 组件映射(84 个共有组件)
@grow-admin-rock/component-driver-antdv Ant Design Vue 组件映射(84 个共有组件)

三个驱动包已对齐全部 84 个共有契约组件(Grow* 前缀),切换 projectSetting.componentLibrary 即可在同一套业务代码下切换 UI 库,无需修改页面中的组件用法。

不包含的组件(参考项目自定义封装)

以下组件属于参考项目(weiming-design)自行封装,不纳入本项目:

组件 说明
GrowIconify 基于 Iconify 的自定义图标组件
GrowTable 基于 vxe-table 的表格封装
GrowCubeTable 基于 @antv/s2 的多维表格
GrowLocalePicker 自定义语言切换器

若业务需要类似能力,请在 DesignCornerstone 业务模块中自行实现,不要放入 rock-components 契约层。

About

一个基于Vue3+TypeScript开发的Admin框架!

Topics

Resources

Stars

73 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages