@pactor-app/app 1.47.0 → 1.48.1

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,
@@ -1009,7 +1010,7 @@ var defaultSitemapActions = {
1009
1010
 
1010
1011
  // src/sitemap/SitemapApp.tsx
1011
1012
  var import_core4 = require("@pactor-app/core");
1012
- var import_router3 = require("@pactor-app/router");
1013
+ var import_router4 = require("@pactor-app/router");
1013
1014
  var import_runtime6 = require("@pactor-app/runtime");
1014
1015
  var import_components2 = require("@pactor-app/ui/components");
1015
1016
  var import_react8 = require("react");
@@ -1277,6 +1278,7 @@ function toSitemapNavItems(menuTree) {
1277
1278
  }
1278
1279
 
1279
1280
  // src/sitemap/route.ts
1281
+ var import_router3 = require("@pactor-app/router");
1280
1282
  function resolveSitemapRoute(sitemap, registry, pathname) {
1281
1283
  const match = registry.findByPath(pathname);
1282
1284
  const menu = match?.menu ?? null;
@@ -1286,16 +1288,33 @@ function resolveSitemapRoute(sitemap, registry, pathname) {
1286
1288
  const layoutName = menu.layout ?? sitemap.layout ?? Object.keys(sitemap.layouts)[0] ?? "";
1287
1289
  const highlightId = menu.activeMenu ?? menu.id;
1288
1290
  const highlightItem = registry.findById(highlightId);
1291
+ const page = sitemap.pages.find((p) => p.id === menu.pageId);
1292
+ const params = page !== void 0 && page.path !== menu.path ? (0, import_router3.match)(page.path, pathname)?.params ?? match.params : match.params;
1289
1293
  return {
1290
1294
  menu,
1291
1295
  route: {
1292
1296
  pageId: menu.pageId,
1293
1297
  layoutName,
1298
+ layoutChain: resolveLayoutChain(sitemap, layoutName),
1294
1299
  highlightKey: highlightItem?.path ?? highlightId,
1295
- params: match.params
1300
+ params
1296
1301
  }
1297
1302
  };
1298
1303
  }
1304
+ function resolveLayoutChain(sitemap, layoutName) {
1305
+ const chain = [layoutName];
1306
+ const seen = new Set(chain);
1307
+ let current = layoutName;
1308
+ for (; ; ) {
1309
+ const parent = sitemap.layouts[current]?.parent;
1310
+ if (parent === void 0 || seen.has(parent) || sitemap.layouts[parent] === void 0) {
1311
+ return chain;
1312
+ }
1313
+ chain.unshift(parent);
1314
+ seen.add(parent);
1315
+ current = parent;
1316
+ }
1317
+ }
1299
1318
  var defaultSitemapGuard = ({
1300
1319
  sitemap,
1301
1320
  pathname,
@@ -1381,7 +1400,7 @@ function SitemapAppInner(props) {
1381
1400
  };
1382
1401
  }, [fetcher, baseUrl, props.httpAdapter, JSON.stringify(sources)]);
1383
1402
  const registry = (0, import_react8.useMemo)(
1384
- () => state.status === "ready" ? new import_router3.MenuRegistry(mergeSitemapMenus(state.sitemap)) : null,
1403
+ () => state.status === "ready" ? new import_router4.MenuRegistry(mergeSitemapMenus(state.sitemap)) : null,
1385
1404
  [state]
1386
1405
  );
1387
1406
  const resolution = (0, import_react8.useMemo)(
@@ -1414,24 +1433,24 @@ function SitemapAppInner(props) {
1414
1433
  if (resolution.route === null) {
1415
1434
  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
1435
  }
1417
- const { pageId, layoutName, highlightKey, params } = resolution.route;
1436
+ const { pageId, layoutChain, highlightKey, params } = resolution.route;
1418
1437
  return (
1419
- // key 只随布局名变化:切换导航(同布局)时布局常驻,
1420
- // 仅 PageOutlet 内的页面随路由重建
1438
+ // key 只随最外层布局名变化:布局链共享前缀时外层布局常驻,
1439
+ // 仅分叉的内层布局与 PageOutlet 内页面随路由重建
1421
1440
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(NavHighlightContext.Provider, { value: highlightKey, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1422
1441
  SitemapLayout,
1423
1442
  {
1424
1443
  sitemap,
1425
1444
  registry,
1426
1445
  pageId,
1427
- layoutName,
1446
+ layoutChain,
1428
1447
  pathname,
1429
1448
  query,
1430
1449
  navigate,
1431
1450
  routeParams: params,
1432
1451
  props
1433
1452
  },
1434
- layoutName
1453
+ layoutChain[0]
1435
1454
  ) })
1436
1455
  );
1437
1456
  }
@@ -1439,14 +1458,13 @@ function SitemapLayout({
1439
1458
  sitemap,
1440
1459
  registry,
1441
1460
  pageId,
1442
- layoutName,
1461
+ layoutChain,
1443
1462
  pathname,
1444
1463
  query,
1445
1464
  navigate: hostNavigate,
1446
1465
  routeParams,
1447
1466
  props
1448
1467
  }) {
1449
- const { t } = (0, import_runtime6.useI18n)();
1450
1468
  const sessionService = props.sessionService ?? "session.context";
1451
1469
  const session = (0, import_react8.useMemo)(
1452
1470
  () => ({
@@ -1494,23 +1512,6 @@ function SitemapLayout({
1494
1512
  () => ({ ...defaultSitemapActions, ...props.actions }),
1495
1513
  [props.actions]
1496
1514
  );
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
1515
  const messagesSignature = JSON.stringify(props.messages ?? {});
1515
1516
  const pageFactory = (0, import_react8.useMemo)(
1516
1517
  () => createPactorPage({
@@ -1541,6 +1542,9 @@ function SitemapLayout({
1541
1542
  );
1542
1543
  const layoutFactory = (0, import_react8.useMemo)(
1543
1544
  () => createPactorPage({
1545
+ // loader 注入使布局运行时的 OverlayHost 支持 page 类型弹层
1546
+ //(布局内按钮经 overlay.open 打开懒加载表单页,如创建实例)
1547
+ loader,
1544
1548
  services,
1545
1549
  actions,
1546
1550
  navigate,
@@ -1552,6 +1556,7 @@ function SitemapLayout({
1552
1556
  }),
1553
1557
  // eslint-disable-next-line react-hooks/exhaustive-deps
1554
1558
  [
1559
+ loader,
1555
1560
  services,
1556
1561
  actions,
1557
1562
  navigate,
@@ -1563,11 +1568,15 @@ function SitemapLayout({
1563
1568
  ]
1564
1569
  );
1565
1570
  const Page = pageFactory;
1566
- const LayoutPage = layoutFactory;
1567
1571
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1568
- OutletContext.Provider,
1572
+ LayoutLevel,
1569
1573
  {
1570
- value: (
1574
+ sitemap,
1575
+ chain: layoutChain,
1576
+ index: 0,
1577
+ sessionService,
1578
+ layoutFactory,
1579
+ outlet: (
1571
1580
  // 页面实例:key 随 pageId + pathname 变化(route 参数变化需重建,
1572
1581
  // PactorPage 的 route prop 不参与运行时重建)——布局保持挂载
1573
1582
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -1578,8 +1587,57 @@ function SitemapLayout({
1578
1587
  },
1579
1588
  `${pageId}@${pathname}`
1580
1589
  )
1590
+ )
1591
+ }
1592
+ );
1593
+ }
1594
+ function LayoutLevel({
1595
+ sitemap,
1596
+ chain,
1597
+ index,
1598
+ sessionService,
1599
+ layoutFactory,
1600
+ outlet
1601
+ }) {
1602
+ const { t } = (0, import_runtime6.useI18n)();
1603
+ const layoutName = chain[index];
1604
+ const layoutDsl = sitemap.layouts[layoutName];
1605
+ const layoutPage = (0, import_react8.useMemo)(() => {
1606
+ if (layoutDsl === void 0) {
1607
+ return null;
1608
+ }
1609
+ return {
1610
+ dsl: layoutDsl.dsl,
1611
+ page: { id: `layout:${layoutName}`, title: layoutName },
1612
+ // 布局级 state / lifecycle 透传(布局运行时常驻,state 跨页面保持)
1613
+ state: layoutDsl.state,
1614
+ lifecycle: layoutDsl.lifecycle,
1615
+ dataSources: {
1616
+ session: { type: "service", service: sessionService, autoLoad: true }
1617
+ },
1618
+ body: layoutDsl.body
1619
+ };
1620
+ }, [layoutDsl, layoutName, sessionService]);
1621
+ if (layoutDsl === void 0 || layoutPage === null) {
1622
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Centered, { children: t("pactor.sitemap.layoutMissing", { name: layoutName }) });
1623
+ }
1624
+ const LayoutPage = layoutFactory;
1625
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1626
+ OutletContext.Provider,
1627
+ {
1628
+ value: index === chain.length - 1 ? outlet : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1629
+ LayoutLevel,
1630
+ {
1631
+ sitemap,
1632
+ chain,
1633
+ index: index + 1,
1634
+ sessionService,
1635
+ layoutFactory,
1636
+ outlet
1637
+ },
1638
+ chain[index + 1]
1581
1639
  ),
1582
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(LayoutPage, { pageDsl: layoutPage })
1640
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(LayoutPage, { pageDsl: layoutPage }, layoutName)
1583
1641
  }
1584
1642
  );
1585
1643
  }
@@ -1619,6 +1677,7 @@ function Centered({ children }) {
1619
1677
  defaultSitemapGuard,
1620
1678
  fetchSitemap,
1621
1679
  mergeSitemapMenus,
1680
+ resolveLayoutChain,
1622
1681
  resolveSitemapRoute,
1623
1682
  toSitemapNavItems,
1624
1683
  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
@@ -1279,6 +1279,7 @@ function toSitemapNavItems(menuTree) {
1279
1279
  }
1280
1280
 
1281
1281
  // src/sitemap/route.ts
1282
+ import { match as matchPath } from "@pactor-app/router";
1282
1283
  function resolveSitemapRoute(sitemap, registry, pathname) {
1283
1284
  const match = registry.findByPath(pathname);
1284
1285
  const menu = match?.menu ?? null;
@@ -1288,16 +1289,33 @@ function resolveSitemapRoute(sitemap, registry, pathname) {
1288
1289
  const layoutName = menu.layout ?? sitemap.layout ?? Object.keys(sitemap.layouts)[0] ?? "";
1289
1290
  const highlightId = menu.activeMenu ?? menu.id;
1290
1291
  const highlightItem = registry.findById(highlightId);
1292
+ const page = sitemap.pages.find((p) => p.id === menu.pageId);
1293
+ const params = page !== void 0 && page.path !== menu.path ? matchPath(page.path, pathname)?.params ?? match.params : match.params;
1291
1294
  return {
1292
1295
  menu,
1293
1296
  route: {
1294
1297
  pageId: menu.pageId,
1295
1298
  layoutName,
1299
+ layoutChain: resolveLayoutChain(sitemap, layoutName),
1296
1300
  highlightKey: highlightItem?.path ?? highlightId,
1297
- params: match.params
1301
+ params
1298
1302
  }
1299
1303
  };
1300
1304
  }
1305
+ function resolveLayoutChain(sitemap, layoutName) {
1306
+ const chain = [layoutName];
1307
+ const seen = new Set(chain);
1308
+ let current = layoutName;
1309
+ for (; ; ) {
1310
+ const parent = sitemap.layouts[current]?.parent;
1311
+ if (parent === void 0 || seen.has(parent) || sitemap.layouts[parent] === void 0) {
1312
+ return chain;
1313
+ }
1314
+ chain.unshift(parent);
1315
+ seen.add(parent);
1316
+ current = parent;
1317
+ }
1318
+ }
1301
1319
  var defaultSitemapGuard = ({
1302
1320
  sitemap,
1303
1321
  pathname,
@@ -1416,24 +1434,24 @@ function SitemapAppInner(props) {
1416
1434
  if (resolution.route === null) {
1417
1435
  return props.renderNotFound !== void 0 ? /* @__PURE__ */ jsx6(Fragment, { children: props.renderNotFound(pathname) }) : /* @__PURE__ */ jsx6(Centered, { children: t("pactor.sitemap.notFound", { path: pathname }) });
1418
1436
  }
1419
- const { pageId, layoutName, highlightKey, params } = resolution.route;
1437
+ const { pageId, layoutChain, highlightKey, params } = resolution.route;
1420
1438
  return (
1421
- // key 只随布局名变化:切换导航(同布局)时布局常驻,
1422
- // 仅 PageOutlet 内的页面随路由重建
1439
+ // key 只随最外层布局名变化:布局链共享前缀时外层布局常驻,
1440
+ // 仅分叉的内层布局与 PageOutlet 内页面随路由重建
1423
1441
  /* @__PURE__ */ jsx6(NavHighlightContext.Provider, { value: highlightKey, children: /* @__PURE__ */ jsx6(
1424
1442
  SitemapLayout,
1425
1443
  {
1426
1444
  sitemap,
1427
1445
  registry,
1428
1446
  pageId,
1429
- layoutName,
1447
+ layoutChain,
1430
1448
  pathname,
1431
1449
  query,
1432
1450
  navigate,
1433
1451
  routeParams: params,
1434
1452
  props
1435
1453
  },
1436
- layoutName
1454
+ layoutChain[0]
1437
1455
  ) })
1438
1456
  );
1439
1457
  }
@@ -1441,14 +1459,13 @@ function SitemapLayout({
1441
1459
  sitemap,
1442
1460
  registry,
1443
1461
  pageId,
1444
- layoutName,
1462
+ layoutChain,
1445
1463
  pathname,
1446
1464
  query,
1447
1465
  navigate: hostNavigate,
1448
1466
  routeParams,
1449
1467
  props
1450
1468
  }) {
1451
- const { t } = useI18n2();
1452
1469
  const sessionService = props.sessionService ?? "session.context";
1453
1470
  const session = useMemo2(
1454
1471
  () => ({
@@ -1496,23 +1513,6 @@ function SitemapLayout({
1496
1513
  () => ({ ...defaultSitemapActions, ...props.actions }),
1497
1514
  [props.actions]
1498
1515
  );
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
1516
  const messagesSignature = JSON.stringify(props.messages ?? {});
1517
1517
  const pageFactory = useMemo2(
1518
1518
  () => createPactorPage({
@@ -1543,6 +1543,9 @@ function SitemapLayout({
1543
1543
  );
1544
1544
  const layoutFactory = useMemo2(
1545
1545
  () => createPactorPage({
1546
+ // loader 注入使布局运行时的 OverlayHost 支持 page 类型弹层
1547
+ //(布局内按钮经 overlay.open 打开懒加载表单页,如创建实例)
1548
+ loader,
1546
1549
  services,
1547
1550
  actions,
1548
1551
  navigate,
@@ -1554,6 +1557,7 @@ function SitemapLayout({
1554
1557
  }),
1555
1558
  // eslint-disable-next-line react-hooks/exhaustive-deps
1556
1559
  [
1560
+ loader,
1557
1561
  services,
1558
1562
  actions,
1559
1563
  navigate,
@@ -1565,11 +1569,15 @@ function SitemapLayout({
1565
1569
  ]
1566
1570
  );
1567
1571
  const Page = pageFactory;
1568
- const LayoutPage = layoutFactory;
1569
1572
  return /* @__PURE__ */ jsx6(
1570
- OutletContext.Provider,
1573
+ LayoutLevel,
1571
1574
  {
1572
- value: (
1575
+ sitemap,
1576
+ chain: layoutChain,
1577
+ index: 0,
1578
+ sessionService,
1579
+ layoutFactory,
1580
+ outlet: (
1573
1581
  // 页面实例:key 随 pageId + pathname 变化(route 参数变化需重建,
1574
1582
  // PactorPage 的 route prop 不参与运行时重建)——布局保持挂载
1575
1583
  /* @__PURE__ */ jsx6(
@@ -1580,8 +1588,57 @@ function SitemapLayout({
1580
1588
  },
1581
1589
  `${pageId}@${pathname}`
1582
1590
  )
1591
+ )
1592
+ }
1593
+ );
1594
+ }
1595
+ function LayoutLevel({
1596
+ sitemap,
1597
+ chain,
1598
+ index,
1599
+ sessionService,
1600
+ layoutFactory,
1601
+ outlet
1602
+ }) {
1603
+ const { t } = useI18n2();
1604
+ const layoutName = chain[index];
1605
+ const layoutDsl = sitemap.layouts[layoutName];
1606
+ const layoutPage = useMemo2(() => {
1607
+ if (layoutDsl === void 0) {
1608
+ return null;
1609
+ }
1610
+ return {
1611
+ dsl: layoutDsl.dsl,
1612
+ page: { id: `layout:${layoutName}`, title: layoutName },
1613
+ // 布局级 state / lifecycle 透传(布局运行时常驻,state 跨页面保持)
1614
+ state: layoutDsl.state,
1615
+ lifecycle: layoutDsl.lifecycle,
1616
+ dataSources: {
1617
+ session: { type: "service", service: sessionService, autoLoad: true }
1618
+ },
1619
+ body: layoutDsl.body
1620
+ };
1621
+ }, [layoutDsl, layoutName, sessionService]);
1622
+ if (layoutDsl === void 0 || layoutPage === null) {
1623
+ return /* @__PURE__ */ jsx6(Centered, { children: t("pactor.sitemap.layoutMissing", { name: layoutName }) });
1624
+ }
1625
+ const LayoutPage = layoutFactory;
1626
+ return /* @__PURE__ */ jsx6(
1627
+ OutletContext.Provider,
1628
+ {
1629
+ value: index === chain.length - 1 ? outlet : /* @__PURE__ */ jsx6(
1630
+ LayoutLevel,
1631
+ {
1632
+ sitemap,
1633
+ chain,
1634
+ index: index + 1,
1635
+ sessionService,
1636
+ layoutFactory,
1637
+ outlet
1638
+ },
1639
+ chain[index + 1]
1583
1640
  ),
1584
- children: /* @__PURE__ */ jsx6(LayoutPage, { pageDsl: layoutPage })
1641
+ children: /* @__PURE__ */ jsx6(LayoutPage, { pageDsl: layoutPage }, layoutName)
1585
1642
  }
1586
1643
  );
1587
1644
  }
@@ -1620,6 +1677,7 @@ export {
1620
1677
  defaultSitemapGuard,
1621
1678
  fetchSitemap,
1622
1679
  mergeSitemapMenus,
1680
+ resolveLayoutChain,
1623
1681
  resolveSitemapRoute,
1624
1682
  toSitemapNavItems,
1625
1683
  withHttpHooks,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pactor-app/app",
3
- "version": "1.47.0",
3
+ "version": "1.48.1",
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.47.0",
50
- "@pactor-app/expression": "1.47.0",
51
- "@pactor-app/resource": "1.47.0",
52
- "@pactor-app/permission": "1.47.0",
53
- "@pactor-app/runtime": "1.47.0",
54
- "@pactor-app/ui": "1.47.0",
55
- "@pactor-app/schema": "1.47.0",
56
- "@pactor-app/router": "1.47.0"
49
+ "@pactor-app/core": "1.48.1",
50
+ "@pactor-app/expression": "1.48.1",
51
+ "@pactor-app/resource": "1.48.1",
52
+ "@pactor-app/permission": "1.48.1",
53
+ "@pactor-app/schema": "1.48.1",
54
+ "@pactor-app/runtime": "1.48.1",
55
+ "@pactor-app/ui": "1.48.1",
56
+ "@pactor-app/router": "1.48.1"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "react": ">=18",