@pactor-app/skills 1.33.0 → 1.35.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/package.json +1 -1
- package/pactor-prd/SKILL.md +20 -23
- package/pactor-prd/references/acceptance-checklist.md +1 -1
- package/pactor-prd/references/data-api.md +35 -0
- package/pactor-prd/references/incremental-wireframe-delta.md +8 -9
- package/pactor-prd/references/page-inventory-and-files.md +2 -2
- package/pactor-prd/references/page-templates.md +59 -12
- package/pactor-prd/references/project-structure.md +1 -1
- package/pactor-prd/template/README.md +4 -4
- package/pactor-prd/template/app/api/d/[...parts]/route.ts +80 -0
- package/pactor-prd/template/app/layout.tsx +1 -3
- package/pactor-prd/template/next-env.d.ts +2 -2
- package/pactor-prd/template/next.config.js +2 -2
- package/pactor-prd/template/package.json +4 -4
- package/pactor-prd/template/pnpm-lock.yaml +97 -97
- package/pactor-prd/template/src/pactor/App.tsx +3 -2
- package/pactor-prd/template/src/pactor/actions.ts +5 -431
- package/pactor-prd/template/src/pactor/config.ts +12 -4
- package/pactor-prd/template/src/server/yaml/common/dashboard.yaml +1 -1
- package/pactor-prd/template/src/server/yaml/common/login.yaml +46 -75
- package/pactor-prd/template/src/server/yaml/models/data-list.yaml +344 -0
- package/pactor-prd/template/src/server/yaml/provider/provider-detail.yaml +2 -2
- package/pactor-prd/template/src/server/yaml/provider/provider-edit.yaml +1 -1
- package/pactor-prd/template/src/server/yaml/provider/provider-list.yaml +1 -1
- package/pactor-prd/template/src/server/yaml/route/route-detail.yaml +1 -1
- package/pactor-prd/template/src/server/yaml/route/route-edit.yaml +1 -1
- package/pactor-prd/template/src/server/yaml/route/route-list.yaml +1 -1
- package/pactor-prd/template/src/server/yaml/sitemap.yaml +23 -6
- package/pactor-prd/references/mock-data-routes.md +0 -66
- package/pactor-prd/template/app/api/data/[...parts]/route.ts +0 -118
- package/pactor-prd/template/app/login.css +0 -122
- package/pactor-prd/template/src/pactor/mocks/audit-mock.ts +0 -403
- package/pactor-prd/template/src/pactor/mocks/call-logs-mock.ts +0 -875
- package/pactor-prd/template/src/pactor/mocks/client-debug-mock.ts +0 -188
- package/pactor-prd/template/src/pactor/mocks/client-templates-mock.ts +0 -497
- package/pactor-prd/template/src/pactor/mocks/clients-mock.ts +0 -483
- package/pactor-prd/template/src/pactor/mocks/costs-mock.ts +0 -406
- package/pactor-prd/template/src/pactor/mocks/dashboard-mock.ts +0 -192
- package/pactor-prd/template/src/pactor/mocks/model-debug-mock.ts +0 -144
- package/pactor-prd/template/src/pactor/mocks/models-mock.ts +0 -799
- package/pactor-prd/template/src/pactor/mocks/ops-usage-mock.ts +0 -806
- package/pactor-prd/template/src/pactor/mocks/providers-mock.ts +0 -424
- package/pactor-prd/template/src/pactor/mocks/routes-mock.ts +0 -604
- package/pactor-prd/template/src/pactor/services.ts +0 -185
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# 数据列表标准骨架 —— 「搜索 + 表格 + 抽屉新建/编辑」数据列表页的复制起点。
|
|
2
|
+
# 本页已注册进 sitemap.yaml(菜单「页面骨架 / 数据列表」,路径 /models/data-list),
|
|
3
|
+
# 在模板工程中可直接运行预览;生成业务列表页时复制本文件到 yaml/<模块>/ 下,
|
|
4
|
+
# 替换页面信息、字段、接口名(data-list → 业务资源名)与文案。
|
|
5
|
+
# 结构与交互约定见 skills/pactor-prd/references/page-templates.md「数据列表标准模型」。
|
|
6
|
+
dsl: "1.0"
|
|
7
|
+
page:
|
|
8
|
+
id: data-list
|
|
9
|
+
title: 数据列表
|
|
10
|
+
name: dataList
|
|
11
|
+
state:
|
|
12
|
+
# 搜索条件镜像:Form submit 后写入,驱动列表请求参数
|
|
13
|
+
keyword: ""
|
|
14
|
+
status: all
|
|
15
|
+
editingId: ""
|
|
16
|
+
# 表单条件显隐镜像(onValuesChange 写入;按页面需要增减,可多个)
|
|
17
|
+
fType: ""
|
|
18
|
+
dataSources:
|
|
19
|
+
list:
|
|
20
|
+
type: http
|
|
21
|
+
autoLoad: true
|
|
22
|
+
request:
|
|
23
|
+
method: GET
|
|
24
|
+
url: /api/d/data-list
|
|
25
|
+
params:
|
|
26
|
+
keyword: ${state.keyword}
|
|
27
|
+
status: ${state.status}
|
|
28
|
+
editing: # 编辑预填;autoLoad: false,打开抽屉前手动 reload
|
|
29
|
+
type: http
|
|
30
|
+
autoLoad: false
|
|
31
|
+
request:
|
|
32
|
+
method: GET
|
|
33
|
+
url: /api/d/data-list/${state.editingId}
|
|
34
|
+
body:
|
|
35
|
+
type: Page
|
|
36
|
+
children:
|
|
37
|
+
- type: Breadcrumb
|
|
38
|
+
props:
|
|
39
|
+
items:
|
|
40
|
+
- label: 页面骨架
|
|
41
|
+
- label: 数据列表
|
|
42
|
+
# 标题行:左标题 + 副标题,右新建按钮
|
|
43
|
+
- type: Flex
|
|
44
|
+
props:
|
|
45
|
+
justify: space-between
|
|
46
|
+
align: center
|
|
47
|
+
children:
|
|
48
|
+
- type: Flex
|
|
49
|
+
props:
|
|
50
|
+
direction: vertical
|
|
51
|
+
gap: 2
|
|
52
|
+
children:
|
|
53
|
+
- type: Typography
|
|
54
|
+
props:
|
|
55
|
+
variant: h3
|
|
56
|
+
text: 数据列表
|
|
57
|
+
- type: Typography
|
|
58
|
+
props:
|
|
59
|
+
variant: muted
|
|
60
|
+
text: 标准列表示例:搜索 + 表格 + 抽屉新建/编辑,复制本页生成业务列表。
|
|
61
|
+
- type: Button
|
|
62
|
+
props:
|
|
63
|
+
text: + 新建记录
|
|
64
|
+
variant: primary
|
|
65
|
+
events:
|
|
66
|
+
onClick:
|
|
67
|
+
- action: setState # 清空编辑态与显隐镜像
|
|
68
|
+
params:
|
|
69
|
+
editingId: ""
|
|
70
|
+
fType: ""
|
|
71
|
+
- action: reload # 清空 editing 数据(editingId 为空 → 空对象)
|
|
72
|
+
params:
|
|
73
|
+
dataSource: editing
|
|
74
|
+
- action: overlay.open # 锚点 &entityDrawer 在此定义,表格行内用 *entityDrawer 引用
|
|
75
|
+
params:
|
|
76
|
+
overlay: &entityDrawer
|
|
77
|
+
type: drawer
|
|
78
|
+
title: "${state.editingId == '' ? '新建记录' : '编辑记录'}"
|
|
79
|
+
width: 720
|
|
80
|
+
content:
|
|
81
|
+
type: Form
|
|
82
|
+
props:
|
|
83
|
+
name: entityForm
|
|
84
|
+
layout: vertical
|
|
85
|
+
initialValues: ${data.editing}
|
|
86
|
+
events:
|
|
87
|
+
# 条件显隐字段镜像到 state(其他字段变化时保持原值)
|
|
88
|
+
onValuesChange:
|
|
89
|
+
- action: setState
|
|
90
|
+
params:
|
|
91
|
+
fType: "${event.type != null ? event.type : state.fType}"
|
|
92
|
+
onSubmit:
|
|
93
|
+
- action: request
|
|
94
|
+
params:
|
|
95
|
+
method: POST
|
|
96
|
+
url: /api/d/ops/data-list-save
|
|
97
|
+
body:
|
|
98
|
+
id: ${state.editingId}
|
|
99
|
+
name: ${event.name}
|
|
100
|
+
code: ${event.code}
|
|
101
|
+
type: ${event.type}
|
|
102
|
+
extra: ${event.extra}
|
|
103
|
+
sort: ${event.sort}
|
|
104
|
+
status: ${event.status}
|
|
105
|
+
- action: message
|
|
106
|
+
params:
|
|
107
|
+
messageType: success
|
|
108
|
+
text: 已保存(演示)
|
|
109
|
+
- action: overlay.close
|
|
110
|
+
children:
|
|
111
|
+
# 分区结构:标题 → 分割线 → 字段组(不用 Card);多个分区重复此结构
|
|
112
|
+
- type: Flex
|
|
113
|
+
props:
|
|
114
|
+
direction: vertical
|
|
115
|
+
gap: middle
|
|
116
|
+
children:
|
|
117
|
+
- type: Text
|
|
118
|
+
props:
|
|
119
|
+
text: 基本信息
|
|
120
|
+
strong: true
|
|
121
|
+
- type: Separator
|
|
122
|
+
- type: Flex
|
|
123
|
+
props:
|
|
124
|
+
direction: vertical
|
|
125
|
+
gap: small
|
|
126
|
+
children:
|
|
127
|
+
- type: Field
|
|
128
|
+
props:
|
|
129
|
+
label: 名称
|
|
130
|
+
required: true
|
|
131
|
+
children:
|
|
132
|
+
- type: Input
|
|
133
|
+
props:
|
|
134
|
+
name: name
|
|
135
|
+
placeholder: 请输入名称
|
|
136
|
+
rules:
|
|
137
|
+
- required: true
|
|
138
|
+
message: 请输入名称
|
|
139
|
+
# 编辑时禁用关键字段
|
|
140
|
+
- type: Field
|
|
141
|
+
props:
|
|
142
|
+
label: 编码
|
|
143
|
+
required: true
|
|
144
|
+
children:
|
|
145
|
+
- type: Input
|
|
146
|
+
props:
|
|
147
|
+
name: code
|
|
148
|
+
placeholder: 大写字母 + 下划线,如 DEMO_ITEM
|
|
149
|
+
disabled: ${state.editingId != ''}
|
|
150
|
+
rules:
|
|
151
|
+
- required: true
|
|
152
|
+
message: 请输入编码
|
|
153
|
+
# 成对字段用 Grid 两列
|
|
154
|
+
- type: Grid
|
|
155
|
+
props:
|
|
156
|
+
columns: 2
|
|
157
|
+
gap: 16
|
|
158
|
+
children:
|
|
159
|
+
- type: Field
|
|
160
|
+
props:
|
|
161
|
+
label: 类型
|
|
162
|
+
required: true
|
|
163
|
+
children:
|
|
164
|
+
- type: Select
|
|
165
|
+
props:
|
|
166
|
+
name: type
|
|
167
|
+
placeholder: 请选择类型
|
|
168
|
+
options: ${data.list.filters.type}
|
|
169
|
+
rules:
|
|
170
|
+
- required: true
|
|
171
|
+
message: 请选择类型
|
|
172
|
+
- type: Field
|
|
173
|
+
props:
|
|
174
|
+
label: 状态
|
|
175
|
+
children:
|
|
176
|
+
- type: Select
|
|
177
|
+
props:
|
|
178
|
+
name: status
|
|
179
|
+
value: 启用
|
|
180
|
+
options: ${data.list.filters.status}
|
|
181
|
+
- type: Grid
|
|
182
|
+
props:
|
|
183
|
+
columns: 2
|
|
184
|
+
gap: 16
|
|
185
|
+
children:
|
|
186
|
+
- type: Field
|
|
187
|
+
props:
|
|
188
|
+
label: 排序
|
|
189
|
+
children:
|
|
190
|
+
- type: NumberInput
|
|
191
|
+
props:
|
|
192
|
+
name: sort
|
|
193
|
+
className: w-full
|
|
194
|
+
min: 0
|
|
195
|
+
precision: 0
|
|
196
|
+
# 条件显隐字段:依赖镜像 state
|
|
197
|
+
- when: ${state.fType == '高级'}
|
|
198
|
+
type: Field
|
|
199
|
+
props:
|
|
200
|
+
label: 扩展说明
|
|
201
|
+
children:
|
|
202
|
+
- type: Input
|
|
203
|
+
props:
|
|
204
|
+
name: extra
|
|
205
|
+
placeholder: 高级类型的扩展配置说明
|
|
206
|
+
# 底部按钮:取消关抽屉,保存触发 Form onSubmit
|
|
207
|
+
- type: Flex
|
|
208
|
+
props:
|
|
209
|
+
justify: end
|
|
210
|
+
gap: small
|
|
211
|
+
className: mt-4
|
|
212
|
+
children:
|
|
213
|
+
- type: Button
|
|
214
|
+
props:
|
|
215
|
+
text: 取消
|
|
216
|
+
events:
|
|
217
|
+
onClick:
|
|
218
|
+
- action: overlay.close
|
|
219
|
+
- type: Button
|
|
220
|
+
props:
|
|
221
|
+
text: 保存
|
|
222
|
+
variant: primary
|
|
223
|
+
submit: true
|
|
224
|
+
htmlType: submit
|
|
225
|
+
- action: reload # overlay.open 阻塞到关闭,关闭后刷新列表
|
|
226
|
+
params:
|
|
227
|
+
dataSource: list
|
|
228
|
+
# 搜索栏:Form(layout: inline) + Field 包裹控件
|
|
229
|
+
- type: Card
|
|
230
|
+
children:
|
|
231
|
+
- type: Form
|
|
232
|
+
props:
|
|
233
|
+
name: search
|
|
234
|
+
layout: inline
|
|
235
|
+
events:
|
|
236
|
+
onSubmit:
|
|
237
|
+
- action: setState
|
|
238
|
+
params:
|
|
239
|
+
keyword: ${event.keyword}
|
|
240
|
+
status: ${event.status}
|
|
241
|
+
- action: reload
|
|
242
|
+
params:
|
|
243
|
+
dataSource: list
|
|
244
|
+
children:
|
|
245
|
+
- type: Field
|
|
246
|
+
props:
|
|
247
|
+
label: 编码/名称
|
|
248
|
+
children:
|
|
249
|
+
- type: Input
|
|
250
|
+
props:
|
|
251
|
+
name: keyword
|
|
252
|
+
placeholder: 支持模糊查询
|
|
253
|
+
- type: Field
|
|
254
|
+
props:
|
|
255
|
+
label: 状态
|
|
256
|
+
children:
|
|
257
|
+
- type: Select
|
|
258
|
+
props:
|
|
259
|
+
name: status
|
|
260
|
+
placeholder: 请选择状态
|
|
261
|
+
options: ${data.list.filters.status} # 选项由接口下发
|
|
262
|
+
# 按钮也放进 Field:label 为 U+00A0 不间断空格(普通空格会被压缩),与左侧控件基线对齐
|
|
263
|
+
- type: Field
|
|
264
|
+
props:
|
|
265
|
+
label: " "
|
|
266
|
+
children:
|
|
267
|
+
- type: Flex
|
|
268
|
+
props:
|
|
269
|
+
gap: small
|
|
270
|
+
align: center
|
|
271
|
+
children:
|
|
272
|
+
- type: Button
|
|
273
|
+
props:
|
|
274
|
+
text: 查询
|
|
275
|
+
submit: true
|
|
276
|
+
htmlType: submit
|
|
277
|
+
- type: Button
|
|
278
|
+
props:
|
|
279
|
+
text: 重置
|
|
280
|
+
events:
|
|
281
|
+
onClick:
|
|
282
|
+
- action: form.setValues
|
|
283
|
+
params:
|
|
284
|
+
form: search
|
|
285
|
+
values:
|
|
286
|
+
keyword: ""
|
|
287
|
+
status: all
|
|
288
|
+
- action: setState
|
|
289
|
+
params:
|
|
290
|
+
keyword: ""
|
|
291
|
+
status: all
|
|
292
|
+
- action: reload
|
|
293
|
+
params:
|
|
294
|
+
dataSource: list
|
|
295
|
+
# 表格:名称列/操作列按钮统一 variant: link size: small
|
|
296
|
+
- type: Table
|
|
297
|
+
props:
|
|
298
|
+
dataSource: ${data.list.items}
|
|
299
|
+
rowKey: id
|
|
300
|
+
pagination: false
|
|
301
|
+
className: mt-4
|
|
302
|
+
columns:
|
|
303
|
+
- title: 名称
|
|
304
|
+
cell:
|
|
305
|
+
type: Button
|
|
306
|
+
props:
|
|
307
|
+
text: ${row.name}
|
|
308
|
+
variant: link
|
|
309
|
+
size: small
|
|
310
|
+
events:
|
|
311
|
+
onClick: &editRow # 行点击 = 编辑,锚点在此定义
|
|
312
|
+
- action: setState # 写入编辑 id + 显隐镜像(取自行数据,不等 editing 接口)
|
|
313
|
+
params:
|
|
314
|
+
editingId: ${row.id}
|
|
315
|
+
fType: ${row.type}
|
|
316
|
+
- action: reload
|
|
317
|
+
params:
|
|
318
|
+
dataSource: editing
|
|
319
|
+
- action: overlay.open
|
|
320
|
+
params:
|
|
321
|
+
overlay: *entityDrawer
|
|
322
|
+
- action: reload
|
|
323
|
+
params:
|
|
324
|
+
dataSource: list
|
|
325
|
+
- title: 编码
|
|
326
|
+
dataIndex: code
|
|
327
|
+
- title: 类型
|
|
328
|
+
dataIndex: type
|
|
329
|
+
- title: 状态
|
|
330
|
+
dataIndex: status
|
|
331
|
+
- title: 操作
|
|
332
|
+
cell:
|
|
333
|
+
type: Button
|
|
334
|
+
props:
|
|
335
|
+
text: 编辑
|
|
336
|
+
variant: link
|
|
337
|
+
size: small
|
|
338
|
+
events:
|
|
339
|
+
onClick: *editRow # 与名称列同一动作
|
|
340
|
+
- type: Text
|
|
341
|
+
props:
|
|
342
|
+
text: 共 ${data.list.total} 条
|
|
343
|
+
type: secondary
|
|
344
|
+
className: mt-3
|
|
@@ -12,12 +12,12 @@ dataSources:
|
|
|
12
12
|
autoLoad: true
|
|
13
13
|
request:
|
|
14
14
|
method: GET
|
|
15
|
-
url: /api/
|
|
15
|
+
url: /api/d/providers/${route.params.code}
|
|
16
16
|
upstream:
|
|
17
17
|
type: http
|
|
18
18
|
request:
|
|
19
19
|
method: GET
|
|
20
|
-
url: /api/
|
|
20
|
+
url: /api/d/providers/${route.params.code}/pull-models
|
|
21
21
|
body:
|
|
22
22
|
type: Page
|
|
23
23
|
children:
|
|
@@ -49,6 +49,14 @@ pages:
|
|
|
49
49
|
}
|
|
50
50
|
# 模块二:模型路由 / 路由(卡片式列表 + 单页纵向详情 + 单页表单)
|
|
51
51
|
- { id: route-list, path: /routes, title: 路由, version: 1, lazy: true }
|
|
52
|
+
# 页面骨架:数据列表标准模型(搜索 + 表格 + 抽屉新建/编辑),可复制生成业务列表页
|
|
53
|
+
- {
|
|
54
|
+
id: data-list,
|
|
55
|
+
path: /models/data-list,
|
|
56
|
+
title: 数据列表,
|
|
57
|
+
version: 1,
|
|
58
|
+
lazy: true,
|
|
59
|
+
}
|
|
52
60
|
- {
|
|
53
61
|
id: route-create,
|
|
54
62
|
path: /routes/new,
|
|
@@ -116,6 +124,19 @@ menus:
|
|
|
116
124
|
title: 路由,
|
|
117
125
|
layout: admin,
|
|
118
126
|
}
|
|
127
|
+
- id: skeletons
|
|
128
|
+
type: group
|
|
129
|
+
title: 页面骨架
|
|
130
|
+
icon: folder
|
|
131
|
+
order: 4
|
|
132
|
+
children:
|
|
133
|
+
- {
|
|
134
|
+
id: data-list,
|
|
135
|
+
type: page,
|
|
136
|
+
pageId: data-list,
|
|
137
|
+
title: 数据列表,
|
|
138
|
+
layout: admin,
|
|
139
|
+
}
|
|
119
140
|
# 隐藏页(不出现在菜单,经列表页跳转进入;activeMenu 保持所属菜单高亮)
|
|
120
141
|
- {
|
|
121
142
|
id: provider-create,
|
|
@@ -234,15 +255,11 @@ layouts:
|
|
|
234
255
|
props: { className: shell-content }
|
|
235
256
|
children:
|
|
236
257
|
- type: PageOutlet
|
|
237
|
-
#
|
|
258
|
+
# 登录页外壳:纯页面出口(登录页由 Login 业务组件自带整页品牌视觉与背景装饰)
|
|
238
259
|
login:
|
|
239
260
|
dsl: "1.0"
|
|
240
261
|
body:
|
|
241
|
-
type:
|
|
242
|
-
props:
|
|
243
|
-
className: login-page
|
|
244
|
-
children:
|
|
245
|
-
- type: PageOutlet
|
|
262
|
+
type: PageOutlet
|
|
246
263
|
# 裸布局:无外壳,页面铺满
|
|
247
264
|
blank:
|
|
248
265
|
dsl: "1.0"
|
|
@@ -1,66 +0,0 @@
|
|
|
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` 的原因。
|
|
@@ -1,118 +0,0 @@
|
|
|
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;
|