@pactor-app/skills 1.34.0 → 1.40.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-dsl/SKILL.md +28 -1
- package/pactor-dsl/references/dsl-authoring.md +174 -36
- package/pactor-prd/SKILL.md +20 -21
- package/pactor-prd/references/acceptance-checklist.md +1 -1
- package/pactor-prd/references/component-mapping.md +1 -1
- package/pactor-prd/references/data-api.md +37 -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.config.js +2 -2
- 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/components/index.ts +1 -4
- 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 +4 -4
- 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 +2 -6
- package/pactor-prd/references/mock-data-routes.md +0 -66
- package/pactor-prd/template/app/api/data/[...parts]/route.ts +0 -160
- package/pactor-prd/template/app/login.css +0 -122
- package/pactor-prd/template/src/pactor/components/chip-filter.tsx +0 -89
- package/pactor-prd/template/src/pactor/components/chip-node.tsx +0 -46
- 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/data-list-mock.ts +0 -84
- 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 -200
package/package.json
CHANGED
package/pactor-dsl/SKILL.md
CHANGED
|
@@ -181,7 +181,33 @@ Facts that are NOT guessable and must be followed:
|
|
|
181
181
|
|
|
182
182
|
- Top level is `{ dsl, page, state?, dataSources?, actions?, components?, lifecycle?, body? }` — `page` is a wrapper object, `dsl: '1.0'` is mandatory.
|
|
183
183
|
- `Table` reads data via an **expression** (`dataSource: ${data.customers}`), not a source name; column templates use the `cell` key (not `render`) with `${row.x}` scope; pagination is `pagination: { pageSize: 5 }`.
|
|
184
|
-
- Form
|
|
184
|
+
- **Inside `Form`, every form control must be wrapped in a `Field`** (`Input`/`NumberInput`/`Select`/`Textarea`/`RadioGroup`/`Checkbox`/`Switch`/`Slider`/`DatePicker` …): label / required / description / error belong to `Field`, the control keeps only its own props (`name` / `placeholder` / `value` / `options` / `rules`). Search-bar buttons (查询/重置) also go inside a `Field` so they align with the inputs; **multiple buttons in one `Field` are placed side by side directly — adjacent buttons get spacing automatically (`[&>button+button]:ml-2`), no extra wrapper or margin needed** (use a `Flex` with `gap` inside the `Field` only when aligning mixed content, e.g. a right-aligned button row). Fields bind via `props.name`; the submit button uses `submit: true` (not `htmlType`); `Form onSubmit` receives all values as `${event.<field>}`.
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
- type: Form
|
|
188
|
+
props: { layout: inline }
|
|
189
|
+
children:
|
|
190
|
+
- type: Field
|
|
191
|
+
props: { label: 关键词 }
|
|
192
|
+
children:
|
|
193
|
+
- type: Input
|
|
194
|
+
props: { name: keyword, placeholder: 输入关键词 }
|
|
195
|
+
- type: Field
|
|
196
|
+
props: { label: 状态 }
|
|
197
|
+
children:
|
|
198
|
+
- type: Select
|
|
199
|
+
props:
|
|
200
|
+
name: status
|
|
201
|
+
options:
|
|
202
|
+
- { label: 启用, value: enabled }
|
|
203
|
+
- { label: 停用, value: disabled }
|
|
204
|
+
- type: Field # 查询/重置按钮同样放 Field 内,与输入框对齐
|
|
205
|
+
children:
|
|
206
|
+
- type: Button
|
|
207
|
+
props: { text: 查询, variant: primary, submit: true }
|
|
208
|
+
- type: Button
|
|
209
|
+
props: { text: 重置 }
|
|
210
|
+
```
|
|
185
211
|
- `navigate` takes `to`; `message` takes `text` + `messageType` (a `type` key in action `params` disables expression evaluation).
|
|
186
212
|
- Query linkage = `onSubmit: [setState, reload]`; the data source's `params` referencing `${state.x}` pick up new values on reload.
|
|
187
213
|
|
|
@@ -357,6 +383,7 @@ pnpm pactor create my-app
|
|
|
357
383
|
- **Do not** rely on frontend permission checks for security; enforce authorization on backend APIs.
|
|
358
384
|
- **Do not** forget to import `@pactor-app/ui/styles.css` when using the default shadcn kit (`@pactor-app/chat/styles.css` additionally for chat surfaces).
|
|
359
385
|
- **Do not** put a `type` key in action `params` (see the events workflow pitfall).
|
|
386
|
+
- **Do not** place bare form controls directly under `Form`; wrap every control (including search-bar buttons) in a `Field`.
|
|
360
387
|
- **Do not** expect DslNode slots (sidebar / toolbar / inspector / …) to render outside a page runtime; in pure React hosts pass ReactNode instead.
|
|
361
388
|
|
|
362
389
|
## References (bundled — works from npm install)
|
|
@@ -178,7 +178,9 @@ Event payloads (`${event}`):
|
|
|
178
178
|
| Button `onClick` | none |
|
|
179
179
|
| Input / Select / Switch `onChange` | the new value (`${event}`) |
|
|
180
180
|
| Tabs `onChange` | new active key |
|
|
181
|
-
| Table `onChange` | `{ current, pageSize, total }` |
|
|
181
|
+
| Table `onChange` | `{ current, pageSize, total, field?, order? }` |
|
|
182
|
+
| Table `onSelectChange` | selected row keys array (`rowSelection: true`) |
|
|
183
|
+
| Table `onRowDrop` | `{ dragKey, dropKey, position: 'before' \| 'after' }` (`rowDraggable: true`) |
|
|
182
184
|
| Form `onSubmit` | all values (`${event.name}`) |
|
|
183
185
|
| Form `onValuesChange` | ONLY the changed field `{ <field>: newValue }` |
|
|
184
186
|
|
|
@@ -198,6 +200,7 @@ Action catalog:
|
|
|
198
200
|
| `call` | `name` | run a top-level named action, inherits caller scope |
|
|
199
201
|
| `delay` | `ms` | wait |
|
|
200
202
|
| `form.setValues` | `values` (req), `form?` (default `default`) | write field values; does NOT trigger validation or onValuesChange |
|
|
203
|
+
| `form.submit` | `form?` (default `default`) | trigger the form's native submit — runs validation and `onSubmit`; for submit buttons outside the `<form>` DOM (e.g. Drawer `footer` slot) |
|
|
201
204
|
| `overlay.open` / `overlay.close` | see below | open overlay and await close; close `payload` becomes open's result |
|
|
202
205
|
|
|
203
206
|
`overlay.open` params: `{ overlay: { type: modal|drawer, title?, width?, content: <DslNode> | page: { pageId }, params? } }`. Overlay content evaluates in overlay scope: `${params.x}` = injected params, `${parent.x}` = parent page snapshot, `${state}`/`${data}` still live.
|
|
@@ -249,17 +252,24 @@ body:
|
|
|
249
252
|
- action: reload
|
|
250
253
|
params: { dataSource: customers }
|
|
251
254
|
children:
|
|
252
|
-
- type:
|
|
253
|
-
props: {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
255
|
+
- type: Field # Form 内控件必须放 Field 内(label 归 Field)
|
|
256
|
+
props: { label: Keyword }
|
|
257
|
+
children:
|
|
258
|
+
- type: Input
|
|
259
|
+
props: { name: keyword, placeholder: Name / owner }
|
|
260
|
+
- type: Field
|
|
261
|
+
props: { label: Status }
|
|
262
|
+
children:
|
|
263
|
+
- type: Select
|
|
264
|
+
props:
|
|
265
|
+
name: status
|
|
266
|
+
options:
|
|
267
|
+
- { label: Enabled, value: active }
|
|
268
|
+
- { label: Disabled, value: inactive }
|
|
269
|
+
- type: Field # 查询按钮也放 Field 内,与输入框对齐
|
|
270
|
+
children:
|
|
271
|
+
- type: Button
|
|
272
|
+
props: { text: Search, variant: primary, submit: true } # submit: true, NOT htmlType
|
|
263
273
|
- type: Card
|
|
264
274
|
props: { title: Customer Data }
|
|
265
275
|
children:
|
|
@@ -315,15 +325,19 @@ events:
|
|
|
315
325
|
- action: message
|
|
316
326
|
params: { messageType: error, text: Save failed, please retry }
|
|
317
327
|
children:
|
|
318
|
-
- type:
|
|
319
|
-
props:
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
328
|
+
- type: Field
|
|
329
|
+
props: { label: Name, required: true }
|
|
330
|
+
children:
|
|
331
|
+
- type: Input
|
|
332
|
+
props:
|
|
333
|
+
name: name
|
|
334
|
+
value: ${params.name} # field-level initial value (row data backfill)
|
|
335
|
+
rules:
|
|
336
|
+
- { required: true, message: Please enter a name }
|
|
337
|
+
- type: Field
|
|
338
|
+
children:
|
|
339
|
+
- type: Button
|
|
340
|
+
props: { text: Save, variant: primary, submit: true }
|
|
327
341
|
- action: if # runs after the drawer closes; cancel (X / mask / Esc) skips
|
|
328
342
|
params:
|
|
329
343
|
condition: ${actions.editResult.success}
|
|
@@ -334,6 +348,60 @@ events:
|
|
|
334
348
|
params: { messageType: success, text: Customer updated }
|
|
335
349
|
```
|
|
336
350
|
|
|
351
|
+
### 8.2.1 Controlled overlay nodes (state + `open` expression)
|
|
352
|
+
|
|
353
|
+
Alternative to `overlay.open`: declare `Drawer` / `Dialog` / `AlertDialog` as ordinary DSL nodes with `open: ${state.x}` and drive them with `setState`. Use this when the overlay is a fixed part of the page tree (templates, statically known content). Key wiring:
|
|
354
|
+
|
|
355
|
+
```yaml
|
|
356
|
+
state: { drawerOpen: false }
|
|
357
|
+
body:
|
|
358
|
+
type: Page
|
|
359
|
+
children:
|
|
360
|
+
- type: Button # open
|
|
361
|
+
props: { text: 新建, variant: primary }
|
|
362
|
+
events:
|
|
363
|
+
onClick:
|
|
364
|
+
- action: setState
|
|
365
|
+
params: { drawerOpen: true }
|
|
366
|
+
- type: Drawer
|
|
367
|
+
props:
|
|
368
|
+
title: 编辑
|
|
369
|
+
open: "${state.drawerOpen}"
|
|
370
|
+
style: { width: 480, maxWidth: 'none' }
|
|
371
|
+
okText: 保存 # 默认 footer [取消, 保存]
|
|
372
|
+
events:
|
|
373
|
+
onClose: # 关闭图标 / mask / Esc / 默认「取消」
|
|
374
|
+
- action: setState
|
|
375
|
+
params: { drawerOpen: false }
|
|
376
|
+
onOk: # 默认「保存」→ form.submit 桥接校验 + onSubmit
|
|
377
|
+
- action: form.submit
|
|
378
|
+
children:
|
|
379
|
+
- type: Form
|
|
380
|
+
events:
|
|
381
|
+
onSubmit:
|
|
382
|
+
- action: message
|
|
383
|
+
params: { messageType: success, text: 保存成功 }
|
|
384
|
+
- action: setState
|
|
385
|
+
params: { drawerOpen: false }
|
|
386
|
+
children:
|
|
387
|
+
- type: Field
|
|
388
|
+
props: { label: 名称, required: true }
|
|
389
|
+
children:
|
|
390
|
+
- type: Input
|
|
391
|
+
props:
|
|
392
|
+
name: name
|
|
393
|
+
rules:
|
|
394
|
+
- { required: true, message: 请输入名称 }
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Rules of thumb:
|
|
398
|
+
|
|
399
|
+
- `Drawer` 默认带右上角关闭图标(`closable: false` 关闭)与默认 footer([取消 → onClose, 确定 → onOk],右对齐、固定底部);关闭链路在未 wiring `onClose` 时也会默认自闭合,wiring `onClose` 回写 state 是推荐做法。
|
|
400
|
+
- 表单抽屉优先用 `okText` + `events.onOk: form.submit`(最简);需要非按钮组布局时才给 `footer` 传 DslNode 或 `false`。
|
|
401
|
+
- A `submit: true` button only submits the enclosing `<form>`; from the `footer` slot (outside the `<form>` DOM) use the `form.submit` action instead — it runs the same validation + `onSubmit` chain.
|
|
402
|
+
- `AlertDialog` in controlled mode never self-closes: wire BOTH `events.onConfirm` and `events.onCancel` to `setState x=false`.
|
|
403
|
+
- `${form.x}` is written to state only on successful submit; for per-step data in a wizard, submit at each step (or read Form values only after submit).
|
|
404
|
+
|
|
337
405
|
### 8.3 Server-side pagination
|
|
338
406
|
|
|
339
407
|
Table's own `pagination` paginates the data already bound to it (client-side). For a server-paginated API, turn it off and pair the table with a standalone `Pagination` component driving state + reload:
|
|
@@ -372,6 +440,28 @@ dataSources:
|
|
|
372
440
|
|
|
373
441
|
Table also supports `rowKey` (row key field, default `'id'`); Table `onChange` (`{ current, pageSize, total }`) exists for its built-in client-side pager.
|
|
374
442
|
|
|
443
|
+
### 8.3.1 Table extras: tree data, row drag, sticky header
|
|
444
|
+
|
|
445
|
+
```yaml
|
|
446
|
+
- type: Table
|
|
447
|
+
props:
|
|
448
|
+
dataSource: ${data.menus} # rows containing a `children` array render as a tree table automatically
|
|
449
|
+
childrenColumnName: children # default; set another field name, or false to disable tree mode
|
|
450
|
+
indentSize: 16 # px per tree level (default 16); expand toggles render in the first column
|
|
451
|
+
defaultExpandAllRows: true # start fully expanded (default: all collapsed, toggle via the row arrow)
|
|
452
|
+
rowDraggable: true # grip handle column; drag to reorder within the same parent level
|
|
453
|
+
scrollY: 400 # px; body scrolls vertically with the header stuck on top
|
|
454
|
+
events:
|
|
455
|
+
onRowDrop: # reorder already applied internally; use this to persist
|
|
456
|
+
- action: request
|
|
457
|
+
params:
|
|
458
|
+
url: /api/menus/reorder
|
|
459
|
+
method: POST
|
|
460
|
+
body: ${event} # { dragKey, dropKey, position }
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Tree tables sort recursively per level and paginate by top-level rows. Sortable columns (`sorter: true`), `rowSelection`, fixed columns (`fixed: right` on the action column) all compose with the above.
|
|
464
|
+
|
|
375
465
|
### 8.4 Confirm then act
|
|
376
466
|
|
|
377
467
|
```yaml
|
|
@@ -396,23 +486,67 @@ events:
|
|
|
396
486
|
state:
|
|
397
487
|
cityMap: { Zhejiang: [{ label: Hangzhou, value: Hangzhou }], Jiangsu: [{ label: Nanjing, value: Nanjing }] }
|
|
398
488
|
cityOptions: []
|
|
399
|
-
# fields:
|
|
400
|
-
- type:
|
|
401
|
-
props: {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
489
|
+
# fields (each control wrapped in Field, as required inside Form):
|
|
490
|
+
- type: Field
|
|
491
|
+
props: { label: Province }
|
|
492
|
+
children:
|
|
493
|
+
- type: Select
|
|
494
|
+
props: { name: region, options: [{ label: Zhejiang, value: Zhejiang }, { label: Jiangsu, value: Jiangsu }] }
|
|
495
|
+
events:
|
|
496
|
+
onChange:
|
|
497
|
+
- action: form.setValues # clear the child field first
|
|
498
|
+
params: { values: { city: '' } } # add `form: edit` when targeting a named form
|
|
499
|
+
- action: setState # then update child options (computed member access)
|
|
500
|
+
params: { cityOptions: ${state.cityMap[event]} }
|
|
501
|
+
- type: Field
|
|
502
|
+
props: { label: City }
|
|
503
|
+
children:
|
|
504
|
+
- type: Select
|
|
505
|
+
props:
|
|
506
|
+
name: city
|
|
507
|
+
options: ${state.cityOptions}
|
|
508
|
+
disabled: ${state.cityOptions.length === 0}
|
|
414
509
|
```
|
|
415
510
|
|
|
511
|
+
### 8.6 Card list page (CardList / EntityCard business components)
|
|
512
|
+
|
|
513
|
+
`CardList` renders a grid of `EntityCard` summary cards in one node — no `For` + nested `Flex`/`Text`/`Tag` scaffolding. Item fields mirror `EntityCard` props; extra business fields ride along and come back in event payloads.
|
|
514
|
+
|
|
515
|
+
```yaml
|
|
516
|
+
state:
|
|
517
|
+
plugins:
|
|
518
|
+
- id: 1
|
|
519
|
+
icon: users # Icon whitelist name (or `logo` image URL)
|
|
520
|
+
title: 用户管理
|
|
521
|
+
subtitle: v0.1.0 # auxiliary line under the title
|
|
522
|
+
description: 管理插件安装状态与运行生命周期,支持启停与依赖检查。
|
|
523
|
+
tags: [{ text: 已安装, color: success }] # Tag colors: default/success/warning/error/info
|
|
524
|
+
actions: [{ text: 停用 }] # footer buttons, right-aligned, muted bar
|
|
525
|
+
body:
|
|
526
|
+
- type: CardList
|
|
527
|
+
props:
|
|
528
|
+
items: ${state.plugins}
|
|
529
|
+
columns: 3 # grid columns, default 3; 1 = single wide rows
|
|
530
|
+
gap: 16 # default 16
|
|
531
|
+
showCount: true # footer "共 N 个" count line
|
|
532
|
+
# createText: 新建插件 # dashed "create" placeholder card in the first cell
|
|
533
|
+
# hoverable: true # whole-card pointer + hover highlight
|
|
534
|
+
events:
|
|
535
|
+
onCreateClick: { action: message, params: { messageType: info, text: open create dialog } }
|
|
536
|
+
onItemClick: { action: message, params: { messageType: info, text: "进入详情:${event.item.title}" } }
|
|
537
|
+
onItemAction: # ${event} = { key, text, item, index }
|
|
538
|
+
- action: confirm
|
|
539
|
+
id: opConfirm
|
|
540
|
+
params: { title: "确认${event.text}「${event.item.title}」?" }
|
|
541
|
+
- action: if
|
|
542
|
+
params:
|
|
543
|
+
condition: ${actions.opConfirm}
|
|
544
|
+
then: [{ action: message, params: { messageType: success, text: done } }]
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
- `EntityCard` standalone: `icon`/`logo`, `title`, `subtitle`, `tags[]`, `description` (two-line clamp), `meta` (right side of the middle row), `actions[]` (`{ text, key?, variant?, danger? }`), `footerBackground` (muted footer bar, default `true`), `hoverable`. Events `onClick` (whole card) and `onAction` (`{ key, text, index }`) — footer button clicks never trigger the card click.
|
|
548
|
+
- Empty `items` renders an `Empty` state (`emptyText` overrides the default).
|
|
549
|
+
|
|
416
550
|
## 9. Common mistakes (observed — do not do these)
|
|
417
551
|
|
|
418
552
|
| Wrong | Right |
|
|
@@ -423,6 +557,10 @@ state:
|
|
|
423
557
|
| Column template key `render` | `cell` |
|
|
424
558
|
| `pagination: true` | `pagination: { pageSize: 10 }` |
|
|
425
559
|
| `htmlType: submit` | `submit: true` |
|
|
560
|
+
| Bare `Input`/`Select`/`Button` directly under `Form` | Wrap every form control (incl. search-bar buttons) in `Field`; label / required / description / error belong to `Field` |
|
|
561
|
+
| Relying on `Field required: true` for validation | It is a visual `*` marker only — declare `rules: [{ required: true, message: ... }]` on the CONTROL (`Input`/`Select`/…); Form validates control rules on submit |
|
|
562
|
+
| `style` / `className` as node-level keys (sibling of `props:`) | They are component props — always nest them inside `props: { className: ..., style: ... }`; node-level keys are silently ignored |
|
|
563
|
+
| `children` nested inside `props:` | `children` is a NODE-level key (sibling of `props:`); nested under `props:` it is silently dropped |
|
|
426
564
|
| `navigate` param `path` / `url` | `to` |
|
|
427
565
|
| `message` param `type: success` | `messageType: success` — a `type` (or `action`) key inside action `params` makes the engine treat params as a nested DSL/action config and SKIP expression evaluation |
|
|
428
566
|
| Reading full form values in `onValuesChange` | It only carries the changed field; use `onSubmit` or `${form.x}` |
|
package/pactor-prd/SKILL.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pactor-prd
|
|
3
3
|
description: |
|
|
4
|
-
Use when PRD or markdown wireframes drive Pactor YAML pages (skill id pactor-prd). Greenfield: copy template/ into a new repo. Incremental: in an existing Pactor project, add or update pages from new/changed wireframe files (DSL, sitemap, menus,
|
|
5
|
-
version: 1.
|
|
4
|
+
Use when PRD or markdown wireframes drive Pactor YAML pages (skill id pactor-prd). Greenfield: copy template/ into a new repo. Incremental: in an existing Pactor project, add or update pages from new/changed wireframe files (DSL, sitemap, menus, data API, styles). Triggers: pactor-prd, prd-to-app, 线框图增量, 已有项目加页面, PRD 更新页面.
|
|
5
|
+
version: 1.5.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# PRD → YAML 页面应用生成
|
|
9
9
|
|
|
10
|
-
把「产品文档 + Markdown 线框图」落到 **YAML 页面 + sitemap
|
|
10
|
+
把「产品文档 + Markdown 线框图」落到 **YAML 页面 + sitemap/数据接口/样式**;支持 **两种模式**(先选型再执行):
|
|
11
11
|
|
|
12
12
|
| 模式 | 何时用 | 脚手架 |
|
|
13
13
|
| --- | --- | --- |
|
|
@@ -17,6 +17,7 @@ version: 1.3.0
|
|
|
17
17
|
## 输入要求
|
|
18
18
|
|
|
19
19
|
- PRD/线框图:总览 README(模块 × 页面 × 优先级)+ 分页线框 markdown(ASCII + 区域 + 交互)
|
|
20
|
+
- **接口文档/说明(可选)**:API 路径、方法、参数、返回结构;存在时页面 dataSources 直接对接文档接口,不再走 `/api/d/...` 约定
|
|
20
21
|
- **增量**:可只提供 **变更的线框图文件** 列表(或 PRD 目录 + 标明 diff 范围)
|
|
21
22
|
- 原产品代码仓(可选):抽设计令牌、自定义 CSS、交互
|
|
22
23
|
- 目标:空目录(新建工程)或已有工程根目录(增量)
|
|
@@ -25,20 +26,21 @@ version: 1.3.0
|
|
|
25
26
|
|
|
26
27
|
## 红线(必须遵守)
|
|
27
28
|
|
|
28
|
-
1. **dataSources 一律 `type: http
|
|
29
|
+
1. **dataSources 一律 `type: http`,不写 `type: service`**;**PRD 含接口文档/说明时,url 取文档中的接口地址**(方法、路径、返回形状对齐,见「1. 输入解析」);无接口文档时指向 `/api/d/...`(转发层空数据回退,后续按 `references/data-api.md` 接真实接口)
|
|
29
30
|
2. **布局与间距以样板页为准**:新建工程时以 `template/` 同类页为准;**增量时以目标工程内同模块/同类型页**为准。线框图只提取结构与功能,不逐像素还原 ASCII
|
|
30
|
-
3. **样式与原产品一致**:主题令牌(含深浅色)、自定义 CSS
|
|
31
|
+
3. **样式与原产品一致**:主题令牌(含深浅色)、自定义 CSS、换肤入口全部移植对齐,见 `references/theme-and-style.md`
|
|
31
32
|
4. **功能与原产品一致**:线框图描述的筛选/搜索/跳转/弹窗/提交等交互必须经 events/actions 闭环可用,禁止只画静态骨架;错误分支(校验失败提示等)也要实现
|
|
32
33
|
5. **表单一律用 `Form` 组件**:凡是一组「字段 + 校验 + 提交」的表单——独立表单页、页面内嵌的配置面板(如树/列表右侧的编辑表单)、弹窗/抽屉表单、**搜索栏(关键词 + 查询/重置 的筛选区,用 `Form layout: inline`)**——都必须用 `Form` 包裹,字段经 `props.name` 绑定,校验用字段级 `rules`(required + message),提交用 `submit: true` 的 Button 触发 `onSubmit`;**禁止**散放 `Input`/`Select` + `setState` 攒草稿 + `if` 动作手写校验。上下结构的字段一律用 `Field` 包裹:label、必填星号(`required: true`)、行右提示(`labelHint`)由 `Field` 渲染,控件上只留 `name`/`rules` 等(`Form layout: inline` 的搜索栏除外,label 直接挂控件);禁止手写 `Text("*") + Label` 拼标签。回填联动(如点击列表/树节点填充表单)用 `form.setValues` 动作,不要绑 `${state.x}` 到字段 value;即点即生效的 ChipFilter 快筛行没有提交语义,不属于表单
|
|
33
34
|
6. **列表页操作列规范**:操作列按钮样式统一 `Button variant: link`;「编辑」与「新建」指向同一个表单(同一路由页或同一弹窗),唯一区别是编辑以行数据预填(路由页 `initialValues: ${data.detail}`,弹窗/内嵌面板 `form.setValues`);**数据列表页(搜索 + 表格 + 新建/编辑)以 `template/src/server/yaml/models/data-list.yaml` 为标准骨架生成**(结构与要点见 `references/page-templates.md`「数据列表标准模型」)
|
|
34
35
|
7. **文案以线框图为准**:标题、按钮、空态文案逐字取自线框图/PRD
|
|
35
|
-
8.
|
|
36
|
+
8. **登录页用 `Login` 业务组件**:PRD 含登录页时,以模板 `yaml/common/login.yaml`(`type: Login`)为样板生成,禁止手写 Card+Form+自定义 CSS;**原型未明确布局方式时必须向用户提问**选择 `layout`(`split` 左右分栏 / `centered` 居中 / `brand-card` 品牌卡片 / `overlap` 卡片叠加 / `backdrop` 整页渐变背景,附效果简述,必要时含 `brandSide`/`brandRatio`/`authVariant` 修饰项),原型可判定布局则直接用、不提问;用户不答默认 `split` + `authVariant: card`;品牌区文案/logo/插画取自 PRD,提交闭环沿用 `onSubmit` → `request` + `message` + `app.reload` 模式
|
|
37
|
+
9. DSL 语法细节(组件 props、表达式、动作)以 `pactor-dsl` skill 与 packages/ui 当前导出为准,本 skill 只约定流程与产出物
|
|
36
38
|
|
|
37
39
|
## 流程总览
|
|
38
40
|
|
|
39
41
|
```text
|
|
40
|
-
选型 → 1 解析 → 2 脚手架(仅新建工程)→ 3 页面 → 4
|
|
41
|
-
增量:2 改为「摸底」;1/3 只处理 delta;
|
|
42
|
+
选型 → 1 解析 → 2 脚手架(仅新建工程)→ 3 页面 → 4 样式 → 5 验收
|
|
43
|
+
增量:2 改为「摸底」;1/3 只处理 delta;5 变更页全验 + 少量回归
|
|
42
44
|
```
|
|
43
45
|
|
|
44
46
|
详细增量步骤:**`references/incremental-wireframe-delta.md`**。
|
|
@@ -47,6 +49,7 @@ version: 1.3.0
|
|
|
47
49
|
|
|
48
50
|
- 读 README / 变更线框图:模块树、页面 id(kebab-case)、path、菜单、P0/P1/P2
|
|
49
51
|
- 逐页读线框:页面类型、区域、字段、交互
|
|
52
|
+
- **接口文档解析**:PRD 含接口文档/说明时,提取「资源 → 路径 / 方法 / 返回形状」接口清单,页面 dataSources 以该清单为准;无接口文档则按 `references/data-api.md` 命名约定写 `/api/d/...`
|
|
50
53
|
- **新建工程**:输出全量 **页面清单表**(`references/page-inventory-and-files.md`)
|
|
51
54
|
- **增量**:扫描工程现状清单 + 输出 **delta 表**(add / update / deprecate,`references/incremental-wireframe-delta.md`);P2 不建文件
|
|
52
55
|
|
|
@@ -57,7 +60,7 @@ version: 1.3.0
|
|
|
57
60
|
- **整体复制 `template/`** 为新项目起点;不要用 `pnpm create pactor-app` 替代
|
|
58
61
|
- 工程名:README 产品名 → kebab-case 英文名,创建前确认一次
|
|
59
62
|
- `pnpm install` + dev 验证;核对 `references/project-structure.md`
|
|
60
|
-
- 参考模块 provider/route 作样板;`yaml/models/` 是页面骨架目录(按模块目录结构存放,已注册 sitemap
|
|
63
|
+
- 参考模块 provider/route 作样板;`yaml/models/` 是页面骨架目录(按模块目录结构存放,已注册 sitemap、菜单「页面骨架」分组可直接预览),生成时复制到目标模块目录改字段与文案;**全站生成完成后删除**参考模块与 models/
|
|
61
64
|
|
|
62
65
|
**增量**
|
|
63
66
|
|
|
@@ -71,17 +74,13 @@ version: 1.3.0
|
|
|
71
74
|
- 校验见 `page-inventory-and-files.md`(全量)或 `incremental-wireframe-delta.md`(增量)
|
|
72
75
|
- 按 `references/component-mapping.md` 映射组件;按 `references/page-templates.md` 选列表/详情/表单模板(**数据列表页直接复制 `yaml/models/data-list.yaml` 替换占位符**);表单一律 `Form` 包裹
|
|
73
76
|
- 同步更新 `sitemap.yaml` 的 `pages` + `menus`(hidden 详情/编辑带 `activeMenu`)
|
|
74
|
-
- 每页 dataSources 全部 `type: http
|
|
77
|
+
- 每页 dataSources 全部 `type: http`:**有接口文档 → url 取文档接口地址**;无 → 按 `references/data-api.md` 命名约定写 `/api/d/...`(转发层空数据回退,无需 mock);交互 events + actions 闭环
|
|
75
78
|
|
|
76
|
-
### 4.
|
|
77
|
-
|
|
78
|
-
- 按 `references/mock-data-routes.md` 注册/更新路由表与 `src/pactor/mocks/`;**增量只动变更页涉及的接口**
|
|
79
|
-
|
|
80
|
-
### 5. 样式移植
|
|
79
|
+
### 4. 样式移植
|
|
81
80
|
|
|
82
81
|
- 按 `references/theme-and-style.md`;**增量**仅受影响页面/令牌(新建工程做全站对齐)
|
|
83
82
|
|
|
84
|
-
###
|
|
83
|
+
### 5. 功能与视觉验收(必经,不可跳过)
|
|
85
84
|
|
|
86
85
|
- `references/acceptance-checklist.md`:**新建工程**逐页全站验收;**增量**对 **所有变更页** 全项验收 + **至少 1~2 个未改页回归**(菜单可达、控制台无新增 error)
|
|
87
86
|
|
|
@@ -90,16 +89,16 @@ version: 1.3.0
|
|
|
90
89
|
| 文件 | 何时加载 |
|
|
91
90
|
| --- | --- |
|
|
92
91
|
| `template/` | 阶段 2 **仅新建工程**:复制为新工程起点;增量勿用 |
|
|
93
|
-
| `references/incremental-wireframe-delta.md` | **已有工程**:线框图 add/update/deprecate、改 sitemap
|
|
92
|
+
| `references/incremental-wireframe-delta.md` | **已有工程**:线框图 add/update/deprecate、改 sitemap/数据接口/样式 |
|
|
94
93
|
| `references/project-structure.md` | 阶段 2:目录结构约定与 sitemap.yaml 骨架 |
|
|
95
94
|
| `references/component-mapping.md` | 阶段 3:线框图区域 → pactor 组件映射 |
|
|
96
95
|
| `references/page-templates.md` | 阶段 3:列表/详情/表单页 yaml 模板;数据列表页默认用「数据列表标准模型」(搜索 Form + 表格 + 抽屉新建/编辑) |
|
|
97
|
-
| `references/
|
|
98
|
-
| `references/theme-and-style.md` | 阶段
|
|
99
|
-
| `references/acceptance-checklist.md` | 阶段
|
|
96
|
+
| `references/data-api.md` | 阶段 3:数据接口层约定(纯转发、路径即契约、空数据回退、路径命名) |
|
|
97
|
+
| `references/theme-and-style.md` | 阶段 4:主题令牌提取、皮肤、自定义 CSS、换肤 |
|
|
98
|
+
| `references/acceptance-checklist.md` | 阶段 5:验收清单与结果输出格式 |
|
|
100
99
|
| `references/page-inventory-and-files.md` | 阶段 1–3:清单列 → 目录与文件批量创建、与 sitemap 对齐 |
|
|
101
100
|
|
|
102
101
|
## Sibling skills
|
|
103
102
|
|
|
104
|
-
- **`pactor-dsl`** — 无线框图的单页/清单加页;**有线框图 +
|
|
103
|
+
- **`pactor-dsl`** — 无线框图的单页/清单加页;**有线框图 + 数据接口/sitemap 联动**仍用本 skill 增量模式
|
|
105
104
|
- **`pactor-project`** — `pnpm create pactor-app` 空壳(**不要**用其替代本 skill 的 template 复制)
|
|
@@ -45,5 +45,5 @@
|
|
|
45
45
|
|
|
46
46
|
- **dev server 不会因 yaml 改动热重载**(yaml 在服务端模块加载时读取):改 `src/server/yaml/**` 后重启 dev server 再验
|
|
47
47
|
- **端口冲突**:启动前 `lsof -nP -iTCP:<port> -sTCP:LISTEN`,已有 dev server 直接复用
|
|
48
|
-
- **两段数据路径被当详情解析**:选项类接口用单段路径(`/api/
|
|
48
|
+
- **两段数据路径被当详情解析**:选项类接口用单段路径(`/api/d/project-members` 而非 `project/members`)
|
|
49
49
|
- **toast 消失快误判未触发**:验证错误提示时在操作后立即截图/读 DOM
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
| 下拉筛选(操作人等枚举) | `Select` | `options` 可绑 `${data.xxx}`(接口下发选项,单段路径) |
|
|
14
14
|
| 日期范围筛选 | `DatePicker` | range 模式绑 state,onChange reload |
|
|
15
15
|
| 数据表格 | `Table` | `columns` + `dataSource: ${data.x.items}`;操作列按钮统一 `Button variant: link`;「编辑」与「新建」共用同一表单,仅以预填数据区分(initialValues / form.setValues) |
|
|
16
|
-
| 卡片网格(约 3-4 列) | `
|
|
16
|
+
| 卡片网格(约 3-4 列) | `CardList`(业务组件) | 数据项直接携带 `icon`/`title`/`subtitle`/`tags`/`description`/`meta`/`actions` 字段;`columns`、`createText`(首格虚线新建卡)、`showCount`、事件 `onItemClick`/`onItemAction`/`onCreateClick`(`${event}` 含 item/index/key);单卡即 `EntityCard`,无需 `For`+`Card` 手写三层结构 |
|
|
17
17
|
| 状态徽标(启用/停用/告警) | `Badge` / `StatusBadge` | 颜色语义化,状态文案取自线框图 |
|
|
18
18
|
| 启停开关图标(⏻) | `StatusToggleIcon` | 卡片/行内启停,onChange 闭环动作 + reload |
|
|
19
19
|
| 权限控制显隐(无权限隐藏新建卡等) | 表达式 `hidden` / `if` 动作 | 绑 `${data.session.permissions}` 等,按会话裁剪 |
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# 数据接口层(接口文档优先 + 纯转发空数据约定)
|
|
2
|
+
|
|
3
|
+
**接口文档驱动(优先)**:PRD/产品文档含接口文档或接口说明时,页面 dataSources 的 url 直接取文档中的接口地址——方法、参数、返回形状均以文档为准,不再使用下述 `/api/d/...` 命名约定。同源相对路径直接写;跨域绝对地址写完整 URL(`type: http` 原生支持)。转发层与命名约定仅在**无接口文档**时兜底。
|
|
4
|
+
|
|
5
|
+
页面 DSL 的 http 数据源统一入口:`app/api/d/[...parts]/route.ts`(**纯转发层,不含任何路径映射**)。**路径即契约**:DSL 中的 url(如 `/api/d/providers`)就是服务端接口路径;转发层按相同路径、方法、查询与请求体原样转发到 `$NEXT_PUBLIC_API_BASE_URL/api/d/<parts>`(携带会话 cookie,匿名 401)。
|
|
6
|
+
|
|
7
|
+
**没有服务层接口时返回空数据(不报错)**:上游不可达(连接失败)、接口不存在(404)或返回非 JSON(如命中前端兜底 HTML)时,GET 返回 `{}`、POST 返回 `{ ok: true }`,页面按空态渲染。接真实后端只需把 `NEXT_PUBLIC_API_BASE_URL`(next.config.js 的 `API_BASE_URLS`)换成真实地址,页面 yaml 与转发层零改动。
|
|
8
|
+
|
|
9
|
+
## 命名约定
|
|
10
|
+
|
|
11
|
+
- **单段路径**:`/api/d/projects`、`/api/d/project-members`——列表/选项类一律单段 kebab-case
|
|
12
|
+
- **禁止** `/api/d/project/members` 这种两段写法:`[...parts]` 会把第二段当详情 id 解析(`project/detail?id=members`),踩过的坑
|
|
13
|
+
- 详情:`/api/d/<resource>/<code|id>`;子资源:`/api/d/<resource>/<code>/<sub>`;写操作:`/api/d/ops/<action>`(POST)
|
|
14
|
+
- 数据源 url 与页面 id 语义对应:`project-list` 页 → `/api/d/projects`
|
|
15
|
+
|
|
16
|
+
## 转发层结构
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
浏览器(同源 /api/d/*)
|
|
20
|
+
→ app/api/d/[...parts]/route.ts 纯转发:会话鉴权(匿名 401)→
|
|
21
|
+
fetch $NEXT_PUBLIC_API_BASE_URL/api/d/<parts>
|
|
22
|
+
不可达 / 404 / 非 JSON → 空数据(GET {} / POST { ok: true })
|
|
23
|
+
→ 服务端接口 真实后端(与 DSL 同路径契约)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
完整实现见模板工程 `app/api/d/[...parts]/route.ts`。
|
|
27
|
+
|
|
28
|
+
## 对页面 yaml 的要求
|
|
29
|
+
|
|
30
|
+
- dataSources 一律 `type: http`;有接口文档时 url 取文档接口地址(方法/返回形状对齐),否则按命名约定写 `/api/d/...` 最终路径
|
|
31
|
+
- 列表接口返回形状约定 `{ items, total, filters? }`;详情接口对空 id 返回空对象而非 404(新建预填链路依赖)
|
|
32
|
+
- 选项类接口(如下拉的操作人列表)用单段路径,页面 `options: ${data.xxx}` 绑定
|
|
33
|
+
- 写操作走 `ops/` 前缀 POST,页面提交闭环(request → message → reload)不依赖返回体内容
|
|
34
|
+
|
|
35
|
+
## 替换为真实接口
|
|
36
|
+
|
|
37
|
+
把 `NEXT_PUBLIC_API_BASE_URL` 换成真实后端基准地址即可——页面 yaml 与转发层代码零改动。页面 dataSources 声明在无后端/真实后端两种状态下都无需改动,这就是必须 `type: http` 且路径即契约的原因。
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
- `src/server/yaml/sitemap.yaml`(或等价 sitemap 源)
|
|
10
10
|
- 页面 yaml 目录(**模式 B** `yaml/<模块>/<id>.yaml` 或 **模式 A** `yaml/pages/*.page.yaml` — 见 `pactor-dsl/references/page-catalog-and-layout.md`)
|
|
11
|
-
-
|
|
11
|
+
- 数据接口层:`app/api/d/[...parts]/route.ts` 纯转发(路径即契约、无服务层接口时返回空数据,见 `data-api.md`);**无 mock 层**(若工程仍用 `type: service`,增量页 **仍须 http**,并迁移或仅对新页遵守红线)
|
|
12
12
|
|
|
13
13
|
不满足时:先 **`pactor-project`** 或 **新建工程 `pactor-prd` 模式** 搭壳,再回来做增量。
|
|
14
14
|
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
|
|
42
42
|
| 变更 | 动作 |
|
|
43
43
|
| --- | --- |
|
|
44
|
-
| **add 页面** | `mkdir` 模块目录;新建 yaml;`sitemap.yaml` 增加 `pages` + `menus`;
|
|
44
|
+
| **add 页面** | `mkdir` 模块目录;新建 yaml;`sitemap.yaml` 增加 `pages` + `menus`;dataSources 直接声明最终接口路径(无需注册) |
|
|
45
45
|
| **update 页面** | 改对应 yaml(结构/字段/交互/文案);若 path 或 id 变则同步 `sitemap.yaml`、检查 `router.push` / 菜单 `pageId` |
|
|
46
|
-
| **update 仅文案/字段** | 只改 yaml
|
|
47
|
-
| **add 数据源** | yaml 内 `dataSources`
|
|
48
|
-
| **样式** | 仅当线框图要求新页面级样式时改 `globals.css` /
|
|
49
|
-
| **deprecate** | 删 yaml;从 `pages`/`menus`
|
|
46
|
+
| **update 仅文案/字段** | 只改 yaml |
|
|
47
|
+
| **add 数据源** | yaml 内 `dataSources` 声明即可(路径即契约,转发层无需注册) |
|
|
48
|
+
| **样式** | 仅当线框图要求新页面级样式时改 `globals.css` / 皮肤;见 `theme-and-style.md` |
|
|
49
|
+
| **deprecate** | 删 yaml;从 `pages`/`menus` 移除;全局搜 `pageId` / 路径引用 |
|
|
50
50
|
|
|
51
51
|
**禁止**:为增量在仓库根再复制一份 `template/` 覆盖工程。
|
|
52
52
|
|
|
@@ -56,7 +56,6 @@
|
|
|
56
56
|
| --- | --- | --- |
|
|
57
57
|
| 脚手架 | 复制 `template/` | **跳过**;仅摸底 |
|
|
58
58
|
| 页面 | 全量清单 N 文件 | 仅 **delta** 行对应文件 |
|
|
59
|
-
| mock | 全表扩展 | 只加/改涉及接口 |
|
|
60
59
|
| 样式 | 全站移植 | 仅受影响页面/令牌 |
|
|
61
60
|
| 验收 | 全站清单 | **变更页全量验** + **抽 1~2 条旧页回归**(见 acceptance-checklist) |
|
|
62
61
|
|
|
@@ -65,7 +64,7 @@
|
|
|
65
64
|
```text
|
|
66
65
|
对每个 add:存在 yaml 且 sitemap.pages 含 id 且 menus 正确
|
|
67
66
|
对每个 update:page.id 未变(除非走 rename 流程)且 validate 通过
|
|
68
|
-
|
|
67
|
+
数据接口:变更页涉及的所有 http 路径在 Network 200(无后端时为空数据 {},页面按空态渲染)
|
|
69
68
|
```
|
|
70
69
|
|
|
71
70
|
`npx pactor validate` 仅覆盖改动的 yaml 路径。
|
|
@@ -73,4 +72,4 @@ mock:变更页涉及的所有 http 路径在 Network 200
|
|
|
73
72
|
## 5. 与 `pactor-dsl` 的分工
|
|
74
73
|
|
|
75
74
|
- **单页小改**、无线框图、只改 DSL 语法:可用 **`pactor-dsl`**。
|
|
76
|
-
- **线框图驱动**、含
|
|
75
|
+
- **线框图驱动**、含 数据接口/sitemap/多页联动:用 **本增量流程**(仍属 `pactor-prd`)。
|
|
@@ -30,7 +30,7 @@ src/server/yaml/
|
|
|
30
30
|
3. **一次更新 sitemap**:
|
|
31
31
|
- `pages`:清单中每一页一条(含 hidden 的详情/编辑)
|
|
32
32
|
- `menus`:仅「进导航」的页进 group;hidden 页带 `activeMenu`
|
|
33
|
-
4.
|
|
33
|
+
4. **数据源声明**:按 `references/data-api.md` 命名约定,每个 http 数据源在 yaml 内直接声明最终接口路径(转发层纯转发,**无需注册**);无后端时页面按空态渲染。
|
|
34
34
|
5. **交叉检查**(生成后必做):
|
|
35
35
|
|
|
36
36
|
```text
|
|
@@ -53,7 +53,7 @@ src/server/yaml/
|
|
|
53
53
|
|
|
54
54
|
## 参考模块(模板内)
|
|
55
55
|
|
|
56
|
-
复制 `pactor-prd/template/` 后,保留 `yaml/provider/`、`yaml/route/` 作写法样本;新模块 **照同样目录深度**
|
|
56
|
+
复制 `pactor-prd/template/` 后,保留 `yaml/provider/`、`yaml/route/` 作写法样本;新模块 **照同样目录深度** 新增;全站生成完成后删除参考模块。
|
|
57
57
|
|
|
58
58
|
## 已有工程(增量)
|
|
59
59
|
|