@pactor-app/skills 1.35.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 +5 -3
- package/pactor-prd/references/component-mapping.md +1 -1
- package/pactor-prd/references/data-api.md +4 -2
- package/pactor-prd/template/src/pactor/components/index.ts +1 -4
- 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/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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: pactor-prd
|
|
3
3
|
description: |
|
|
4
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
|
+
version: 1.5.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# PRD → YAML 页面应用生成
|
|
@@ -17,6 +17,7 @@ version: 1.4.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,7 +26,7 @@ version: 1.4.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
31
|
3. **样式与原产品一致**:主题令牌(含深浅色)、自定义 CSS、换肤入口全部移植对齐,见 `references/theme-and-style.md`
|
|
31
32
|
4. **功能与原产品一致**:线框图描述的筛选/搜索/跳转/弹窗/提交等交互必须经 events/actions 闭环可用,禁止只画静态骨架;错误分支(校验失败提示等)也要实现
|
|
@@ -48,6 +49,7 @@ version: 1.4.0
|
|
|
48
49
|
|
|
49
50
|
- 读 README / 变更线框图:模块树、页面 id(kebab-case)、path、菜单、P0/P1/P2
|
|
50
51
|
- 逐页读线框:页面类型、区域、字段、交互
|
|
52
|
+
- **接口文档解析**:PRD 含接口文档/说明时,提取「资源 → 路径 / 方法 / 返回形状」接口清单,页面 dataSources 以该清单为准;无接口文档则按 `references/data-api.md` 命名约定写 `/api/d/...`
|
|
51
53
|
- **新建工程**:输出全量 **页面清单表**(`references/page-inventory-and-files.md`)
|
|
52
54
|
- **增量**:扫描工程现状清单 + 输出 **delta 表**(add / update / deprecate,`references/incremental-wireframe-delta.md`);P2 不建文件
|
|
53
55
|
|
|
@@ -72,7 +74,7 @@ version: 1.4.0
|
|
|
72
74
|
- 校验见 `page-inventory-and-files.md`(全量)或 `incremental-wireframe-delta.md`(增量)
|
|
73
75
|
- 按 `references/component-mapping.md` 映射组件;按 `references/page-templates.md` 选列表/详情/表单模板(**数据列表页直接复制 `yaml/models/data-list.yaml` 替换占位符**);表单一律 `Form` 包裹
|
|
74
76
|
- 同步更新 `sitemap.yaml` 的 `pages` + `menus`(hidden 详情/编辑带 `activeMenu`)
|
|
75
|
-
- 每页 dataSources 全部 `type: http
|
|
77
|
+
- 每页 dataSources 全部 `type: http`:**有接口文档 → url 取文档接口地址**;无 → 按 `references/data-api.md` 命名约定写 `/api/d/...`(转发层空数据回退,无需 mock);交互 events + actions 闭环
|
|
76
78
|
|
|
77
79
|
### 4. 样式移植
|
|
78
80
|
|
|
@@ -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}` 等,按会话裁剪 |
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 数据接口层(接口文档优先 + 纯转发空数据约定)
|
|
2
|
+
|
|
3
|
+
**接口文档驱动(优先)**:PRD/产品文档含接口文档或接口说明时,页面 dataSources 的 url 直接取文档中的接口地址——方法、参数、返回形状均以文档为准,不再使用下述 `/api/d/...` 命名约定。同源相对路径直接写;跨域绝对地址写完整 URL(`type: http` 原生支持)。转发层与命名约定仅在**无接口文档**时兜底。
|
|
2
4
|
|
|
3
5
|
页面 DSL 的 http 数据源统一入口:`app/api/d/[...parts]/route.ts`(**纯转发层,不含任何路径映射**)。**路径即契约**:DSL 中的 url(如 `/api/d/providers`)就是服务端接口路径;转发层按相同路径、方法、查询与请求体原样转发到 `$NEXT_PUBLIC_API_BASE_URL/api/d/<parts>`(携带会话 cookie,匿名 401)。
|
|
4
6
|
|
|
@@ -25,7 +27,7 @@
|
|
|
25
27
|
|
|
26
28
|
## 对页面 yaml 的要求
|
|
27
29
|
|
|
28
|
-
- dataSources 一律 `type: http
|
|
30
|
+
- dataSources 一律 `type: http`;有接口文档时 url 取文档接口地址(方法/返回形状对齐),否则按命名约定写 `/api/d/...` 最终路径
|
|
29
31
|
- 列表接口返回形状约定 `{ items, total, filters? }`;详情接口对空 id 返回空对象而非 404(新建预填链路依赖)
|
|
30
32
|
- 选项类接口(如下拉的操作人列表)用单段路径,页面 `options: ${data.xxx}` 绑定
|
|
31
33
|
- 写操作走 `ops/` 前缀 POST,页面提交闭环(request → message → reload)不依赖返回体内容
|
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 页面 DSL 引用的自定义组件注册表(页面级自定义组件
|
|
5
5
|
* `components={{...}}`),经 SitemapApp 的 `components` prop 全局注册。
|
|
6
|
+
* ChipFilter / ChipNode 快筛组件已内置在 @pactor-app/ui,无需在此注册。
|
|
6
7
|
*/
|
|
7
|
-
import { ChipNode } from "./chip-node";
|
|
8
|
-
import { ChipFilter } from "./chip-filter";
|
|
9
8
|
import { StatusBadge } from "./status-badge";
|
|
10
9
|
import { LogStatus } from "./log-status";
|
|
11
10
|
import { DebugChat } from "./debug-chat";
|
|
@@ -13,8 +12,6 @@ import { CardLink, StatusToggleIcon, TextLink } from "./card-links";
|
|
|
13
12
|
import { ThemeSwitcher } from "./theme-switcher";
|
|
14
13
|
|
|
15
14
|
export const pactorComponents: Record<string, unknown> = {
|
|
16
|
-
ChipNode,
|
|
17
|
-
ChipFilter,
|
|
18
15
|
StatusBadge,
|
|
19
16
|
LogStatus,
|
|
20
17
|
DebugChat,
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 快筛 Chip 组(列表页共享的自定义组件):Chip 列表整体作为单一组件,
|
|
5
|
-
* 选项(value / 文案 / 计数 / 色点)由接口返回,页面 DSL 不再逐枚硬编码 ChipNode。
|
|
6
|
-
* 选项文案含计数,由接口侧预计算(表达式引擎不支持数组方法与函数调用)。
|
|
7
|
-
*
|
|
8
|
-
* 兼容各列表页的两种交互(Provider / Model / Route / Client / ClientTemplate):
|
|
9
|
-
* - mode="single":单选,value 为字符串。点击已选项回到 allValue(默认 'all'),
|
|
10
|
-
* 与「状态 / 类型」单选快筛的 toggle 语义一致;
|
|
11
|
-
* - mode="multi":多选,value 为已选值数组。选项中含 value === allValue 的
|
|
12
|
-
* 「全部」项时,点击它清空选择、仅在无选择时高亮(标签 / 厂商快筛语义);
|
|
13
|
-
* 不含「全部」项时纯切换、不选即全部(Provider 类型快筛语义)。
|
|
14
|
-
*
|
|
15
|
-
* DSL 用法(${event} = 新值,single 为字符串 / multi 为数组):
|
|
16
|
-
* {
|
|
17
|
-
* type: "ChipFilter",
|
|
18
|
-
* props: {
|
|
19
|
-
* mode: single,
|
|
20
|
-
* value: "${state.statusChip}",
|
|
21
|
-
* items: "${data.providers.filters.status}",
|
|
22
|
-
* },
|
|
23
|
-
* events: { onChange: [setState, reload] },
|
|
24
|
-
* }
|
|
25
|
-
*/
|
|
26
|
-
import { ChipNode } from "./chip-node";
|
|
27
|
-
|
|
28
|
-
export interface ChipFilterItem {
|
|
29
|
-
/** 提交值('all' 为约定的「全部」项,可用 allValue 改写) */
|
|
30
|
-
value: string;
|
|
31
|
-
/** 展示文案(含计数,接口预计算) */
|
|
32
|
-
text: string;
|
|
33
|
-
/** 可选状态色点 */
|
|
34
|
-
dot?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function ChipFilter({
|
|
38
|
-
items,
|
|
39
|
-
mode = "single",
|
|
40
|
-
value,
|
|
41
|
-
allValue = "all",
|
|
42
|
-
onChange,
|
|
43
|
-
}: {
|
|
44
|
-
items?: ChipFilterItem[];
|
|
45
|
-
mode?: "single" | "multi";
|
|
46
|
-
value?: string | string[];
|
|
47
|
-
allValue?: string;
|
|
48
|
-
onChange?: (value: string | string[]) => void;
|
|
49
|
-
}) {
|
|
50
|
-
if (!Array.isArray(items)) return null;
|
|
51
|
-
const selectedValues = Array.isArray(value) ? value : [];
|
|
52
|
-
return (
|
|
53
|
-
<>
|
|
54
|
-
{items.map((item) => {
|
|
55
|
-
const selected =
|
|
56
|
-
mode === "multi"
|
|
57
|
-
? item.value === allValue
|
|
58
|
-
? selectedValues.length === 0
|
|
59
|
-
: selectedValues.includes(item.value)
|
|
60
|
-
: (value ?? allValue) === item.value;
|
|
61
|
-
return (
|
|
62
|
-
<ChipNode
|
|
63
|
-
key={item.value}
|
|
64
|
-
text={item.text}
|
|
65
|
-
dot={item.dot}
|
|
66
|
-
selected={selected}
|
|
67
|
-
onClick={() => {
|
|
68
|
-
if (mode === "multi") {
|
|
69
|
-
if (item.value === allValue) {
|
|
70
|
-
onChange?.([]);
|
|
71
|
-
} else {
|
|
72
|
-
onChange?.(
|
|
73
|
-
selected
|
|
74
|
-
? selectedValues.filter((v) => v !== item.value)
|
|
75
|
-
: [...selectedValues, item.value],
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
} else {
|
|
79
|
-
onChange?.(
|
|
80
|
-
selected && item.value !== allValue ? allValue : item.value,
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
}}
|
|
84
|
-
/>
|
|
85
|
-
);
|
|
86
|
-
})}
|
|
87
|
-
</>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 快筛 Chip(Provider / Model / Route 列表页共享的自定义组件)。
|
|
5
|
-
* 未选中 = outline 小按钮(border-border,hover 走 accent);
|
|
6
|
-
* 选中 = 主色实底 + 主色前景文字(一眼可辨的激活态)。
|
|
7
|
-
* text / selected 由运行时对 props 模板求值后传入(selected 接收布尔表达式结果);
|
|
8
|
-
* dot 为可选状态色点(Provider 状态快筛使用);
|
|
9
|
-
* 点击动作由 DSL 节点 events.onClick 注入(运行时执行为回调,语义同 Button onClick)。
|
|
10
|
-
*
|
|
11
|
-
* DSL 用法:
|
|
12
|
-
* {
|
|
13
|
-
* type: "ChipNode",
|
|
14
|
-
* props: {
|
|
15
|
-
* text: "全部 ${counts.all.enabled}/${counts.all.total}",
|
|
16
|
-
* selected: "${state.statusChip == 'all'}",
|
|
17
|
-
* },
|
|
18
|
-
* events: { onClick: [setState / reload ...] },
|
|
19
|
-
* }
|
|
20
|
-
*/
|
|
21
|
-
export function ChipNode({
|
|
22
|
-
text,
|
|
23
|
-
selected,
|
|
24
|
-
dot,
|
|
25
|
-
onClick,
|
|
26
|
-
}: {
|
|
27
|
-
text?: string;
|
|
28
|
-
selected?: boolean;
|
|
29
|
-
dot?: string;
|
|
30
|
-
onClick?: () => void;
|
|
31
|
-
}) {
|
|
32
|
-
return (
|
|
33
|
-
<button
|
|
34
|
-
type="button"
|
|
35
|
-
onClick={onClick}
|
|
36
|
-
className={`inline-flex h-8 shrink-0 cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-xl border px-3 text-xs font-medium outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 ${
|
|
37
|
-
selected
|
|
38
|
-
? "border-primary bg-primary text-primary-foreground shadow-sm"
|
|
39
|
-
: "border-border bg-card hover:bg-accent hover:text-accent-foreground"
|
|
40
|
-
}`}
|
|
41
|
-
>
|
|
42
|
-
{dot ? <span style={{ color: dot, fontSize: 10 }}>●</span> : null}
|
|
43
|
-
{text}
|
|
44
|
-
</button>
|
|
45
|
-
);
|
|
46
|
-
}
|