重命名CubeListPager和CubeListToolbarSearch
|
# èœå•驱动路由注册设计文档
> **文档版本**:v0.2(设计完善ä¸ï¼‰
> **适用范围**:框架开å‘者ã€ä¸šåŠ¡åº”ç”¨å¼€å‘者
> **创建日期**:2026-05-04
---
## 1. è®¾è®¡èƒŒæ™¯ä¸Žç›®æ ‡
### 1.1 背景
本框架定ä½ä¸º Vue 3 + TypeScript çš„**约定优先å‰ç«¯æ¡†æž¶**ï¼Œè®¾è®¡æ€æƒ³ç±»æ¯” **ASP.NET Core MVC**:路由ä¸éœ€è¦æ‰‹åŠ¨ä¸€ä¸€å£°æ˜Žï¼Œè€Œæ˜¯ç”±æ¡†æž¶æ ¹æ®çº¦å®šè‡ªåŠ¨æŽ¨å¯¼ã€‚
在 ASP.NET Core MVC ä¸ï¼ŒæŽ§åˆ¶å™¨æ–¹æ³•(Actionï¼‰è‡ªåŠ¨æ˜ å°„ä¸ºè·¯ç”±ï¼Œæ— éœ€æ¯ä¸ªæŽ¥å£éƒ½åœ¨ `Startup` é‡Œæ‰‹åŠ¨æ³¨å†Œã€‚æœ¬æ¡†æž¶å°†è¿™ä¸€æ€æƒ³å¼•å…¥å‰ç«¯ï¼š**èœå•æ•°æ®å³è·¯ç”±å£°æ˜Ž**——åŽç«¯è¿”回的èœå•æ ‘å·²ç»åŒ…å«äº†æ‰€æœ‰ä¸šåŠ¡è·¯å¾„ï¼Œæ¡†æž¶æ®æ¤è‡ªåŠ¨æ³¨å†Œè·¯ç”±ï¼Œæ— éœ€å‰ç«¯å¼€å‘者é€ä¸€ `addRoute`。
### 1.2 类比关系
```
ASP.NET Core MVC → 本框架
──────────────────────────────────────────────────────────
路由约定(Convention-based routing) → èœå•驱动路由注册
Controller / Action 自动å‘现 → èœå•å¶å节点自动注册
DefaultController(兜底逻辑) → DefaultListPage(框架默认页)
Global Filter(全局拦截) → router.beforeEach(路由守å«ï¼‰
Area / Module → èœå•æ ‘çˆ¶èŠ‚ç‚¹ï¼ˆåˆ†ç»„ï¼‰
```
### 1.3 æ ¸å¿ƒç›®æ ‡
| ç›®æ ‡ | 说明 |
| ------------ | ---------------------------------------------------------------- |
| é›¶é…置路由 | 新业务路径åªéœ€åœ¨åŽç«¯èœå•ä¸é…ç½®ï¼Œæ— éœ€å‰ç«¯æ”¹åŠ¨è·¯ç”±æ–‡ä»¶ |
| 开箱å³ç”¨é¡µé¢ | èœå•å¶å节点自动使用 `DefaultListPage`ï¼Œæ— éœ€æ¯ä¸ªè·¯ç”±éƒ½æŒ‡å®šç»„ä»¶ |
| çµæ´»è¦†ç›– | 业务应用å¯åœ¨ `initApp` 或页é¢ç»„ä»¶ä¸è¦†ç›–特定路径的组件 |
| 刷新稳定 | 页é¢åˆ·æ–°åŽè·¯ç”±ä¾ç„¶å¯ç”¨ï¼Œä¸å‡ºçŽ°ç™½å±æˆ– 404 |
| 优先级明确 | 应用级预注册 > æ¡†æž¶åŠ¨æ€æ³¨å†Œï¼ˆèœå•) > catch-all 兜底,行为å¯é¢„期 |
---
## 2. 方案对比
在设计过程ä¸è¯„估了三ç§å®žçŽ°è·¯å¾„ï¼š
| 方案 | 机制 | 优点 | 缺点 | 结论 |
| ----------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------------- | ------------ |
| **方案 1**:catch-all 延迟检查 | 访问未知路由时在 `DefaultEntity` 组件内部检查èœå•,命ä¸åŽåŠ¨æ€æ³¨å†Œå¹¶é‡å®šå‘ | 实现最简å•,ä¸éœ€è¦æ”¹åŠ¨è·¯ç”±å®ˆå«ç»“æž„ | 首次访问产生åŒé‡å¯¼èˆªï¼Œhistory 留下多余记录,用户体验较差 | 䏿ލè |
| **方案 2**:èœå•åŠ è½½åŽæ‰¹é‡æ³¨å†Œ | èœå•æ•°æ®åŠ è½½å®ŒæˆåŽï¼Œåœ¨è·¯ç”±å®ˆå«ä¸æ‰¹é‡ `addRoute` 全部å¶å节点 | 路由表干净,导航直接命ä¸ç›®æ ‡è·¯ç”±ï¼Œæ— 冗余跳转 | åˆ·æ–°é¡µé¢æ—¶éœ€ç‰å¾…èœå•åŠ è½½ï¼Œéœ€è¦ `/loading` ä¸è½¬é¡µå¤„ç† | 推è |
| **方案 3**:方案 2 + 组件解æžä¼˜å…ˆçº§ | åŒæ–¹æ¡ˆ 2,é¢å¤–支æŒåº”用级预注册覆盖,内置表å•页路由路径约定 | æœ€çµæ´»ï¼Œæ”¯æŒå¤šå±‚覆盖,与 Section Override 系统ååŒ | 设计ç¨å¤æ‚,需制定路由类型约定(列表 vs 表å•) | **最优选择** |
**本文采用方案 3**。
---
## 3. æŽ¨èæ–¹æ¡ˆè¯¦ç»†è®¾è®¡
### 3.1 æ ¸å¿ƒæµç¨‹
#### æ£å¸¸ç™»å½•æµç¨‹
```
用户登录
│
â–¼
router.beforeEach 触å‘
│
├─ æ— token → é‡å®šå‘ /login
│
â–¼
有 tokenï¼Œæ£€æŸ¥æ˜¯å¦æœ‰èœå•æ•°æ®
│
├─ æ— èœå• → fetchMenuAsync() → registerMenuRoutes(router, flatMenus)
│ │
│ ┌──────────────────────┘
│ ▼
│ é历èœå•å¶å节点,跳过已注册路径
│ resolvePageComponent(path) 确定组件
│ router.addRoute({ path, component })
│
├─ 有èœå•(已注册) → 直接放行
│
â–¼
检查 to.name === 'DefaultEntity'(命ä¸äº† catch-all)
│
├─ 是 → next({ ...to, replace: true }) 釿–°å¯¼èˆªï¼Œå‘½ä¸æ–°æ³¨å†Œè·¯ç”±
│
└─ å¦ â†’ next()
```
#### åˆ·æ–°é¡µé¢æµç¨‹
```
页é¢åˆ·æ–°
│
â–¼
Vue Router åˆå§‹åŒ–ï¼Œé™æ€è·¯ç”±å¯ç”¨ï¼ŒåЍæ€è·¯ç”±å°šæœªæ³¨å†Œ
│
â–¼
router.beforeEach 触å‘ï¼ˆç›®æ ‡è·¯ç”±å¯èƒ½å‘½ä¸ catch-all)
│
â–¼
å‘现 !menuStore.hasMenus
│
â–¼
é‡å®šå‘到 /loading(ä¿å˜ redirect 傿•°ï¼‰
│
â–¼
Loading 页é¢ç‰å¾… fetchMenuAsync() + registerMenuRoutes()
│
â–¼
æ³¨å†Œå®Œæˆ â†’ é‡å®šå‘å›žåŽŸå§‹è·¯å¾„ï¼ˆå‘½ä¸æ–°æ³¨å†Œè·¯ç”±ï¼‰
```
### 3.2 路由组件解æžä¼˜å…ˆçº§
```
┌──────────────────────────────────────────────────────────â”
│ 优先级 1(最高):应用级预注册路由 │
│ apps/my-app/src/main.ts ä¸ initApp 回调里 │
│ router.addRoute({ path: '/user', component: UserList }) │
├──────────────────────────────────────────────────────────┤
│ 优先级 2:框架èœå•åŠ¨æ€æ³¨å†Œ │
│ èœå•å¶å节点 → resolvePageComponent(path) │
│ → DefaultListPage 或 DefaultFormPage │
├──────────────────────────────────────────────────────────┤
│ 优先级 3(最低):框架 catch-all 兜底 │
│ path: '/:pathMatch(.*)*' → DefaultEntity │
│ (显示 404 æç¤ºæˆ–框架兜底内容) │
└──────────────────────────────────────────────────────────┘
```
**å®žçŽ°ä¾æ®**:Vue Router çš„ `addRoute` åœ¨è·¯ç”±è¡¨ä¸æŒ‰æ³¨å†Œé¡ºåºæŽ’列。当应用级路由**先于**æ¡†æž¶åŠ¨æ€æ³¨å†Œæ—¶ï¼Œå…¶ä¼˜å…ˆçº§æ›´é«˜ï¼›catch-all å§‹ç»ˆåœ¨é™æ€è·¯ç”±ä¸æœ€åŽåŒ¹é…。框架在注册èœå•路由时跳过已å˜åœ¨è·¯å¾„,确ä¿åº”用级路由ä¸è¢«è¦†ç›–。
### 3.3 `registerMenuRoutes` 伪代ç
```typescript
// core/utils/menuRoutes.ts
import type { Router } from 'vue-router';
import type { FlatMenuItem } from '@/stores/menu';
/**
* æ ¹æ®è·¯å¾„约定推æ–页é¢ç»„件。
*
* çº¦å®šè§„åˆ™ï¼ˆæ— éœ€åŽç«¯æ”¹é€ ,纯å‰ç«¯çº¦å®šï¼‰ï¼š
* - /createã€/newã€/add 结尾 → DefaultFormPage(新建表å•)
* - /edit/:idã€/update/:id → DefaultFormPage(编辑表å•)
* - 其余路径 → DefaultListPage(列表页)
*/
function resolvePageComponent(path: string) {
if (/\/(create|new|add)$/.test(path) || /\/(edit|update)(\/|$)/.test(path)) {
return () => import('@/pages/DefaultFormPage.vue');
}
return () => import('@/pages/DefaultListPage.vue');
}
/**
* å°†èœå•å¶åèŠ‚ç‚¹æ‰¹é‡æ³¨å†Œä¸ºåЍæ€è·¯ç”±ã€‚
*
* @param router Vue Router 实例
* @param menus å·²æ‹å¹³çš„èœå•列表(æ¥è‡ª menuStore.flatMenus)
*/
export function registerMenuRoutes(router: Router, menus: FlatMenuItem[]): void {
// 获å–当å‰å·²æ³¨å†Œè·¯å¾„集åˆï¼Œé¿å…é‡å¤æ³¨å†Œï¼ˆåº”用级预注册路由å—åˆ°ä¿æŠ¤ï¼‰
const existingPaths = new Set(router.getRoutes().map(r => r.path));
// ç›é€‰å¶å节点:在 flatMenus ä¸ï¼Œæ²¡æœ‰å…¶ä»–èœå•以该 id 为 parentId çš„å³ä¸ºå¶å
const parentIds = new Set(menus.map(m => m.parentId).filter(Boolean));
const leafMenus = menus.filter(m => !parentIds.has(m.id) && m.path);
for (const menu of leafMenus) {
if (existingPaths.has(menu.path)) {
// åº”ç”¨å·²é¢„æ³¨å†Œè¯¥è·¯å¾„ï¼Œè·³è¿‡ï¼Œä¿æŒåº”用级路由优先
continue;
}
router.addRoute({
path: menu.path,
name: `menu-${menu.id}`, // é¿å…ä¸Žé™æ€è·¯ç”±å冲çª
component: resolvePageComponent(menu.path),
meta: {
auth: true,
menuId: menu.id,
title: menu.title,
},
});
}
}
```
### 3.4 路由守å«ä¿®æ”¹ç‚¹
```typescript
// core/router/index.ts(修改部分)
import { registerMenuRoutes } from '@/utils/menuRoutes';
router.beforeEach(async (to, from, next) => {
// 1. Token æ£€æŸ¥ï¼ˆçŽ°æœ‰é€»è¾‘ä¿æŒä¸å˜ï¼‰
// ...
// 2. 用户信æ¯ï¼ˆçŽ°æœ‰é€»è¾‘ä¿æŒä¸å˜ï¼‰
// ...
// 3. èœå•åŠ è½½ + 动æ€è·¯ç”±æ³¨å†Œ
if (!menuStore.hasMenus) {
await menuStore.fetchMenuAsync();
// ↠新增:èœå•åŠ è½½å®ŒæˆåŽæ‰¹é‡æ³¨å†ŒåЍæ€è·¯ç”±
registerMenuRoutes(router, menuStore.flatMenus);
}
// 4. è®¾ç½®å½“å‰æ¿€æ´»èœå•ï¼ˆçŽ°æœ‰é€»è¾‘ä¿æŒä¸å˜ï¼‰
if (menuStore.hasMenus) {
menuStore.setActiveMenuByPath(to.path);
}
// 5. ↠新增:若当å‰å¯¼èˆªå‘½ä¸äº† catch-allï¼Œéœ€é‡æ–°å¯¼èˆªä»¥å‘½ä¸æ–°æ³¨å†Œçš„路由
if (to.name === 'DefaultEntity' && menuStore.hasMenus) {
return next({ ...to, replace: true });
}
next();
});
```
### 3.5 路径约定详解
| 路径示例 | 匹é…规则 | è§£æžç»“æžœ |
| ------------------ | ----------------- | ----------------- |
| `/user` | 默认 | `DefaultListPage` |
| `/user/role` | 默认 | `DefaultListPage` |
| `/user/create` | 以 `/create` 结尾 | `DefaultFormPage` |
| `/user/new` | 以 `/new` 结尾 | `DefaultFormPage` |
| `/user/edit/123` | å« `/edit/` | `DefaultFormPage` |
| `/user/update/123` | å« `/update/` | `DefaultFormPage` |
| `/order/add` | 以 `/add` 结尾 | `DefaultFormPage` |
> **扩展说明**:若未æ¥éœ€è¦åŽç«¯æ˜¾å¼å£°æ˜Žé¡µé¢ç±»åž‹ï¼Œå¯åœ¨ `FlatMenuItem` ä¸å¢žåŠ `pageType?: 'list' | 'form' | 'detail'` å—æ®µï¼Œ`resolvePageComponent` 优先读å–è¯¥å—æ®µï¼Œè·¯å¾„约定作为回退。
---
## 4. 目录结构
### 4.1 新增文件
```
core/
└── utils/
└── menuRoutes.ts ↠新增:registerMenuRoutes 工具函数
```
### 4.2 修改文件
```
core/
├── router/
│ └── index.ts ↠修改:èœå•åŠ è½½åŽè°ƒç”¨ registerMenuRoutesï¼Œå¤„ç† catch-all é‡å¯¼èˆª
└── routes/
└── index.ts ↠å¯é€‰ä¿®æ”¹ï¼šcatch-all çš„ DefaultEntity 组件是å¦ä¿ç•™ï¼ˆå»ºè®®ä¿ç•™ä½œä¸º 404 兜底)
```
### 4.3 完整目录上下文
```
core/
├── utils/
│ ├── menuRoutes.ts ↠新增
│ ├── api-helpers.ts
│ ├── request.ts
│ └── ...
├── router/
│ └── index.ts ↠修改
├── routes/
│ └── index.ts ↠å¯é€‰ä¿®æ”¹
├── pages/
│ ├── DefaultListPage.vue ↠现有,èœå•路由默认组件
│ ├── DefaultFormPage.vue ↠现有,表å•路由默认组件
│ └── DefaultEntity.vue ↠现有,catch-all 兜底(ä¿ç•™ï¼‰
└── stores/
└── menu.ts ↠现有,æä¾› flatMenus æ•°æ®
```
---
## 5. 使用示例
### 5.1 é›¶é…置(开箱å³ç”¨ï¼‰
åŽç«¯èœå•é…置了 `/user` 路径åŽï¼Œå‰ç«¯**æ— éœ€ä»»ä½•æ”¹åŠ¨**,刷新å³å¯é€šè¿‡ `/user` 访问一个基于 `DefaultListPage` 的页é¢ã€‚
```
åŽç«¯æ–°å¢žèœå•项:{ path: '/order', title: '订å•管ç†', ... }
↓
框架自动注册路由:router.addRoute({ path: '/order', component: DefaultListPage })
↓
用户访问 /order → 看到默认列表页(å«é»˜è®¤æœç´¢æ ã€è¡¨æ ¼ã€åˆ†é¡µï¼‰
```
`DefaultListPage` 会自动调用接å£èŽ·å–æ•°æ®å¹¶å±•示,全程ä¸éœ€è¦ä¸šåŠ¡æ–¹å†™ä¸€è¡Œè·¯ç”±ä»£ç 。
### 5.2 应用级覆盖(自定义路由组件)
è‹¥æŸä¸ªè·¯å¾„需è¦å®Œå…¨è‡ªå®šä¹‰çš„页é¢ç»„件,在 `initApp` 回调ä¸**æå‰**注册å³å¯ã€‚æ¡†æž¶åŠ¨æ€æ³¨å†Œæ—¶ä¼šæ£€æµ‹åˆ°è¯¥è·¯å¾„å·²å˜åœ¨ï¼Œè‡ªåŠ¨è·³è¿‡ã€‚
```typescript
// apps/my-app/src/main.ts
import { initApp } from '@cube/core';
import UserListPage from './pages/UserListPage.vue';
initApp({
setup(app, router) {
// 在框架注册èœå•路由å‰ï¼Œæå‰æ³¨å†Œè‡ªå®šä¹‰ç»„ä»¶
router.addRoute({
path: '/user',
name: 'UserList',
component: UserListPage,
meta: { auth: true },
});
},
});
```
### 5.3 页é¢çº§è¦†ç›–ï¼ˆç»“åˆ Section Override 系统)
è‹¥åªéœ€æ›¿æ¢ `DefaultListPage` çš„æŸä¸ª Section(如æœç´¢æ ï¼‰ï¼Œæ— éœ€è‡ªå®šä¹‰æ•´ä¸ªé¡µé¢ï¼Œç›´æŽ¥ä½¿ç”¨ Section Override 系统:
```vue
<!-- apps/my-app/src/pages/OrderList.vue -->
<script setup lang="ts">
import { provide } from 'vue';
import { SearchBarKey, TableContentKey } from '@cube/core/composables/useSections';
import OrderSearchBar from './OrderSearchBar.vue';
import OrderTable from './OrderTable.vue';
// 覆盖æœç´¢æ å’Œè¡¨æ ¼ï¼Œå…¶ä½™ Section 使用框架默认
provide(SearchBarKey, OrderSearchBar);
provide(TableContentKey, OrderTable);
</script>
<template>
<!-- DefaultListPage 会通过 inject è‡ªåŠ¨ä½¿ç”¨ä¸Šé¢ provide 的组件 -->
<DefaultListPage />
</template>
```
也å¯ç›´æŽ¥ä½¿ç”¨å…·å Slot(更简æ´ï¼Œé€‚åˆç®€å•覆盖):
```vue
<template>
<DefaultListPage>
<template #search>
<OrderSearchBar />
</template>
</DefaultListPage>
</template>
```
---
## 6. 实施清å•(Phase 1)
以下为具体实施æ¥éª¤ï¼ŒæŒ‰é¡ºåºæ‰§è¡Œï¼š
```
1. 创建 core/utils/menuRoutes.ts
- 实现 resolvePageComponent(path: string) 函数
- 实现 registerMenuRoutes(router, menus) 函数
- 编写å•元测试(vitest)覆盖路径约定的å„ç§æƒ…况
2. 修改 core/router/index.ts
- 在 fetchMenuAsync() 调用之åŽï¼ŒåŠ å…¥ registerMenuRoutes(router, menuStore.flatMenus)
- 在 next() 之å‰ï¼ŒåŠ å…¥ catch-all 检测逻辑(to.name === 'DefaultEntity')
3. 验è¯åˆ·æ–°åœºæ™¯
- 确认 /loading 页é¢åœ¨èœå•æœªåŠ è½½æ—¶æ£ç¡®ä¸è½¬
- 确认èœå•åŠ è½½å®ŒæˆåŽå¯é‡å®šå‘å›žåŽŸå§‹è·¯å¾„å¹¶å‘½ä¸æ–°æ³¨å†Œè·¯ç”±
4. 验è¯åº”用级覆盖
- 在测试应用的 initApp ä¸é¢„注册一个自定义组件路由
- ç¡®è®¤æ¡†æž¶åŠ¨æ€æ³¨å†Œè·³è¿‡è¯¥è·¯å¾„
- 确认访问该路径时使用的是应用级组件
5. 验è¯è·¯å¾„约定
- 访问 /user/createï¼Œç¡®è®¤åŠ è½½ DefaultFormPage
- 访问 /userï¼Œç¡®è®¤åŠ è½½ DefaultListPage
- 访问 /user/edit/1ï¼Œç¡®è®¤åŠ è½½ DefaultFormPage
6. æ›´æ–° FlatMenuItem 类型定义(å¯é€‰ï¼‰
- 在 core/stores/menu.ts ä¸ä¸º FlatMenuItem æ·»åŠ pageType?: 'list' | 'form' | 'detail'
- 在 resolvePageComponent ä¸ä¼˜å…ˆè¯»å–è¯¥å—æ®µ
7. æ›´æ–° core/types/index.ts
- 导出 registerMenuRoutes 供外部按需使用
8. 补充文档示例代ç 到 docs/menu-driven-routing.md(本文档)
```
---
## 7. 未æ¥è§„划(Phase 2)
### 7.1 文件路由自动扫æï¼ˆç±» Nuxt 文件路由)
Phase 2 将引入**文件扫æè‡ªåŠ¨æ³¨å†Œ**机制,彻底消除手动 `addRoute` çš„æ ·æ¿ä»£ç :
```
apps/my-app/src/pages/
├── user/
│ ├── index.vue → 路由 /user (列表页)
│ ├── create.vue → 路由 /user/create (新建表å•)
│ └── edit/
│ └── [id].vue → 路由 /user/edit/:id (编辑表å•)
└── order/
└── index.vue → 路由 /order
```
框架æä¾› Vite æ’件,在构建时扫æ `pages/` 目录,自动生æˆè·¯ç”±é…置并注入,与èœå•驱动路由ååŒå·¥ä½œï¼š**文件路由的优先级ç‰åŒäºŽ"应用级预注册"**,高于框架èœå•åŠ¨æ€æ³¨å†Œã€‚
### 7.2 路由元信æ¯è‡ªåŠ¨æŽ¨æ–
从èœå•æ•°æ®è‡ªåŠ¨å¡«å……è·¯ç”±çš„ `meta` å—æ®µï¼š
```typescript
meta: {
title: menu.title, // 用于é¢åŒ…屑ã€é¡µé¢æ ‡é¢˜
icon: menu.icon, // 用于 Tab é¡µå›¾æ ‡
menuId: menu.id, // 用于激活èœå•高亮
keepAlive: true, // å¯ä»Žèœå•é…ç½®ä¸è¯»å–
}
```
### 7.3 动æ€è·¯ç”±çƒæ›´æ–°
èœå•æ•°æ®å˜æ›´ï¼ˆå¦‚æƒé™è°ƒæ•´ï¼‰åŽï¼Œæ”¯æŒåœ¨ä¸åˆ·æ–°é¡µé¢çš„æƒ…况下动æ€ç§»é™¤æˆ–æ·»åŠ è·¯ç”±ï¼Œå®žçŽ°æƒé™çš„实时生效。
---
## 8. çº¦å®šå¼ Section 组件自动å‘现
### 8.1 åŠŸèƒ½ç›®æ ‡
在第 5.3 节的"页é¢çº§ Section 覆盖"方案ä¸ï¼Œä¸šåС开å‘者需è¦åœ¨ `index.vue` 里手动 `provide` 覆盖组件。
本功能通过**文件系统约定**è‡ªåŠ¨å®Œæˆ `provide`,彻底消除 Section è¦†ç›–çš„æ ·æ¿ä»£ç :
> **约定**ï¼šé¡µé¢æ–‡ä»¶å¤¹ä¸‹å˜åœ¨ä¸Ž SectionKey åŒå的组件文件时,框架自动将其注入为对应 Section çš„è¦†ç›–ç»„ä»¶ï¼Œæ— éœ€æ‰‹åŠ¨ `provide`。
```
apps/my-app/src/pages/
├── user/
│ ├── index.vue ↠页é¢å…¥å£ï¼ˆå¯çœç•¥ï¼Œè·¯ç”±ç”±èœå•驱动自动注册)
│ ├── SearchBar.vue ↠自动覆盖 SearchBarKey → 替æ¢é»˜è®¤æœç´¢æ
│ └── TableContent.vue ↠自动覆盖 TableContentKey → 替æ¢é»˜è®¤è¡¨æ ¼
└── order/
└── FormContent.vue ↠自动覆盖 FormContentKey → 替æ¢é»˜è®¤è¡¨å•内容
```
### 8.2 关键约æŸä¸Žè®¾è®¡å†³ç–
Vite 在**构建时**陿€åˆ†æž `import.meta.glob`ï¼Œä¸æ”¯æŒè¿è¡Œæ—¶åЍæ€è·¯å¾„æ‹¼æŽ¥ã€‚å› æ¤ glob 调用必须写在**应用代ç **ä¸ï¼Œæ¡†æž¶æä¾›å·¥å…·å‡½æ•° `registerPageSections`ï¼Œåº”ç”¨ä¼ å…¥ glob 结果å³å¯ã€‚
文件夹路径(相对于 `pages/`ï¼‰ç›´æŽ¥æ˜ å°„ä¸ºè·¯ç”±è·¯å¾„ï¼š
| glob key | 路由路径 | Section å |
| ------------------------------------ | ------------- | -------------- |
| `./pages/user/SearchBar.vue` | `/user` | `SearchBar` |
| `./pages/user/TableContent.vue` | `/user` | `TableContent` |
| `./pages/order/list/FormContent.vue` | `/order/list` | `FormContent` |
åªå¤„ç†**大写嗿¯å¼€å¤´**(PascalCase)的文件,排除 `index.vue` ç‰å…¥å£æ–‡ä»¶ã€‚
### 8.3 实现方案
#### Step 1:App å¯åŠ¨æ³¨å†Œï¼ˆåº”ç”¨ä¾§ï¼‰
```typescript
// apps/my-app/src/main.ts
import { initApp } from '@core/initApp';
import { registerPageSections } from '@core/utils/pageSections';
const pageModules = import.meta.glob('./pages/**/*.vue');
initApp((app) => {
registerPageSections(app, pageModules);
});
```
#### Step 2:工具函数(框架侧,新增 `core/utils/pageSections.ts`)
```typescript
import type { App } from 'vue';
import { defineAsyncComponent } from 'vue';
import { PageSectionRegistryKey, SectionKeyMap } from '@core/composables/useSections';
type GlobModule = Record<string, () => Promise<{ default: unknown }>>;
type SectionRegistry = Record<string, Record<string, () => Promise<{ default: unknown }>>>;
function parseGlobKey(key: string): { routePath: string; sectionName: string } | null {
const match = key.match(/^\.\/pages\/(.+)\/([A-Z][A-Za-z]+)\.vue$/);
if (!match) return null;
const [, folderPath, sectionName] = match;
return { routePath: '/' + folderPath, sectionName };
}
export function registerPageSections(app: App, modules: GlobModule): void {
const registry: SectionRegistry = {};
for (const [key, loader] of Object.entries(modules)) {
const parsed = parseGlobKey(key);
if (!parsed) continue;
const { routePath, sectionName } = parsed;
if (!SectionKeyMap[sectionName]) continue;
registry[routePath] ??= {};
registry[routePath][sectionName] = loader;
}
app.provide(PageSectionRegistryKey, registry);
}
```
#### Step 3:新增 `SectionKeyMap` 和 `PageSectionRegistryKey`(`core/composables/useSections.ts`)
```typescript
/** Section åç§° → InjectionKey æ˜ å°„è¡¨ï¼Œç”¨äºŽçº¦å®šå¼è‡ªåЍå‘现 */
export const SectionKeyMap: Record<string, InjectionKey<Component>> = {
PageHeader: PageHeaderKey,
SearchBar: SearchBarKey,
TableToolbar: TableToolbarKey,
TableContent: TableContentKey,
Pagination: PaginationKey,
PageFooter: PageFooterKey,
FormContent: FormContentKey,
FormActions: FormActionsKey,
};
/** å…¨å±€é¡µé¢ Section 注册表的 InjectionKey */
export const PageSectionRegistryKey: InjectionKey<
Record<string, Record<string, () => Promise<{ default: unknown }>>>
> = Symbol('PageSectionRegistry');
```
#### Step 4:`DefaultListPage` / `DefaultFormPage` è¯»å–æ³¨å†Œè¡¨
在 `<script setup>` ä¸ï¼ˆåœ¨ inject è°ƒç”¨ä¹‹å‰æ‰§è¡Œï¼‰ï¼š
```typescript
import { inject, provide, defineAsyncComponent, useRoute } from 'vue';
import type { Component } from 'vue';
import { PageSectionRegistryKey, SectionKeyMap } from '@core/composables/useSections';
const route = useRoute();
const registry = inject(PageSectionRegistryKey, {} as Record<string, Record<string, () => Promise<{ default: unknown }>>>);
const pageOverrides = registry[route.path] ?? {};
// 约定å¼è¦†ç›–:优先级低于手动 provide,但高于应用级 provide
for (const [name, loader] of Object.entries(pageOverrides)) {
const key = SectionKeyMap[name];
if (key) {
provide(key, defineAsyncComponent(loader as () => Promise<{ default: Component }>));
}
}
// inject 在æ¤ä¹‹åŽæ‰§è¡Œï¼Œå¯è¯»å–到上é¢çš„ provide
```
### 8.4 完整优先级链
```
┌──────────────────────────────────────────────────────────────â”
│ 优先级 1(最高):具å Slot │
│ <DefaultListPage><template #search>...</template></...> │
├──────────────────────────────────────────────────────────────┤
│ 优先级 2:页é¢å†…手动 provide │
│ index.vue setup() ä¸ provide(SearchBarKey, MyComp) │
├──────────────────────────────────────────────────────────────┤
│ 优先级 3:约定å¼è‡ªåЍå‘çŽ°ï¼ˆæœ¬ç« ï¼‰ │
│ pages/user/SearchBar.vue → 自动 provide 给 /user 路由 │
├──────────────────────────────────────────────────────────────┤
│ 优先级 4:应用级 provide │
│ initApp å›žè°ƒä¸ app.provide(SearchBarKey, AppSearchBar) │
├──────────────────────────────────────────────────────────────┤
│ 优先级 5(最低):框架默认 │
│ inject(SearchBarKey, DefaultSearchBar) │
└──────────────────────────────────────────────────────────────┘
```
### 8.5 SectionKey 与文件å对应表
| 文件å | 对应 SectionKey | 适用页é¢ç±»åž‹ |
| ------------------ | ----------------- | --------------- |
| `PageHeader.vue` | `PageHeaderKey` | 列表页 + 表å•页 |
| `SearchBar.vue` | `SearchBarKey` | 列表页 |
| `TableToolbar.vue` | `TableToolbarKey` | 列表页 |
| `TableContent.vue` | `TableContentKey` | 列表页 |
| `Pagination.vue` | `PaginationKey` | 列表页 |
| `PageFooter.vue` | `PageFooterKey` | 列表页 + 表å•页 |
| `FormContent.vue` | `FormContentKey` | 表å•页 |
| `FormActions.vue` | `FormActionsKey` | 表å•页 |
### 8.6 实施清å•(Phase 2)
```
1. 修改 core/composables/useSections.ts
- 新增 SectionKeyMap(Section å → InjectionKey æ˜ å°„ï¼‰
- 新增 PageSectionRegistryKey(全局注册表 InjectionKey)
2. 创建 core/utils/pageSections.ts
- 实现 parseGlobKey(key) è§£æžå‡½æ•°
- 实现 registerPageSections(app, modules) 注册函数
3. 修改 core/pages/DefaultListPage.vue
- 在 <script setup> inject 之å‰è¯»å–æ³¨å†Œè¡¨å¹¶æ‰§è¡Œçº¦å®šå¼ provide
4. 修改 core/pages/DefaultFormPage.vue
- åŒä¸Š
5. 在示例应用(apps/cube-admin/src/main.tsï¼‰ä¸æŽ¥å…¥
- æ·»åŠ import.meta.glob('./pages/**/*.vue')
- 调用 registerPageSections(app, pageModules)
6. 验è¯çº¦å®šå¼è¦†ç›–与优先级
```
---
## 附录:相关设计文档
| 文档 | 说明 |
| ------------------------------------- | --------------------------------------------- |
| `docs/page-override-system-design.md` | Section Override 系统设计(与本文ååŒå·¥ä½œï¼‰ |
| `docs/menu-driven-routing.md` | èœå•é©±åŠ¨è·¯ç”±ä¸Žçº¦å®šå¼ Section å‘现设计(本文) |
| `docs/design.md` | Boreal Admin 设计系统文档 |
| `docs/project-specification.md` | 项目规范与编ç 约定 |
|