create-pactor-app 1.21.0 → 1.22.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/package.json +1 -1
- package/templates/next-app/README.md +5 -4
- package/templates/next-app/app/api/users/route.ts +57 -0
- package/templates/next-app/app/layout.tsx +2 -4
- package/templates/next-app/app/theme.css +111 -0
- package/templates/next-app/next.config.js +32 -0
- package/templates/next-app/package.json +10 -7
- package/templates/next-app/pnpm-workspace.yaml +7 -0
- package/templates/next-app/scripts/build.mjs +35 -0
- package/templates/next-app/src/pactor/config.ts +11 -8
- package/templates/next-app/src/server/accounts.ts +1 -1
- package/templates/next-app/src/server/sitemap.ts +113 -517
- package/templates/next-app/src/server/yaml/pages/home.page.yaml +23 -0
- package/templates/next-app/src/server/yaml/pages/login.page.yaml +69 -0
- package/templates/next-app/src/server/yaml/pages/monthly-detail.page.yaml +23 -0
- package/templates/next-app/src/server/yaml/pages/monthly.page.yaml +41 -0
- package/templates/next-app/src/server/yaml/pages/register.page.yaml +23 -0
- package/templates/next-app/src/server/yaml/pages/users.page.yaml +85 -0
- package/templates/next-app/src/server/yaml/sitemap.yaml +148 -0
- package/templates/spa/README.md +4 -5
- package/templates/spa/package.json +4 -4
- package/templates/spa/pnpm-workspace.yaml +6 -0
- package/templates/spa/src/pactor/config.ts +2 -6
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { PageDsl, PermissionConfig } from "@pactor-app/core";
|
|
5
|
+
import { parseDocument } from "yaml";
|
|
2
6
|
|
|
3
7
|
import type { LayoutDsl, SitemapResource } from "pactor-app";
|
|
4
8
|
|
|
@@ -8,551 +12,143 @@ import type { DemoUser } from "./accounts";
|
|
|
8
12
|
* Mock sitemap 数据源(design/dynamic-page.md 方案 B 示例实现)。
|
|
9
13
|
* 真实项目由后端接口返回同构对象——端点按会话裁剪:
|
|
10
14
|
* 匿名 → public 页(登录/注册);登录后 → 该角色可见的 menus / pages / permissions。
|
|
15
|
+
*
|
|
16
|
+
* 入口数据(元数据 / layouts / pages / menus 全量)在 yaml/sitemap.yaml,
|
|
17
|
+
* 页面 DSL 在 yaml/pages/<id>.page.yaml;本文件只负责:加载 YAML、
|
|
18
|
+
* 按会话裁剪可见性(public / permission / 需登录)、注入会话动态字段
|
|
19
|
+
* (updatedAt / user / permissions)。
|
|
11
20
|
*/
|
|
12
21
|
|
|
13
|
-
|
|
14
|
-
* 服务端下发的接口地址:DSL 中直接引用后端绝对地址(跨域),
|
|
15
|
-
* 客户端 httpHooks 自动改写进项目内代理前缀。登录 / 登出即普通 HTTP
|
|
16
|
-
* 调用,DSL 用内置 request 动作表达,无需客户端注册服务。
|
|
17
|
-
*/
|
|
18
|
-
const apiBase = "http://localhost:9011";
|
|
22
|
+
/* ---------------- YAML DSL 文件加载(src/server/yaml/,启动时读入内存) ---------------- */
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
/* 布局 body 内 ${data.session.*} 由客户端注入的 session dataSource 提供
|
|
22
|
-
*(user / app / menus);${route.params.pathname} 为当前路径(客户端注入)。 */
|
|
24
|
+
const YAML_DIR = join(process.cwd(), "src/server/yaml");
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
};
|
|
26
|
+
/** 解析 YAML 文件;语法错误时 fail-fast(parseDocument 默认静默容错,结构会悄悄残缺) */
|
|
27
|
+
function loadYaml<T>(file: string): T {
|
|
28
|
+
const doc = parseDocument(readFileSync(join(YAML_DIR, file), "utf8"));
|
|
29
|
+
if (doc.errors.length > 0) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`${file} 解析失败:${doc.errors.map((e) => e.message.split("\n")[0]).join(";")}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return doc.toJS() as T;
|
|
35
|
+
}
|
|
179
36
|
|
|
180
|
-
|
|
37
|
+
/** sitemap.yaml 中的页面声明:可见性标记(public / permission)+ DSL 加载方式(lazy) */
|
|
38
|
+
interface PageEntry {
|
|
39
|
+
id: string;
|
|
40
|
+
path: string;
|
|
41
|
+
title: string;
|
|
42
|
+
version: number;
|
|
43
|
+
public?: boolean;
|
|
44
|
+
permission?: string;
|
|
45
|
+
/** 不随 sitemap 内联 pageDsl,客户端经 /api/pages/{id} 懒加载 */
|
|
46
|
+
lazy?: boolean;
|
|
47
|
+
}
|
|
181
48
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
};
|
|
49
|
+
interface SitemapFile {
|
|
50
|
+
id: string;
|
|
51
|
+
resourceType: "sitemap";
|
|
52
|
+
version: number;
|
|
53
|
+
dslVersion: string;
|
|
54
|
+
app: { name: string };
|
|
55
|
+
layout: string;
|
|
56
|
+
auth: { loginPath: string };
|
|
57
|
+
layouts: Record<string, LayoutDsl>;
|
|
58
|
+
pages: PageEntry[];
|
|
59
|
+
menus: SitemapResource["menus"];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const site = loadYaml<SitemapFile>("sitemap.yaml");
|
|
273
63
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
};
|
|
64
|
+
/** 页面 DSL:按 id 约定从 pages/<id>.page.yaml 加载;lazy 页不在此内联 */
|
|
65
|
+
const pageDsls = new Map(
|
|
66
|
+
site.pages.filter((p) => !p.lazy).map((p) => [p.id, loadYaml<PageDsl>(`pages/${p.id}.page.yaml`)]),
|
|
67
|
+
);
|
|
68
|
+
const lazyPageDsls = new Map(
|
|
69
|
+
site.pages.filter((p) => p.lazy).map((p) => [p.id, loadYaml<PageDsl>(`pages/${p.id}.page.yaml`)]),
|
|
70
|
+
);
|
|
300
71
|
|
|
301
|
-
|
|
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
|
-
};
|
|
72
|
+
/* ---------------- 会话裁剪 ---------------- */
|
|
330
73
|
|
|
331
|
-
|
|
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
|
-
};
|
|
74
|
+
type Visibility = { public?: boolean; permission?: PermissionConfig };
|
|
359
75
|
|
|
360
|
-
/**
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
};
|
|
76
|
+
/** PermissionConfig 归一化为权限码数组(string / string[] / { code } 三种形态) */
|
|
77
|
+
function permissionCodes(permission: PermissionConfig): string[] {
|
|
78
|
+
const code =
|
|
79
|
+
typeof permission === "object" && !Array.isArray(permission)
|
|
80
|
+
? permission.code
|
|
81
|
+
: permission;
|
|
82
|
+
return Array.isArray(code) ? code : [code];
|
|
83
|
+
}
|
|
428
84
|
|
|
429
|
-
|
|
85
|
+
/** 可见性:public 匿名可见;permission 命中任一权限码;无标记需登录 */
|
|
86
|
+
function canSee(entry: Visibility, user: DemoUser | null): boolean {
|
|
87
|
+
if (entry.public) return true;
|
|
88
|
+
if (!user) return false;
|
|
89
|
+
if (entry.permission) {
|
|
90
|
+
return permissionCodes(entry.permission).some((c) =>
|
|
91
|
+
user.permissions.includes(c),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
430
96
|
|
|
431
|
-
|
|
432
|
-
|
|
97
|
+
/** 菜单按会话过滤;group 无可见子项时整组隐藏 */
|
|
98
|
+
function filterMenus(
|
|
99
|
+
menus: SitemapResource["menus"],
|
|
100
|
+
user: DemoUser | null,
|
|
101
|
+
): SitemapResource["menus"] {
|
|
102
|
+
const out: SitemapResource["menus"] = [];
|
|
103
|
+
for (const item of menus) {
|
|
104
|
+
if (!canSee(item, user)) continue;
|
|
105
|
+
if (item.type === "group") {
|
|
106
|
+
const children = filterMenus(item.children ?? [], user);
|
|
107
|
+
if (children.length === 0) continue;
|
|
108
|
+
out.push({ ...item, children });
|
|
109
|
+
} else {
|
|
110
|
+
out.push(item);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
433
114
|
}
|
|
434
115
|
|
|
435
116
|
/**
|
|
436
117
|
* 按会话构建 sitemap:同一 URL,不同用户拿到不同内容。
|
|
437
|
-
* 匿名 → 仅 public 页;guest → + 首页;admin → + 报表组。
|
|
118
|
+
* 匿名 → 仅 public 页;guest → + 首页;admin → + 用户列表 / 报表组。
|
|
438
119
|
*/
|
|
439
120
|
export function buildSitemap(user: DemoUser | null): SitemapResource {
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
121
|
+
const pages: SitemapResource["pages"] = site.pages
|
|
122
|
+
.filter((p) => canSee(p, user))
|
|
123
|
+
.map((p) => ({
|
|
124
|
+
id: p.id,
|
|
125
|
+
path: p.path,
|
|
126
|
+
title: p.title,
|
|
127
|
+
version: p.version,
|
|
128
|
+
// 懒加载页不下发 pageDsl,仅 id / path / version,经 /api/pages/{id} 按需获取
|
|
129
|
+
...(p.lazy ? {} : { pageDsl: pageDsls.get(p.id) }),
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
// sitemap.yaml 原样透传,仅覆盖会话相关部分:pages / menus 按可见性过滤,
|
|
133
|
+
// updatedAt / user / permissions 按会话注入
|
|
529
134
|
return {
|
|
530
|
-
|
|
531
|
-
resourceType: "sitemap",
|
|
532
|
-
version: 12,
|
|
533
|
-
dslVersion: "1.0",
|
|
135
|
+
...site,
|
|
534
136
|
updatedAt: new Date().toISOString(),
|
|
535
|
-
app: { name: "动态页面 Starter" },
|
|
536
137
|
...(user ? { user: { ...user }, permissions: user.permissions } : {}),
|
|
537
|
-
layout: "admin",
|
|
538
|
-
auth: { loginPath: "/login" },
|
|
539
|
-
layouts,
|
|
540
138
|
pages,
|
|
541
|
-
menus,
|
|
139
|
+
menus: filterMenus(site.menus, user),
|
|
542
140
|
};
|
|
543
141
|
}
|
|
544
142
|
|
|
545
|
-
/**
|
|
143
|
+
/** 懒加载页:校验会话与可见性(前端权限只控制 UI,接口必须鉴权) */
|
|
546
144
|
export function getLazyPage(
|
|
547
145
|
pageId: string,
|
|
548
146
|
user: DemoUser | null,
|
|
549
147
|
): { version: number; pageDsl: PageDsl } | null {
|
|
550
|
-
const entry =
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
}
|
|
554
|
-
if (pageId === "monthly" && !canViewReports(user)) {
|
|
148
|
+
const entry = site.pages.find((p) => p.id === pageId);
|
|
149
|
+
const pageDsl = lazyPageDsls.get(pageId);
|
|
150
|
+
if (!entry?.lazy || !pageDsl || !canSee(entry, user)) {
|
|
555
151
|
return null;
|
|
556
152
|
}
|
|
557
|
-
return entry;
|
|
153
|
+
return { version: entry.version, pageDsl };
|
|
558
154
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# 首页 DSL:内联在 sitemap 中下发(需登录)。
|
|
2
|
+
# ${data.session.user.name} 由客户端注入的 session dataSource 提供。
|
|
3
|
+
dsl: "1.0"
|
|
4
|
+
page:
|
|
5
|
+
id: home
|
|
6
|
+
title: 首页
|
|
7
|
+
|
|
8
|
+
body:
|
|
9
|
+
type: Page
|
|
10
|
+
props: { title: 首页 }
|
|
11
|
+
children:
|
|
12
|
+
- type: Alert
|
|
13
|
+
props:
|
|
14
|
+
variant: info
|
|
15
|
+
message: 欢迎回来,${data.session.user.name}!
|
|
16
|
+
description: 本页与外壳布局均由 sitemap 对象下发(design/dynamic-page.md);菜单随角色裁剪。
|
|
17
|
+
style: { marginBottom: 16 }
|
|
18
|
+
- type: Button
|
|
19
|
+
props: { text: 查看月报(需 report:view) }
|
|
20
|
+
events:
|
|
21
|
+
onClick:
|
|
22
|
+
- action: navigate
|
|
23
|
+
params: { to: /reports/monthly }
|