@pactor-app/skills 1.34.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.
Files changed (43) hide show
  1. package/package.json +1 -1
  2. package/pactor-prd/SKILL.md +18 -21
  3. package/pactor-prd/references/acceptance-checklist.md +1 -1
  4. package/pactor-prd/references/data-api.md +35 -0
  5. package/pactor-prd/references/incremental-wireframe-delta.md +8 -9
  6. package/pactor-prd/references/page-inventory-and-files.md +2 -2
  7. package/pactor-prd/references/page-templates.md +59 -12
  8. package/pactor-prd/references/project-structure.md +1 -1
  9. package/pactor-prd/template/README.md +4 -4
  10. package/pactor-prd/template/app/api/d/[...parts]/route.ts +80 -0
  11. package/pactor-prd/template/app/layout.tsx +1 -3
  12. package/pactor-prd/template/next.config.js +2 -2
  13. package/pactor-prd/template/pnpm-lock.yaml +97 -97
  14. package/pactor-prd/template/src/pactor/App.tsx +3 -2
  15. package/pactor-prd/template/src/pactor/actions.ts +5 -431
  16. package/pactor-prd/template/src/pactor/config.ts +12 -4
  17. package/pactor-prd/template/src/server/yaml/common/dashboard.yaml +1 -1
  18. package/pactor-prd/template/src/server/yaml/common/login.yaml +46 -75
  19. package/pactor-prd/template/src/server/yaml/models/data-list.yaml +4 -4
  20. package/pactor-prd/template/src/server/yaml/provider/provider-detail.yaml +2 -2
  21. package/pactor-prd/template/src/server/yaml/provider/provider-edit.yaml +1 -1
  22. package/pactor-prd/template/src/server/yaml/provider/provider-list.yaml +1 -1
  23. package/pactor-prd/template/src/server/yaml/route/route-detail.yaml +1 -1
  24. package/pactor-prd/template/src/server/yaml/route/route-edit.yaml +1 -1
  25. package/pactor-prd/template/src/server/yaml/route/route-list.yaml +1 -1
  26. package/pactor-prd/template/src/server/yaml/sitemap.yaml +2 -6
  27. package/pactor-prd/references/mock-data-routes.md +0 -66
  28. package/pactor-prd/template/app/api/data/[...parts]/route.ts +0 -160
  29. package/pactor-prd/template/app/login.css +0 -122
  30. package/pactor-prd/template/src/pactor/mocks/audit-mock.ts +0 -403
  31. package/pactor-prd/template/src/pactor/mocks/call-logs-mock.ts +0 -875
  32. package/pactor-prd/template/src/pactor/mocks/client-debug-mock.ts +0 -188
  33. package/pactor-prd/template/src/pactor/mocks/client-templates-mock.ts +0 -497
  34. package/pactor-prd/template/src/pactor/mocks/clients-mock.ts +0 -483
  35. package/pactor-prd/template/src/pactor/mocks/costs-mock.ts +0 -406
  36. package/pactor-prd/template/src/pactor/mocks/dashboard-mock.ts +0 -192
  37. package/pactor-prd/template/src/pactor/mocks/data-list-mock.ts +0 -84
  38. package/pactor-prd/template/src/pactor/mocks/model-debug-mock.ts +0 -144
  39. package/pactor-prd/template/src/pactor/mocks/models-mock.ts +0 -799
  40. package/pactor-prd/template/src/pactor/mocks/ops-usage-mock.ts +0 -806
  41. package/pactor-prd/template/src/pactor/mocks/providers-mock.ts +0 -424
  42. package/pactor-prd/template/src/pactor/mocks/routes-mock.ts +0 -604
  43. package/pactor-prd/template/src/pactor/services.ts +0 -200
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pactor-app/skills",
3
- "version": "1.34.0",
3
+ "version": "1.35.0",
4
4
  "description": "Agent skills for rapidly building pactor Server-Driven UI applications",
5
5
  "keywords": [
6
6
  "pactor",
@@ -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, mock API, styles). Triggers: pactor-prd, prd-to-app, 线框图增量, 已有项目加页面, PRD 更新页面.
5
- version: 1.3.0
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.4.0
6
6
  ---
7
7
 
8
8
  # PRD → YAML 页面应用生成
9
9
 
10
- 把「产品文档 + Markdown 线框图」落到 **YAML 页面 + sitemap/mock/样式**;支持 **两种模式**(先选型再执行):
10
+ 把「产品文档 + Markdown 线框图」落到 **YAML 页面 + sitemap/数据接口/样式**;支持 **两种模式**(先选型再执行):
11
11
 
12
12
  | 模式 | 何时用 | 脚手架 |
