@pactor-app/app 1.46.0 → 1.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -124,6 +124,8 @@ import { SitemapApp } from '@pactor-app/app';
124
124
 
125
125
  内置能力(无需自行拼装):多源拉取合并(冲突后源覆盖前源)、`menus × pages` 继承(`path` / `title` 缺省自引用页面)、MenuRegistry 路由解析(含 `:param`)、访问守卫(缺省匿名守卫:非 `public` 且无 user → `navigate(auth.loginPath)`,`guard` prop 可替换为角色鉴权等策略)、DSL 布局渲染(`PageOutlet` 页面出口、`SitemapMenu` 选中态与 `activeMenu` 高亮)、页面内联 / 懒加载(`id@version` 缓存,`pageEndpoint` 可配)、`session.context` 服务桥(DSL 经 `${data.session.*}` 读 user / app / menus)、缺省动作(`app.reload` 整页重载、`service.call` 服务调用,宿主 `actions` 同名覆盖)、布局常驻(同布局名切换导航仅内容区重建)。布局名解析链:`menu.layout` → 顶层 `layout` → `layouts` 首键。
126
126
 
127
+ 布局可声明 `state` / `lifecycle`(布局运行时常驻,state 跨页面保持);`LayoutDsl.parent` 引用另一布局形成**嵌套链**——如 `agent.parent = 'admin'` 时渲染为 `admin`(其 `PageOutlet` 渲染 `agent` 布局,`agent` 的 `PageOutlet` 渲染页面)。每级布局是独立运行时(独立 state / lifecycle / 弹层宿主),导航时链共享前缀的外层布局保持挂载(一级外壳不重建),仅分叉层重建;`parent` 悬空或成环时链截断兜底。隐藏页(不进菜单的路由页)用菜单条目的 `activeMenu` 指向一级菜单 id,访问时外层导航菜单保持对应选中态。
128
+
127
129
  | Props | 说明 |
128
130
  | --- | --- |
129
131
  | `sources`(必填) | sitemap 接口 URL 列表(相对路径以 `baseUrl` 补全) |
@@ -139,7 +141,7 @@ import { SitemapApp } from '@pactor-app/app';
139
141
  | `sessionService` | 会话桥服务名(缺省 `session.context`) |
140
142
  | `loading` / `renderError` / `renderNotFound` | 状态视图定制 |
141
143
 
142
- 配套导出(高级用法):`fetchSitemap`、`createSitemapLoader`、`mergeSitemapMenus`、`toSitemapNavItems`、`resolveSitemapRoute`、`defaultSitemapGuard`、`defaultSitemapActions`、`createHttpHooks`、`withHttpHooks`、`createSitemapSessionServices` 与类型 `SitemapResource` / `SitemapMenuItem` / `SitemapPageEntry` / `LayoutDsl` / `SitemapRouteMatch` / `SitemapRouteResolution` / `SitemapGuard` / `SitemapSessionContext`。完整示例工程见 `starters/next-app-starter`(宿主路由模式)与 `starters/spa-starter`(纯前端内置路由模式)。
144
+ 配套导出(高级用法):`fetchSitemap`、`createSitemapLoader`、`mergeSitemapMenus`、`toSitemapNavItems`、`resolveSitemapRoute`、`resolveLayoutChain`、`defaultSitemapGuard`、`defaultSitemapActions`、`createHttpHooks`、`withHttpHooks`、`createSitemapSessionServices` 与类型 `SitemapResource` / `SitemapMenuItem` / `SitemapPageEntry` / `LayoutDsl` / `SitemapRouteMatch` / `SitemapRouteResolution` / `SitemapGuard` / `SitemapSessionContext`。完整示例工程见 `starters/next-app-starter`(宿主路由模式)与 `starters/spa-starter`(纯前端内置路由模式)。
143
145
 
144
146
  ## 文档
145
147
 
