@pactor-app/skills 1.24.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/LICENSE +21 -0
- package/README.md +40 -0
- package/package.json +26 -0
- package/pactor/SKILL.md +373 -0
- package/pactor/references/dsl-authoring.md +448 -0
- package/prd-to-app/SKILL.md +75 -0
- package/prd-to-app/references/acceptance-checklist.md +47 -0
- package/prd-to-app/references/component-mapping.md +41 -0
- package/prd-to-app/references/mock-data-routes.md +66 -0
- package/prd-to-app/references/page-templates.md +274 -0
- package/prd-to-app/references/project-structure.md +89 -0
- package/prd-to-app/references/theme-and-style.md +41 -0
- package/prd-to-app/template/README.md +26 -0
- package/prd-to-app/template/app/[[...slug]]/page.tsx +7 -0
- package/prd-to-app/template/app/api/auth/login/route.ts +32 -0
- package/prd-to-app/template/app/api/auth/logout/route.ts +22 -0
- package/prd-to-app/template/app/api/data/[...parts]/route.ts +118 -0
- package/prd-to-app/template/app/api/public/pages/[id]/route.ts +40 -0
- package/prd-to-app/template/app/api/sitemap/route.ts +21 -0
- package/prd-to-app/template/app/api/users/route.ts +57 -0
- package/prd-to-app/template/app/favicon.ico +0 -0
- package/prd-to-app/template/app/globals.css +248 -0
- package/prd-to-app/template/app/layout.tsx +49 -0
- package/prd-to-app/template/app/login.css +122 -0
- package/prd-to-app/template/eslint.config.mjs +18 -0
- package/prd-to-app/template/next-env.d.ts +7 -0
- package/prd-to-app/template/next.config.js +32 -0
- package/prd-to-app/template/package.json +34 -0
- package/prd-to-app/template/pnpm-lock.yaml +6854 -0
- package/prd-to-app/template/pnpm-workspace.yaml +7 -0
- package/prd-to-app/template/postcss.config.mjs +7 -0
- package/prd-to-app/template/public/file.svg +1 -0
- package/prd-to-app/template/public/globe.svg +1 -0
- package/prd-to-app/template/public/next.svg +1 -0
- package/prd-to-app/template/public/skins/chenwunaifu.css +111 -0
- package/prd-to-app/template/public/skins/taijingshenlin.css +109 -0
- package/prd-to-app/template/public/skins/xuanzhimoyun.css +110 -0
- package/prd-to-app/template/public/vercel.svg +1 -0
- package/prd-to-app/template/public/window.svg +1 -0
- package/prd-to-app/template/scripts/build.mjs +35 -0
- package/prd-to-app/template/src/pactor/App.tsx +55 -0
- package/prd-to-app/template/src/pactor/actions.ts +438 -0
- package/prd-to-app/template/src/pactor/components/card-links.tsx +86 -0
- package/prd-to-app/template/src/pactor/components/chip-filter.tsx +89 -0
- package/prd-to-app/template/src/pactor/components/chip-node.tsx +46 -0
- package/prd-to-app/template/src/pactor/components/debug-chat.tsx +189 -0
- package/prd-to-app/template/src/pactor/components/index.ts +28 -0
- package/prd-to-app/template/src/pactor/components/log-status.tsx +64 -0
- package/prd-to-app/template/src/pactor/components/status-badge.tsx +46 -0
- package/prd-to-app/template/src/pactor/components/theme-switcher.tsx +136 -0
- package/prd-to-app/template/src/pactor/config.ts +28 -0
- package/prd-to-app/template/src/pactor/mocks/audit-mock.ts +403 -0
- package/prd-to-app/template/src/pactor/mocks/call-logs-mock.ts +875 -0
- package/prd-to-app/template/src/pactor/mocks/client-debug-mock.ts +188 -0
- package/prd-to-app/template/src/pactor/mocks/client-templates-mock.ts +497 -0
- package/prd-to-app/template/src/pactor/mocks/clients-mock.ts +483 -0
- package/prd-to-app/template/src/pactor/mocks/costs-mock.ts +406 -0
- package/prd-to-app/template/src/pactor/mocks/dashboard-mock.ts +192 -0
- package/prd-to-app/template/src/pactor/mocks/model-debug-mock.ts +144 -0
- package/prd-to-app/template/src/pactor/mocks/models-mock.ts +799 -0
- package/prd-to-app/template/src/pactor/mocks/ops-usage-mock.ts +806 -0
- package/prd-to-app/template/src/pactor/mocks/providers-mock.ts +424 -0
- package/prd-to-app/template/src/pactor/mocks/routes-mock.ts +604 -0
- package/prd-to-app/template/src/pactor/services.ts +185 -0
- package/prd-to-app/template/src/pactor/skin.ts +52 -0
- package/prd-to-app/template/src/server/accounts.ts +63 -0
- package/prd-to-app/template/src/server/cors.ts +34 -0
- package/prd-to-app/template/src/server/sitemap.ts +176 -0
- package/prd-to-app/template/src/server/yaml/common/dashboard.yaml +498 -0
- package/prd-to-app/template/src/server/yaml/common/login.yaml +84 -0
- package/prd-to-app/template/src/server/yaml/provider/provider-create.yaml +340 -0
- package/prd-to-app/template/src/server/yaml/provider/provider-detail.yaml +780 -0
- package/prd-to-app/template/src/server/yaml/provider/provider-edit.yaml +366 -0
- package/prd-to-app/template/src/server/yaml/provider/provider-list.yaml +1989 -0
- package/prd-to-app/template/src/server/yaml/route/route-create.yaml +511 -0
- package/prd-to-app/template/src/server/yaml/route/route-detail.yaml +610 -0
- package/prd-to-app/template/src/server/yaml/route/route-edit.yaml +809 -0
- package/prd-to-app/template/src/server/yaml/route/route-list.yaml +1793 -0
- package/prd-to-app/template/src/server/yaml/sitemap.yaml +250 -0
- package/prd-to-app/template/tsconfig.json +34 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# mock 数据路由模板
|
|
2
|
+
|
|
3
|
+
页面 DSL 的 http 数据源统一入口:`app/api/data/[...path]/route.ts`。目标:原型阶段零后端依赖;接真实后端时只改这一层,页面 yaml 不动。
|
|
4
|
+
|
|
5
|
+
## 命名约定
|
|
6
|
+
|
|
7
|
+
- **单段路径**:`/api/data/projects`、`/api/data/project-members`——列表/选项类一律单段 kebab-case
|
|
8
|
+
- **禁止** `/api/data/project/members` 这种两段写法:`[...path]` 会把第二段当详情 id 解析(`project/detail?id=members`),踩过的坑
|
|
9
|
+
- 详情:`/api/data/<resource>/<code|id>`;子资源:`/api/data/<resource>/<code>/<sub>`
|
|
10
|
+
- 数据源 url 与页面 id 语义对应:`project-list` 页 → `/api/data/projects`
|
|
11
|
+
|
|
12
|
+
## 路由模板
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// app/api/data/[...path]/route.ts
|
|
16
|
+
/**
|
|
17
|
+
* 页面数据接口(mock):页面 DSL 的 http 数据源统一入口。
|
|
18
|
+
* 接入真实后端时改写 ROUTE_TABLE 的实现(或调整 baseUrl 直连后端)。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** 单段路径 → mock handler */
|
|
22
|
+
const ROUTE_TABLE: Record<string, (params: Record<string, unknown>) => unknown> = {
|
|
23
|
+
projects: (params) => filterProjects(params), // mock 函数就近定义或放 src/server/mocks/
|
|
24
|
+
"project-members": () => PROJECT_MEMBERS,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** 详情:<resource>/<id> */
|
|
28
|
+
const DETAIL_TABLE: Record<string, (id: string) => unknown> = {
|
|
29
|
+
projects: (code) => PROJECTS.find((p) => p.code === code) ?? null,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export async function GET(
|
|
33
|
+
request: Request,
|
|
34
|
+
context: { params: Promise<{ path: string[] }> },
|
|
35
|
+
) {
|
|
36
|
+
const { path } = await context.params;
|
|
37
|
+
const params = Object.fromEntries(new URL(request.url).searchParams.entries());
|
|
38
|
+
|
|
39
|
+
if (path.length === 1) {
|
|
40
|
+
const handler = ROUTE_TABLE[path[0]!];
|
|
41
|
+
if (!handler) return Response.json({ error: `未知数据接口: /api/data/${path.join("/")}` }, { status: 404 });
|
|
42
|
+
return Response.json(handler(params) ?? null);
|
|
43
|
+
}
|
|
44
|
+
if (path.length === 2) {
|
|
45
|
+
const handler = DETAIL_TABLE[path[0]!];
|
|
46
|
+
if (!handler) return Response.json({ error: "未知详情接口" }, { status: 404 });
|
|
47
|
+
return Response.json(handler(path[1]!) ?? null);
|
|
48
|
+
}
|
|
49
|
+
return Response.json({ error: "不支持的接口路径" }, { status: 404 });
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
注意:`context.params` 的具体类型以目标项目的 Next 版本为准(Next 15+ 为 Promise)。
|
|
54
|
+
|
|
55
|
+
## mock 数据要求
|
|
56
|
+
|
|
57
|
+
- 字段名与线框图「区域说明/字段说明」逐一对齐,不臆造字段
|
|
58
|
+
- 每类数据至少覆盖:正常多条、触发告警/特殊态的条目(如「待填密钥」)、以及空态可被演示(如筛选到无结果)
|
|
59
|
+
- 筛选参数在 handler 内真实过滤(列表页筛选闭环依赖它),不是返回全量
|
|
60
|
+
- 选项类接口(如下拉的操作人列表)单独注册单段路径,页面 `options: ${data.xxx}` 绑定
|
|
61
|
+
|
|
62
|
+
## 替换为真实接口
|
|
63
|
+
|
|
64
|
+
1. 直接把 ROUTE_TABLE handler 改为 `fetch` 真实后端(服务端转发,带鉴权头)
|
|
65
|
+
2. 或调整 pactor 数据源 baseUrl 直连后端,`/api/data/*` 层整体退役
|
|
66
|
+
页面 yaml 的 dataSources 声明两种方案下都无需改动——这就是必须 `type: http` 的原因。
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# 页面 YAML 模板
|
|
2
|
+
|
|
3
|
+
三类常用页面模板。占位符:`{{kebab-id}}` 页面 id、`{{页面标题}}`、`{{模块名}}`、`{{resource}}` 数据接口名(单段,见 mock-data-routes.md)。DSL 语法细节以 pactor skill 与 packages/ui 当前导出为准。
|
|
4
|
+
|
|
5
|
+
## 列表页(筛选 + 表格/卡片 + 操作列)
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
dsl: "1.0"
|
|
9
|
+
page:
|
|
10
|
+
id: {{kebab-id}}
|
|
11
|
+
title: {{页面标题}}
|
|
12
|
+
name: {{camelName}}
|
|
13
|
+
description: {{模块名}} / {{页面标题}}
|
|
14
|
+
state:
|
|
15
|
+
keyword: ""
|
|
16
|
+
status: ""
|
|
17
|
+
statusChip: all
|
|
18
|
+
dataSources:
|
|
19
|
+
list:
|
|
20
|
+
type: http
|
|
21
|
+
autoLoad: true
|
|
22
|
+
request:
|
|
23
|
+
method: GET
|
|
24
|
+
url: /api/data/{{resource}}
|
|
25
|
+
params:
|
|
26
|
+
keyword: ${state.keyword}
|
|
27
|
+
status: ${state.status}
|
|
28
|
+
body:
|
|
29
|
+
type: Page
|
|
30
|
+
children:
|
|
31
|
+
- type: Breadcrumb
|
|
32
|
+
props:
|
|
33
|
+
items:
|
|
34
|
+
- label: {{模块名}}
|
|
35
|
+
- label: {{页面标题}}
|
|
36
|
+
- type: Flex
|
|
37
|
+
props: { align: baseline, gap: middle }
|
|
38
|
+
children:
|
|
39
|
+
- type: Typography
|
|
40
|
+
props: { variant: h3, text: {{页面标题}} }
|
|
41
|
+
- type: Typography
|
|
42
|
+
props: { variant: muted, text: 页面副标题(取自线框图说明文字) }
|
|
43
|
+
# 搜索栏:Form(layout: inline) 包裹,字段 name 绑定;查询 submit: true,重置 form.setValues
|
|
44
|
+
- type: Card
|
|
45
|
+
children:
|
|
46
|
+
- type: Form
|
|
47
|
+
props: { name: filter, layout: inline }
|
|
48
|
+
children:
|
|
49
|
+
- type: Input
|
|
50
|
+
props: { name: keyword, placeholder: 搜索关键词 }
|
|
51
|
+
- type: Select
|
|
52
|
+
props:
|
|
53
|
+
name: status
|
|
54
|
+
placeholder: 状态
|
|
55
|
+
options: ${data.list.filters.status} # 选项也可由接口下发
|
|
56
|
+
allowClear: true
|
|
57
|
+
- type: Button
|
|
58
|
+
props: { text: 查询, variant: primary, submit: true }
|
|
59
|
+
- type: Button
|
|
60
|
+
props: { text: 重置 }
|
|
61
|
+
events:
|
|
62
|
+
onClick:
|
|
63
|
+
- action: form.setValues
|
|
64
|
+
params: { form: filter, values: { keyword: "", status: "" } }
|
|
65
|
+
- action: setState
|
|
66
|
+
params: { keyword: "", status: "" }
|
|
67
|
+
- action: reload
|
|
68
|
+
params: { dataSource: list }
|
|
69
|
+
events:
|
|
70
|
+
onSubmit:
|
|
71
|
+
- action: setState
|
|
72
|
+
params:
|
|
73
|
+
keyword: ${event.keyword}
|
|
74
|
+
status: ${event.status}
|
|
75
|
+
- action: reload
|
|
76
|
+
params: { dataSource: list }
|
|
77
|
+
# 即点即生效的快筛行(ChipFilter)不是表单,可独立于 Form 之外绑 state
|
|
78
|
+
- type: Flex
|
|
79
|
+
props: { align: center, gap: small }
|
|
80
|
+
children:
|
|
81
|
+
- type: Text
|
|
82
|
+
props: { text: 状态, strong: true }
|
|
83
|
+
- type: ChipFilter
|
|
84
|
+
props:
|
|
85
|
+
mode: single
|
|
86
|
+
value: ${state.statusChip}
|
|
87
|
+
items: ${data.list.filters.status}
|
|
88
|
+
events:
|
|
89
|
+
onChange:
|
|
90
|
+
- action: setState
|
|
91
|
+
params: { statusChip: ${event} }
|
|
92
|
+
- action: reload
|
|
93
|
+
params: { dataSource: list }
|
|
94
|
+
# 列表主体:表格(或卡片网格,见 component-mapping.md)
|
|
95
|
+
- type: Table
|
|
96
|
+
props:
|
|
97
|
+
columns: ${data.list.columns}
|
|
98
|
+
dataSource: ${data.list.items}
|
|
99
|
+
events:
|
|
100
|
+
onRowClick:
|
|
101
|
+
- action: router.push
|
|
102
|
+
params: { path: "/{{resource}}/${event.code}" }
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
要点:
|
|
106
|
+
|
|
107
|
+
- 搜索栏(关键词 + 下拉筛选 + 查询/重置)走 `Form layout: inline`:字段 `name` 绑定,查询按钮 `submit: true`,`onSubmit` 中 `setState` + `reload`;重置 = `form.setValues` 清空 + `setState` 复位 + `reload`
|
|
108
|
+
- 即点即生效的筛选(ChipFilter 快筛行)没有提交语义,不是表单,仍可独立绑 state、onChange 即 reload
|
|
109
|
+
- 表格操作列按钮样式统一 `Button variant: link`(编辑/删除/详情等),不混用 text/default 或 TextLink
|
|
110
|
+
- 「新建」→ `router.push` 到表单页(或弹窗表单);操作列「编辑」与「新建」指向**同一个表单**(同一路由页或同一弹窗),唯一区别是编辑以行数据预填:路由页用 `initialValues: ${data.detail}`,弹窗/内嵌面板用 `form.setValues` 写入行数据
|
|
111
|
+
- 空态、加载态不手写,组件内置;但线框图有空态文案时用 props 覆盖
|
|
112
|
+
|
|
113
|
+
## 详情页(面包屑 + 描述列表 + Tab/分区)
|
|
114
|
+
|
|
115
|
+
```yaml
|
|
116
|
+
dsl: "1.0"
|
|
117
|
+
page:
|
|
118
|
+
id: {{kebab-id}}
|
|
119
|
+
title: {{页面标题}}
|
|
120
|
+
name: {{camelName}}
|
|
121
|
+
dataSources:
|
|
122
|
+
detail:
|
|
123
|
+
type: http
|
|
124
|
+
autoLoad: true
|
|
125
|
+
request:
|
|
126
|
+
method: GET
|
|
127
|
+
url: /api/data/{{resource}}/${route.params.code} # 路径参数经 route 注入
|
|
128
|
+
body:
|
|
129
|
+
type: Page
|
|
130
|
+
children:
|
|
131
|
+
- type: Breadcrumb
|
|
132
|
+
props:
|
|
133
|
+
items:
|
|
134
|
+
- label: {{模块名}}
|
|
135
|
+
- { label: {{列表页标题}}, href: /{{resource}} }
|
|
136
|
+
- label: ${data.detail.name}
|
|
137
|
+
- type: Descriptions
|
|
138
|
+
props:
|
|
139
|
+
title: 基本信息
|
|
140
|
+
items: ${data.detail.fields}
|
|
141
|
+
# 线框图为 Tab 时用 Tabs;为纵向分区时用多个 Card + Descriptions 顺序排布
|
|
142
|
+
- type: Tabs
|
|
143
|
+
props:
|
|
144
|
+
items:
|
|
145
|
+
- { key: overview, label: 概览 }
|
|
146
|
+
- { key: changes, label: 变更记录 }
|
|
147
|
+
children: [] # 各 Tab 内容按线框图分区填充
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
要点:
|
|
151
|
+
|
|
152
|
+
- 路径参数(`/projects/:code`)经 `${route.params.code}` 拼进 url
|
|
153
|
+
- 头部操作(启用/停用、编辑、删除)按线框图摆放在标题行右侧,点击后确认弹窗 + 操作成功 message + reload
|
|
154
|
+
|
|
155
|
+
## 表单页(新增/编辑:校验 + 提交闭环)
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
dsl: "1.0"
|
|
159
|
+
page:
|
|
160
|
+
id: {{kebab-id}}
|
|
161
|
+
title: {{页面标题}}
|
|
162
|
+
name: {{camelName}}
|
|
163
|
+
state:
|
|
164
|
+
submitting: false
|
|
165
|
+
dataSources:
|
|
166
|
+
detail: # 编辑页预填;新增页删除本数据源
|
|
167
|
+
type: http
|
|
168
|
+
autoLoad: true
|
|
169
|
+
request:
|
|
170
|
+
method: GET
|
|
171
|
+
url: /api/data/{{resource}}/${route.params.code}
|
|
172
|
+
body:
|
|
173
|
+
type: Page
|
|
174
|
+
children:
|
|
175
|
+
- type: Breadcrumb
|
|
176
|
+
props:
|
|
177
|
+
items:
|
|
178
|
+
- label: {{模块名}}
|
|
179
|
+
- { label: {{列表页标题}}, href: /{{resource}} }
|
|
180
|
+
- label: {{页面标题}}
|
|
181
|
+
- type: Form
|
|
182
|
+
props:
|
|
183
|
+
initialValues: ${data.detail} # 新增页省略
|
|
184
|
+
children:
|
|
185
|
+
# 字段按线框图分区;required / 校验规则照抄线框图字段说明
|
|
186
|
+
- type: Input
|
|
187
|
+
props: { name: name, label: 名称, required: true }
|
|
188
|
+
events:
|
|
189
|
+
onSubmit:
|
|
190
|
+
- action: setState
|
|
191
|
+
params: { submitting: true }
|
|
192
|
+
- action: http.post # mock 阶段可指向 /api/data/{{resource}}
|
|
193
|
+
params:
|
|
194
|
+
url: /api/data/{{resource}}
|
|
195
|
+
body: ${event}
|
|
196
|
+
- action: message
|
|
197
|
+
params: { messageType: success, text: 保存成功 }
|
|
198
|
+
- action: router.push
|
|
199
|
+
params: { path: /{{resource}} }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
要点:
|
|
203
|
+
|
|
204
|
+
- 提交中禁用按钮(`submitting` state),失败分支 `messageType: error` 提示
|
|
205
|
+
- 新增/编辑**共用同一表单**(同一路由页或同一弹窗),按 `${route.params.code}` 是否存在区分(`if` 动作分支);编辑仅比新建多一步预填:`initialValues: ${data.detail}`
|
|
206
|
+
- 取消按钮 → `router.back` 或返回列表页
|
|
207
|
+
|
|
208
|
+
## 内嵌表单面板(列表/树旁的编辑表单、弹窗/抽屉表单)
|
|
209
|
+
|
|
210
|
+
凡「字段 + 校验 + 提交」结构(如树右侧的配置面板、行编辑弹窗)一律 `Form` 包裹,不要散放 `Input` + `setState` 攒草稿:
|
|
211
|
+
|
|
212
|
+
```yaml
|
|
213
|
+
- type: Card
|
|
214
|
+
props: { title: {{面板标题}} }
|
|
215
|
+
children:
|
|
216
|
+
- type: Form
|
|
217
|
+
props:
|
|
218
|
+
name: edit # 多表单共存时命名,form.setValues 按名寻址
|
|
219
|
+
children:
|
|
220
|
+
- type: Input
|
|
221
|
+
props:
|
|
222
|
+
name: name
|
|
223
|
+
label: 名称
|
|
224
|
+
rules:
|
|
225
|
+
- { required: true, message: 请输入名称 }
|
|
226
|
+
- type: RadioGroup
|
|
227
|
+
props:
|
|
228
|
+
name: type
|
|
229
|
+
label: 类型
|
|
230
|
+
options:
|
|
231
|
+
- { label: 目录, value: DIRECTORY }
|
|
232
|
+
- { label: 页面, value: PAGE }
|
|
233
|
+
# 条件字段显隐依赖其他字段的实时值:经 onValuesChange 镜像到 state 后用 when
|
|
234
|
+
- type: Select
|
|
235
|
+
when: ${state.fType == 'PAGE'}
|
|
236
|
+
props:
|
|
237
|
+
name: pageEntry
|
|
238
|
+
label: 页面入口
|
|
239
|
+
options: ${data.pages}
|
|
240
|
+
rules:
|
|
241
|
+
- { required: true, message: 页面入口必选 }
|
|
242
|
+
- type: Flex
|
|
243
|
+
props: { justify: end, gap: middle }
|
|
244
|
+
children:
|
|
245
|
+
- type: Button
|
|
246
|
+
props: { text: 取消 }
|
|
247
|
+
events:
|
|
248
|
+
onClick:
|
|
249
|
+
- action: form.setValues # 重置为初始/空值
|
|
250
|
+
params: { form: edit, values: { name: "", type: DIRECTORY } }
|
|
251
|
+
- type: Button
|
|
252
|
+
props: { text: 保存, variant: primary, submit: true }
|
|
253
|
+
events:
|
|
254
|
+
onValuesChange: # event 为本次变化的字段集;镜像到 state 供 when 条件显隐(其他字段变化时保持原值)
|
|
255
|
+
- action: setState
|
|
256
|
+
params: { fType: "${event.type != null ? event.type : state.fType}" }
|
|
257
|
+
onSubmit: # 仅在校验通过后触发,values 经 ${event.<field>} 读取
|
|
258
|
+
- action: message
|
|
259
|
+
params: { messageType: success, text: 已保存 }
|
|
260
|
+
- action: reload
|
|
261
|
+
params: { dataSource: list }
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
要点:
|
|
265
|
+
|
|
266
|
+
- 点击列表行 / 树节点回填表单:`onClick` 里用 `form.setValues` 动作(`params: { form: <表单名>, values: {...} }`),不要把字段 `value` 绑到 `${state.x}` 再用 setState 同步
|
|
267
|
+
- 校验交给字段 `rules`,提交按钮 `submit: true`;校验失败由 Form 就地展示错误,不再写 `if` + `message` 手工校验链
|
|
268
|
+
- 提交参数是整表 values:`${event}` 或 `${event.<field>}`;提交成功后 values 也可经 `${form.<field>}` 读取
|
|
269
|
+
|
|
270
|
+
## 通用约定
|
|
271
|
+
|
|
272
|
+
- 页面 id kebab-case,与 sitemap.yaml 注册一致;文件按模块分目录存放 `src/server/yaml/<模块>/{{kebab-id}}.yaml`(见 project-structure.md)
|
|
273
|
+
- 表达式:`${state.x}` / `${data.x}` / `${route.params.x}` / `${event}`;支持 `.length`、比较、`&&`/`||`、三元
|
|
274
|
+
- 任何「点击后请求并刷新」的交互都是同一闭环:`setState`(可选)→ http/service 调用 → `reload dataSource` / `router.push`
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# 目标项目目录结构约定
|
|
2
|
+
|
|
3
|
+
基于 `pnpm create pactor-app`(next-app 模板)扩展。加 ✅ 的是模板自带,➕ 的是本流程新增/改造。
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
<project>/
|
|
7
|
+
├── app/
|
|
8
|
+
│ ├── [[...slug]]/ ✅ pactor 动态路由承载页(不改)
|
|
9
|
+
│ ├── api/
|
|
10
|
+
│ │ ├── sitemap/route.ts ✅ 返回 sitemap 入口数据(由 src/server/sitemap.ts 组装)
|
|
11
|
+
│ │ ├── public/pages/[id]/route.ts ➕ 懒加载页面端点,返回单页 envelope(id/resourceType/version/dslVersion/schema/updatedAt)
|
|
12
|
+
│ │ ├── auth/ · users/ ✅ 模板自带(演示账号,按需保留)
|
|
13
|
+
│ │ └── data/[...path]/route.ts ➕ mock 数据统一入口(见 mock-data-routes.md)
|
|
14
|
+
│ ├── globals.css ✅ 全局样式(皮肤导入处)
|
|
15
|
+
│ ├── layout.tsx ✅ 根布局(自定义页面 CSS 在这里挂载)
|
|
16
|
+
│ └── theme.css ✅ 主题令牌(样式移植的主战场)
|
|
17
|
+
├── src/
|
|
18
|
+
│ ├── pactor/
|
|
19
|
+
│ │ ├── config.ts ✅ pactor 运行时配置(sitemapSources / baseUrl / pageEndpoint)
|
|
20
|
+
│ │ └── App.tsx ✅ 客户端入口(SitemapApp 接线、注册自定义组件/动作处)
|
|
21
|
+
│ └── server/
|
|
22
|
+
│ ├── sitemap.ts ➕ 递归收集 yaml/ 模块子目录建 id→路径索引、按会话裁剪(只加过滤,不拼接业务字段)
|
|
23
|
+
│ └── yaml/
|
|
24
|
+
│ ├── sitemap.yaml ✅ 布局 DSL:app / auth / layouts / pages / menus
|
|
25
|
+
│ └── <模块>/ ➕ 页面按 PRD 模块分目录存放(如 user/ order/ project/)
|
|
26
|
+
│ └── <id>.yaml 每页一个文件,页面 id = 文件名(不含 .yaml)
|
|
27
|
+
└── package.json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## sitemap.yaml 骨架
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
# 页面 DSL 按模块分目录存放:yaml/<模块>/<id>.yaml,页面 id = 文件名;
|
|
34
|
+
# src/server/sitemap.ts 递归收集各模块目录建 id→路径索引;
|
|
35
|
+
# lazy: true 不随 sitemap 内联下发,客户端经 /api/public/pages/{id} 懒加载
|
|
36
|
+
id: site
|
|
37
|
+
resourceType: sitemap
|
|
38
|
+
version: 1
|
|
39
|
+
dslVersion: "1.0"
|
|
40
|
+
app: { name: <产品名,取自 PRD> }
|
|
41
|
+
layout: admin
|
|
42
|
+
auth: { loginPath: /login }
|
|
43
|
+
|
|
44
|
+
pages:
|
|
45
|
+
- { id: login, path: /login, title: 登录, version: 1, public: true }
|
|
46
|
+
- { id: dashboard, path: /, title: 仪表盘, version: 1, lazy: true }
|
|
47
|
+
# 每个 PRD 页面一条:id kebab-case、path 与线框图「页面路径」一致、lazy: true
|
|
48
|
+
- { id: project-list, path: /projects, title: 项目, version: 1, lazy: true }
|
|
49
|
+
- { id: project-detail, path: /projects/:code, title: 项目详情, version: 1, lazy: true }
|
|
50
|
+
|
|
51
|
+
menus:
|
|
52
|
+
# 菜单分组与 PRD 模块树一一对应;详情/编辑页 hidden: true + activeMenu 指向列表项
|
|
53
|
+
- id: assets
|
|
54
|
+
type: group
|
|
55
|
+
title: 资源管理
|
|
56
|
+
icon: database
|
|
57
|
+
order: 2
|
|
58
|
+
children:
|
|
59
|
+
- { id: projects, type: page, pageId: project-list, title: 项目 }
|
|
60
|
+
- { id: project-detail, type: page, pageId: project-detail, title: 项目详情, hidden: true, activeMenu: projects }
|
|
61
|
+
|
|
62
|
+
layouts:
|
|
63
|
+
admin:
|
|
64
|
+
dsl: "1.0"
|
|
65
|
+
body: # 管理端外壳(侧边导航 + PageOutlet),模板自带,按需调整
|
|
66
|
+
login:
|
|
67
|
+
dsl: "1.0"
|
|
68
|
+
body: # 登录页布局(居中单栏),与原产品登录页结构对齐
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 约定
|
|
72
|
+
|
|
73
|
+
- 页面按 PRD 模块树分目录:`yaml/<模块>/<id>.yaml`(如 `yaml/user/user-list.yaml`);模块目录名用 kebab-case 英文域,按 PRD 模块树划分(如 user / order / report)
|
|
74
|
+
- 页面 id kebab-case,= 文件名(不含 `.yaml`);`pages:` 注册、`menus:` 引用、文件名三者一致
|
|
75
|
+
- 模板自带的 `yaml/pages/*.page.yaml` 平铺写法仅适合页面极少的 demo;模块 ≥2 或页面 >5 时一律按模块分目录(模板目录可整体替换)
|
|
76
|
+
- 列表/详情/编辑成组路径:`/projects`、`/projects/:code`、`/projects/:code/edit`、`/projects/new`
|
|
77
|
+
- 详情/编辑页不进菜单(`hidden: true`),用 `activeMenu` 保持列表菜单高亮
|
|
78
|
+
- 菜单图标仅一级菜单配置(目录 group 与「首页」这类独立页面项都要配),用 pactor Icon 白名单 kebab-case 名;二级及以下菜单项一律不配图标
|
|
79
|
+
- 登录/注册页 `public: true` + `layout: login`;其余默认需登录
|
|
80
|
+
- `src/server/sitemap.ts` 只按会话过滤(public/permission),不要在代码里拼接业务字段——数据归 yaml
|
|
81
|
+
- 懒加载页面端点放 `app/api/public/pages/[id]/route.ts`;框架缺省请求 `/api/pages/{id}`,因此**必须**在 `src/pactor/config.ts` 显式配置并传给 SitemapApp,否则客户端请求错路径:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// src/pactor/config.ts
|
|
85
|
+
export const pageEndpoint = (pageId: string) => `/api/public/pages/${pageId}`;
|
|
86
|
+
|
|
87
|
+
// src/pactor/App.tsx
|
|
88
|
+
<SitemapApp ... pageEndpoint={pageEndpoint} />
|
|
89
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# 主题与样式移植指南
|
|
2
|
+
|
|
3
|
+
目标:生成项目与原产品**看起来是同一个产品**——令牌、深浅色、自定义 CSS、换肤能力全部对齐。样式不一致是验收不通过项,不是优化项。
|
|
4
|
+
|
|
5
|
+
## 1. 提取原产品设计令牌
|
|
6
|
+
|
|
7
|
+
从原产品代码(globals.css / tailwind 配置 / 主题文件)提取语义令牌,逐项写入生成项目的 `app/theme.css`:
|
|
8
|
+
|
|
9
|
+
- 必查令牌:`--background` `--foreground` `--card` `--primary` `--secondary` `--muted` `--accent` `--border` `--destructive` 及各自 `-foreground`,`--radius`
|
|
10
|
+
- **深浅色两份**:`:root:not([data-pactor-theme='dark'])` 与 `[data-pactor-theme='dark']` 都要覆盖,漏一份验收必挂
|
|
11
|
+
- 原产品用的字体、阴影、侧栏宽度等也一并记录并移植
|
|
12
|
+
|
|
13
|
+
提取方法:浏览器 DevTools 读取原产品 computed 值,或直接抄其 css 变量定义;禁止凭感觉调色。
|
|
14
|
+
|
|
15
|
+
## 2. 页面级自定义 CSS 完整移植
|
|
16
|
+
|
|
17
|
+
原产品的页面级样式(典型:登录页渐变背景 + 磨砂卡片)必须**原样复制**并在 `app/layout.tsx` 挂载(在 `@pactor-app/ui/styles.css` 之后导入,后加载者胜出)。
|
|
18
|
+
|
|
19
|
+
常见教训:页面级 CSS「文件已复制」但没在 layout.tsx 挂载,样式静默丢失。**挂载后必须浏览器打开对应页面目检**,不以文件存在为完成标准。
|
|
20
|
+
|
|
21
|
+
## 3. 换肤能力对齐
|
|
22
|
+
|
|
23
|
+
原产品支持多套皮肤时:
|
|
24
|
+
|
|
25
|
+
- 每套皮肤一个 CSS 文件(如 `public/skins/<key>.css`),内容是同名语义令牌的覆盖(含深色变体)
|
|
26
|
+
- 皮肤经 `<link rel="stylesheet" data-pactor-skin>` 在 ui styles 之后加载;首屏用 layout.tsx 内联脚本同步注入避免闪烁,运行时换肤「新 link 加载完成再移除旧 link」
|
|
27
|
+
- 换肤入口(ThemeSwitcher 之类)放在与原产品一致的位置(如侧栏底部/用户菜单)
|
|
28
|
+
- 皮肤数量、名称、缺省皮肤与原产品一致
|
|
29
|
+
- 图表组件随肤切换是库内建能力(`useChartTheme` 监听皮肤 link 变化),无需额外处理;验收时确认图表颜色随换肤变化
|
|
30
|
+
|
|
31
|
+
## 4. DSL 侧样式红线
|
|
32
|
+
|
|
33
|
+
- 组件自定义一律 `className` + 语义令牌:`bg-(--muted)`、`text-(--muted-foreground)`、`border-(--border)`
|
|
34
|
+
- **禁止写死色值**(`#xxx`、`rgb(...)`)——深浅色与换肤会破
|
|
35
|
+
- 间距优先组件 props(`gap: small/middle/large`),避免散落的 margin 工具类
|
|
36
|
+
|
|
37
|
+
## 5. 样式验收(并入 acceptance-checklist)
|
|
38
|
+
|
|
39
|
+
- 与原产品同页面并排对比:背景、卡片、主色按钮、文字层级无明显色差
|
|
40
|
+
- 深浅色各切一次:无白边、无死色、无对比度异常
|
|
41
|
+
- 每套皮肤切一次:全站(含图表)随肤变化
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# prd-to-app 模板工程
|
|
2
|
+
|
|
3
|
+
可运行的参考实现:Next.js + pactor,页面全部 YAML DSL 描述(`src/server/yaml/`),数据一律 http 数据源 + mock 路由(`app/api/data/[...path]`)。
|
|
4
|
+
|
|
5
|
+
## 包含内容
|
|
6
|
+
|
|
7
|
+
- **公共页面**:`yaml/common/`(login 登录页、dashboard 仪表盘)
|
|
8
|
+
- **两个参考模块**:
|
|
9
|
+
- `yaml/provider/`(供应商:卡片式列表 / 详情 / 新增 / 编辑)
|
|
10
|
+
- `yaml/route/`(路由:卡片式列表 / 单页纵向详情 / 单页表单新增 / 编辑)
|
|
11
|
+
- **完整接线**:`src/pactor/`(SitemapApp 接线、pageEndpoint、皮肤加载、自定义组件与动作)、`src/server/`(yaml 递归加载 + 会话裁剪)、`app/api/`(sitemap / public/pages / data / auth / users)
|
|
12
|
+
- **三套皮肤**:`public/skins/`(深浅色俱全),侧栏换肤入口
|
|
13
|
+
|
|
14
|
+
## 使用方式
|
|
15
|
+
|
|
16
|
+
生成新项目时**整体复制本目录**作为起点(而不是从空模板开始),然后:
|
|
17
|
+
|
|
18
|
+
1. `package.json` 的 name 改为新工程名
|
|
19
|
+
2. `src/server/yaml/sitemap.yaml`:app 名称、pages、menus 按 PRD 页面清单重写(保留 layouts)
|
|
20
|
+
3. 保留 `common/` 公共页面按需调整;参考模块目录可保留作样板,新模块照其结构新增
|
|
21
|
+
4. mock 数据在 `src/pactor/mocks/` 与 `app/api/data/[...path]/route.ts` 路由表中按新域扩展
|
|
22
|
+
5. `pnpm install && pnpm dev`(端口 9011)验证
|
|
23
|
+
|
|
24
|
+
## 演示账号
|
|
25
|
+
|
|
26
|
+
见 `src/server/accounts.ts`(模板自带演示账号,按需替换)。
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
import { findAccount, SESSION_COOKIE } from "@/server/accounts";
|
|
4
|
+
import { corsHeaders, corsPreflight } from "@/server/cors";
|
|
5
|
+
|
|
6
|
+
/** 演示登录:校验账号 → 写会话 cookie → 返回用户信息 */
|
|
7
|
+
export async function POST(request: Request) {
|
|
8
|
+
const body = (await request.json().catch(() => ({}))) as {
|
|
9
|
+
username?: string;
|
|
10
|
+
password?: string;
|
|
11
|
+
};
|
|
12
|
+
const user = findAccount(body.username ?? "", body.password ?? "");
|
|
13
|
+
if (!user) {
|
|
14
|
+
return Response.json(
|
|
15
|
+
{ error: "用户名或密码错误" },
|
|
16
|
+
{ status: 401, headers: corsHeaders(request) },
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
const response = NextResponse.json(
|
|
20
|
+
{ token: `demo-${user.id}`, user },
|
|
21
|
+
{ headers: corsHeaders(request) },
|
|
22
|
+
);
|
|
23
|
+
response.cookies.set(SESSION_COOKIE, user.username, {
|
|
24
|
+
httpOnly: true,
|
|
25
|
+
sameSite: "lax",
|
|
26
|
+
path: "/",
|
|
27
|
+
});
|
|
28
|
+
return response;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 预检(跨源 POST JSON) */
|
|
32
|
+
export const OPTIONS = corsPreflight;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
import { SESSION_COOKIE } from "@/server/accounts";
|
|
4
|
+
import { corsHeaders, corsPreflight } from "@/server/cors";
|
|
5
|
+
|
|
6
|
+
/** 演示登出:清会话 cookie */
|
|
7
|
+
export async function POST(request: Request) {
|
|
8
|
+
const response = NextResponse.json(
|
|
9
|
+
{ ok: true },
|
|
10
|
+
{ headers: corsHeaders(request) },
|
|
11
|
+
);
|
|
12
|
+
response.cookies.set(SESSION_COOKIE, "", {
|
|
13
|
+
httpOnly: true,
|
|
14
|
+
sameSite: "lax",
|
|
15
|
+
path: "/",
|
|
16
|
+
maxAge: 0,
|
|
17
|
+
});
|
|
18
|
+
return response;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 预检(跨源 POST JSON) */
|
|
22
|
+
export const OPTIONS = corsPreflight;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { cookies } from "next/headers";
|
|
2
|
+
|
|
3
|
+
import { findUserBySession, SESSION_COOKIE } from "@/server/accounts";
|
|
4
|
+
import { corsHeaders, corsPreflight } from "@/server/cors";
|
|
5
|
+
import { pactorServices } from "@/pactor/services";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 页面数据接口(mock):页面 DSL 的 http 数据源统一入口。
|
|
9
|
+
* URL 路径段 → 服务名映射(scripts/to-http-dsl.mjs 中的同一张表);
|
|
10
|
+
* 接入真实后端时改写本表(或在 src/pactor/config.ts 调整 baseUrl 直连后端)。
|
|
11
|
+
*
|
|
12
|
+
* 查询参数原为 service 包装层的入参(布尔快筛在 DSL 侧是一族一个参数),
|
|
13
|
+
* 经 HTTP 序列化为字符串,此处把 'true'/'false' 还原为布尔后再复用
|
|
14
|
+
* 与客户端注册表同一套包装层(src/pactor/services.ts)。
|
|
15
|
+
* 接口侧鉴权:匿名一律 401。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** 路径段 → 服务名 */
|
|
19
|
+
const ROUTE_TABLE: Record<string, string> = {
|
|
20
|
+
dashboard: "dashboard.query",
|
|
21
|
+
providers: "provider.list",
|
|
22
|
+
models: "model.list",
|
|
23
|
+
routes: "route.list",
|
|
24
|
+
clients: "client.list",
|
|
25
|
+
"client-templates": "clientTemplate.list",
|
|
26
|
+
"call-logs": "callLog.list",
|
|
27
|
+
costs: "cost.list",
|
|
28
|
+
audit: "audit.list",
|
|
29
|
+
"audit-operators": "audit.operators",
|
|
30
|
+
"ops/model-usage": "ops.modelUsage",
|
|
31
|
+
"ops/provider-usage": "ops.providerUsage",
|
|
32
|
+
"ops/client-usage": "ops.clientUsage",
|
|
33
|
+
"ops/provider-availability": "ops.availability",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** 详情 / 子资源:<res>/<code|id> 或 <res>/<code>/<sub> */
|
|
37
|
+
const DETAIL_TABLE: Record<string, string> = {
|
|
38
|
+
providers: "provider.detail",
|
|
39
|
+
models: "model.detail",
|
|
40
|
+
routes: "route.detail",
|
|
41
|
+
clients: "client.detail",
|
|
42
|
+
"client-templates": "clientTemplate.detail",
|
|
43
|
+
"call-logs": "callLog.detail",
|
|
44
|
+
costs: "cost.detail",
|
|
45
|
+
audit: "audit.detail",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const SUB_TABLE: Record<string, string> = {
|
|
49
|
+
"providers/pull-models": "provider.pullModels",
|
|
50
|
+
"client-templates/created-clients": "clientTemplate.createdClients",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** 详情类服务的标识参数名(code / id) */
|
|
54
|
+
const ID_PARAM: Record<string, string> = {
|
|
55
|
+
"callLog.detail": "id",
|
|
56
|
+
"cost.detail": "id",
|
|
57
|
+
"audit.detail": "id",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function resolveService(parts: string[]): { service: string; id?: string } | null {
|
|
61
|
+
if (parts.length === 0 || parts.length > 3) return null;
|
|
62
|
+
if (parts.length === 1 || parts.length === 2 && parts[0] === "ops") {
|
|
63
|
+
const service = ROUTE_TABLE[parts.join("/")];
|
|
64
|
+
return service ? { service } : null;
|
|
65
|
+
}
|
|
66
|
+
const [resource, id, sub] = parts;
|
|
67
|
+
if (parts.length === 3) {
|
|
68
|
+
const service = SUB_TABLE[`${resource}/${sub}`];
|
|
69
|
+
return service ? { service, id } : null;
|
|
70
|
+
}
|
|
71
|
+
const service = DETAIL_TABLE[resource];
|
|
72
|
+
return service ? { service, id } : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** query string → 服务入参:'true'/'false' 还原为布尔,其余保持字符串 */
|
|
76
|
+
function coerceParams(searchParams: URLSearchParams): Record<string, unknown> {
|
|
77
|
+
const params: Record<string, unknown> = {};
|
|
78
|
+
for (const [key, value] of searchParams.entries()) {
|
|
79
|
+
params[key] = value === "true" ? true : value === "false" ? false : value;
|
|
80
|
+
}
|
|
81
|
+
return params;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function GET(
|
|
85
|
+
request: Request,
|
|
86
|
+
context: RouteContext<"/api/data/[...parts]">,
|
|
87
|
+
) {
|
|
88
|
+
const store = await cookies();
|
|
89
|
+
const user = findUserBySession(store.get(SESSION_COOKIE)?.value);
|
|
90
|
+
if (!user) {
|
|
91
|
+
return Response.json(
|
|
92
|
+
{ error: "未登录" },
|
|
93
|
+
{ status: 401, headers: corsHeaders(request) },
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { parts } = await context.params;
|
|
98
|
+
const resolved = resolveService(parts);
|
|
99
|
+
if (!resolved) {
|
|
100
|
+
return Response.json(
|
|
101
|
+
{ error: `未知数据接口: /api/data/${parts.join("/")}` },
|
|
102
|
+
{ status: 404, headers: corsHeaders(request) },
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const params = coerceParams(new URL(request.url).searchParams);
|
|
107
|
+
if (resolved.id !== undefined) {
|
|
108
|
+
params[ID_PARAM[resolved.service] ?? "code"] = resolved.id;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const result = await pactorServices[resolved.service](params);
|
|
112
|
+
return Response.json(result ?? null, {
|
|
113
|
+
headers: { "cache-control": "no-store", ...corsHeaders(request) },
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 预检(跨源带凭据 / 自定义头时) */
|
|
118
|
+
export const OPTIONS = corsPreflight;
|