13
13
  | --- | --- | --- |
@@ -25,20 +25,21 @@ version: 1.3.0
25
25
 
26
26
  ## 红线(必须遵守)
27
27
 
28
- 1. **dataSources 一律 `type: http`**,指向 `/api/data/...`,不写 `type: service`,便于后续替换真实接口
28
+ 1. **dataSources 一律 `type: http`**,指向 `/api/d/...`,不写 `type: service`,便于后续替换真实接口
29
29
  2. **布局与间距以样板页为准**:新建工程时以 `template/` 同类页为准;**增量时以目标工程内同模块/同类型页**为准。线框图只提取结构与功能,不逐像素还原 ASCII
30
- 3. **样式与原产品一致**:主题令牌(含深浅色)、自定义 CSS(如登录页)、换肤入口全部移植对齐,见 `references/theme-and-style.md`
30
+ 3. **样式与原产品一致**:主题令牌(含深浅色)、自定义 CSS、换肤入口全部移植对齐,见 `references/theme-and-style.md`
31
31
  4. **功能与原产品一致**:线框图描述的筛选/搜索/跳转/弹窗/提交等交互必须经 events/actions 闭环可用,禁止只画静态骨架;错误分支(校验失败提示等)也要实现
32
32
  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
33
  6. **列表页操作列规范**:操作列按钮样式统一 `Button variant: link`;「编辑」与「新建」指向同一个表单(同一路由页或同一弹窗),唯一区别是编辑以行数据预填(路由页 `initialValues: ${data.detail}`,弹窗/内嵌面板 `form.setValues`);**数据列表页(搜索 + 表格 + 新建/编辑)以 `template/src/server/yaml/models/data-list.yaml` 为标准骨架生成**(结构与要点见 `references/page-templates.md`「数据列表标准模型」)
34
34
  7. **文案以线框图为准**:标题、按钮、空态文案逐字取自线框图/PRD
35
- 8. DSL 语法细节(组件 props、表达式、动作)以 `pactor-dsl` skill 与 packages/ui 当前导出为准,本 skill 只约定流程与产出物
35
+ 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` 模式
36
+ 9. DSL 语法细节(组件 props、表达式、动作)以 `pactor-dsl` skill 与 packages/ui 当前导出为准,本 skill 只约定流程与产出物
36
37
 
37
38
  ## 流程总览
38
39
 
39
40
  ```text
