create-pactor-app 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.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -0
  3. package/dist/cli.js +220 -0
  4. package/package.json +47 -0
  5. package/templates/next-app/README.md +112 -0
  6. package/templates/next-app/app/[[...slug]]/page.tsx +7 -0
  7. package/templates/next-app/app/api/auth/login/route.ts +32 -0
  8. package/templates/next-app/app/api/auth/logout/route.ts +22 -0
  9. package/templates/next-app/app/api/pages/[id]/route.ts +40 -0
  10. package/templates/next-app/app/api/sitemap/route.ts +21 -0
  11. package/templates/next-app/app/favicon.ico +0 -0
  12. package/templates/next-app/app/globals.css +12 -0
  13. package/templates/next-app/app/layout.tsx +28 -0
  14. package/templates/next-app/eslint.config.mjs +18 -0
  15. package/templates/next-app/package.json +30 -0
  16. package/templates/next-app/postcss.config.mjs +7 -0
  17. package/templates/next-app/public/file.svg +1 -0
  18. package/templates/next-app/public/globe.svg +1 -0
  19. package/templates/next-app/public/next.svg +1 -0
  20. package/templates/next-app/public/vercel.svg +1 -0
  21. package/templates/next-app/public/window.svg +1 -0
  22. package/templates/next-app/src/pactor/App.tsx +47 -0
  23. package/templates/next-app/src/pactor/config.ts +17 -0
  24. package/templates/next-app/src/server/accounts.ts +63 -0
  25. package/templates/next-app/src/server/cors.ts +34 -0
  26. package/templates/next-app/src/server/sitemap.ts +558 -0
  27. package/templates/next-app/tsconfig.json +34 -0
  28. package/templates/spa/README.md +97 -0
  29. package/templates/spa/index.html +13 -0
  30. package/templates/spa/package.json +29 -0
  31. package/templates/spa/src/main.tsx +12 -0
  32. package/templates/spa/src/pactor/App.tsx +27 -0
  33. package/templates/spa/src/pactor/config.ts +18 -0
  34. package/templates/spa/tsconfig.json +19 -0
  35. package/templates/spa/vite.config.ts +14 -0
