@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 +43 -0
- package/dist/index.cjs +614 -0
- package/dist/index.d.cts +262 -3
- package/dist/index.d.ts +262 -3
- package/dist/index.js +609 -0
- package/package.json +9 -9
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
|
});
|