40
- 选型 → 1 解析 → 2 脚手架(仅新建工程)→ 3 页面 → 4 mock → 5 样式 → 6 验收
41
- 增量:2 改为「摸底」;1/3 只处理 delta;6 变更页全验 + 少量回归
41
+ 选型 → 1 解析 → 2 脚手架(仅新建工程)→ 3 页面 → 4 样式 → 5 验收
42
+ 增量:2 改为「摸底」;1/3 只处理 delta;5 变更页全验 + 少量回归
42
43
  ```
43
44
 
44
45
  详细增量步骤:**`references/incremental-wireframe-delta.md`**。
@@ -57,7 +58,7 @@ version: 1.3.0
57
58
  - **整体复制 `template/`** 为新项目起点;不要用 `pnpm create pactor-app` 替代
58
59
  - 工程名:README 产品名 → kebab-case 英文名,创建前确认一次
59
60
  - `pnpm install` + dev 验证;核对 `references/project-structure.md`
60
- - 参考模块 provider/route 作样板;`yaml/models/` 是页面骨架目录(按模块目录结构存放,已注册 sitemap、菜单「页面骨架」分组可直接预览),生成时复制到目标模块目录改字段与文案;**全站生成完成后删除**参考模块、models/ 及对应 mock
61
+ - 参考模块 provider/route 作样板;`yaml/models/` 是页面骨架目录(按模块目录结构存放,已注册 sitemap、菜单「页面骨架」分组可直接预览),生成时复制到目标模块目录改字段与文案;**全站生成完成后删除**参考模块与 models/
61
62
 
62
63
  **增量**
63
64
 
@@ -71,17 +72,13 @@ version: 1.3.0
71
72
  - 校验见 `page-inventory-and-files.md`(全量)或 `incremental-wireframe-delta.md`(增量)
72
73
  - 按 `references/component-mapping.md` 映射组件;按 `references/page-templates.md` 选列表/详情/表单模板(**数据列表页直接复制 `yaml/models/data-list.yaml` 替换占位符**);表单一律 `Form` 包裹
73
74
  - 同步更新 `sitemap.yaml` 的 `pages` + `menus`(hidden 详情/编辑带 `activeMenu`)
74
- - 每页 dataSources 全部 `type: http`;交互 events + actions 闭环
75
+ - 每页 dataSources 全部 `type: http`,url 按 `references/data-api.md` 命名约定直接写最终接口路径(无后端时转发层返回空数据,无需 mock);交互 events + actions 闭环
75
76
 
76
- ### 4. mock 服务
77
-
78
- - 按 `references/mock-data-routes.md` 注册/更新路由表与 `src/pactor/mocks/`;**增量只动变更页涉及的接口**
79
-
80
- ### 5. 样式移植
77
+ ### 4. 样式移植
81
78
 
82
79
  - 按 `references/theme-and-style.md`;**增量**仅受影响页面/令牌(新建工程做全站对齐)
83
80
 
84
- ### 6. 功能与视觉验收(必经,不可跳过)
81
+ ### 5. 功能与视觉验收(必经,不可跳过)
85
82
 
86
83
  - `references/acceptance-checklist.md`:**新建工程**逐页全站验收;**增量**对 **所有变更页** 全项验收 + **至少 1~2 个未改页回归**(菜单可达、控制台无新增 error)
87
84
 
@@ -90,16 +87,16 @@ version: 1.3.0
90
87
  | 文件 | 何时加载 |
91
88
  | --- | --- |
92
89
  | `template/` | 阶段 2 **仅新建工程**:复制为新工程起点;增量勿用 |
93
- | `references/incremental-wireframe-delta.md` | **已有工程**:线框图 add/update/deprecate、改 sitemap/mock/样式 |
90
+ | `references/incremental-wireframe-delta.md` | **已有工程**:线框图 add/update/deprecate、改 sitemap/数据接口/样式 |
94
91
  | `references/project-structure.md` | 阶段 2:目录结构约定与 sitemap.yaml 骨架 |
95
92
  | `references/component-mapping.md` | 阶段 3:线框图区域 → pactor 组件映射 |
96
93
  | `references/page-templates.md` | 阶段 3:列表/详情/表单页 yaml 模板;数据列表页默认用「数据列表标准模型」(搜索 Form + 表格 + 抽屉新建/编辑) |
97
- | `references/mock-data-routes.md` | 阶段 4:mock 路由表模板与命名约定 |
98
- | `references/theme-and-style.md` | 阶段 5:主题令牌提取、皮肤、自定义 CSS、换肤 |
99
- | `references/acceptance-checklist.md` | 阶段 6:验收清单与结果输出格式 |
94
+ | `references/data-api.md` | 阶段 3:数据接口层约定(纯转发、路径即契约、空数据回退、路径命名) |
95
+ | `references/theme-and-style.md` | 阶段 4:主题令牌提取、皮肤、自定义 CSS、换肤 |
96
+ | `references/acceptance-checklist.md` | 阶段 5:验收清单与结果输出格式 |
100
97
  | `references/page-inventory-and-files.md` | 阶段 1–3:清单列 → 目录与文件批量创建、与 sitemap 对齐 |
101
98
 
102
99
  ## Sibling skills
103
100
 
104
- - **`pactor-dsl`** — 无线框图的单页/清单加页;**有线框图 + mock/sitemap 联动**仍用本 skill 增量模式
101
+ - **`pactor-dsl`** — 无线框图的单页/清单加页;**有线框图 + 数据接口/sitemap 联动**仍用本 skill 增量模式
105
102
  - **`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/data/project-members` 而非 `project/members`)
48
+ - **两段数据路径被当详情解析**:选项类接口用单段路径(`/api/d/project-members` 而非 `project/members`)
49
49
  - **toast 消失快误判未触发**:验证错误提示时在操作后立即截图/读 DOM
@@ -0,0 +1,35 @@
1
+ # 数据接口层(纯转发 + 空数据约定)
2
+
3
+ 页面 DSL 的 http 数据源统一入口:`app/api/d/[...parts]/route.ts`(**纯转发层,不含任何路径映射**)。**路径即契约**:DSL 中的 url(如 `/api/d/providers`)就是服务端接口路径;转发层按相同路径、方法、查询与请求体原样转发到 `$NEXT_PUBLIC_API_BASE_URL/api/d/<parts>`(携带会话 cookie,匿名 401)。
4
+
5
+ **没有服务层接口时返回空数据(不报错)**:上游不可达(连接失败)、接口不存在(404)或返回非 JSON(如命中前端兜底 HTML)时,GET 返回 `{}`、POST 返回 `{ ok: true }`,页面按空态渲染。接真实后端只需把 `NEXT_PUBLIC_API_BASE_URL`(next.config.js 的 `API_BASE_URLS`)换成真实地址,页面 yaml 与转发层零改动。
6
+
7
+ ## 命名约定
8
+
9
+ - **单段路径**:`/api/d/projects`、`/api/d/project-members`——列表/选项类一律单段 kebab-case
10
+ - **禁止** `/api/d/project/members` 这种两段写法:`[...parts]` 会把第二段当详情 id 解析(`project/detail?id=members`),踩过的坑
11
+ - 详情:`/api/d/<resource>/<code|id>`;子资源:`/api/d/<resource>/<code>/<sub>`;写操作:`/api/d/ops/<action>`(POST)
12
+ - 数据源 url 与页面 id 语义对应:`project-list` 页 → `/api/d/projects`
13
+
14
+ ## 转发层结构
15
+
16
+ ```text
17
+ 浏览器(同源 /api/d/*)
18
+ → app/api/d/[...parts]/route.ts 纯转发:会话鉴权(匿名 401)→
19
+ fetch $NEXT_PUBLIC_API_BASE_URL/api/d/<parts>
20
+ 不可达 / 404 / 非 JSON → 空数据(GET {} / POST { ok: true })
21
+ → 服务端接口 真实后端(与 DSL 同路径契约)
22
+ ```
23
+
24
+ 完整实现见模板工程 `app/api/d/[...parts]/route.ts`。
25
+
26
+ ## 对页面 yaml 的要求
27
+
28
+ - dataSources 一律 `type: http`,url 按命名约定直接写最终接口路径
29
+ - 列表接口返回形状约定 `{ items, total, filters? }`;详情接口对空 id 返回空对象而非 404(新建预填链路依赖)
30
+ - 选项类接口(如下拉的操作人列表)用单段路径,页面 `options: ${data.xxx}` 绑定
31
+ - 写操作走 `ops/` 前缀 POST,页面提交闭环(request → message → reload)不依赖返回体内容
32
+
33
+ ## 替换为真实接口
34
+
35
+ 把 `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
- - PRD 风格 mock:`app/api/data/[...parts]/route.ts` + `src/pactor/mocks/`(若工程仍用 `type: service`,增量页 **仍须 http**,并迁移或仅对新页遵守红线)
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`;mock 路由表 + mocks 文件 |
44
+ | **add 页面** | `mkdir` 模块目录;新建 yaml;`sitemap.yaml` 增加 `pages` + `menus`;dataSources 直接声明最终接口路径(无需注册) |
45
45
  | **update 页面** | 改对应 yaml(结构/字段/交互/文案);若 path 或 id 变则同步 `sitemap.yaml`、检查 `router.push` / 菜单 `pageId` |
46
- | **update 仅文案/字段** | 只改 yaml;mock 字段变更时改 `mocks/*` 与路由返回形状 |
47
- | **add 数据源** | yaml 内 `dataSources` + `app/api/data/...` 注册 + mock handler |
48
- | **样式** | 仅当线框图要求新页面级样式时改 `globals.css` / `login.css` / 皮肤;见 `theme-and-style.md` |
49
- | **deprecate** | 删 yaml;从 `pages`/`menus` 移除;删 mock 路由项;全局搜 `pageId` / 路径引用 |
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
- mock:变更页涉及的所有 http 路径在 Network 200
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
- - **线框图驱动**、含 mock/sitemap/多页联动:用 **本增量流程**(仍属 `pactor-prd`)。
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. **mock 批量注册**:按 `references/mock-data-routes.md`,每个 http 数据源在 `app/api/data/[...parts]/route.ts` 路由表增加一项;`src/pactor/mocks/` 按域拆分文件。
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/` 作写法样本;新模块 **照同样目录深度** 新增;全站生成完成后删除参考模块及其 mock 路由项。
56
+ 复制 `pactor-prd/template/` 后,保留 `yaml/provider/`、`yaml/route/` 作写法样本;新模块 **照同样目录深度** 新增;全站生成完成后删除参考模块。
57
57
 
58
58
  ## 已有工程(增量)
59
59
 
@@ -1,12 +1,12 @@
1
1
  # 页面 YAML 模板
2
2
 
3
- 常用页面模板。占位符:`{{kebab-id}}` 页面 id、`{{页面标题}}`、`{{模块名}}`、`{{resource}}` 数据接口名(单段,见 mock-data-routes.md)、`{{实体名}}` 业务实体中文名。DSL 语法细节以 `pactor-dsl` skill 与 packages/ui 当前导出为准。
3
+ 常用页面模板。占位符:`{{kebab-id}}` 页面 id、`{{页面标题}}`、`{{模块名}}`、`{{resource}}` 数据接口名(单段,见 data-api.md)、`{{实体名}}` 业务实体中文名。DSL 语法细节以 `pactor-dsl` skill 与 packages/ui 当前导出为准。
4
4
 
5
5
  **数据列表页(搜索 + 表格 + 新建/编辑)默认套用「数据列表标准模型」**;仅当新建/编辑是独立路由表单页时才用轻量变体。
6
6
 
7
7
  ## 数据列表标准模型(搜索 Form + 表格 + 抽屉新建/编辑)
8
8
 
9
- 从成熟实现提取的完整闭环:搜索过滤、新建/编辑共用抽屉表单(分区 + 条件显隐 + 预填)、操作列 link 按钮。**完整可拷贝的骨架文件:`template/src/server/yaml/models/data-list.yaml`**(已注册进模板 sitemap 的可运行示例,菜单「页面骨架 / 数据列表」直接预览;复制后替换页面信息、字段、接口名与文案即可,配套 mock 为 `template/src/pactor/mocks/data-list-mock.ts`)。结构照抄,字段按线框图替换:
9
+ 从成熟实现提取的完整闭环:搜索过滤、新建/编辑共用抽屉表单(分区 + 条件显隐 + 预填)、操作列 link 按钮。**完整可拷贝的骨架文件:`template/src/server/yaml/models/data-list.yaml`**(已注册进模板 sitemap 的可运行示例,菜单「页面骨架 / 数据列表」直接预览;复制后替换页面信息、字段、接口名与文案即可)。结构照抄,字段按线框图替换:
10
10
 
11
11
  ```yaml
12
12
  dsl: "1.0"
@@ -27,7 +27,7 @@ dataSources:
27
27
  autoLoad: true
28
28
  request:
29
29
  method: GET
30
- url: /api/data/{{resource}}
30
+ url: /api/d/{{resource}}
31
31
  params:
32
32
  keyword: ${state.keyword}
33
33
  status: ${state.status}
@@ -36,7 +36,7 @@ dataSources:
36
36
  autoLoad: false
37
37
  request:
38
38
  method: GET
39
- url: /api/data/{{resource}}/${state.editingId}
39
+ url: /api/d/{{resource}}/${state.editingId}
40
40
  body:
41
41
  type: Page
42
42
  children:
@@ -99,7 +99,7 @@ body:
99
99
  - action: request
100
100
  params:
101
101
  method: POST
102
- url: /api/data/ops/{{resource}}-save
102
+ url: /api/d/ops/{{resource}}-save
103
103
  body:
104
104
  id: ${state.editingId}
105
105
  name: ${event.name}
@@ -332,11 +332,58 @@ body:
332
332
  - **新建/编辑共用同一抽屉**:YAML 锚点 `&entityDrawer` 在「新建」按钮的 `overlay.open` 中定义(必须先出现),表格行内 `*entityDrawer` 引用;`title` 用 `${state.editingId == '' ? ... }` 三元切换;唯一区别是编辑以 `initialValues: ${data.editing}` 预填
333
333
  - **抽屉宽度 720**(`overlay.open type: drawer` 的 `width` 生效);抽屉内分区**不用 Card**:`Text(strong) 标题 → Separator → 字段组`,多个分区重复此结构
334
334
  - **搜索栏按钮对齐**:查询/重置放进 `Field`,其 `label` 用不间断空格 U+00A0(直接敲 `" "` 普通空格可能被压缩),与左侧控件基线对齐
335
- - **Form 不能嵌套**(渲染原生 `<form>`,submit 冒泡):抽屉内只用一个 Form;主从多值(如字典项)的子项编辑在**同一 Form** 内做「子 Table + 行下展开表单」,`onSubmit` 用 `if` 动作按 state 分流提交目标(子项保存走独立 ops 接口 + `reload editing`);表达式不支持数组方法/箭头函数,子项草稿必须走 mock 接口不能前端拼
335
+ - **Form 不能嵌套**(渲染原生 `<form>`,submit 冒泡):抽屉内只用一个 Form;主从多值(如字典项)的子项编辑在**同一 Form** 内做「子 Table + 行下展开表单」,`onSubmit` 用 `if` 动作按 state 分流提交目标(子项保存走独立 ops 接口 + `reload editing`);表达式不支持数组方法/箭头函数,子项草稿必须走服务端接口不能前端拼
336
336
  - **编辑预填链路**:`setState(editingId + 显隐镜像)` → `reload editing` → `overlay.open`;镜像值取自行数据(`${row.type}`),不要等 editing 接口
337
337
  - **overlay.open 阻塞到关闭**,关闭后 `reload list`;新建按钮同理(先重置 state 再 reload editing 清空)
338
338
  - **条件显隐**:`onValuesChange` 把字段值镜像到 state(`"${event.x != null ? event.x : state.x}"` 保持其他字段变化时原值),再用 `when` 控制显隐;编辑时关键字段 `disabled: ${state.editingId != ''}`
339
- - 空 `editingId` 时 mock 的 detail 接口须返回空对象而非 404
339
+ - 空 `editingId` 时后端 detail 接口须返回空对象而非 404(转发层本身对 404 也回退空对象,但真实后端仍建议遵守该约定)
340
+
341
+ ## 登录页(Login 业务组件)
342
+
343
+ 登录页一律用 DSL `type: Login` 业务组件,禁止手写 Card+Form+自定义 CSS。**样板即模板工程的 `template/src/server/yaml/common/login.yaml`**(可直接运行预览),核心结构:
344
+
345
+ ```yaml
346
+ body:
347
+ type: Login
348
+ props:
349
+ layout: split # split(默认) / centered / brand-card / overlap / backdrop
350
+ authVariant: card # 认证区容器:plain 裸表单 / card 常规卡片 / float 大悬浮卡片
351
+ brand: # 品牌区(文案取自 PRD;illustration 缺省内置插画,传 "" 隐藏)
352
+ name: {{产品名}}
353
+ title: {{品牌主标题}}
354
+ description: {{品牌说明}}
355
+ title: 登录
356
+ subtitle: {{副标题}}
357
+ submitText: 登 录
358
+ # registerLink / forgotLink / footer 按需添加(href 跳转或 onRegisterClick/onForgotClick 事件)
359
+ events:
360
+ onSubmit: # values 经 ${event.username} / ${event.password} 读取
361
+ - id: loginResult
362
+ action: request
363
+ params:
364
+ method: POST
365
+ url: {{登录接口地址}}
366
+ body: { username: ${event.username}, password: ${event.password} }
367
+ - action: if
368
+ params:
369
+ condition: ${actions.loginResult == null || actions.loginResult.token == null}
370
+ then:
371
+ - action: message
372
+ params: { messageType: error, text: 用户名或密码错误 }
373
+ - action: if
374
+ params:
375
+ condition: ${actions.loginResult != null && actions.loginResult.token != null}
376
+ then:
377
+ - action: app.reload
378
+ params: { to: / }
379
+ ```
380
+
381
+ 要点:
382
+
383
+ - **布局是产品决策**:原型未明确布局时必须向用户提问(5 种 `layout` + `brandSide`/`brandRatio`/`authVariant` 等修饰),用户不答默认 `split` + `card`;原型可判定则不提问
384
+ - 页面布局(sitemap `layouts`)用纯 PageOutlet 外壳(模板为 `login` 布局),组件自带整页渐变、背景装饰与品牌面板,不要再套自定义背景容器
385
+ - 可配置项权威定义见 `openspec/specs/ui-login-component` 与组件源码(`packages/ui/src/components/business/Login/`);未配置的可选元素不渲染
386
+ - 校验失败由组件内部 Form rules 就地提示;登录失败/成功分支在 DSL 动作链用 `if` + `message` 实现
340
387
 
341
388
  ## 列表页(轻量变体:新建/编辑跳独立表单页)
342
389
 
@@ -357,7 +404,7 @@ dataSources:
357
404
  autoLoad: true
358
405
  request:
359
406
  method: GET
360
- url: /api/data/{{resource}}
407
+ url: /api/d/{{resource}}
361
408
  params:
362
409
  keyword: ${state.keyword}
363
410
  status: ${state.status}
@@ -460,7 +507,7 @@ dataSources:
460
507
  autoLoad: true
461
508
  request:
462
509
  method: GET
463
- url: /api/data/{{resource}}/${route.params.code} # 路径参数经 route 注入
510
+ url: /api/d/{{resource}}/${route.params.code} # 路径参数经 route 注入
464
511
  body:
465
512
  type: Page
466
513
  children:
@@ -504,7 +551,7 @@ dataSources:
504
551
  autoLoad: true
505
552
  request:
506
553
  method: GET
507
- url: /api/data/{{resource}}/${route.params.code}
554
+ url: /api/d/{{resource}}/${route.params.code}
508
555
  body:
509
556
  type: Page
510
557
  children:
@@ -533,9 +580,9 @@ body:
533
580
  onSubmit:
534
581
  - action: setState
535
582
  params: { submitting: true }
536
- - action: http.post # mock 阶段可指向 /api/data/{{resource}}
583
+ - action: http.post # 指向 /api/d/{{resource}}(无后端时转发层返回 { ok: true })
537
584
  params:
538
- url: /api/data/{{resource}}
585
+ url: /api/d/{{resource}}
539
586
  body: ${event}
540
587
  - action: message
541
588
  params: { messageType: success, text: 保存成功 }
@@ -10,7 +10,7 @@
10
10
  │ │ ├── sitemap/route.ts ✅ 返回 sitemap 入口数据(由 src/server/sitemap.ts 组装)
11
11
  │ │ ├── public/pages/[id]/route.ts ➕ 懒加载页面端点,返回单页 envelope(id/resourceType/version/dslVersion/schema/updatedAt)
12
12
  │ │ ├── auth/ · users/ ✅ 模板自带(演示账号,按需保留)
13
- │ │ └── data/[...path]/route.ts ➕ mock 数据统一入口(见 mock-data-routes.md)
13
+ │ │ └── d/[...parts]/route.ts ➕ 数据统一入口(纯转发 → $NEXT_PUBLIC_API_BASE_URL;无后端返回空数据,见 data-api.md)
14
14
  │ ├── globals.css ✅ 全局样式(皮肤导入处)
15
15
  │ ├── layout.tsx ✅ 根布局(自定义页面 CSS 在这里挂载)
16
16
  │ └── theme.css ✅ 主题令牌(样式移植的主战场)
@@ -1,6 +1,6 @@
1
1
  # pactor-prd 模板工程
2
2
 
3
- 可运行的参考实现:Next.js + pactor,页面全部 YAML DSL 描述(`src/server/yaml/`),数据一律 http 数据源 + mock 路由(`app/api/data/[...path]`)。
3
+ 可运行的参考实现:Next.js + pactor,页面全部 YAML DSL 描述(`src/server/yaml/`),数据一律 http 数据源,经纯转发层(`app/api/d/[...parts]`)转发到 `NEXT_PUBLIC_API_BASE_URL`;没有服务层接口时返回空数据,页面按空态渲染。
4
4
 
5
5
  ## 包含内容
6
6
 
@@ -8,7 +8,7 @@
8
8
  - **两个参考模块**:
9
9
  - `yaml/provider/`(供应商:卡片式列表 / 详情 / 新增 / 编辑)
10
10
  - `yaml/route/`(路由:卡片式列表 / 单页纵向详情 / 单页表单新增 / 编辑)
11
- - **完整接线**:`src/pactor/`(SitemapApp 接线、pageEndpoint、皮肤加载、自定义组件与动作)、`src/server/`(yaml 递归加载 + 会话裁剪)、`app/api/`(sitemap / public/pages / data / auth / users)
11
+ - **完整接线**:`src/pactor/`(SitemapApp 接线、pageEndpoint、皮肤加载、自定义组件与动作)、`src/server/`(yaml 递归加载 + 会话裁剪)、`app/api/`(sitemap / public/pages / d 转发层 / auth / users)
12
12
  - **三套皮肤**:`public/skins/`(深浅色俱全),侧栏换肤入口
13
13
 
14
14
  ## 使用方式
@@ -18,8 +18,8 @@
18
18
  1. `package.json` 的 name 改为新工程名
19
19
  2. `src/server/yaml/sitemap.yaml`:app 名称、pages、menus 按 PRD 页面清单重写(保留 layouts)
20
20
  3. 保留 `common/` 公共页面按需调整;参考模块目录可保留作样板,新模块照其结构新增
21
- 4. mock 数据在 `src/pactor/mocks/` 与 `app/api/data/[...path]/route.ts` 路由表中按新域扩展
22
- 5. `pnpm install && pnpm dev`(端口 9011)验证
21
+ 4. 数据接口:dataSources 的 url 直接写最终接口路径(`/api/d/...`,路径即契约);接真实后端时改 `next.config.js` 的 `API_BASE_URLS` 即可,页面 yaml 不动
22
+ 5. `pnpm install && pnpm dev`(端口 9011)验证(无后端时数据区为空态)
23
23
 
24
24
  ## 演示账号
25
25
 
@@ -0,0 +1,80 @@
1
+ import { cookies } from "next/headers";
2
+
3
+ import { serverBaseUrl } from "@/pactor/config";
4
+ import { findUserBySession, SESSION_COOKIE } from "@/server/accounts";
5
+ import { corsHeaders, corsPreflight } from "@/server/cors";
6
+
7
+ /**
8
+ * 页面数据接口统一入口(纯转发):页面 DSL 的 http 数据源(如
9
+ * /api/d/providers)即服务端接口路径,本路由按相同路径、方法、查询与
10
+ * 请求体原样转发到 $serverBaseUrl/api/d/<parts>(src/pactor/config.ts),
11
+ * 并携带会话 cookie;匿名一律 401,不转发。
12
+ *
13
+ * 没有服务层接口时返回空数据(不报错):上游不可达(连接失败)、接口
14
+ * 不存在(404)或返回非 JSON(如命中前端兜底的 HTML)时,GET 返回 {}、
15
+ * POST 返回 { ok: true },页面按空态渲染;接真实后端只需把
16
+ * next.config.js 的 API_BASE_URLS 换成真实地址,页面 yaml 与本路由零改动。
17
+ */
18
+
19
+ /** 无服务层接口时的空数据(GET → 空对象,POST → 成功占位) */
20
+ function emptyData(request: Request): Response {
21
+ const isGet = request.method === "GET" || request.method === "HEAD";
22
+ return Response.json(isGet ? {} : { ok: true }, {
23
+ headers: { "cache-control": "no-store", ...corsHeaders(request) },
24
+ });
25
+ }
26
+
27
+ /** 原样转发:同路径、同方法、同查询、同请求体,携带会话 cookie */
28
+ async function forward(request: Request, parts: string[]): Promise<Response> {
29
+ const url = new URL(request.url);
30
+ const target = `${serverBaseUrl}/api/d/${parts.join("/")}${url.search}`;
31
+ const headers = new Headers();
32
+ const cookie = request.headers.get("cookie");
33
+ if (cookie) headers.set("cookie", cookie);
34
+ const contentType = request.headers.get("content-type");
35
+ if (contentType) headers.set("content-type", contentType);
36
+ const init: RequestInit = { method: request.method, headers };
37
+ if (request.method !== "GET" && request.method !== "HEAD") {
38
+ init.body = await request.text();
39
+ }
40
+ let upstream: globalThis.Response;
41
+ try {
42
+ upstream = await fetch(target, init);
43
+ } catch {
44
+ return emptyData(request); // 上游不可达 → 空数据
45
+ }
46
+ const upstreamType = upstream.headers.get("content-type") ?? "";
47
+ if (upstream.status === 404 || !upstreamType.includes("json")) {
48
+ return emptyData(request); // 接口不存在 / 命中前端兜底 HTML → 空数据
49
+ }
50
+ const body = await upstream.arrayBuffer();
51
+ const resHeaders: Record<string, string> = {
52
+ "cache-control": "no-store",
53
+ "content-type": upstreamType,
54
+ ...corsHeaders(request),
55
+ };
56
+ return new Response(body, { status: upstream.status, headers: resHeaders });
57
+ }
58
+
59
+ async function handle(
60
+ request: Request,
61
+ context: RouteContext<"/api/d/[...parts]">,
62
+ ) {
63
+ const store = await cookies();
64
+ const user = findUserBySession(store.get(SESSION_COOKIE)?.value);
65
+ if (!user) {
66
+ return Response.json(
67
+ { error: "未登录" },
68
+ { status: 401, headers: corsHeaders(request) },
69
+ );
70
+ }
71
+
72
+ const { parts } = await context.params;
73
+ return forward(request, parts);
74
+ }
75
+
76
+ export const GET = handle;
77
+ export const POST = handle;
78
+
79
+ /** 预检(跨源带凭据 / 自定义头时) */
80
+ export const OPTIONS = corsPreflight;
@@ -3,12 +3,10 @@ import { Geist, Geist_Mono } from "next/font/google";
3
3
  import { PactorApp } from "@/pactor/App";
4
4
  // 顺序约定:globals(本项目 tailwind 工具类 + 皮肤无关的外壳规则)→
5
5
  // ui 预建样式(同层冲突时 ui 胜出,保证其内部 base/响应式变体排序生效,
6
- // 如 Sidebar 的 hidden md:block)→ login.css(登录页视觉,须在 ui 之后,
7
- // 经 data-slot 钩子压过组件默认样式)。主题令牌由 public/skins/<skin>.css
6
+ // 如 Sidebar 的 hidden md:block)。主题令牌由 public/skins/<skin>.css
8
7
  // 经下方内联脚本在首屏注入(link 后加载者胜出)。
9
8
  import "./globals.css";
10
9
  import "@pactor-app/ui/styles.css";
11
- import "./login.css";
12
10
 
13
11
  export const metadata: Metadata = {
14
12
  title: "Pactor Console",
@@ -9,8 +9,8 @@
9
9
  * - 构建环境可设 NEXT_PUBLIC_API_BASE_URL 显式覆盖(域名不符合约定时)。
10
10
  */
11
11
  const API_BASE_URLS = {
12
- /** 本地开发:dev 端口 9011 */
13
- development: 'http://localhost:9011',
12
+ /** 本地开发:无服务层接口(转发命中前端兜底 → 返回空数据);接本地后端时改为其地址 */
13
+ development: 'http://localhost:9011/backend',
14
14
  /** 测试环境:替换为实际测试环境 API 域名 */
15
15
  test: 'https://api-test.example.com',
16
16
  /** 生产环境:替换为实际生产 API 域名 */