@@ -0,0 +1,558 @@
1
+ import type { PageDsl } from "@pactor-app/core";
2
+
3
+ import type { LayoutDsl, SitemapResource } from "pactor-app";
4
+
5
+ import type { DemoUser } from "./accounts";
6
+
7
+ /**
8
+ * Mock sitemap 数据源(design/dynamic-page.md 方案 B 示例实现)。
9
+ * 真实项目由后端接口返回同构对象——端点按会话裁剪:
10
+ * 匿名 → public 页(登录/注册);登录后 → 该角色可见的 menus / pages / permissions。
11
+ */
12
+
13
+ /**
14
+ * 服务端下发的接口地址:DSL 中直接引用后端绝对地址(跨域),
15
+ * 客户端 httpHooks 自动改写进项目内代理前缀。登录 / 登出即普通 HTTP
16
+ * 调用,DSL 用内置 request 动作表达,无需客户端注册服务。
17
+ */
18
+ const apiBase = "http://localhost:9011";
19
+
20
+ /* ---------------- 布局(DSL 定义,PageOutlet 为页面内容出口) ---------------- */
21
+ /* 布局 body 内 ${data.session.*} 由客户端注入的 session dataSource 提供
22
+ *(user / app / menus);${route.params.pathname} 为当前路径(客户端注入)。 */
23
+
24
+ const layouts: Record<string, LayoutDsl> = {
25
+ admin: {
26
+ dsl: "1.0",
27
+ body: {
28
+ type: "Flex",
29
+ props: { gap:0, direction: "horizontal", style: { height: "100vh" } },
30
+ children: [
31
+ {
32
+ type: "Flex",
33
+ props: {
34
+ direction: "vertical",
35
+ style: {
36
+ width: 224,
37
+ flexShrink: 0,
38
+ borderRight: "1px solid var(--border)",
39
+ },
40
+ },
41
+ children: [
42
+ {
43
+ type: "Flex",
44
+ props: {
45
+ style: {
46
+ padding: "16px 16px",
47
+ fontWeight: 600,
48
+ borderBottom: "1px solid var(--border)",
49
+ },
50
+ },
51
+ children: [
52
+ { type: "Text", props: { text: "${data.session.app.name}" } },
53
+ ],
54
+ },
55
+ {
56
+ type: "SitemapMenu",
57
+ props: {
58
+ items: "${data.session.menus}",
59
+ collapsible: true,
60
+ },
61
+ events: {
62
+ onSelect: [
63
+ { action: "navigate", params: { to: "${event}" } },
64
+ ],
65
+ },
66
+ },
67
+ ],
68
+ },
69
+ {
70
+ type: "Flex",
71
+ props: { direction: "vertical", style: { flex: 1, minWidth: 0 } },
72
+ children: [
73
+ {
74
+ type: "Flex",
75
+ props: {
76
+ justify: "end",
77
+ align: "center",
78
+ style: {
79
+ padding: "10px 20px",
80
+ borderBottom: "1px solid var(--border)",
81
+ },
82
+ },
83
+ children: [
84
+ {
85
+ type: "DropdownMenu",
86
+ props: {
87
+ items: [
88
+ {
89
+ label: "个人设置",
90
+ value: "settings",
91
+ icon: "settings",
92
+ },
93
+ { label: "退出登录", value: "logout", icon: "log-out" },
94
+ ],
95
+ },
96
+ events: {
97
+ onSelect: [
98
+ {
99
+ action: "if",
100
+ params: {
101
+ condition: "${event == 'logout'}",
102
+ then: [
103
+ {
104
+ action: "request",
105
+ params: {
106
+ method: "POST",
107
+ url: `${apiBase}/api/auth/logout`,
108
+ },
109
+ },
110
+ {
111
+ action: "app.reload",
112
+ params: { to: "/login" },
113
+ },
114
+ ],
115
+ else: [
116
+ {
117
+ action: "message",
118
+ params: { text: "演示环境未实现个人设置" },
119
+ },
120
+ ],
121
+ },
122
+ },
123
+ ],
124
+ },
125
+ children: [
126
+ {
127
+ type: "Button",
128
+ props: {
129
+ text: "${data.session.user.name}",
130
+ variant: "text",
131
+ },
132
+ },
133
+ ],
134
+ },
135
+ ],
136
+ },
137
+ {
138
+ type: "Flex",
139
+ props: {
140
+ direction: "vertical",
141
+ style: { flex: 1, minHeight: 0, overflow: "auto" },
142
+ },
143
+ children: [{ type: "PageOutlet" }],
144
+ },
145
+ ],
146
+ },
147
+ ],
148
+ },
149
+ },
150
+ login: {
151
+ dsl: "1.0",
152
+ body: {
153
+ type: "Flex",
154
+ props: {
155
+ direction: "vertical",
156
+ justify: "center",
157
+ align: "center",
158
+ style: { minHeight: "100vh", padding: 24 },
159
+ },
160
+ children: [
161
+ {
162
+ type: "Flex",
163
+ props: {
164
+ direction: "vertical",
165
+ style: {
166
+ width: 380,
167
+ padding: 24,
168
+ border: "1px solid var(--border)",
169
+ borderRadius: 12,
170
+ },
171
+ },
172
+ children: [{ type: "PageOutlet" }],
173
+ },
174
+ ],
175
+ },
176
+ },
177
+ blank: { dsl: "1.0", body: { type: "PageOutlet" } },
178
+ };
179
+
180
+ /* ---------------- 页面 DSL(内联页) ---------------- */
181
+
182
+ const loginPage: PageDsl = {
183
+ dsl: "1.0",
184
+ page: { id: "login", title: "登录" },
185
+ state: { submitting: false },
186
+ body: {
187
+ type: "Form",
188
+ events: {
189
+ onSubmit: [
190
+ { action: "setState", params: { submitting: true } },
191
+ {
192
+ id: "loginResult",
193
+ action: "request",
194
+ params: {
195
+ method: "POST",
196
+ url: `${apiBase}/api/auth/login`,
197
+ body: {
198
+ username: "${event.username}",
199
+ password: "${event.password}",
200
+ },
201
+ },
202
+ },
203
+ { action: "setState", params: { submitting: false } },
204
+ {
205
+ action: "if",
206
+ params: {
207
+ condition: "${actions.loginResult.token}",
208
+ then: [
209
+ // 登录成功整页重载:sitemap 端点按已登录会话重新裁剪内容
210
+ { action: "app.reload", params: { to: "/" } },
211
+ ],
212
+ else: [
213
+ {
214
+ action: "message",
215
+ params: {
216
+ messageType: "error",
217
+ text: "登录失败:用户名或密码错误",
218
+ },
219
+ },
220
+ ],
221
+ },
222
+ },
223
+ ],
224
+ },
225
+ children: [
226
+ {
227
+ type: "Alert",
228
+ props: {
229
+ variant: "info",
230
+ message: "演示账号 admin / pactor123,guest / guest123",
231
+ description:
232
+ "admin 可见「报表」菜单(report:view);guest 仅可见首页。登录后 sitemap 按角色重新下发。",
233
+ style: { marginBottom: 16 },
234
+ },
235
+ },
236
+ {
237
+ type: "Input",
238
+ props: {
239
+ name: "username",
240
+ label: "用户名",
241
+ placeholder: "请输入用户名",
242
+ rules: [{ required: true, message: "请输入用户名" }],
243
+ },
244
+ },
245
+ {
246
+ type: "Input",
247
+ props: {
248
+ name: "password",
249
+ label: "密码",
250
+ type: "password",
251
+ placeholder: "请输入密码",
252
+ rules: [{ required: true, message: "请输入密码" }],
253
+ },
254
+ },
255
+ {
256
+ type: "Flex",
257
+ props: { justify: "end" },
258
+ children: [
259
+ {
260
+ type: "Button",
261
+ props: {
262
+ text: "登 录",
263
+ variant: "primary",
264
+ submit: true,
265
+ loading: "${state.submitting}",
266
+ },
267
+ },
268
+ ],
269
+ },
270
+ ],
271
+ },
272
+ };
273
+
274
+ const registerPage: PageDsl = {
275
+ dsl: "1.0",
276
+ page: { id: "register", title: "注册" },
277
+ body: {
278
+ type: "Page",
279
+ props: { title: "注册" },
280
+ children: [
281
+ {
282
+ type: "Alert",
283
+ props: {
284
+ variant: "warning",
285
+ message: "演示环境不开放注册",
286
+ description: "请使用登录页的演示账号体验不同角色的菜单差异。",
287
+ style: { marginBottom: 16 },
288
+ },
289
+ },
290
+ {
291
+ type: "Button",
292
+ props: { text: "返回登录", variant: "primary" },
293
+ events: {
294
+ onClick: [{ action: "navigate", params: { to: "/login" } }],
295
+ },
296
+ },
297
+ ],
298
+ },
299
+ };
300
+
301
+ const homePage: PageDsl = {
302
+ dsl: "1.0",
303
+ page: { id: "home", title: "首页" },
304
+ body: {
305
+ type: "Page",
306
+ props: { title: "首页" },
307
+ children: [
308
+ {
309
+ type: "Alert",
310
+ props: {
311
+ variant: "info",
312
+ message: "欢迎回来,${data.session.user.name}!",
313
+ description:
314
+ "本页与外壳布局均由 sitemap 对象下发(design/dynamic-page.md);菜单随角色裁剪。",
315
+ style: { marginBottom: 16 },
316
+ },
317
+ },
318
+ {
319
+ type: "Button",
320
+ props: { text: "查看月报(需 report:view)" },
321
+ events: {
322
+ onClick: [
323
+ { action: "navigate", params: { to: "/reports/monthly" } },
324
+ ],
325
+ },
326
+ },
327
+ ],
328
+ },
329
+ };
330
+
331
+ const monthlyDetailPage: PageDsl = {
332
+ dsl: "1.0",
333
+ page: { id: "monthly-detail", title: "月报详情" },
334
+ body: {
335
+ type: "Page",
336
+ props: { title: "月报详情" },
337
+ children: [
338
+ {
339
+ type: "Alert",
340
+ props: {
341
+ variant: "info",
342
+ message: "路径参数演示:id = ${route.params.id}",
343
+ description: "隐藏页(不出现在菜单),经列表页跳转进入。",
344
+ style: { marginBottom: 16 },
345
+ },
346
+ },
347
+ {
348
+ type: "Button",
349
+ props: { text: "返回月报" },
350
+ events: {
351
+ onClick: [
352
+ { action: "navigate", params: { to: "/reports/monthly" } },
353
+ ],
354
+ },
355
+ },
356
+ ],
357
+ },
358
+ };
359
+
360
+ /** 懒加载页(pageDsl 不下发,客户端经 loadPage 按需获取) */
361
+ const lazyPages: Record<string, { version: number; pageDsl: PageDsl }> = {
362
+ monthly: {
363
+ version: 7,
364
+ pageDsl: {
365
+ dsl: "1.0",
366
+ page: { id: "monthly", title: "月报" },
367
+ body: {
368
+ type: "Page",
369
+ props: { title: "本月经营月报" },
370
+ children: [
371
+ {
372
+ type: "Flex",
373
+ props: { gap: 16, style: { marginBottom: 16 } },
374
+ children: [
375
+ {
376
+ type: "Card",
377
+ props: { title: "营收" },
378
+ children: [
379
+ {
380
+ type: "Statistic",
381
+ props: { title: "本月(万元)", value: 128.6 },
382
+ },
383
+ ],
384
+ },
385
+ {
386
+ type: "Card",
387
+ props: { title: "新增客户" },
388
+ children: [
389
+ { type: "Statistic", props: { title: "本月(家)", value: 342 } },
390
+ ],
391
+ },
392
+ {
393
+ type: "Card",
394
+ props: { title: "转化率" },
395
+ children: [
396
+ { type: "Statistic", props: { title: "本月(%)", value: 23.8 } },
397
+ ],
398
+ },
399
+ ],
400
+ },
401
+ {
402
+ type: "Alert",
403
+ props: {
404
+ variant: "success",
405
+ message: "本页 DSL 由 /api/pages/monthly 懒加载下发",
406
+ description:
407
+ "sitemap 中该页仅声明 id / path / version(monthly@7),点击进入时才请求页面 DSL。",
408
+ style: { marginBottom: 16 },
409
+ },
410
+ },
411
+ {
412
+ type: "Button",
413
+ props: { text: "查看详情(隐藏页 + 路径参数)" },
414
+ events: {
415
+ onClick: [
416
+ {
417
+ action: "navigate",
418
+ params: { to: "/reports/monthly/2026-09" },
419
+ },
420
+ ],
421
+ },
422
+ },
423
+ ],
424
+ },
425
+ },
426
+ },
427
+ };
428
+
429
+ /* ---------------- 页面 / 菜单定义 ---------------- */
430
+
431
+ function canViewReports(user: DemoUser | null): boolean {
432
+ return user !== null && user.permissions.includes("report:view");
433
+ }
434
+
435
+ /**
436
+ * 按会话构建 sitemap:同一 URL,不同用户拿到不同内容。
437
+ * 匿名 → 仅 public 页;guest → + 首页;admin → + 报表组。
438
+ */
439
+ export function buildSitemap(user: DemoUser | null): SitemapResource {
440
+ const reports = canViewReports(user);
441
+
442
+ const pages: SitemapResource["pages"] = [
443
+ { id: "login", path: "/login", title: "登录", version: 1, pageDsl: loginPage },
444
+ {
445
+ id: "register",
446
+ path: "/register",
447
+ title: "注册",
448
+ version: 1,
449
+ pageDsl: registerPage,
450
+ },
451
+ ];
452
+ if (user) {
453
+ pages.push({ id: "home", path: "/", title: "首页", version: 3, pageDsl: homePage });
454
+ }
455
+ if (reports) {
456
+ // 懒加载:不下发 pageDsl,仅 id / path / version(monthly@7)
457
+ pages.push({ id: "monthly", path: "/reports/monthly", title: "月报", version: 7 });
458
+ pages.push({
459
+ id: "monthly-detail",
460
+ path: "/reports/monthly/:id",
461
+ title: "月报详情",
462
+ version: 2,
463
+ pageDsl: monthlyDetailPage,
464
+ });
465
+ }
466
+
467
+ const menus: SitemapResource["menus"] = [
468
+ {
469
+ id: "login",
470
+ type: "page",
471
+ pageId: "login",
472
+ title: "登录",
473
+ layout: "login",
474
+ public: true,
475
+ hidden: true,
476
+ },
477
+ {
478
+ id: "register",
479
+ type: "page",
480
+ pageId: "register",
481
+ title: "注册",
482
+ layout: "login",
483
+ public: true,
484
+ hidden: true,
485
+ },
486
+ ];
487
+ if (user) {
488
+ // path / title 缺省继承自 pages[],仅导航元数据(icon / order / layout)
489
+ menus.push({ id: "home", type: "page", pageId: "home", title: "首页", icon: "home", layout: "admin", order: 1 });
490
+ }
491
+ if (reports) {
492
+ menus.push({
493
+ id: "reports",
494
+ type: "group",
495
+ title: "报表",
496
+ icon: "chart",
497
+ order: 2,
498
+ children: [
499
+ {
500
+ id: "monthly",
501
+ type: "page",
502
+ pageId: "monthly",
503
+ title: "报表中心",
504
+ icon: "chart",
505
+ layout: "admin",
506
+ permission: "report:view",
507
+ },
508
+ {
509
+ id: "monthly-detail",
510
+ type: "page",
511
+ pageId: "monthly-detail",
512
+ title: "月报详情",
513
+ layout: "admin",
514
+ hidden: true,
515
+ activeMenu: "monthly",
516
+ },
517
+ ],
518
+ });
519
+ menus.push({
520
+ id: "help",
521
+ type: "link",
522
+ title: "帮助",
523
+ path: "https://github.com/",
524
+ external: true,
525
+ order: 3,
526
+ });
527
+ }
528
+
529
+ return {
530
+ id: "site",
531
+ resourceType: "sitemap",
532
+ version: 12,
533
+ dslVersion: "1.0",
534
+ updatedAt: new Date().toISOString(),
535
+ app: { name: "动态页面 Starter" },
536
+ ...(user ? { user: { ...user }, permissions: user.permissions } : {}),
537
+ layout: "admin",
538
+ auth: { loginPath: "/login" },
539
+ layouts,
540
+ pages,
541
+ menus,
542
+ };
543
+ }
544
+
545
+ /** 懒加载页:校验会话与角色(前端权限只控制 UI,接口必须鉴权) */
546
+ export function getLazyPage(
547
+ pageId: string,
548
+ user: DemoUser | null,
549
+ ): { version: number; pageDsl: PageDsl } | null {
550
+ const entry = lazyPages[pageId];
551
+ if (!entry) {
552
+ return null;
553
+ }
554
+ if (pageId === "monthly" && !canViewReports(user)) {
555
+ return null;
556
+ }
557
+ return entry;
558
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }
@@ -0,0 +1,97 @@
1
+ # __APP_NAME__
2
+
3
+ `design/dynamic-page.md` 的纯前端(SPA)示例:Vite + React,无 Next.js 等
4
+ 宿主框架。与 `next-app-starter` 的关键差异——**不向 `SitemapApp` 传
5
+ `pathname` / `navigate`**,框架内置浏览器路由(History API 导航 +
6
+ popstate 前进后退)接管全部路由:
7
+
8
+ ```tsx
9
+ // src/pactor/App.tsx —— 全部装配
10
+ <SitemapApp
11
+ sources={sitemapSources} // sitemap 接口(相对路径)
12
+ baseUrl={apiBaseUrl} // 外部接口基准地址(相对路径补全)
13
+ pageEndpoint={pageEndpoint} // 懒加载页面端点
14
+ httpAdapter={httpHooks} // 框架 createHttpHooks:跨域改写 + 响应拆包
15
+ />
16
+ ```
17
+
18
+ 本模板不自带后端:接口复用 `next-app-starter` 的 mock API
19
+ (`apiBaseUrl` 缺省指向 `http://localhost:9011`,已开通 CORS)。
20
+
21
+ ## 快速开始
22
+
23
+ ```bash
24
+ # 1. 启动接口服务(mock API:sitemap / 页面懒加载 / 认证)
25
+ cd ../next-app-starter && pnpm install && pnpm dev # :9011
26
+
27
+ # 2. 启动本模板
28
+ cd ../spa-starter && pnpm install && pnpm dev # http://localhost:9012
29
+ ```
30
+
31
+ 演示账号:`admin / pactor123`(可见「报表」菜单,含懒加载月报页)、
32
+ `guest / guest123`(仅首页)。匿名访问自动跳转登录页(内置守卫)。
33
+
34
+ ## 目录结构
35
+
36
+ ```
37
+ src/
38
+ main.tsx # React 入口(导入 @pactor-app/ui/styles.css,必需)
39
+ pactor/
40
+ App.tsx # SitemapApp 装配(纯前端模式:零路由配置;HTTP 钩子用框架 createHttpHooks)
41
+ config.ts # sitemapSources / apiBaseUrl / pageEndpoint
42
+ vite.config.ts # react 插件(无代理:接口服务端已开通 CORS,直连)
43
+ ```
44
+
45
+ ## 纯前端模式说明
46
+
47
+ `SitemapApp` 两种路由模式(详见 `packages/app/README.md`):
48
+
49
+ - **纯前端 SPA(本模板)**:不传路由 props。内置浏览器路由处理导航、
50
+ 前进 / 后退、地址栏同步与 query 解析(`${route.query.x}` 可读);
51
+ 匿名守卫重定向同样走 `pushState`。
52
+ - **宿主路由(next-app-starter)**:注入 `pathname` / `navigate`,
53
+ 框架不持有路由。
54
+
55
+ 登录 / 登出后 sitemap 需按新会话重新下发,模板用框架内置 `app.reload`
56
+ 动作整页重载(`window.location.assign`),两种模式一致。登录 / 登出
57
+ 本身是普通 HTTP 调用,由服务端下发的 DSL 用内置 `request` 动作直接
58
+ 表达(见 next-app-starter 的 `src/server/sitemap.ts`),客户端零服务代码。
59
+
60
+ ## 跨域与 HTTP 钩子
61
+
62
+ 外部接口以 `apiBaseUrl`(`src/pactor/config.ts`,缺省
63
+ `http://localhost:9011`,即 next-app-starter 的 dev server)为基准:
64
+ `sitemapSources` / `pageEndpoint` 只写相对路径,由 `baseUrl` 补全为
65
+ 外部绝对地址。**假定服务端已开通 CORS,浏览器直连,无需项目内代理**——
66
+ 接口方(next-app-starter)已在路由层放开跨源并支持凭据
67
+ (Allow-Origin 回显 + Allow-Credentials + 预检响应),cookie 会话跨源
68
+ 可达(框架 fetch 均带 `credentials: 'include'`)。
69
+
70
+ **HTTP 钩子**(框架默认实现 `createHttpHooks`,`src/pactor/App.tsx`):
71
+ - `request`:缺省不改写地址(CORS 直连);服务端不开 CORS 时传
72
+ `proxyPrefix` 把跨域地址改写到项目内代理前缀(需自行配置
73
+ Vite `server.proxy` / nginx 转发);可注入公共头(Authorization 等)
74
+ - `response`:`{ code, data, message }` 包裹结构拆包(业务错误码抛错);
75
+ 后端包裹协议不同传 `envelope: false` 或自行实现 `PactorHttpHooks`
76
+
77
+ ## 接入真实后端
78
+
79
+ 1. `src/pactor/config.ts`:`sitemapSources` 改为你的接口 URL(可多个),
80
+ `apiBaseUrl` 改为接口基准地址。
81
+ 2. 服务端按 `SitemapResource`(自 `pactor-app` 导入类型)返回 JSON:
82
+ - `layouts`:DSL 布局(`PageOutlet` 为页面出口,`SitemapMenu` 渲染导航,
83
+ `${data.session.*}` 读会话数据)
84
+ - `pages`:页面定义(`pageDsl` 内联或缺省,`version` 参与缓存键 `id@version`)
85
+ - `menus`:导航 / 路由树(`pageId` 引用页面、`layout` 引用布局、
86
+ `public` 免登录;`path` / `title` 缺省从引用页面继承)
87
+ - `auth.loginPath`:未登录重定向目标
88
+ 3. 懒加载页:实现 `GET {pageEndpoint}/{id}` 返回 PageResource envelope
89
+ (缺省 `/api/pages/{id}`)。
90
+ 4. 登录 / 登出等接口调用直接在 DSL 中用内置 `request` 动作表达(URL
91
+ 指向你的真实接口);有客户端服务层时可用内置 `service.call` 动作
92
+ 桥接,`session.context` 会话桥由框架内置,均无需自行注册。
93
+
94
+ > 依赖说明:`SitemapApp` 于 `@pactor-app/app` v1.19.0 引入。本模板开发期间
95
+ > pactor 依赖经 `link:../../packages/*` 指向本仓;`v1.19.0` 发布到 npm 后
96
+ > 改回 `^1.19.0` 版本号即可独立使用。`react-router-dom` 是
97
+ > `@pactor-app/app` 的 peer 依赖(整应用模式需要),纯前端模式不会用到。
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>" />
7
+ <title>Pactor SPA Starter</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>