@pactor-app/app 1.18.0 → 1.20.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
@@ -98,6 +98,49 @@ const Page = createPactorPage({ navigate: (to) => navigate(to) });
98
98
  需要完全自定义链接渲染(prefetch、scroll restoration 等)时,可经 `components` 注册表覆盖同名组件,样式子组件从 `@pactor-app/ui/ui` 导入(`BreadcrumbLink` 支持 `render` prop 换成宿主路由组件)。
99
99
  - `route.params` / `route.query` 经 props 纯数据注入(如 Next.js 的 `searchParams`)
100
100
 
101
+ ## Sitemap 动态页面(SitemapApp)
102
+
103
+ `design/dynamic-page.md` 的框架实现:一个 sitemap 对象(页面 url / 名称 / 结构 / DSL / DSL 布局)驱动整个应用。两种路由模式:**纯前端 SPA** 不传路由 props(内置浏览器路由:History API 导航 + popstate 前进后退);**宿主路由**(Next.js App Router、react-router 等)注入 `pathname` / `navigate`:
104
+
105
+ ```tsx
106
+ import { SitemapApp } from '@pactor-app/app';
107
+
108
+ // 纯前端 SPA:只配 sitemap 源即可
109
+ <SitemapApp
110
+ sources={['/api/sitemap']}
111
+ services={{ 'auth.login': login }}
112
+ />
113
+
114
+ // Next.js(App Router):挂在 layout.tsx(跨导航常驻,切换导航只刷新内容区)
115
+ <SitemapApp
116
+ sources={['/api/sitemap']} // sitemap 接口 URL(可多源合并)
117
+ baseUrl="https://api.example.com" // 可选:sources / pageEndpoint 相对路径基准
118
+ pathname={usePathname()}
119
+ navigate={(to) => router.push(to)}
120
+ services={{ 'auth.login': login }} // 业务服务(session.context 自动注册)
121
+ httpAdapter={httpHooks} // 可选:跨域改写 / 响应拆包
122
+ />
123
+ ```
124
+
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
+
127
+ | Props | 说明 |
128
+ | --- | --- |
129
+ | `sources`(必填) | sitemap 接口 URL 列表(相对路径以 `baseUrl` 补全) |
130
+ | `baseUrl` | 接口基准地址:`sources` / `pageEndpoint` 只写相对路径时以此为基准补全 |
131
+ | `pathname` / `navigate` | 宿主路由注入;缺省用内置浏览器路由(纯前端 SPA 零配置);http(s) 外链自动新标签打开 |
132
+ | `query` | 当前 query(`${route.query.x}` 可读);缺省自 `location.search` 解析 |
133
+ | `pageEndpoint` | 懒加载页面端点(缺省 `/api/pages/{id}`) |
134
+ | `services` / `components` | 业务扩展(`components` 中 `PageOutlet` / `SitemapMenu` 为布局保留名) |
135
+ | `actions` | 业务动作;内置 `app.reload` / `service.call` 默认值,同名覆盖 |
136
+ | `guard` | 访问守卫(缺省 `defaultSitemapGuard`:匿名访问非 `public` 页 → 重定向 `auth.loginPath`) |
137
+ | `httpAdapter` | `PactorHttpHooks`:所有 HTTP 出口(sitemap 源 / 页面端点 / 页面运行时)的 request / response 统一加工;`createHttpHooks()` 为默认实现(`{ code, data, message }` 拆包;可选 `proxyPrefix` 代理改写 / `headers` 公共头) |
138
+ | `fetcher` | fetch 实现注入(SSR / 测试) |
139
+ | `sessionService` | 会话桥服务名(缺省 `session.context`) |
140
+ | `loading` / `renderError` / `renderNotFound` | 状态视图定制 |
141
+
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`(纯前端内置路由模式)。
143
+
101
144
  ## 文档
102
145
 
103
146
  完整文档见 <https://github.com/426-330/pactor/tree/main/docs>(`pnpm docs:dev` 本地启动文档站)。
package/dist/index.cjs CHANGED
@@ -25,9 +25,20 @@ __export(index_exports, {
25
25
  PactorPage: () => PactorPage,
26
26
  PageErrorBoundary: () => PageErrorBoundary,
27
27
  ReactRouterBinding: () => ReactRouterBinding,
28
+ SitemapApp: () => SitemapApp,
29
+ createHttpHooks: () => createHttpHooks,
28
30
  createPactorApplication: () => createPactorApplication,
29
31
  createPactorPage: () => createPactorPage,
30
32
  createRuntimeHttpAdapter: () => createRuntimeHttpAdapter,
33
+ createSitemapLoader: () => createSitemapLoader,
34
+ createSitemapSessionServices: () => createSitemapSessionServices,
35
+ defaultSitemapActions: () => defaultSitemapActions,
36
+ defaultSitemapGuard: () => defaultSitemapGuard,
37
+ fetchSitemap: () => fetchSitemap,
38
+ mergeSitemapMenus: () => mergeSitemapMenus,
39
+ resolveSitemapRoute: () => resolveSitemapRoute,
40
+ toSitemapNavItems: () => toSitemapNavItems,
41
+ withHttpHooks: () => withHttpHooks,
31
42
  wrapHttpAdapter: () => wrapHttpAdapter
32
43
  });
33
44
  module.exports = __toCommonJS(index_exports);
@@ -878,6 +889,598 @@ function EmbeddedPage({
878
889
  }
879
890
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_runtime5.I18nContextProvider, { value: i18n, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(InteractionProvider2, { bridge: assembly.uiBridge, children: content }) });
880
891
  }
892
+
893
+ // src/sitemap/actions.ts
894
+ var defaultSitemapActions = {
895
+ "app.reload": (action) => {
896
+ const to = action.params?.["to"];
897
+ if (typeof window !== "undefined") {
898
+ window.location.assign(typeof to === "string" && to !== "" ? to : "/");
899
+ }
900
+ return { ok: true };
901
+ },
902
+ "service.call": async (action, ctx) => {
903
+ const name = action.params?.["service"];
904
+ if (typeof name !== "string" || name === "") {
905
+ return {
906
+ ok: false,
907
+ error: new Error("[@pactor-app/app] service.call \u7F3A\u5C11 params.service")
908
+ };
909
+ }
910
+ const handler = ctx.context.services.get(name);
911
+ if (!handler) {
912
+ return {
913
+ ok: false,
914
+ error: new Error(`[@pactor-app/app] service \u672A\u6CE8\u518C: ${name}`)
915
+ };
916
+ }
917
+ try {
918
+ const callParams = action.params?.["params"];
919
+ const data = await handler(
920
+ typeof callParams === "object" && callParams !== null ? callParams : {}
921
+ );
922
+ return { ok: true, data };
923
+ } catch (cause) {
924
+ return {
925
+ ok: false,
926
+ error: cause instanceof Error ? cause : new Error(String(cause))
927
+ };
928
+ }
929
+ }
930
+ };
931
+
932
+ // src/sitemap/SitemapApp.tsx
933
+ var import_router3 = require("@pactor-app/router");
934
+ var import_components2 = require("@pactor-app/ui/components");
935
+ var import_react8 = require("react");
936
+
937
+ // src/sitemap/browser-route.ts
938
+ var import_react7 = require("react");
939
+ var NAV_EVENT = "pactor:sitemap-navigate";
940
+ function readLocation() {
941
+ return `${window.location.pathname}${window.location.search}`;
942
+ }
943
+ function subscribe(callback) {
944
+ window.addEventListener("popstate", callback);
945
+ window.addEventListener(NAV_EVENT, callback);
946
+ return () => {
947
+ window.removeEventListener("popstate", callback);
948
+ window.removeEventListener(NAV_EVENT, callback);
949
+ };
950
+ }
951
+ function useBrowserRoute() {
952
+ const location = (0, import_react7.useSyncExternalStore)(
953
+ subscribe,
954
+ readLocation,
955
+ // SSR 快照:服务端无 window,宿主应注入 pathname / navigate
956
+ () => "/"
957
+ );
958
+ const navigate = (0, import_react7.useCallback)((to) => {
959
+ window.history.pushState(null, "", to);
960
+ window.dispatchEvent(new Event(NAV_EVENT));
961
+ }, []);
962
+ return (0, import_react7.useMemo)(() => {
963
+ const qIndex = location.indexOf("?");
964
+ return {
965
+ pathname: qIndex >= 0 ? location.slice(0, qIndex) : location,
966
+ query: Object.fromEntries(
967
+ new URLSearchParams(qIndex >= 0 ? location.slice(qIndex + 1) : "")
968
+ ),
969
+ navigate
970
+ };
971
+ }, [location, navigate]);
972
+ }
973
+
974
+ // src/sitemap/http.ts
975
+ function createHttpHooks(options = {}) {
976
+ const { proxyPrefix } = options;
977
+ const envelope = options.envelope ?? true;
978
+ return {
979
+ request(config) {
980
+ let url = config.url;
981
+ if (proxyPrefix !== void 0 && /^https?:\/\//.test(url) && typeof window !== "undefined" && !url.startsWith(window.location.origin)) {
982
+ const target = new URL(url);
983
+ url = `${proxyPrefix}${target.pathname}${target.search}`;
984
+ }
985
+ const extra = typeof options.headers === "function" ? options.headers() : options.headers;
986
+ return {
987
+ ...config,
988
+ url,
989
+ headers: { ...config.headers, ...extra }
990
+ };
991
+ },
992
+ response(data) {
993
+ if (!envelope) {
994
+ return data;
995
+ }
996
+ if (typeof data === "object" && data !== null && "code" in data && "data" in data) {
997
+ const wrapped = data;
998
+ if (wrapped.code !== 0) {
999
+ throw new Error(wrapped.message ?? `\u4E1A\u52A1\u9519\u8BEF\u7801 ${wrapped.code}`);
1000
+ }
1001
+ return wrapped.data;
1002
+ }
1003
+ return data;
1004
+ }
1005
+ };
1006
+ }
1007
+ function withHttpHooks(fetcher, hooks) {
1008
+ if (hooks === void 0) {
1009
+ return fetcher;
1010
+ }
1011
+ return (async (input, init) => {
1012
+ let url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1013
+ let requestInit = init;
1014
+ if (hooks.request !== void 0) {
1015
+ const config = await hooks.request({
1016
+ method: init?.method ?? "GET",
1017
+ url,
1018
+ headers: headersToRecord(init?.headers),
1019
+ body: init?.body ?? void 0
1020
+ });
1021
+ url = config.url;
1022
+ requestInit = {
1023
+ ...init,
1024
+ method: config.method ?? init?.method ?? "GET",
1025
+ headers: config.headers,
1026
+ body: config.body === void 0 ? init?.body ?? null : config.body
1027
+ };
1028
+ }
1029
+ const res = await fetcher(url, requestInit);
1030
+ if (hooks.response === void 0 || !res.ok) {
1031
+ return res;
1032
+ }
1033
+ const data = await hooks.response(await res.json());
1034
+ return new Response(JSON.stringify(data), {
1035
+ status: res.status,
1036
+ statusText: res.statusText,
1037
+ headers: res.headers
1038
+ });
1039
+ });
1040
+ }
1041
+ function headersToRecord(headers) {
1042
+ if (headers === void 0) {
1043
+ return void 0;
1044
+ }
1045
+ if (headers instanceof Headers) {
1046
+ return Object.fromEntries(headers.entries());
1047
+ }
1048
+ if (Array.isArray(headers)) {
1049
+ return Object.fromEntries(headers);
1050
+ }
1051
+ return { ...headers };
1052
+ }
1053
+
1054
+ // src/sitemap/url.ts
1055
+ function resolveUrl(url, baseUrl) {
1056
+ if (baseUrl === void 0 || baseUrl === "" || /^(https?:)?\/\//i.test(url)) {
1057
+ return url;
1058
+ }
1059
+ return `${baseUrl.replace(/\/+$/, "")}/${url.replace(/^\/+/, "")}`;
1060
+ }
1061
+
1062
+ // src/sitemap/fetch.ts
1063
+ async function fetchSitemap(sources, options = {}) {
1064
+ const fetcher = withHttpHooks(options.fetcher ?? fetch, options.httpHooks);
1065
+ const responses = await Promise.all(
1066
+ sources.map(async (url) => {
1067
+ const res = await fetcher(resolveUrl(url, options.baseUrl), {
1068
+ cache: "no-store",
1069
+ // 跨源 cookie 会话(服务端开通 CORS + Allow-Credentials 时生效;
1070
+ // 同源行为不变)
1071
+ credentials: "include"
1072
+ });
1073
+ if (!res.ok) {
1074
+ throw new Error(`[@pactor-app/app] sitemap \u6E90 ${url} \u8BF7\u6C42\u5931\u8D25\uFF1AHTTP ${res.status}`);
1075
+ }
1076
+ return await res.json();
1077
+ })
1078
+ );
1079
+ if (responses.length === 0) {
1080
+ throw new Error("[@pactor-app/app] sources \u4E3A\u7A7A\uFF1A\u8BF7\u914D\u7F6E\u81F3\u5C11\u4E00\u4E2A sitemap \u63A5\u53E3 URL");
1081
+ }
1082
+ const [first, ...rest] = responses;
1083
+ const merged = {
1084
+ ...first,
1085
+ layouts: { ...first.layouts },
1086
+ pages: [...first.pages],
1087
+ menus: [...first.menus]
1088
+ };
1089
+ for (const source of rest) {
1090
+ mergeById(merged.pages, source.pages, "pages");
1091
+ mergeById(merged.menus, source.menus, "menus");
1092
+ for (const [name, layout] of Object.entries(source.layouts ?? {})) {
1093
+ if (merged.layouts[name] !== void 0) {
1094
+ console.warn(`[@pactor-app/app] sitemap layouts \u952E\u51B2\u7A81\uFF1A${name}\uFF0C\u540E\u6E90\u8986\u76D6\u524D\u6E90`);
1095
+ }
1096
+ merged.layouts[name] = layout;
1097
+ }
1098
+ }
1099
+ return merged;
1100
+ }
1101
+ function mergeById(target, incoming, label) {
1102
+ for (const item of incoming) {
1103
+ const index = target.findIndex(
1104
+ (t) => t.id === item.id || item.path !== void 0 && t.path === item.path
1105
+ );
1106
+ if (index >= 0) {
1107
+ console.warn(`[@pactor-app/app] sitemap ${label} \u51B2\u7A81\uFF1A${item.id}\uFF0C\u540E\u6E90\u8986\u76D6\u524D\u6E90`);
1108
+ target[index] = item;
1109
+ } else {
1110
+ target.push(item);
1111
+ }
1112
+ }
1113
+ }
1114
+
1115
+ // src/sitemap/loader.ts
1116
+ var defaultPageEndpoint = (pageId) => `/api/pages/${pageId}`;
1117
+ function createSitemapLoader(sitemap, options = {}) {
1118
+ const pageEndpoint = options.pageEndpoint ?? defaultPageEndpoint;
1119
+ const sessionService = options.sessionService ?? "session.context";
1120
+ const fetcher = withHttpHooks(options.fetcher ?? fetch, options.httpHooks);
1121
+ const pageById = new Map(sitemap.pages.map((p) => [p.id, p]));
1122
+ const cache = /* @__PURE__ */ new Map();
1123
+ return {
1124
+ async loadPage(pageId) {
1125
+ const entry = pageById.get(pageId);
1126
+ const version = entry?.version ?? 1;
1127
+ const cacheKey = `${pageId}@${version}`;
1128
+ const hit = cache.get(cacheKey);
1129
+ if (hit) {
1130
+ return hit;
1131
+ }
1132
+ let resource;
1133
+ if (entry?.pageDsl !== void 0) {
1134
+ resource = {
1135
+ id: pageId,
1136
+ resourceType: "page",
1137
+ version,
1138
+ dslVersion: entry.pageDsl.dsl ?? "1.0",
1139
+ schema: entry.pageDsl
1140
+ };
1141
+ } else {
1142
+ const res = await fetcher(resolveUrl(pageEndpoint(pageId), options.baseUrl), {
1143
+ cache: "no-store",
1144
+ credentials: "include"
1145
+ });
1146
+ if (!res.ok) {
1147
+ throw new Error(
1148
+ `[@pactor-app/app] \u9875\u9762 ${pageId} \u52A0\u8F7D\u5931\u8D25\uFF1AHTTP ${res.status}`
1149
+ );
1150
+ }
1151
+ resource = await res.json();
1152
+ }
1153
+ if (typeof resource.schema === "object" && resource.schema !== null) {
1154
+ const schema = resource.schema;
1155
+ schema.dataSources = {
1156
+ session: { type: "service", service: sessionService, autoLoad: true },
1157
+ ...schema.dataSources
1158
+ };
1159
+ }
1160
+ cache.set(cacheKey, resource);
1161
+ return resource;
1162
+ },
1163
+ async loadFragment(fragmentId) {
1164
+ throw new Error(`[@pactor-app/app] \u672A\u5B9E\u73B0\u7684 fragment \u52A0\u8F7D\uFF1A${fragmentId}`);
1165
+ },
1166
+ async loadBootstrap() {
1167
+ return sitemap;
1168
+ }
1169
+ };
1170
+ }
1171
+
1172
+ // src/sitemap/merge.ts
1173
+ function mergeSitemapMenus(sitemap) {
1174
+ const pageById = new Map(sitemap.pages.map((p) => [p.id, p]));
1175
+ const walk = (items) => items.map((item) => {
1176
+ const page = item.pageId !== void 0 ? pageById.get(item.pageId) : void 0;
1177
+ const merged = {
1178
+ ...item,
1179
+ path: item.path ?? page?.path,
1180
+ title: item.title ?? page?.title ?? item.id
1181
+ };
1182
+ if (item.children !== void 0) {
1183
+ merged.children = walk(item.children);
1184
+ }
1185
+ return merged;
1186
+ });
1187
+ return walk(sitemap.menus ?? []);
1188
+ }
1189
+ function toSitemapNavItems(menuTree) {
1190
+ return menuTree.map((item) => ({
1191
+ key: item.path ?? item.id,
1192
+ label: item.title,
1193
+ icon: item.icon,
1194
+ disabled: item.disabled,
1195
+ children: item.children !== void 0 ? toSitemapNavItems(item.children) : void 0
1196
+ }));
1197
+ }
1198
+
1199
+ // src/sitemap/route.ts
1200
+ function resolveSitemapRoute(sitemap, registry, pathname) {
1201
+ const match = registry.findByPath(pathname);
1202
+ const menu = match?.menu ?? null;
1203
+ if (match === null || menu === null || menu.pageId === void 0) {
1204
+ return { menu, route: null };
1205
+ }
1206
+ const layoutName = menu.layout ?? sitemap.layout ?? Object.keys(sitemap.layouts)[0] ?? "";
1207
+ const highlightId = menu.activeMenu ?? menu.id;
1208
+ const highlightItem = registry.findById(highlightId);
1209
+ return {
1210
+ menu,
1211
+ route: {
1212
+ pageId: menu.pageId,
1213
+ layoutName,
1214
+ highlightKey: highlightItem?.path ?? highlightId,
1215
+ params: match.params
1216
+ }
1217
+ };
1218
+ }
1219
+ var defaultSitemapGuard = ({
1220
+ sitemap,
1221
+ pathname,
1222
+ menu
1223
+ }) => {
1224
+ const loginPath = sitemap.auth?.loginPath ?? "/login";
1225
+ if (sitemap.user === void 0 && pathname !== loginPath && (menu === null || menu.public !== true)) {
1226
+ return { allow: false, redirectTo: loginPath };
1227
+ }
1228
+ return { allow: true };
1229
+ };
1230
+
1231
+ // src/sitemap/session.ts
1232
+ function createSitemapSessionServices(session, serviceName = "session.context") {
1233
+ return {
1234
+ [serviceName]: () => Promise.resolve(session)
1235
+ };
1236
+ }
1237
+
1238
+ // src/sitemap/SitemapApp.tsx
1239
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1240
+ var OutletContext = (0, import_react8.createContext)(null);
1241
+ function PageOutlet() {
1242
+ return (0, import_react8.use)(OutletContext);
1243
+ }
1244
+ var NavHighlightContext = (0, import_react8.createContext)("");
1245
+ function SitemapMenu({
1246
+ items,
1247
+ collapsible,
1248
+ onSelect
1249
+ }) {
1250
+ const highlight = (0, import_react8.use)(NavHighlightContext);
1251
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1252
+ import_components2.Menu,
1253
+ {
1254
+ items,
1255
+ collapsible,
1256
+ selectedKey: highlight,
1257
+ onSelect
1258
+ }
1259
+ );
1260
+ }
1261
+ function SitemapApp(props) {
1262
+ const { sources, fetcher, baseUrl } = props;
1263
+ const browserRoute = useBrowserRoute();
1264
+ const pathname = props.pathname ?? browserRoute.pathname;
1265
+ const query = props.query ?? browserRoute.query;
1266
+ const navigate = props.navigate ?? browserRoute.navigate;
1267
+ const [state, setState] = (0, import_react8.useState)({ status: "loading" });
1268
+ (0, import_react8.useEffect)(() => {
1269
+ let cancelled = false;
1270
+ setState({ status: "loading" });
1271
+ fetchSitemap(sources, {
1272
+ fetcher,
1273
+ baseUrl,
1274
+ httpHooks: props.httpAdapter
1275
+ }).then((sitemap2) => {
1276
+ if (!cancelled) {
1277
+ setState({ status: "ready", sitemap: sitemap2 });
1278
+ }
1279
+ }).catch((error) => {
1280
+ if (!cancelled) {
1281
+ setState({
1282
+ status: "error",
1283
+ error: error instanceof Error ? error : new Error(String(error))
1284
+ });
1285
+ }
1286
+ });
1287
+ return () => {
1288
+ cancelled = true;
1289
+ };
1290
+ }, [fetcher, baseUrl, props.httpAdapter, JSON.stringify(sources)]);
1291
+ const registry = (0, import_react8.useMemo)(
1292
+ () => state.status === "ready" ? new import_router3.MenuRegistry(mergeSitemapMenus(state.sitemap)) : null,
1293
+ [state]
1294
+ );
1295
+ const resolution = (0, import_react8.useMemo)(
1296
+ () => state.status === "ready" && registry !== null ? resolveSitemapRoute(state.sitemap, registry, pathname) : null,
1297
+ [state, registry, pathname]
1298
+ );
1299
+ const sitemap = state.status === "ready" ? state.sitemap : null;
1300
+ const guard = props.guard ?? defaultSitemapGuard;
1301
+ const decision = (0, import_react8.useMemo)(
1302
+ () => sitemap !== null && resolution !== null ? guard({ sitemap, pathname, menu: resolution.menu }) : { allow: true },
1303
+ [sitemap, resolution, guard, pathname]
1304
+ );
1305
+ (0, import_react8.useEffect)(() => {
1306
+ if (!decision.allow) {
1307
+ navigate(decision.redirectTo);
1308
+ }
1309
+ }, [decision, navigate]);
1310
+ if (state.status === "loading") {
1311
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Centered, { children: props.loading ?? "\u52A0\u8F7D\u4E2D\u2026" });
1312
+ }
1313
+ if (state.status === "error") {
1314
+ return props.renderError !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: props.renderError(state.error) }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Centered, { children: [
1315
+ "sitemap \u52A0\u8F7D\u5931\u8D25\uFF1A",
1316
+ state.error.message
1317
+ ] });
1318
+ }
1319
+ if (sitemap === null || registry === null || resolution === null) {
1320
+ return null;
1321
+ }
1322
+ if (!decision.allow) {
1323
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Centered, { children: "\u91CD\u5B9A\u5411\u2026" });
1324
+ }
1325
+ if (resolution.route === null) {
1326
+ return props.renderNotFound !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: props.renderNotFound(pathname) }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Centered, { children: [
1327
+ "\u9875\u9762\u4E0D\u5B58\u5728\uFF08",
1328
+ pathname,
1329
+ "\uFF09"
1330
+ ] });
1331
+ }
1332
+ const { pageId, layoutName, highlightKey, params } = resolution.route;
1333
+ return (
1334
+ // key 只随布局名变化:切换导航(同布局)时布局常驻,
1335
+ // 仅 PageOutlet 内的页面随路由重建
1336
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(NavHighlightContext.Provider, { value: highlightKey, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1337
+ SitemapLayout,
1338
+ {
1339
+ sitemap,
1340
+ registry,
1341
+ pageId,
1342
+ layoutName,
1343
+ pathname,
1344
+ query,
1345
+ navigate,
1346
+ routeParams: params,
1347
+ props
1348
+ },
1349
+ layoutName
1350
+ ) })
1351
+ );
1352
+ }
1353
+ function SitemapLayout({
1354
+ sitemap,
1355
+ registry,
1356
+ pageId,
1357
+ layoutName,
1358
+ pathname,
1359
+ query,
1360
+ navigate: hostNavigate,
1361
+ routeParams,
1362
+ props
1363
+ }) {
1364
+ const sessionService = props.sessionService ?? "session.context";
1365
+ const session = (0, import_react8.useMemo)(
1366
+ () => ({
1367
+ user: sitemap.user,
1368
+ app: { name: "Pactor App", ...sitemap.app },
1369
+ menus: toSitemapNavItems(registry.tree())
1370
+ }),
1371
+ [sitemap, registry]
1372
+ );
1373
+ const navigate = (0, import_react8.useMemo)(
1374
+ () => (to) => {
1375
+ if (/^https?:\/\//.test(to)) {
1376
+ window.open(to, "_blank", "noopener");
1377
+ } else {
1378
+ hostNavigate(to);
1379
+ }
1380
+ },
1381
+ [hostNavigate]
1382
+ );
1383
+ const loader = (0, import_react8.useMemo)(
1384
+ () => createSitemapLoader(sitemap, {
1385
+ pageEndpoint: props.pageEndpoint,
1386
+ fetcher: props.fetcher,
1387
+ sessionService,
1388
+ baseUrl: props.baseUrl,
1389
+ httpHooks: props.httpAdapter
1390
+ }),
1391
+ [
1392
+ sitemap,
1393
+ props.pageEndpoint,
1394
+ props.fetcher,
1395
+ props.baseUrl,
1396
+ props.httpAdapter,
1397
+ sessionService
1398
+ ]
1399
+ );
1400
+ const services = (0, import_react8.useMemo)(
1401
+ () => ({
1402
+ ...createSitemapSessionServices(session, sessionService),
1403
+ ...props.services
1404
+ }),
1405
+ [session, sessionService, props.services]
1406
+ );
1407
+ const actions = (0, import_react8.useMemo)(
1408
+ () => ({ ...defaultSitemapActions, ...props.actions }),
1409
+ [props.actions]
1410
+ );
1411
+ const layoutDsl = sitemap.layouts[layoutName];
1412
+ const layoutPage = (0, import_react8.useMemo)(() => {
1413
+ if (layoutDsl === void 0) {
1414
+ return null;
1415
+ }
1416
+ return {
1417
+ dsl: layoutDsl.dsl,
1418
+ page: { id: `layout:${layoutName}`, title: layoutName },
1419
+ dataSources: {
1420
+ session: { type: "service", service: sessionService, autoLoad: true }
1421
+ },
1422
+ body: layoutDsl.body
1423
+ };
1424
+ }, [layoutDsl, layoutName, sessionService]);
1425
+ if (layoutDsl === void 0 || layoutPage === null) {
1426
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Centered, { children: [
1427
+ "\u5E03\u5C40\u672A\u5B9A\u4E49\uFF1A",
1428
+ layoutName
1429
+ ] });
1430
+ }
1431
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1432
+ OutletContext.Provider,
1433
+ {
1434
+ value: (
1435
+ // 页面实例:key 随 pageId + pathname 变化(route 参数变化需重建,
1436
+ // PactorPage 的 route prop 不参与运行时重建)——布局保持挂载
1437
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1438
+ PactorPage,
1439
+ {
1440
+ pageId,
1441
+ route: { params: routeParams, query },
1442
+ loader,
1443
+ services,
1444
+ actions,
1445
+ permissions: sitemap.permissions ?? [],
1446
+ navigate,
1447
+ httpAdapter: props.httpAdapter,
1448
+ components: props.components
1449
+ },
1450
+ `${pageId}@${pathname}`
1451
+ )
1452
+ ),
1453
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1454
+ PactorPage,
1455
+ {
1456
+ pageDsl: layoutPage,
1457
+ services,
1458
+ actions,
1459
+ navigate,
1460
+ httpAdapter: props.httpAdapter,
1461
+ components: { PageOutlet, SitemapMenu, ...props.components }
1462
+ }
1463
+ )
1464
+ }
1465
+ );
1466
+ }
1467
+ function Centered({ children }) {
1468
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1469
+ "div",
1470
+ {
1471
+ style: {
1472
+ minHeight: "60vh",
1473
+ display: "flex",
1474
+ flexDirection: "column",
1475
+ alignItems: "center",
1476
+ justifyContent: "center",
1477
+ gap: 8,
1478
+ color: "var(--muted-foreground, #6b7280)"
1479
+ },
1480
+ children
1481
+ }
1482
+ );
1483
+ }
881
1484
  // Annotate the CommonJS export names for ESM import in node:
882
1485
  0 && (module.exports = {
883
1486
  Application,
@@ -885,8 +1488,19 @@ function EmbeddedPage({
885
1488
  PactorPage,
886
1489
  PageErrorBoundary,
887
1490
  ReactRouterBinding,
1491
+ SitemapApp,
1492
+ createHttpHooks,
888
1493
  createPactorApplication,
889
1494
  createPactorPage,
890
1495
  createRuntimeHttpAdapter,
1496
+ createSitemapLoader,
1497
+ createSitemapSessionServices,
1498
+ defaultSitemapActions,
1499
+ defaultSitemapGuard,
1500
+ fetchSitemap,
1501
+ mergeSitemapMenus,
1502
+ resolveSitemapRoute,
1503
+ toSitemapNavItems,
1504
+ withHttpHooks,
891
1505
  wrapHttpAdapter
892
1506
  });