package/dist/index.cjs CHANGED
@@ -37,6 +37,7 @@ __export(index_exports, {
37
37
  defaultSitemapGuard: () => defaultSitemapGuard,
38
38
  fetchSitemap: () => fetchSitemap,
39
39
  mergeSitemapMenus: () => mergeSitemapMenus,
40
+ resolveLayoutChain: () => resolveLayoutChain,
40
41
  resolveSitemapRoute: () => resolveSitemapRoute,
41
42
  toSitemapNavItems: () => toSitemapNavItems,
42
43
  withHttpHooks: () => withHttpHooks,
@@ -1291,11 +1292,26 @@ function resolveSitemapRoute(sitemap, registry, pathname) {
1291
1292
  route: {
1292
1293
  pageId: menu.pageId,
1293
1294
  layoutName,
1295
+ layoutChain: resolveLayoutChain(sitemap, layoutName),
1294
1296
  highlightKey: highlightItem?.path ?? highlightId,
1295
1297
  params: match.params
1296
1298
  }
1297
1299
  };
1298
1300
  }
1301
+ function resolveLayoutChain(sitemap, layoutName) {
1302
+ const chain = [layoutName];
1303
+ const seen = new Set(chain);
1304
+ let current = layoutName;
1305
+ for (; ; ) {
1306
+ const parent = sitemap.layouts[current]?.parent;
1307
+ if (parent === void 0 || seen.has(parent) || sitemap.layouts[parent] === void 0) {
1308
+ return chain;
1309
+ }
1310
+ chain.unshift(parent);
1311
+ seen.add(parent);
1312
+ current = parent;
1313
+ }
1314
+ }
1299
1315
  var defaultSitemapGuard = ({
1300
1316
  sitemap,
1301
1317
  pathname,
@@ -1414,24 +1430,24 @@ function SitemapAppInner(props) {
1414
1430
  if (resolution.route === null) {
1415
1431
  return props.renderNotFound !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: props.renderNotFound(pathname) }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Centered, { children: t("pactor.sitemap.notFound", { path: pathname }) });
1416
1432
  }
1417
- const { pageId, layoutName, highlightKey, params } = resolution.route;
1433
+ const { pageId, layoutChain, highlightKey, params } = resolution.route;
1418
1434
  return (
1419
- // key 只随布局名变化:切换导航(同布局)时布局常驻,
1420
- // 仅 PageOutlet 内的页面随路由重建
1435
+ // key 只随最外层布局名变化:布局链共享前缀时外层布局常驻,
1436
+ // 仅分叉的内层布局与 PageOutlet 内页面随路由重建
1421
1437
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(NavHighlightContext.Provider, { value: highlightKey, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1422
1438
  SitemapLayout,
1423
1439
  {
1424
1440
  sitemap,
1425
1441
  registry,
1426
1442
  pageId,
1427
- layoutName,
1443
+ layoutChain,
1428
1444
  pathname,
1429
1445
  query,
1430
1446
  navigate,
1431
1447
  routeParams: params,
1432
1448
  props
1433
1449
  },
1434
- layoutName
1450
+ layoutChain[0]
1435
1451
  ) })
1436
1452
  );
1437
1453
  }
