NewLife/cube-front

feat(core): 核心框架功能增强

- initApp 初始化流程优化
- router 增加路由守卫与权限处理
- 新增 useSections 分区段组合式函数
- 新增 menuRoutes / pageSections 工具函数
- 新增 DefaultEntity 通用实体页面
- DataSet 数据集基类更新
笑笑 authored at 2026-05-05 22:33:39
4eccfcb
Tree
1 Parent(s) 08bbee4
Summary: 11 changed files with 460 additions and 17 deletions.
Modified +11 -1
Added +38 -0
Modified +2 -2
Modified +61 -10
Modified +5 -1
Added +59 -0
Modified +129 -2
Modified +6 -1
Modified +9 -0
Added +51 -0
Added +89 -0
Modified +11 -1
diff --git a/core/client.d.ts b/core/client.d.ts
index 62f0688..9e4899d 100644
--- a/core/client.d.ts
+++ b/core/client.d.ts
@@ -11,6 +11,7 @@ declare module 'virtual:cube-front-routes' {
 }
 
 declare module 'virtual:cube-front-micro-apps' {
+  import type { MicroAppConfigItem } from './microAppRouter';
   const appConfigs: MicroAppConfigItem[];
   export default appConfigs;
 }
@@ -18,11 +19,20 @@ declare module 'virtual:cube-front-micro-apps' {
 declare module 'virtual:cube-front-config' {
   const configData: Record<string, string>;
   const currentEnv: string;
-  const config: { configData: Record<string, string>; currentEnv: string; };
+  const config: { configData: Record<string, string>; currentEnv: string };
   export { configData, currentEnv };
   export default config;
 }
 
+declare module 'virtual:cube-front-sections' {
+  /** 子应用 views 目录中扫描到的 Section 覆盖组件懒加载映射。
+   *  key 格式:`./views/<folderPath>/<SectionName>.vue`
+   *  由 vite:cube-front-sections 插件在构建时自动生成,开发模式下支持 HMR。
+   */
+  const modules: Record<string, () => Promise<{ default: unknown }>>;
+  export default modules;
+}
+
 interface Window {
   router: import('vue-router').Router;
   store: import('pinia').Pinia;
Added +38 -0
diff --git a/core/composables/useSections.ts b/core/composables/useSections.ts
new file mode 100644
index 0000000..5558be1
--- /dev/null
+++ b/core/composables/useSections.ts
@@ -0,0 +1,38 @@
+import type { InjectionKey, Component } from 'vue';
+
+// ─── 页面级默认组件 Keys ───────────────────────────────────────
+export const DefaultListPageKey: InjectionKey<Component> = Symbol('DefaultListPage');
+export const PageNotFoundKey: InjectionKey<Component> = Symbol('PageNotFound');
+
+// ─── 列表页 Section Keys ──────────────────────────────────────────
+export const ListPageHeaderKey: InjectionKey<Component> = Symbol('ListPageHeader');
+export const ListSearchBarKey: InjectionKey<Component> = Symbol('ListSearchBar');
+export const ListToolbarKey: InjectionKey<Component> = Symbol('ListToolbar');
+export const ListTableContentKey: InjectionKey<Component> = Symbol('ListTableContent');
+export const ListPaginationKey: InjectionKey<Component> = Symbol('ListPagination');
+export const ListPageFooterKey: InjectionKey<Component> = Symbol('ListPageFooter');
+
+// ─── 表单页 Section Keys ──────────────────────────────────────────
+export const FormPageHeaderKey: InjectionKey<Component> = Symbol('FormPageHeader');
+export const FormContentKey: InjectionKey<Component> = Symbol('FormContent');
+export const FormActionsKey: InjectionKey<Component> = Symbol('FormActions');
+
+// ─── 可被约定式自动发现的名称 → InjectionKey 映射 ───────────────
+export const SectionKeyMap: Record<string, InjectionKey<Component>> = {
+  DefaultListPage: DefaultListPageKey,
+  PageNotFound: PageNotFoundKey,
+  ListPageHeader: ListPageHeaderKey,
+  ListSearchBar: ListSearchBarKey,
+  ListToolbar: ListToolbarKey,
+  ListTableContent: ListTableContentKey,
+  ListPagination: ListPaginationKey,
+  ListPageFooter: ListPageFooterKey,
+  FormPageHeader: FormPageHeaderKey,
+  FormContent: FormContentKey,
+  FormActions: FormActionsKey,
+};
+
+// ─── 全局页面 Section 注册表(约定式自动发现用)──────────────────
+export const PageSectionRegistryKey: InjectionKey<
+  Record<string, Record<string, () => Promise<{ default: unknown }>>>
+> = Symbol('PageSectionRegistry');
Modified +2 -2
diff --git a/core/dataset/data-set/DataSet.ts b/core/dataset/data-set/DataSet.ts
index f901a5a..d9991d3 100644
--- a/core/dataset/data-set/DataSet.ts
+++ b/core/dataset/data-set/DataSet.ts
@@ -470,11 +470,11 @@ export class DataSet<T, Q> {
   }
 
   /**
-   * 更新记录
+   * 远程更新记录
    * @param data 要更新的数据
    * @returns Promise包含更新结果
    */
-  async update(data: T): Promise<T> {
+  async remoteUpdate(data: T): Promise<T> {
     if (!this._transport?.update) {
       throw new Error('Update transport not configured');
     }
Modified +61 -10
diff --git a/core/initApp.ts b/core/initApp.ts
index 5ea91a4..c27878d 100644
--- a/core/initApp.ts
+++ b/core/initApp.ts
@@ -43,8 +43,14 @@ import router from './router';
 import i18n from './i18n';
 import './global.css';
 import MainLayout from './layouts/MainLayout/index.vue';
-import { appProvide, hasProvided, LayoutKey, getProvidedKeys } from './composables/useProvideInject';
-
+import {
+  appProvide,
+  hasProvided,
+  LayoutKey,
+  getProvidedKeys,
+} from './composables/useProvideInject';
+import { registerPageSections } from './utils/pageSections';
+import autoSectionModules from 'virtual:cube-front-sections';
 
 /**
  * 应用配置选项接口
@@ -73,11 +79,43 @@ export interface AppConfigOptions {
  * @param utils.hasProvided - 检查某个键是否已经被提供的函数
  * @param utils.getProvidedKeys - 获取所有已提供键的函数
  */
-export type ConfigureFunction = (app: App2<Element>, utils: {
-  provide: typeof appProvide;
-  hasProvided: typeof hasProvided;
-  getProvidedKeys: typeof getProvidedKeys;
-}) => void;
+export type ConfigureFunction = (
+  app: App2<Element>,
+  utils: {
+    provide: typeof appProvide;
+    hasProvided: typeof hasProvided;
+    getProvidedKeys: typeof getProvidedKeys;
+  },
+) => void;
+
+/**
+ * initApp 选项接口
+ *
+ * @property configure - 可选的应用配置函数,在插件安装后、挂载前执行
+ * @property sections  - 可选的额外 Section 模块映射(优先级高于插件自动扫描结果)。
+ *                       通常无需手动传入;Vite 插件 `vite:cube-front-sections` 会自动
+ *                       扫描子应用的 `src/views/` 目录并通过虚拟模块 `virtual:cube-front-sections`
+ *                       提供给框架。仅在需要手动覆盖时使用此字段。
+ *
+ * @example 标准子应用入口(无需配置,插件自动处理)
+ * ```typescript
+ * import { initApp } from '@core/initApp';
+ * initApp();
+ * ```
+ *
+ * @example 手动补充额外 Section(与插件自动发现合并)
+ * ```typescript
+ * initApp({
+ *   sections: {
+ *     './views/special/ListToolbar.vue': () => import('./views/special/ListToolbar.vue'),
+ *   },
+ * });
+ * ```
+ */
+export interface InitAppOptions {
+  configure?: ConfigureFunction;
+  sections?: Record<string, () => Promise<{ default: unknown }>>;
+}
 
 /**
  * 初始化 Vue 应用
@@ -138,7 +176,14 @@ export type ConfigureFunction = (app: App2<Element>, utils: {
  * });
  * ```
  */
-export const initApp = async (configure?: ConfigureFunction) => {
+export const initApp = async (optionsOrConfigure?: InitAppOptions | ConfigureFunction) => {
+  // 向后兼容:支持直接传入 configure 函数
+  const options: InitAppOptions =
+    typeof optionsOrConfigure === 'function'
+      ? { configure: optionsOrConfigure }
+      : (optionsOrConfigure ?? {});
+
+  const { configure, sections } = options;
 
   const pinia = createPinia();
 
@@ -150,12 +195,19 @@ export const initApp = async (configure?: ConfigureFunction) => {
   app.use(i18n); // 添加 i18n 插件
   app.use(ElementPlus);
 
+  // 注册页面 Section 覆盖组件
+  // 优先级:options.sections(手动指定)> virtual:cube-front-sections(插件自动扫描)
+  const mergedSections = { ...autoSectionModules, ...(sections ?? {}) };
+  if (Object.keys(mergedSections).length > 0) {
+    registerPageSections(app, mergedSections);
+  }
+
   // 执行外部配置函数,允许外部覆盖内部配置
   // 这里外部可以提供自定义的依赖注入值,优先于默认值
   await configure?.(app, {
     provide: appProvide,
     hasProvided,
-    getProvidedKeys
+    getProvidedKeys,
   });
 
   // 提供默认的依赖注入值
@@ -171,4 +223,3 @@ export const initApp = async (configure?: ConfigureFunction) => {
   window.router = router;
   window.store = pinia;
 };
-
Modified +5 -1
diff --git a/core/main.ts b/core/main.ts
index 74814fe..ba089b0 100644
--- a/core/main.ts
+++ b/core/main.ts
@@ -1,3 +1,7 @@
 import { initApp } from './initApp';
+import TopMenuLayout from './layouts/TopMenu/index.vue';
+import { LayoutKey } from './composables/useProvideInject';
 
-initApp();
+initApp((app, { provide }) => {
+  provide(app, LayoutKey, TopMenuLayout, { override: true });
+});
Added +59 -0
diff --git a/core/pages/DefaultEntity.vue b/core/pages/DefaultEntity.vue
new file mode 100644
index 0000000..5dbe2ea
--- /dev/null
+++ b/core/pages/DefaultEntity.vue
@@ -0,0 +1,59 @@
+<template>
+  <component
+    :is="matchedMenu ? resolvedDefaultListPageComponent : resolvedPageNotFoundComponent"
+    v-bind="matchedMenu ? { title: matchedMenu.title ?? matchedMenu.name } : {}"
+  />
+</template>
+
+<script setup lang="ts">
+import { computed, defineAsyncComponent, inject, watchEffect } from 'vue';
+import type { Component } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { useMenuStore, type FlatMenuItem } from '@core/stores/menu';
+import {
+  DefaultListPageKey,
+  PageNotFoundKey,
+  PageSectionRegistryKey,
+} from '@core/composables/useSections';
+import { registerMenuRoutes } from '@core/utils/menuRoutes';
+import FrameworkDefaultListPage from '@core/views/index.vue';
+import FrameworkPageNotFound from './PageNotFound.vue';
+
+type SectionRegistry = Record<string, Record<string, () => Promise<{ default: unknown }>>>;
+
+const route = useRoute();
+const router = useRouter();
+const menuStore = useMenuStore();
+
+const injectedDefaultListPageComponent = inject(DefaultListPageKey, FrameworkDefaultListPage);
+const injectedPageNotFoundComponent = inject(PageNotFoundKey, FrameworkPageNotFound);
+const sectionRegistry = inject(PageSectionRegistryKey, {} as SectionRegistry);
+
+const matchedMenu = computed<FlatMenuItem | undefined>(() =>
+  menuStore.flatMenus?.find((item) => item.path === route.path),
+);
+
+const resolvedDefaultListPageComponent = computed<Component>(() => {
+  const loader = sectionRegistry[route.path]?.DefaultListPage;
+  return loader
+    ? defineAsyncComponent(loader as () => Promise<{ default: Component }>)
+    : injectedDefaultListPageComponent;
+});
+
+const resolvedPageNotFoundComponent = computed<Component>(() => {
+  const loader = sectionRegistry[route.path]?.PageNotFound;
+  return loader
+    ? defineAsyncComponent(loader as () => Promise<{ default: Component }>)
+    : injectedPageNotFoundComponent;
+});
+
+watchEffect(() => {
+  const menus = menuStore.flatMenus;
+  if (!menus?.length || !matchedMenu.value) {
+    return;
+  }
+
+  registerMenuRoutes(router, menus);
+  menuStore.setActiveMenuByPath(route.path);
+});
+</script>
Modified +129 -2
diff --git a/core/plugin/index.ts b/core/plugin/index.ts
index 5cde98f..2644d85 100644
--- a/core/plugin/index.ts
+++ b/core/plugin/index.ts
@@ -1,5 +1,5 @@
 import path from 'path';
-import { type PluginOption, type ResolvedConfig } from 'vite';
+import { type PluginOption, type ResolvedConfig, type ViteDevServer } from 'vite';
 import { type ConfigRoute } from '../typings.d';
 import fs from 'fs';
 import type { MicroAppConfig } from '../microAppRouter';
@@ -13,6 +13,7 @@ export default function vitePluginCubeFront() {
   const appName = 'app';
   const microAppsName = 'micro-apps';
   const configName = 'config';
+  const sectionsName = 'sections';
 
   /** 配置信息 */
   let config: ResolvedConfig & { routes: ConfigRoute[] };
@@ -146,5 +147,131 @@ export default { configData, currentEnv };
     },
   };
 
-  return [viteCubeApp, viteCubeAppNames, viteCubeConfig];
+  // Section 组件自动发现虚拟模块插件
+  // 虚拟模块 ID: virtual:cube-front-sections
+  // 运行时在 initApp.ts 中自动导入并调用 registerPageSections
+  let sectionsConfig: (ResolvedConfig & { routes: ConfigRoute[] }) | null = null;
+
+  const viteCubeSections: PluginOption = {
+    name: `vite:${virtualModuleNamePrefix}-${sectionsName}`,
+    enforce: 'pre',
+    configResolved: (cfg) => {
+      sectionsConfig = cfg as ResolvedConfig & { routes: ConfigRoute[] };
+    },
+    resolveId(id: string) {
+      if (id === virtualModuleIdPrefix + sectionsName) {
+        return resolvedVirtualModuleIdPrefix + sectionsName;
+      }
+    },
+    load(id: string) {
+      if (id === resolvedVirtualModuleIdPrefix + sectionsName) {
+        if (!sectionsConfig) return generateSectionsCode([]);
+        const viewsDir = resolveAppViewsDir(sectionsConfig.configFile, sectionsConfig.root);
+        if (!viewsDir) return generateSectionsCode([]);
+        const sections = scanSectionFiles(viewsDir);
+        return generateSectionsCode(sections);
+      }
+    },
+    // 开发服务器:监听 views 目录变化,新增/删除 Section 文件时失效虚拟模块
+    configureServer(server: ViteDevServer) {
+      const onFileChange = (filePath: string) => {
+        // 只响应 views/ 目录下的 PascalCase Vue 文件变化
+        if (/views[/\\][^/\\]+[/\\][A-Z][A-Za-z]+\.vue$/.test(filePath)) {
+          const mod = server.moduleGraph.getModuleById(
+            resolvedVirtualModuleIdPrefix + sectionsName,
+          );
+          if (mod) {
+            server.moduleGraph.invalidateModule(mod);
+            server.ws.send({ type: 'full-reload', path: '*' });
+          }
+        }
+      };
+      server.watcher.on('add', onFileChange);
+      server.watcher.on('unlink', onFileChange);
+    },
+  };
+
+  return [viteCubeApp, viteCubeAppNames, viteCubeConfig, viteCubeSections];
+}
+
+// ─── 工具函数(模块顶层,供插件复用)──────────────────────────────────────
+
+/**
+ * 根据当前 configFile 路径推断子应用的 views 目录。
+ *
+ * 约定:子应用的 vite.config.ts 位于 `<monorepo>/apps/<appName>/` 目录下,
+ * 其 views 视图目录为 `<monorepo>/apps/<appName>/src/views/`。
+ *
+ * @param configFile  Vite 解析后的 config.configFile(绝对路径)
+ * @param root        Vite 解析后的 config.root(monorepo 根目录)
+ * @returns 子应用 views 目录的绝对路径,若无法推断则返回 null
+ */
+function resolveAppViewsDir(configFile: string | undefined, root: string): string | null {
+  if (!configFile) return null;
+  const configDir = path.dirname(configFile);
+  // 判断 vite.config.ts 是否位于 apps/<xxx>/ 目录
+  const rel = path.relative(root, configDir);
+  // rel 在 Windows: "apps\cube-admin" / POSIX: "apps/cube-admin"
+  const parts = rel.split(path.sep);
+  if (parts.length >= 2 && parts[0] === 'apps') {
+    return path.join(configDir, 'src', 'views');
+  }
+  return null;
+}
+
+/**
+ * 递归扫描 views 目录,收集符合 Section 命名约定的 Vue 文件。
+ *
+ * 约定:文件名须为 PascalCase(首字母大写,仅含字母),例如 `ListSearchBar.vue`。
+ * 排除 `index.vue`、`form.vue` 等小写开头的框架保留文件。
+ *
+ * @param viewsDir  views 目录绝对路径
+ * @returns 所有符合条件文件的 { key, absPath } 列表
+ *          key:     模块映射键(如 `./views/user/ListSearchBar.vue`)
+ *          absPath: 绝对路径(POSIX 风格,用于生成 import() 语句)
+ */
+function scanSectionFiles(viewsDir: string): Array<{ key: string; absPath: string }> {
+  if (!fs.existsSync(viewsDir)) return [];
+
+  const result: Array<{ key: string; absPath: string }> = [];
+
+  function walk(dir: string, relPath: string) {
+    const entries = fs.readdirSync(dir, { withFileTypes: true });
+    for (const entry of entries) {
+      const fullPath = path.join(dir, entry.name);
+      if (entry.isDirectory()) {
+        walk(fullPath, `${relPath}/${entry.name}`);
+      } else if (entry.isFile() && entry.name.endsWith('.vue')) {
+        const nameWithoutExt = entry.name.slice(0, -4);
+        // 只收集 PascalCase 文件名(首字母大写,仅含字母)
+        if (/^[A-Z][A-Za-z]+$/.test(nameWithoutExt)) {
+          result.push({
+            key: `./views${relPath}/${entry.name}`,
+            // 统一为 POSIX 正斜杠,Windows 环境下 import() 也可识别
+            absPath: fullPath.replace(/\\/g, '/'),
+          });
+        }
+      }
+    }
+  }
+
+  walk(viewsDir, '');
+  return result;
+}
+
+/**
+ * 生成 virtual:cube-front-sections 的模块代码。
+ * 导出一个 Record<string, () => Promise<{default:unknown}>> 对象,
+ * 可直接传入 registerPageSections(app, modules)。
+ */
+function generateSectionsCode(sections: Array<{ key: string; absPath: string }>): string {
+  if (sections.length === 0) {
+    return `// virtual:cube-front-sections — no section overrides found\nconst modules = {};\nexport default modules;\n`;
+  }
+  const lines = sections
+    .map(
+      ({ key, absPath }) => `  ${JSON.stringify(key)}: () => import(${JSON.stringify(absPath)}),`,
+    )
+    .join('\n');
+  return `// virtual:cube-front-sections — auto-generated\nconst modules = {\n${lines}\n};\nexport default modules;\n`;
 }
Modified +6 -1
diff --git a/core/router/index.ts b/core/router/index.ts
index 44aaa14..bca5f48 100644
--- a/core/router/index.ts
+++ b/core/router/index.ts
@@ -5,6 +5,7 @@ import { useUserStore } from '../stores/user';
 import { useMenuStore } from '../stores/menu';
 import { getAccessToken } from '../utils/token';
 import { getUrlHashToken } from '../utils/token';
+import { registerMenuRoutes } from '../utils/menuRoutes';
 
 // 创建路由实例
 const router: Router = createRouter({
@@ -14,7 +15,7 @@ const router: Router = createRouter({
 
 // 先初始化微前端应用路由(异步操作)
 // 我们不会等待其完成再导出router,但会在导航守卫中检查初始化状态
-initAppRoutes(router).catch(error => {
+initAppRoutes(router).catch((error) => {
   console.error('初始化微应用路由失败:', error);
 });
 
@@ -93,6 +94,10 @@ router.beforeEach(async (to, from, next) => {
       if (!menuStore.hasMenus) {
         try {
           await menuStore.fetchMenuAsync();
+          // 菜单加载完成后,批量注册菜单叶子节点路由
+          if (menuStore.flatMenus) {
+            registerMenuRoutes(router, menuStore.flatMenus);
+          }
         } catch (error) {
           console.error('获取菜单信息失败:', error);
           // 获取菜单失败但不影响导航,继续放行
Modified +9 -0
diff --git a/core/routes/index.ts b/core/routes/index.ts
index 42b3b70..bf8246e 100644
--- a/core/routes/index.ts
+++ b/core/routes/index.ts
@@ -39,6 +39,15 @@ const routes: ConfigRoute[] = [
       auth: false, // 不需要认证
     },
   },
+  {
+    path: '/:pathMatch(.*)*',
+    name: 'DefaultEntity',
+    component: () => import('../pages/DefaultEntity.vue'),
+    meta: {
+      title: '默认页面',
+      auth: true,
+    },
+  },
 ];
 
 export default routes;
Added +51 -0
diff --git a/core/utils/menuRoutes.ts b/core/utils/menuRoutes.ts
new file mode 100644
index 0000000..8eb57f2
--- /dev/null
+++ b/core/utils/menuRoutes.ts
@@ -0,0 +1,51 @@
+import type { Router } from 'vue-router';
+import type { FlatMenuItem } from '@core/stores/menu';
+
+/**
+ * 根据路径约定推断页面组件。
+ *
+ * 约定规则(纯前端约定,无需后端改造):
+ *   - /create、/new、/add 结尾 → form.vue(新建表单)
+ *   - /edit/:id、/update/:id  → form.vue(编辑表单)
+ *   - 其余路径               → index.vue(列表页)
+ */
+function resolvePageComponent(path: string) {
+  if (/\/(create|new|add)$/.test(path) || /\/(edit|update)(\/|$)/.test(path)) {
+    return () => import('@core/views/form.vue');
+  }
+  return () => import('@core/views/index.vue');
+}
+
+/**
+ * 将菜单叶子节点批量注册为动态路由。
+ *
+ * 优先级说明:
+ *   - 已存在路径(应用级预注册)不会被覆盖
+ *   - 每个菜单叶子节点默认使用框架 index.vue / form.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));
+
+  // 筛选叶子节点:没有其他 menu 以其 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.name || menu.id}`,
+      component: resolvePageComponent(menu.path),
+      meta: {
+        auth: true,
+        menuId: menu.id,
+        title: menu.title ?? menu.name,
+      },
+    });
+  }
+}
Added +89 -0
diff --git a/core/utils/pageSections.ts b/core/utils/pageSections.ts
new file mode 100644
index 0000000..e356f1e
--- /dev/null
+++ b/core/utils/pageSections.ts
@@ -0,0 +1,89 @@
+import type { App } from 'vue';
+import { defineAsyncComponent } from 'vue';
+import { PageSectionRegistryKey, SectionKeyMap } from '@core/composables/useSections';
+import type { Component } from 'vue';
+
+type GlobLoader = () => Promise<{ default: unknown }>;
+type GlobModule = Record<string, GlobLoader>;
+type SectionRegistry = Record<string, Record<string, GlobLoader>>;
+
+/**
+ * 解析 glob key,提取路由路径和 Section 名称。
+ *
+ * 规则:
+ *   - 匹配 `./views/<folderPath>/<PascalCaseName>.vue`(与 core/views 保持一致)
+ *   - 文件名必须大写字母开头(PascalCase),否则跳过(排除 index.vue、form.vue 等)
+ *   - 文件名去掉 .vue 后须在 SectionKeyMap 中存在,否则跳过
+ *
+ * 示例:
+ *   './views/user/ListSearchBar.vue' → { routePath: '/user', sectionName: 'ListSearchBar' }
+ *   './views/order/list/FormContent.vue' → { routePath: '/order/list', sectionName: 'FormContent' }
+ */
+function parseGlobKey(key: string): { routePath: string; sectionName: string } | null {
+  const match = key.match(/^\.\/views\/(.+)\/([A-Z][A-Za-z]+)\.vue$/);
+  if (!match) return null;
+  const [, folderPath, sectionName] = match;
+  return { routePath: '/' + folderPath, sectionName };
+}
+
+/**
+ * 将 import.meta.glob 结果注册为全局页面 Section 覆盖注册表。
+ *
+ * 在应用启动时(initApp 回调中)调用,一次性注册所有页面的覆盖组件:
+ *
+ * ```typescript
+ * import { registerPageSections } from '@core/utils/pageSections';
+ * const viewModules = import.meta.glob('./views/**\/*.vue');
+ * initApp((app) => {
+ *   registerPageSections(app, viewModules);
+ * });
+ * ```
+ *
+ * 注意:`import.meta.glob` 调用必须写在应用代码中(Vite 静态分析限制),
+ *       不能在框架核心代码中自动执行。
+ *
+ * 目录约定:各子应用使用 `src/views/` 文件夹存放页面 Section 覆盖组件,
+ *            与框架的 `core/views/` 保持一致的约定。
+ *
+ * @param app     Vue App 实例
+ * @param modules import.meta.glob 结果
+ */
+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; // 非 Section 文件,跳过
+
+    registry[routePath] ??= {};
+    registry[routePath][sectionName] = loader;
+  }
+
+  app.provide(PageSectionRegistryKey, registry);
+}
+
+/**
+ * (供 DefaultListPage / DefaultFormPage 内部使用)
+ * 将注册表中当前路由对应的覆盖组件 provide 给子组件树。
+ *
+ * 须在 <script setup> 中调用,且在 inject 语句之前执行。
+ */
+export function applyPageSectionOverrides(
+  routePath: string,
+  registry: Record<string, Record<string, GlobLoader>>,
+  provideKey: (key: symbol, comp: Component) => void,
+): void {
+  const overrides = registry[routePath] ?? {};
+  for (const [name, loader] of Object.entries(overrides)) {
+    const injectionKey = SectionKeyMap[name];
+    if (injectionKey) {
+      provideKey(
+        injectionKey as unknown as symbol,
+        defineAsyncComponent(loader as () => Promise<{ default: Component }>),
+      );
+    }
+  }
+}