@@ -1439,14 +1455,13 @@ function SitemapLayout({
1439
1455
  sitemap,
1440
1456
  registry,
1441
1457
  pageId,
1442
- layoutName,
1458
+ layoutChain,
1443
1459
  pathname,
1444
1460
  query,
1445
1461
  navigate: hostNavigate,
1446
1462
  routeParams,
1447
1463
  props
1448
1464
  }) {
1449
- const { t } = (0, import_runtime6.useI18n)();
1450
1465
  const sessionService = props.sessionService ?? "session.context";
1451
1466
  const session = (0, import_react8.useMemo)(
1452
1467
  () => ({
@@ -1494,23 +1509,6 @@ function SitemapLayout({
1494
1509
  () => ({ ...defaultSitemapActions, ...props.actions }),
1495
1510
  [props.actions]
1496
1511
  );
1497
- const layoutDsl = sitemap.layouts[layoutName];
1498
- const layoutPage = (0, import_react8.useMemo)(() => {
1499
- if (layoutDsl === void 0) {
1500
- return null;
1501
- }
1502
- return {
1503
- dsl: layoutDsl.dsl,
1504
- page: { id: `layout:${layoutName}`, title: layoutName },
1505
- dataSources: {
1506
- session: { type: "service", service: sessionService, autoLoad: true }
1507
- },
1508
- body: layoutDsl.body
1509
- };
1510
- }, [layoutDsl, layoutName, sessionService]);
1511
- if (layoutDsl === void 0 || layoutPage === null) {
1512
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Centered, { children: t("pactor.sitemap.layoutMissing", { name: layoutName }) });
1513
- }
1514
1512
  const messagesSignature = JSON.stringify(props.messages ?? {});
1515
1513
  const pageFactory = (0, import_react8.useMemo)(
1516
1514
  () => createPactorPage({
@@ -1541,6 +1539,9 @@ function SitemapLayout({
1541
1539
  );
1542
1540
  const layoutFactory = (0, import_react8.useMemo)(
1543
1541
  () => createPactorPage({
1542
+ // loader 注入使布局运行时的 OverlayHost 支持 page 类型弹层
1543
+ //(布局内按钮经 overlay.open 打开懒加载表单页,如创建实例)
1544
+ loader,
1544
1545
  services,
1545
1546
  actions,
1546
1547
  navigate,
@@ -1552,6 +1553,7 @@ function SitemapLayout({
1552
1553
  }),
1553
1554
  // eslint-disable-next-line react-hooks/exhaustive-deps
1554
1555
  [
1556
+ loader,
1555
1557
  services,
1556
1558
  actions,
1557
1559
  navigate,
@@ -1563,11 +1565,15 @@ function SitemapLayout({
1563
1565
  ]
1564
1566
  );
1565
1567
  const Page = pageFactory;
1566
- const LayoutPage = layoutFactory;
1567
1568
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1568
- OutletContext.Provider,
1569
+ LayoutLevel,
1569
1570
  {
1570
- value: (
1571
+ sitemap,
1572
+ chain: layoutChain,
1573
+ index: 0,
1574
+ sessionService,
1575
+ layoutFactory,
1576
+ outlet: (
1571
1577
  // 页面实例:key 随 pageId + pathname 变化(route 参数变化需重建,
1572
1578
  // PactorPage 的 route prop 不参与运行时重建)——布局保持挂载
1573
1579
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -1578,8 +1584,57 @@ function SitemapLayout({
1578
1584
  },
1579
1585
  `${pageId}@${pathname}`
1580
1586
  )
1587
+ )
1588
+ }
1589
+ );
1590
+ }
1591
+ function LayoutLevel({
1592
+ sitemap,
1593
+ chain,
1594
+ index,
1595
+ sessionService,
1596
+ layoutFactory,
1597
+ outlet
1598
+ }) {
1599
+ const { t } = (0, import_runtime6.useI18n)();
1600
+ const layoutName = chain[index];
1601
+ const layoutDsl = sitemap.layouts[layoutName];
1602
+ const layoutPage = (0, import_react8.useMemo)(() => {
1603
+ if (layoutDsl === void 0) {
1604
+ return null;
1605
+ }
1606
+ return {
1607
+ dsl: layoutDsl.dsl,
1608
+ page: { id: `layout:${layoutName}`, title: layoutName },
1609
+ // 布局级 state / lifecycle 透传(布局运行时常驻,state 跨页面保持)
1610
+ state: layoutDsl.state,
1611
+ lifecycle: layoutDsl.lifecycle,
1612
+ dataSources: {
1613
+ session: { type: "service", service: sessionService, autoLoad: true }
1614
+ },
1615
+ body: layoutDsl.body
1616
+ };
1617
+ }, [layoutDsl, layoutName, sessionService]);
1618
+ if (layoutDsl === void 0 || layoutPage === null) {
1619
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Centered, { children: t("pactor.sitemap.layoutMissing", { name: layoutName }) });
1620
+ }
1621
+ const LayoutPage = layoutFactory;
1622
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1623
+ OutletContext.Provider,
1624
+ {
1625
+ value: index === chain.length - 1 ? outlet : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1626
+ LayoutLevel,
1627
+ {
1628
+ sitemap,
1629
+ chain,
1630
+ index: index + 1,
1631
+ sessionService,
1632
+ layoutFactory,
1633
+ outlet
1634
+ },
1635
+ chain[index + 1]
1581
1636
  ),
1582
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(LayoutPage, { pageDsl: layoutPage })
1637
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(LayoutPage, { pageDsl: layoutPage }, layoutName)
1583
1638
  }
1584
1639
  );
1585
1640
  }
@@ -1619,6 +1674,7 @@ function Centered({ children }) {
1619
1674
  defaultSitemapGuard,
1620
1675
  fetchSitemap,
1621
1676
  mergeSitemapMenus,
1677
+ resolveLayoutChain,
1622
1678
  resolveSitemapRoute,
1623
1679
  toSitemapNavItems,
1624
1680
  withHttpHooks,
package/dist/index.d.cts CHANGED
@@ -432,9 +432,20 @@ declare const defaultSitemapActions: Record<string, ActionHandler>;
432
432
  * 服务端按会话裁剪内容(匿名 / 角色),对象本身不为角色建模。
433
433
  */
434
434
  /** 布局定义:DSL 描述外壳结构,PageOutlet 组件为页面内容出口。
435
- * 不套 PageDsl 的 page 包装——id 即 layouts 的键,无重复声明 */
435
+ * 不套 PageDsl 的 page 包装——id 即 layouts 的键,无重复声明。
436
+ * state / lifecycle 可选:布局运行时在相同布局名导航间常驻(key 不变
437
+ * 不重建),其中的 state 跨页面保持(如侧栏选中 / 折叠态),onInit
438
+ * 随布局运行时创建执行一次。
439
+ * parent 可选:引用另一布局作为外层外壳,形成布局嵌套链(如
440
+ * agent.parent = admin → 渲染 admin(PageOutlet=agent(PageOutlet=页面)))。
441
+ * 每级布局是独立运行时;跨页面导航时共享前缀的外层布局保持挂载,
442
+ * 其 state 不受内层布局 / 页面切换影响 */
436
443
  interface LayoutDsl {
437
444
  dsl: string;
445
+ /** 外层布局名,引用 layouts 的键;缺省无外层(自身为最外层) */
446
+ parent?: string;
447
+ state?: PageDsl['state'];
448
+ lifecycle?: PageDsl['lifecycle'];
438
449
  body: DslNode;
439
450
  }
440
451
  /** 页面定义:id 主键;pageDsl 缺省 → loadPage(id) 懒加载 */
@@ -472,7 +483,10 @@ interface SitemapResource extends BootstrapResource {
472
483
  /** 可渲染路由匹配结果(resolveSitemapRoute 产物) */
473
484
  interface SitemapRouteMatch {
474
485
  pageId: string;
486
+ /** 页面直接布局名(布局链的最内层) */
475
487
  layoutName: string;
488
+ /** 布局嵌套链(最外层在前):沿 LayoutDsl.parent 上溯,含自身 */
489
+ layoutChain: string[];
476
490
  /** 菜单高亮 key:activeMenu 指向的菜单项 path(缺省自身 path) */
477
491
  highlightKey: string;
478
492
  params: Record<string, string>;
@@ -528,6 +542,11 @@ interface SitemapRouteResolution {
528
542
  * key 与 toSitemapNavItems 一致(path ?? id)。
529
543
  */
530
544
  declare function resolveSitemapRoute(sitemap: SitemapResource, registry: MenuRegistry, pathname: string): SitemapRouteResolution;
545
+ /**
546
+ * 布局嵌套链(最外层在前):自布局名沿 LayoutDsl.parent 上溯至无外层。
547
+ * parent 悬空(未定义的布局名)或成环时截断,保证有限终止。
548
+ */
549
+ declare function resolveLayoutChain(sitemap: SitemapResource, layoutName: string): string[];
531
550
  /** 守卫判定:放行或重定向 */
532
551
  type SitemapGuardDecision = {
533
552
  allow: true;
@@ -574,7 +593,9 @@ declare const defaultSitemapGuard: SitemapGuard;
574
593
  * 页面内联 / 懒加载(id@version 缓存)、session.context 服务桥
575
594
  * (${data.session.*})、缺省动作(app.reload / service.call)。
576
595
  *
577
- * 布局常驻:切换导航(同布局名)时仅 PageOutlet 内页面随路由重建。
596
+ * 布局常驻与嵌套:LayoutDsl.parent 引用外层布局形成嵌套链(最外层在
597
+ * 前),每级布局为独立运行时;切换导航时链共享前缀的外层布局保持
598
+ * 挂载(state 跨页面保持),仅分叉层与 PageOutlet 内页面随路由重建。
578
599
  */
579
600
  interface SitemapAppProps {
580
601
  /** sitemap 接口 URL 列表(多源合并,冲突后源覆盖前源);相对路径以 baseUrl 补全 */
@@ -686,4 +707,4 @@ interface SitemapSessionContext {
686
707
  /** 注册会话桥服务(缺省名 session.context,与 loader 注入的 dataSource 对应) */
687
708
  declare function createSitemapSessionServices(session: SitemapSessionContext, serviceName?: string): Record<string, ServiceHandler>;
688
709
 
689
- export { Application, type ApplicationProps, type AuthTokenProvider, type CreateHttpHooksOptions, DslPageLoader, type DslPageLoaderProps, type FetchSitemapOptions, type LayoutDsl, type LocaleStorage, type PactorAppSession, type PactorApplication, type PactorApplicationHandle, type PactorApplicationOptions, type PactorHttpHooks, PactorPage, type PactorPageOptions, type PactorPageProps, type PactorRouterOptions, PageErrorBoundary, ReactRouterBinding, SitemapApp, type SitemapAppProps, type SitemapGuard, type SitemapGuardContext, type SitemapGuardDecision, type SitemapLoaderOptions, type SitemapMenuItem, type SitemapNavItem, type SitemapPageEntry, type SitemapResource, type SitemapRouteMatch, type SitemapRouteResolution, type SitemapSessionContext, createHttpHooks, createLocalStorageLocaleStorage, createPactorApplication, createPactorPage, createRuntimeHttpAdapter, createSitemapLoader, createSitemapSessionServices, defaultSitemapActions, defaultSitemapGuard, fetchSitemap, mergeSitemapMenus, resolveSitemapRoute, toSitemapNavItems, withHttpHooks, wrapHttpAdapter };
710
+ export { Application, type ApplicationProps, type AuthTokenProvider, type CreateHttpHooksOptions, DslPageLoader, type DslPageLoaderProps, type FetchSitemapOptions, type LayoutDsl, type LocaleStorage, type PactorAppSession, type PactorApplication, type PactorApplicationHandle, type PactorApplicationOptions, type PactorHttpHooks, PactorPage, type PactorPageOptions, type PactorPageProps, type PactorRouterOptions, PageErrorBoundary, ReactRouterBinding, SitemapApp, type SitemapAppProps, type SitemapGuard, type SitemapGuardContext, type SitemapGuardDecision, type SitemapLoaderOptions, type SitemapMenuItem, type SitemapNavItem, type SitemapPageEntry, type SitemapResource, type SitemapRouteMatch, type SitemapRouteResolution, type SitemapSessionContext, createHttpHooks, createLocalStorageLocaleStorage, createPactorApplication, createPactorPage, createRuntimeHttpAdapter, createSitemapLoader, createSitemapSessionServices, defaultSitemapActions, defaultSitemapGuard, fetchSitemap, mergeSitemapMenus, resolveLayoutChain, resolveSitemapRoute, toSitemapNavItems, withHttpHooks, wrapHttpAdapter };
package/dist/index.d.ts CHANGED
@@ -432,9 +432,20 @@ declare const defaultSitemapActions: Record<string, ActionHandler>;
432
432
  * 服务端按会话裁剪内容(匿名 / 角色),对象本身不为角色建模。
433
433
  */
434
434
  /** 布局定义:DSL 描述外壳结构,PageOutlet 组件为页面内容出口。
435
- * 不套 PageDsl 的 page 包装——id 即 layouts 的键,无重复声明 */
435
+ * 不套 PageDsl 的 page 包装——id 即 layouts 的键,无重复声明。
436
+ * state / lifecycle 可选:布局运行时在相同布局名导航间常驻(key 不变
437
+ * 不重建),其中的 state 跨页面保持(如侧栏选中 / 折叠态),onInit
438
+ * 随布局运行时创建执行一次。
439
+ * parent 可选:引用另一布局作为外层外壳,形成布局嵌套链(如
440
+ * agent.parent = admin → 渲染 admin(PageOutlet=agent(PageOutlet=页面)))。
441
+ * 每级布局是独立运行时;跨页面导航时共享前缀的外层布局保持挂载,
442
+ * 其 state 不受内层布局 / 页面切换影响 */
436
443
  interface LayoutDsl {
437
444
  dsl: string;
445
+ /** 外层布局名,引用 layouts 的键;缺省无外层(自身为最外层) */
446
+ parent?: string;
447
+ state?: PageDsl['state'];
448
+ lifecycle?: PageDsl['lifecycle'];
438
449
  body: DslNode;
439
450
  }
440
451
  /** 页面定义:id 主键;pageDsl 缺省 → loadPage(id) 懒加载 */
@@ -472,7 +483,10 @@ interface SitemapResource extends BootstrapResource {
472
483
  /** 可渲染路由匹配结果(resolveSitemapRoute 产物) */
473
484
  interface SitemapRouteMatch {
474
485
  pageId: string;
486
+ /** 页面直接布局名(布局链的最内层) */
475
487
  layoutName: string;
488
+ /** 布局嵌套链(最外层在前):沿 LayoutDsl.parent 上溯,含自身 */
489
+ layoutChain: string[];
476
490
  /** 菜单高亮 key:activeMenu 指向的菜单项 path(缺省自身 path) */
477
491
  highlightKey: string;
478
492
  params: Record<string, string>;
@@ -528,6 +542,11 @@ interface SitemapRouteResolution {
528
542
  * key 与 toSitemapNavItems 一致(path ?? id)。
529
543
  */
530
544
  declare function resolveSitemapRoute(sitemap: SitemapResource, registry: MenuRegistry, pathname: string): SitemapRouteResolution;
545
+ /**
546
+ * 布局嵌套链(最外层在前):自布局名沿 LayoutDsl.parent 上溯至无外层。
547
+ * parent 悬空(未定义的布局名)或成环时截断,保证有限终止。
548
+ */
549
+ declare function resolveLayoutChain(sitemap: SitemapResource, layoutName: string): string[];
531
550
  /** 守卫判定:放行或重定向 */
532
551
  type SitemapGuardDecision = {
533
552
  allow: true;
@@ -574,7 +593,9 @@ declare const defaultSitemapGuard: SitemapGuard;
574
593
  * 页面内联 / 懒加载(id@version 缓存)、session.context 服务桥
575
594
  * (${data.session.*})、缺省动作(app.reload / service.call)。
576
595
  *
577
- * 布局常驻:切换导航(同布局名)时仅 PageOutlet 内页面随路由重建。
596
+ * 布局常驻与嵌套:LayoutDsl.parent 引用外层布局形成嵌套链(最外层在
597
+ * 前),每级布局为独立运行时;切换导航时链共享前缀的外层布局保持
598
+ * 挂载(state 跨页面保持),仅分叉层与 PageOutlet 内页面随路由重建。
578
599
  */
579
600
  interface SitemapAppProps {
580
601
  /** sitemap 接口 URL 列表(多源合并,冲突后源覆盖前源);相对路径以 baseUrl 补全 */
@@ -686,4 +707,4 @@ interface SitemapSessionContext {
686
707
  /** 注册会话桥服务(缺省名 session.context,与 loader 注入的 dataSource 对应) */
687
708
  declare function createSitemapSessionServices(session: SitemapSessionContext, serviceName?: string): Record<string, ServiceHandler>;
688
709
 
689
- export { Application, type ApplicationProps, type AuthTokenProvider, type CreateHttpHooksOptions, DslPageLoader, type DslPageLoaderProps, type FetchSitemapOptions, type LayoutDsl, type LocaleStorage, type PactorAppSession, type PactorApplication, type PactorApplicationHandle, type PactorApplicationOptions, type PactorHttpHooks, PactorPage, type PactorPageOptions, type PactorPageProps, type PactorRouterOptions, PageErrorBoundary, ReactRouterBinding, SitemapApp, type SitemapAppProps, type SitemapGuard, type SitemapGuardContext, type SitemapGuardDecision, type SitemapLoaderOptions, type SitemapMenuItem, type SitemapNavItem, type SitemapPageEntry, type SitemapResource, type SitemapRouteMatch, type SitemapRouteResolution, type SitemapSessionContext, createHttpHooks, createLocalStorageLocaleStorage, createPactorApplication, createPactorPage, createRuntimeHttpAdapter, createSitemapLoader, createSitemapSessionServices, defaultSitemapActions, defaultSitemapGuard, fetchSitemap, mergeSitemapMenus, resolveSitemapRoute, toSitemapNavItems, withHttpHooks, wrapHttpAdapter };
710
+ export { Application, type ApplicationProps, type AuthTokenProvider, type CreateHttpHooksOptions, DslPageLoader, type DslPageLoaderProps, type FetchSitemapOptions, type LayoutDsl, type LocaleStorage, type PactorAppSession, type PactorApplication, type PactorApplicationHandle, type PactorApplicationOptions, type PactorHttpHooks, PactorPage, type PactorPageOptions, type PactorPageProps, type PactorRouterOptions, PageErrorBoundary, ReactRouterBinding, SitemapApp, type SitemapAppProps, type SitemapGuard, type SitemapGuardContext, type SitemapGuardDecision, type SitemapLoaderOptions, type SitemapMenuItem, type SitemapNavItem, type SitemapPageEntry, type SitemapResource, type SitemapRouteMatch, type SitemapRouteResolution, type SitemapSessionContext, createHttpHooks, createLocalStorageLocaleStorage, createPactorApplication, createPactorPage, createRuntimeHttpAdapter, createSitemapLoader, createSitemapSessionServices, defaultSitemapActions, defaultSitemapGuard, fetchSitemap, mergeSitemapMenus, resolveLayoutChain, resolveSitemapRoute, toSitemapNavItems, withHttpHooks, wrapHttpAdapter };
package/dist/index.js CHANGED
@@ -1293,11 +1293,26 @@ function resolveSitemapRoute(sitemap, registry, pathname) {
1293
1293
  route: {
1294
1294
  pageId: menu.pageId,
1295
1295
  layoutName,
1296
+ layoutChain: resolveLayoutChain(sitemap, layoutName),
1296
1297
  highlightKey: highlightItem?.path ?? highlightId,
1297
1298
  params: match.params
1298
1299
  }
1299
1300
  };
1300
1301
  }
1302
+ function resolveLayoutChain(sitemap, layoutName) {
1303
+ const chain = [layoutName];
1304
+ const seen = new Set(chain);
1305
+ let current = layoutName;
1306
+ for (; ; ) {
1307
+ const parent = sitemap.layouts[current]?.parent;
1308
+ if (parent === void 0 || seen.has(parent) || sitemap.layouts[parent] === void 0) {
1309
+ return chain;
1310
+ }
1311
+ chain.unshift(parent);
1312
+ seen.add(parent);
1313
+ current = parent;
1314
+ }
1315
+ }
1301
1316
  var defaultSitemapGuard = ({
1302
1317
  sitemap,
1303
1318
  pathname,
@@ -1416,24 +1431,24 @@ function SitemapAppInner(props) {
1416
1431
  if (resolution.route === null) {
1417
1432
  return props.renderNotFound !== void 0 ? /* @__PURE__ */ jsx6(Fragment, { children: props.renderNotFound(pathname) }) : /* @__PURE__ */ jsx6(Centered, { children: t("pactor.sitemap.notFound", { path: pathname }) });
1418
1433
  }
1419
- const { pageId, layoutName, highlightKey, params } = resolution.route;
1434
+ const { pageId, layoutChain, highlightKey, params } = resolution.route;
1420
1435
  return (
1421
- // key 只随布局名变化:切换导航(同布局)时布局常驻,
1422
- // 仅 PageOutlet 内的页面随路由重建
1436
+ // key 只随最外层布局名变化:布局链共享前缀时外层布局常驻,
1437
+ // 仅分叉的内层布局与 PageOutlet 内页面随路由重建
1423
1438
  /* @__PURE__ */ jsx6(NavHighlightContext.Provider, { value: highlightKey, children: /* @__PURE__ */ jsx6(
1424
1439
  SitemapLayout,
1425
1440
  {
1426
1441
  sitemap,
1427
1442
  registry,
1428
1443
  pageId,
1429
- layoutName,
1444
+ layoutChain,
1430
1445
  pathname,
1431
1446
  query,
1432
1447
  navigate,
1433
1448
  routeParams: params,
1434
1449
  props
1435
1450
  },
1436
- layoutName
1451
+ layoutChain[0]
1437
1452
  ) })
1438
1453
  );
1439
1454
  }
@@ -1441,14 +1456,13 @@ function SitemapLayout({
1441
1456
  sitemap,
1442
1457
  registry,
1443
1458
  pageId,
1444
- layoutName,
1459
+ layoutChain,
1445
1460
  pathname,
1446
1461
  query,
1447
1462
  navigate: hostNavigate,
1448
1463
  routeParams,
1449
1464
  props
1450
1465
  }) {
1451
- const { t } = useI18n2();
1452
1466
  const sessionService = props.sessionService ?? "session.context";
1453
1467
  const session = useMemo2(
1454
1468
  () => ({
@@ -1496,23 +1510,6 @@ function SitemapLayout({
1496
1510
  () => ({ ...defaultSitemapActions, ...props.actions }),
1497
1511
  [props.actions]
1498
1512
  );
1499
- const layoutDsl = sitemap.layouts[layoutName];
1500
- const layoutPage = useMemo2(() => {
1501
- if (layoutDsl === void 0) {
1502
- return null;
1503
- }
1504
- return {
1505
- dsl: layoutDsl.dsl,
1506
- page: { id: `layout:${layoutName}`, title: layoutName },
1507
- dataSources: {
1508
- session: { type: "service", service: sessionService, autoLoad: true }
1509
- },
1510
- body: layoutDsl.body
1511
- };
1512
- }, [layoutDsl, layoutName, sessionService]);
1513
- if (layoutDsl === void 0 || layoutPage === null) {
1514
- return /* @__PURE__ */ jsx6(Centered, { children: t("pactor.sitemap.layoutMissing", { name: layoutName }) });
1515
- }
1516
1513
  const messagesSignature = JSON.stringify(props.messages ?? {});
1517
1514
  const pageFactory = useMemo2(
1518
1515
  () => createPactorPage({
@@ -1543,6 +1540,9 @@ function SitemapLayout({
1543
1540
  );
1544
1541
  const layoutFactory = useMemo2(
1545
1542
  () => createPactorPage({
1543
+ // loader 注入使布局运行时的 OverlayHost 支持 page 类型弹层
1544
+ //(布局内按钮经 overlay.open 打开懒加载表单页,如创建实例)
1545
+ loader,
1546
1546
  services,
1547
1547
  actions,
1548
1548
  navigate,
@@ -1554,6 +1554,7 @@ function SitemapLayout({
1554
1554
  }),
1555
1555
  // eslint-disable-next-line react-hooks/exhaustive-deps
1556
1556
  [
1557
+ loader,
1557
1558
  services,
1558
1559
  actions,
1559
1560
  navigate,
@@ -1565,11 +1566,15 @@ function SitemapLayout({
1565
1566
  ]
1566
1567
  );
1567
1568
  const Page = pageFactory;
1568
- const LayoutPage = layoutFactory;
1569
1569
  return /* @__PURE__ */ jsx6(
1570
- OutletContext.Provider,
1570
+ LayoutLevel,
1571
1571
  {
1572
- value: (
1572
+ sitemap,
1573
+ chain: layoutChain,
1574
+ index: 0,
1575
+ sessionService,
1576
+ layoutFactory,
1577
+ outlet: (
1573
1578
  // 页面实例:key 随 pageId + pathname 变化(route 参数变化需重建,
1574
1579
  // PactorPage 的 route prop 不参与运行时重建)——布局保持挂载
1575
1580
  /* @__PURE__ */ jsx6(
@@ -1580,8 +1585,57 @@ function SitemapLayout({
1580
1585
  },
1581
1586
  `${pageId}@${pathname}`
1582
1587
  )
1588
+ )
1589
+ }
1590
+ );
1591
+ }
1592
+ function LayoutLevel({
1593
+ sitemap,
1594
+ chain,
1595
+ index,
1596
+ sessionService,
1597
+ layoutFactory,
1598
+ outlet
1599
+ }) {
1600
+ const { t } = useI18n2();
1601
+ const layoutName = chain[index];
1602
+ const layoutDsl = sitemap.layouts[layoutName];
1603
+ const layoutPage = useMemo2(() => {
1604
+ if (layoutDsl === void 0) {
1605
+ return null;
1606
+ }
1607
+ return {
1608
+ dsl: layoutDsl.dsl,
1609
+ page: { id: `layout:${layoutName}`, title: layoutName },
1610
+ // 布局级 state / lifecycle 透传(布局运行时常驻,state 跨页面保持)
1611
+ state: layoutDsl.state,
1612
+ lifecycle: layoutDsl.lifecycle,
1613
+ dataSources: {
1614
+ session: { type: "service", service: sessionService, autoLoad: true }
1615
+ },
1616
+ body: layoutDsl.body
1617
+ };
1618
+ }, [layoutDsl, layoutName, sessionService]);
1619
+ if (layoutDsl === void 0 || layoutPage === null) {
1620
+ return /* @__PURE__ */ jsx6(Centered, { children: t("pactor.sitemap.layoutMissing", { name: layoutName }) });
1621
+ }
1622
+ const LayoutPage = layoutFactory;
1623
+ return /* @__PURE__ */ jsx6(
1624
+ OutletContext.Provider,
1625
+ {
1626
+ value: index === chain.length - 1 ? outlet : /* @__PURE__ */ jsx6(
1627
+ LayoutLevel,
1628
+ {
1629
+ sitemap,
1630
+ chain,
1631
+ index: index + 1,
1632
+ sessionService,
1633
+ layoutFactory,
1634
+ outlet
1635
+ },
1636
+ chain[index + 1]
1583
1637
  ),
1584
- children: /* @__PURE__ */ jsx6(LayoutPage, { pageDsl: layoutPage })
1638
+ children: /* @__PURE__ */ jsx6(LayoutPage, { pageDsl: layoutPage }, layoutName)
1585
1639
  }
1586
1640
  );
1587
1641
  }
@@ -1620,6 +1674,7 @@ export {
1620
1674
  defaultSitemapGuard,
1621
1675
  fetchSitemap,
1622
1676
  mergeSitemapMenus,
1677
+ resolveLayoutChain,
1623
1678
  resolveSitemapRoute,
1624
1679
  toSitemapNavItems,
1625
1680
  withHttpHooks,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pactor-app/app",
3
- "version": "1.46.0",
3
+ "version": "1.48.0",
4
4
  "description": "Pactor application: one-stop composition layer (createPactorApplication) wiring loader, bootstrap, registries, router, layout, interaction and page runtimes",
5
5
  "keywords": [
6
6
  "pactor",
@@ -46,14 +46,14 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@pactor-app/core": "1.46.0",
50
- "@pactor-app/expression": "1.46.0",
51
- "@pactor-app/schema": "1.46.0",
52
- "@pactor-app/permission": "1.46.0",
53
- "@pactor-app/runtime": "1.46.0",
54
- "@pactor-app/ui": "1.46.0",
55
- "@pactor-app/router": "1.46.0",
56
- "@pactor-app/resource": "1.46.0"
49
+ "@pactor-app/core": "1.48.0",
50
+ "@pactor-app/permission": "1.48.0",
51
+ "@pactor-app/expression": "1.48.0",
52
+ "@pactor-app/router": "1.48.0",
53
+ "@pactor-app/runtime": "1.48.0",
54
+ "@pactor-app/resource": "1.48.0",
55
+ "@pactor-app/ui": "1.48.0",
56
+ "@pactor-app/schema": "1.48.0"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "react": ">=18",