@manohub/app-kit 0.1.0 → 0.2.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/README.md CHANGED
@@ -19,8 +19,22 @@
19
19
  - 公开入口名与源码分发期**逐字一致**,消费方的引用语句与样式导入三行不需要改;
20
20
  - 消费方不再需要为本包准备 TSX 编译配置或单例包的类型钉死;
21
21
  - `lint/`、`CONTRACT.md` 仍原样随包分发(消费方的护栏与契约按包内路径引用它们);
22
+ - `skills/` 是随包分发的 AI 技能包(见下);
22
23
  - 包内不存在 `dist/` 时先执行构建再联调。
23
24
 
25
+ ## 技能包(AI 代理用)
26
+
27
+ 随包分发三个技能:`app-kit`(入口编排)/ `app-kit-migrate`(存量应用改造)/ `app-kit-dev`(日常页面开发)。
28
+ 在消费方工程根执行即可落到自己的技能目录(幂等,包升级后重跑即刷新):
29
+
30
+ ```bash
31
+ node node_modules/@manohub/app-kit/skills/install.mjs # → .codebuddy/skills/
32
+ node node_modules/@manohub/app-kit/skills/install.mjs --also-claude # 同时 → .claude/skills/
33
+ node node_modules/@manohub/app-kit/skills/install.mjs --dry-run # 只预览
34
+ ```
35
+
36
+ 装了之后,代理在做接入、迁移或页面开发时会命中本包规范与流程;技能职责边界与维护约定见 `skills/README.md`。
37
+
24
38
  ## 来源
25
39
 
26
40
  本包由 `gsp-cloud-ds/dip/ibp/aihub/aihub-frontend` 的 `app-kit` 分支(导入 commit `315b60f`)迁入,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manohub/app-kit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "子应用统一骨架层:入口编排(createSubApp)、布局契约(AppShell)、页面组件、原子件、服务、样式底座。farris 被收敛在本包内部,对外只暴露标准 API。",
@@ -17,6 +17,7 @@
17
17
  "files": [
18
18
  "dist",
19
19
  "lint",
20
+ "skills",
20
21
  "CONTRACT.md",
21
22
  "README.md"
22
23
  ],
@@ -43,7 +44,7 @@
43
44
  "scripts": {
44
45
  "build": "node ../../scripts/clean-dist.mjs && vue-tsc -p tsconfig.build.json && vite build && node scripts/copy-styles.mjs",
45
46
  "type-check": "vue-tsc --noEmit -p tsconfig.json",
46
- "test:unit": "vitest run && node --test lint/__tests__/guardrails.spec.mjs",
47
+ "test:unit": "vitest run && node --test lint/__tests__/guardrails.spec.mjs __tests__/skills-install.test.mjs",
47
48
  "test:guardrails": "node --test lint/__tests__/guardrails.spec.mjs",
48
49
  "test:watch": "vitest"
49
50
  },
@@ -58,7 +59,7 @@
58
59
  },
59
60
  "dependencies": {
60
61
  "@farris/ui-vue": "^1.8.4",
61
- "@manohub/icon": "workspace:^0.1.0"
62
+ "@manohub/icon": "^0.2.0"
62
63
  },
63
64
  "devDependencies": {
64
65
  "@tanstack/vue-query": "catalog:",
@@ -0,0 +1,43 @@
1
+ # @manohub/app-kit 技能包(随包分发)
2
+
3
+ 本目录是随 npm 包一起发布的 AI 代理技能包,供**消费方**(接入了本包的子应用工程)使用。
4
+
5
+ 契约条款的唯一事实源仍是包内 `CONTRACT.md`;技能只负责「按场景把代理导到正确的流程与查表入口」,
6
+ 不复制条款、不另立规范。
7
+
8
+ ## 三个技能
9
+
10
+ | 技能 | 职责 | 可独立调用 |
11
+ |---|---|---|
12
+ | `app-kit` | 入口编排:识别意图 → 调 `app-kit-migrate` 或 `app-kit-dev`;承载全局硬约束 | 是(子技能也可直接调) |
13
+ | `app-kit-migrate` | 存量应用从直连底层组件库改造到本包:产基线 → 逐文件替换 → 归零 → 验收 → 登记 | 是 |
14
+ | `app-kit-dev` | 按本包规范做日常页面开发:选模板 / 选组件 / 样式纪律 / 缺件处置 / 自检 | 是 |
15
+
16
+ ## 安装到消费方技能目录
17
+
18
+ 在消费方**工程根**执行(缺省落 `.codebuddy/skills/`):
19
+
20
+ ```bash
21
+ node node_modules/@manohub/app-kit/skills/install.mjs
22
+
23
+ node node_modules/@manohub/app-kit/skills/install.mjs --also-claude # 同时落 .claude/skills/
24
+ node node_modules/@manohub/app-kit/skills/install.mjs --target .x/skills
25
+ node node_modules/@manohub/app-kit/skills/install.mjs --dry-run # 只预览不落盘
26
+ ```
27
+
28
+ 安装器是幂等的:每次执行**先清理同名技能目录再整体复制**,所以包升级后重跑一次即刷新到新版内容。
29
+ 它只管理 `app-kit` / `app-kit-migrate` / `app-kit-dev` 这三个目录,不触碰目标目录下的其它内容。
30
+
31
+ ## 维护约定(改本目录时遵守)
32
+
33
+ - 引用规范一律写**消费方视角**路径:`node_modules/@manohub/app-kit/CONTRACT.md` §x.y。
34
+ 禁止出现本机绝对路径、worktree 路径、仓库内相对路径(技能落盘后与包目录分离,这些路径会失效)。
35
+ - `SKILL.md` 只放流程与硬约束(控制在 5k 词内);查表内容(替换映射、场景配方)放各自的 `references/`,按需加载。
36
+ - 三个技能的 `description` 触发条件互不重叠,否则代理会命中错的那个。
37
+ - 技能引用的 `references/` 文件必须真实存在,文件名改动要同步 `SKILL.md`。
38
+ - 改动后跑门禁与打包校验(在仓库根):
39
+
40
+ ```bash
41
+ pnpm --filter @manohub/app-kit test:unit # 契约单测 + 护栏自测
42
+ pnpm release:check # build + verify-pack(含技能包结构校验)
43
+ ```
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: app-kit
3
+ version: 1.0.0
4
+ description: 子应用骨架层 @manohub/app-kit 的入口技能:识别意图并编排到 app-kit-migrate(存量应用改造)或 app-kit-dev(日常页面开发),同时承载全局硬约束。触发条件:用户提到 app-kit、骨架层、AppShell/AppTable/AppPanel 等 App* 组件、应用接入或改造、或要求按骨架层规范写页面时使用。
5
+ ---
6
+
7
+ # app-kit:骨架层入口编排
8
+
9
+ 把「用骨架层做事」收敛到一条路径:先判意图,再交给对应子技能。
10
+ 本技能不设计页面、不改代码,只做路由与硬约束。
11
+
12
+ ## 一、先判意图
13
+
14
+ | 用户意图信号 | 走向 |
15
+ |---|---|
16
+ | 应用尚未接入本包;或页面/组件仍在直连底层组件库、仍写底层风格 prop;或出现「迁移」「改造」「接入」「护栏基线」「违规归零」 | `Skill('app-kit-migrate')` |
17
+ | 应用已接入,要新增或修改页面、组件;或要求「按 app-kit 规范写」「用 AppTable / AppDialog / AppForm…」 | `Skill('app-kit-dev')` |
18
+ | 两者交织(边接入边改页面) | 先跑 `app-kit-migrate` 的接入阶段(护栏能跑起来、基线在案),再进 `app-kit-dev` |
19
+ | 与本包无关(别的技术栈、纯后端问题) | 不使用本技能 |
20
+
21
+ 意图不明时先问一句「是要把存量页面改造过来,还是新写页面」,不要猜。
22
+
23
+ ## 二、全局硬约束(两个子技能都适用)
24
+
25
+ 1. **不得直连底层组件库**:应用侧禁止 `import '@farris/ui-vue'`、禁止把它装成依赖、禁止写它的内部类选择器(`.fv-*`)。
26
+ 组件一律从 `@manohub/app-kit` 取;确需的能力按 `CONTRACT.md` §7 缺件处置流程办,不在应用侧自绘近似件。
27
+ 2. **冲突时按此优先级**:包内 `CONTRACT.md` > 组件源码 `@example` > 消费仓 `AGENTS.md` > 其它文档。
28
+ 发现 `@example` 与契约冲突,按契约写,并把该 `@example` 当缺陷处理。
29
+ 3. **护栏是唯一裁判**:三条护栏(style / component / structure)给出的 `file:line` + `correction` 就是唯一改法,
30
+ 照它改,不要另想一套;每条违规都带 `doc` 锚点,指回契约章节。
31
+ 4. **不重复 `createSubApp` 的职责**:应用侧不写 `createApp` / `mount` / pinia / 路由守卫 / 微前端协议 / 宿主语言监听。
32
+ 5. **样式只写布局**:视觉属性(颜色、字号、边框、阴影)、自写 rem、`!important`、裸元素选择器一律禁止;
33
+ 视觉由骨架层组件与令牌承担。
34
+
35
+ ## 三、固定命令(在应用包根执行)
36
+
37
+ ```bash
38
+ node node_modules/@manohub/app-kit/lint/run-all.mjs # 全量三条护栏
39
+ node node_modules/@manohub/app-kit/lint/run-all.mjs --changed # 只查改动文件(提交前)
40
+ pnpm exec vue-tsc --noEmit # 类型检查
41
+ pnpm build # 生产构建
42
+ ```
43
+
44
+ 技能包内容更新后重新落盘(幂等,包升级后重跑即刷新):
45
+
46
+ ```bash
47
+ node node_modules/@manohub/app-kit/skills/install.mjs
48
+ ```
49
+
50
+ ## 四、失败处理
51
+
52
+ | 现象 | 处理 |
53
+ |---|---|
54
+ | 找不到 `node_modules/@manohub/app-kit` | 在应用包根执行 `pnpm add @manohub/app-kit`(包在 npm 官方仓公开) |
55
+ | 护栏报缺配置文件 | 应用包根缺 `appkit-guardrails.config.json`,配置片段见 `references/adoption.md` |
56
+ | 装到的版本低于 0.1.1 | 0.1.0 的发布物依赖协议有误已作废(被 deprecate),升级到 0.1.1 以上 |
57
+ | 调子技能报「技能不存在」 | 技能未安装或未刷新,跑上面的 `install.mjs` |
58
+ | 类型检查报「无法解析 `*.css`」 | 消费方 tsconfig 的 `types` 需包含 `vite/client`,见 `references/adoption.md` |
59
+
60
+ ## 五、参考
61
+
62
+ - `references/adoption.md`:接入 SOP(安装、样式三行、入口、护栏、类型配置、验收清单、故障对照、升级口径)
63
+ - `references/contract-index.md`:契约速查索引(按主题定位 `CONTRACT.md` 章节,不复制条款)
64
+ - 迁移查表:`../app-kit-migrate/references/migration-map.md`
65
+ - 开发配方:`../app-kit-dev/references/page-recipes.md`
@@ -0,0 +1,139 @@
1
+ # 接入 SOP(子应用接入 `@manohub/app-kit`)
2
+
3
+ 面向**消费方**(子应用工程)的落地步骤。契约条款以 `node_modules/@manohub/app-kit/CONTRACT.md` 为准,
4
+ 本文件只讲「怎么接、怎么验、出问题查哪里」。
5
+
6
+ ## 0. 前置
7
+
8
+ 1. 包管理器统一用 pnpm(npm 的 node_modules 布局会破坏单实例前提)。
9
+ 2. 包发布在 **npm 官方仓**(公开),默认 registry 即可安装,**无需额外配置**。
10
+ 仅当工程把默认 registry 指到了内网镜像时,需显式声明作用域(**不要提交凭据**):
11
+
12
+ ```ini
13
+ @manohub:registry=https://registry.npmjs.org/
14
+ ```
15
+
16
+ 3. 本包在 npm 官方仓公开可见;`^0.1.0` 的发布物依赖协议有误已作废(被 deprecate),**使用 0.1.1 及以后**。
17
+ 若工程启用了「最小发布年龄」类的供应链策略,需为 `@manohub/app-kit` / `@manohub/icon` 开例外。
18
+
19
+ ## 1. 安装
20
+
21
+ ```bash
22
+ pnpm add @manohub/app-kit
23
+ ```
24
+
25
+ - **不要**再单独安装 `@farris/ui-vue` 或 `@manohub/icon`:前者被骨架层收敛、后者由骨架层依赖带入。
26
+ - 应用侧**不得** `import '@farris/ui-vue'`(护栏会拦);组件一律从 `@manohub/app-kit` 取。
27
+ - 需要自备的 peer:`vue` / `vue-router` / `pinia` / `@tanstack/vue-query` / `i18next` 系 —— 它们必须是**唯一实例**。
28
+
29
+ ## 2. 样式:固定三行、顺序即契约
30
+
31
+ ```css
32
+ /* src/style.css */
33
+ @import "@manohub/app-kit/reset.css";
34
+ @import "@manohub/app-kit/styles.css";
35
+ @import "./app.css";
36
+ ```
37
+
38
+ `styles.css` 内部已按 farris CSS → 令牌 → 桥接 → 组件样式的顺序汇总,并保留对 `@farris/ui-vue/index.css`
39
+ 的裸包导入(不要自行把它内联或改写顺序)。
40
+
41
+ ## 3. 入口:走 `createSubApp`
42
+
43
+ ```ts
44
+ // src/main.ts
45
+ import { createSubApp } from '@manohub/app-kit/entry'
46
+ import App from './App.vue'
47
+ import { routes } from './router'
48
+ import './style.css'
49
+
50
+ export const { mount, unmount } = createSubApp({
51
+ rootComponent: App,
52
+ routes,
53
+ // 应用使用 vue-i18n 时以插件注入;不要用本包的 i18n 选项(那是 i18next 语义)
54
+ // extraPlugins: [i18n],
55
+ // 以 `index.html?xxx=1` 直开的入口(iframe / 选择模式)必须登记,否则首屏守卫会清空 query
56
+ // rootPathAliases: ['/index.html'],
57
+ })
58
+ ```
59
+
60
+ `createSubApp` 已统一负责 `.app-container` 包裹、pinia、Farris 插件、VueQuery、路由、挂载、
61
+ 宿主语言监听、首屏守卫、`window.mount/unmount` 与 `microApp.mount/unmount` 协议、非微前端环境自动挂载 ——
62
+ **应用侧不要再手写这些**。
63
+
64
+ ## 4. 护栏(建议接入)
65
+
66
+ 包内随发护栏脚本与 schema,按已安装包路径引用:
67
+
68
+ ```jsonc
69
+ // appkit-guardrails.config.json
70
+ {
71
+ "$schema": "./node_modules/@manohub/app-kit/lint/guardrails.config.schema.json",
72
+ "apps": [{ "dir": ".", "name": "<app-name>", "prefixes": ["<your-prefix>"], "pending": false }],
73
+ "srcGlobs": ["src/**/*.ts", "src/**/*.tsx"],
74
+ "styleGlobs": ["src/**/*.css"],
75
+ "contractDoc": "node_modules/@manohub/app-kit/CONTRACT.md"
76
+ }
77
+ ```
78
+
79
+ ```jsonc
80
+ // package.json
81
+ {
82
+ "scripts": {
83
+ "lint": "node node_modules/@manohub/app-kit/lint/run-all.mjs",
84
+ "lint:changed": "node node_modules/@manohub/app-kit/lint/run-all.mjs --changed"
85
+ }
86
+ }
87
+ ```
88
+
89
+ 存量应用先产基线再逐步归零(口径见 CONTRACT.md §8);新增与改动过的文件必须归零。
90
+ 迁移流程与替换映射见 `../app-kit-migrate/SKILL.md` 与 `../app-kit-migrate/references/migration-map.md`。
91
+
92
+ ## 5. 技能包(随包分发的开发/迁移技能)
93
+
94
+ ```bash
95
+ # 在工程根执行:把三个技能落到 .codebuddy/skills/(幂等,包升级后重跑即刷新)
96
+ node node_modules/@manohub/app-kit/skills/install.mjs
97
+
98
+ node node_modules/@manohub/app-kit/skills/install.mjs --also-claude # 同时落 .claude/skills/
99
+ node node_modules/@manohub/app-kit/skills/install.mjs --dry-run # 只预览
100
+ ```
101
+
102
+ 装了之后,AI 代理在做本项目前端开发或改造时会自动命中骨架层的规范与流程。
103
+
104
+ ## 6. 类型与解析配置
105
+
106
+ - `tsconfig`:`moduleResolution: "bundler"`(包的 `exports` 走子路径,node10 解析不读 `exports`)。
107
+ - 若出现「同名类型互不兼容 / provide 取不到」这类双实例症状,在构建配置里收敛单例:
108
+ `resolve.dedupe: ['vue', 'vue-router', 'pinia', '@farris/ui-vue', '@tanstack/vue-query', ...]`,
109
+ 并在 tsconfig `paths` 把同一批包钉到本工程这一份。
110
+
111
+ ## 7. 验收清单
112
+
113
+ | 项 | 命令 / 做法 | 通过标准 |
114
+ |---|---|---|
115
+ | 类型 | `vue-tsc --noEmit` | 0 错(含 `@manohub/app-kit/entry` 等子路径可解析) |
116
+ | 构建 | `pnpm build` | 成功产出 |
117
+ | 护栏 | `pnpm lint` | 无违规(或与基线一致) |
118
+ | 运行 | 启动后打开页面 | 骨架与组件样式正常、弹层落点正常 |
119
+ | 特殊入口 | 以 `index.html?xxx=1` 直开的入口 | 已在 `rootPathAliases` 登记,query 未被清空 |
120
+
121
+ ## 8. 常见故障对照
122
+
123
+ | 现象 | 原因 | 处理 |
124
+ |---|---|---|
125
+ | `Cannot find module '@manohub/app-kit'` | 未配置 `@manohub` 作用域 registry | 见 §0 |
126
+ | 构建报 `Cannot find module '.../styles/index.css'` | 忘记装包或用了旧版本 | 重新安装并确认版本 |
127
+ | 组件渲染但样式全丢 | 三行样式导入缺失或顺序被打乱 | 恢复 §2 的三行 |
128
+ | provide/inject 失效、组件报错怪异 | 出现第二份 vue / farris | 见 §6 的单例收敛 |
129
+ | 类型检查报「无法解析 `*.css`」 | 消费方 `types` 未包含 `vite/client`,或引入了其它包的 CSS 声明 | 补齐 `types: ["vite/client"]` |
130
+ | 弹层/下拉落点偏移 | 门户容器不是文档原点 | 应用侧只写变量(`--ibp-popup-shift-*`),位移规则由包内桥接层负责 |
131
+ | 页面被跳到根路径、query 丢了 | 直开入口未登记 `rootPathAliases` | 见 §3 注释与契约 §9 |
132
+ | 装到的版本低于 0.1.1 | 0.1.0 已作废 | 升级到 0.1.1+ |
133
+
134
+ ## 9. 升级与破坏性变更
135
+
136
+ - **0.x 单线**:所有消费方跟随同一条版本线,不做多版本共存。
137
+ - 注意语义化版本在 0.x 下的含义:`^0.1.0` **不覆盖** `0.2.0`,跨次版本升级要显式改依赖区间。
138
+ - 破坏性变更升次版本并随发布说明通知;消费方升级后按 §7 复跑验收清单。
139
+ - 版本号与变更记录见发包仓根 `project.json` 与发布记录。
@@ -0,0 +1,43 @@
1
+ # 契约速查索引
2
+
3
+ 用法:先在这里定位章节,再读 `node_modules/@manohub/app-kit/CONTRACT.md` 的对应段落。
4
+ **本文只做索引,不复述条款** —— 口径冲突时一律以契约原文为准。
5
+
6
+ ## 我想知道…
7
+
8
+ | 我想知道 | 看契约 |
9
+ |---|---|
10
+ | 应用怎么接入(入口、样式、依赖) | §1 消费方式 |
11
+ | 包的设计底线(API 面、数据入参、事件、命名) | §2 四条铁律 |
12
+ | 某个 prop 该叫什么、哪些 prop 名禁止 | §3 统一 prop 词表(含「禁止项」) |
13
+ | 一个新页面该怎么搭 | §4.1 三种页面模板 |
14
+ | 双栏页怎么写、分隔线归谁 | §4.2 `AppShell.Split` |
15
+ | 区域容器怎么写、`toolbar` 与 `actions` 怎么分 | §4.3 `AppPanel` |
16
+ | 谁滚、滚在哪一层 | §4.4 两级滚动归属 |
17
+ | 搜索框/按钮该放哪个位置 | §4.5 四处操作位唯一化(含按钮组主色规则) |
18
+ | 加载中 / 空 / 错误三态怎么写 | §4.6 `AppQueryState` |
19
+ | 表单行、只读摘要行、表单内分组 | §4.7 `AppForm` / `AppForm.Item` / `AppForm.Section` |
20
+ | 步骤条怎么写、能不能跳步 | §4.8 `AppSteps` |
21
+ | 卡片式单选怎么写 | §4.9 `AppRadioCard` |
22
+ | 弹窗、页脚按钮、定高滚动、关闭拦截 | §4.10 `AppDialog` |
23
+ | 区块(标题 + 内容)与 `AppPanel` 怎么选 | §4.11 `AppSection` |
24
+ | 行/列怎么排、间距档、栅格列数 | §4.12 `AppLayout`(`Row` / `Column`) |
25
+ | 确认框 / 提示 / 报错怎么写 | §4.13 `messageBox` / `notify` / `loading` |
26
+ | 某个写法是不是违规 | §5 反例库 |
27
+ | 应用侧 CSS 能写什么、不能写什么 | §6 样式纪律 |
28
+ | 需要的能力包里没有怎么办 | §7 缺件处置流程 |
29
+ | 护栏怎么跑、提交前检查怎么挂 | §8 必跑命令与护栏(§8.1 提交前钩子、§8.2 包自身门禁) |
30
+ | 某些「看起来不对」的地方是不是 bug | §9 已知偏差(缩放、初始守卫与选择模式、行对象身份、自建件说明…) |
31
+
32
+ ## 最容易踩的几条(先记住这些再动手)
33
+
34
+ 1. **禁 `100vh`**:子应用被注入宿主容器,高度一律 `100%`(§4.1)。
35
+ 2. **分页跟承载表格的容器走**:表格在 `AppPanel` 里 → 分页放 `AppPanel.Footer`(§4.1、§4.3)。
36
+ 3. **按钮组只有一颗主按钮且在第一位**;其余按钮**必须显式给 `tone`**(缺省 + `solid` 渲染成主色)(§4.5)。
37
+ 4. **筛选放 `toolbar`、操作放 `actions`**,两位不可互换;字段 >3 上提 `AppShell.Filter`(§4.5)。
38
+ 5. **错误态与空态分开**:加载失败传 `error`,不要塞进 `empty`(§4.6、§5)。
39
+ 6. **`AppPanel` 只给真区域**:只是「给一段内容起个名」用 `AppSection`(§4.11)。
40
+ 7. **表格不要套 `height:auto` 的 div**:会让整个 Body 滚、表头跟着滚走(§5)。
41
+ 8. **应用侧 CSS 只写布局**:视觉属性、自写 rem、`!important`、裸元素选择器、`.ak-*` 选择器一律禁止(§6)。
42
+ 9. **缺件走建件流程**,不在页面里自绘近似件,也不直连底层组件库(§7)。
43
+ 10. **`index.html?xxx=1` 这类入口必须登记 `rootPathAliases`**,否则首屏守卫清空 query(§9)。
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: app-kit-dev
3
+ version: 1.0.0
4
+ description: 按 @manohub/app-kit 骨架层规范做日常页面与组件开发:选页面模板、选组件与 prop、守样式纪律、处置缺件、提交前护栏自检。触发条件:用户要求用 app-kit / App* 组件写或改页面、要求按骨架层规范实现列表页/表单/弹窗/树/分页/加载空错三态,或询问骨架层某个用法是否正确时使用。
5
+ ---
6
+
7
+ # app-kit-dev:日常开发辅助
8
+
9
+ 面向**已接入**骨架层的应用。任务是把需求落到「契约允许的写法」上,而不是边写边试。
10
+
11
+ ## 一、开工前必做
12
+
13
+ 1. 读 `node_modules/@manohub/app-kit/CONTRACT.md` 的相关章节(用 `../app-kit/references/contract-index.md` 按主题定位)。
14
+ **契约 > 组件源码 `@example` > 消费仓 `AGENTS.md` > 其它文档**;冲突时按此优先级执行。
15
+ 2. 确认组件是否存在、是否是自建件(自建件有额外约束,如 `AppTree` 的缩进与展开态、`AppNotice` 的语义 tone)。
16
+ 3. 组件不存在时走 `CONTRACT.md` §7 缺件处置流程,**不在应用侧自绘近似件**。
17
+
18
+ ## 二、按意图取配方
19
+
20
+ | 需求 | 取用 |
21
+ |---|---|
22
+ | 新页面(列表 / 表单 / 双栏) | `references/page-recipes.md`「页面模板」;模板选型见 `CONTRACT.md` §4.1 |
23
+ | 表格、筛选、分页、多选 | `references/page-recipes.md`「表格」;四处操作位见 §4.5 |
24
+ | 表单、校验、字段文案 | `references/page-recipes.md`「表单」;§4.7 |
25
+ | 弹窗(含页脚按钮、确认框) | `references/page-recipes.md`「弹窗」;§4.10、§4.13 |
26
+ | 树、卡片单选、步骤条 | `references/page-recipes.md`;§4.9、§4.8 与 §9.6 |
27
+ | 加载 / 空 / 错三态 | `references/page-recipes.md`「三态」;§4.6 |
28
+ | 样式怎么写、哪些禁止 | `references/style-rules.md`;§6 |
29
+ | 不确定某写法是否违规 | `references/style-rules.md`「反例」+ `CONTRACT.md` §5 |
30
+
31
+ ## 三、硬约束(每次改代码前默读)
32
+
33
+ 1. **不直连底层组件库**:不 `import '@farris/ui-vue'`、不写 `.fv-*` 内部类选择器。
34
+ 2. **只用契约内的 prop**:统一词表见 `CONTRACT.md` §3;禁止底层配置对象与底层专有 prop 名,
35
+ 禁止 `enableXxx` / `showXxx` 布尔前缀(`selection={{…}}` 是合法契约,见 §3 说明)。
36
+ 3. **样式只写布局**:禁视觉属性(颜色 / 字号 / 边框 / 阴影)、禁自写 rem、禁 `!important`、禁裸元素选择器;
37
+ 选择器前缀必须已登记。视觉一律走骨架层组件与令牌。
38
+ 4. **不重复骨架职责**:入口编排、pinia、路由、微前端协议、宿主语言监听都不在应用侧写。
39
+ 5. **两处操作位、两级滚动、分页归属**先定清楚再动手(§4.4、§4.5、分页归属按承载表格的容器决定)。
40
+
41
+ ## 四、收工前自检(必跑)
42
+
43
+ ```bash
44
+ node node_modules/@manohub/app-kit/lint/run-all.mjs --changed # 只查本次改动文件
45
+ pnpm exec vue-tsc --noEmit # 类型
46
+ ```
47
+
48
+ - 违规条目自带 `file:line` + `correction`(唯一改法)+ `doc`(契约章节):照改,不要另想一套。
49
+ - 新增文件**必须归零**;若所在文件有存量豁免,只保证自己没新增违规。
50
+ - 页面骨架类违规(`structure/no-shell` 等)表示路由页没用 `AppShell`——这是最容易被漏掉的一类,动手前先看 §4.1。
51
+
52
+ ## 五、失败处理
53
+
54
+ | 现象 | 处理 |
55
+ |---|---|
56
+ | 想要的组件/能力不存在 | 走 `CONTRACT.md` §7 缺件处置流程(先查底层库有无对应件 → 封装或自建 → 页面只用建好的件) |
57
+ | 组件渲染了但样式不对 | 检查 `src/style.css` 三行导入与顺序(§1);不要在页面里补视觉样式 |
58
+ | 视觉想「微调一下」 | 属于违反样式纪律;用语义 prop(`tone` / `shape` / `size`)或换件,必要时提为骨架层待建件 |
59
+ | provide/inject 失效、服务拿不到上下文 | 疑似双实例,检查依赖树与单例收敛(`../app-kit/references/adoption.md`) |
60
+ | 护栏报「前缀未登记」 | 应用类名前缀应在 `appkit-guardrails.config.json` 登记,而不是改类名绕过 |
61
+
62
+ ## 六、参考
63
+
64
+ - `../app-kit/references/contract-index.md`:按主题定位契约章节(含 §4 各组件契约与 §9 已知偏差)
65
+ - `references/page-recipes.md`:页面模板与高频场景配方
66
+ - `references/style-rules.md`:样式纪律与反例
67
+ - `node_modules/@manohub/app-kit/CONTRACT.md`:规范唯一事实源
@@ -0,0 +1,240 @@
1
+ # 页面与高频场景配方
2
+
3
+ 可直接抄的骨架片段。契约原文见 `node_modules/@manohub/app-kit/CONTRACT.md`(章节号已标注)。
4
+
5
+ ---
6
+
7
+ ## 一、三种页面模板(§4.1)
8
+
9
+ ### 模板 A:列表页(页头 + 表格 + 页脚分页)
10
+
11
+ ```tsx
12
+ <AppShell>
13
+ <AppShell.Header title="技能分类" toolbar={<AppSearchBox modelValue={kw} onChange={(v) => (kw = v)} />} />
14
+ <AppShell.Body mode="table">
15
+ <AppTable framed rows={rows} columns={cols} rowKey="id" loading={loading} error={error} />
16
+ </AppShell.Body>
17
+ <AppShell.Footer>
18
+ <AppPagination page={page} pageSize={size} total={total} onChange={(p) => (page = p)} />
19
+ </AppShell.Footer>
20
+ </AppShell>
21
+ ```
22
+
23
+ ### 模板 B:双栏页(左树/列表 + 右列表;栏内滚动归 `AppPanel`)
24
+
25
+ ```tsx
26
+ <AppShell>
27
+ <AppShell.Header title="值映射管理" />
28
+ <AppShell.Body mode="plain">
29
+ <AppShell.Split sidebar={{ width: 208, content: () => (
30
+ <AppPanel title="业务域"><AppTree nodes={nodes} expandedKeys={expandedKeys} onExpandChange={(k) => (expandedKeys = k)} /></AppPanel>
31
+ ) }}>
32
+ <AppPanel
33
+ title="值映射列表"
34
+ toolbar={<AppSearchBox modelValue={kw} onChange={(v) => (kw = v)} />}
35
+ actions={<>
36
+ <AppButton tone="primary" onClick={openCreate}>新建</AppButton>
37
+ <AppButton tone="secondary" onClick={refresh}>刷新</AppButton>
38
+ </>}>
39
+ <AppTable framed rows={rows} columns={cols} rowKey="id" />
40
+ <AppPanel.Footer><AppPagination page={page} pageSize={size} total={total} onChange={(p) => (page = p)} /></AppPanel.Footer>
41
+ </AppPanel>
42
+ </AppShell.Split>
43
+ </AppShell.Body>
44
+ </AppShell>
45
+ ```
46
+
47
+ - 分页放**表格所在容器**的 Footer;无数据或加载失败时**不要渲染分页**(§5 反例)。
48
+ - 侧栏不加内距、业务侧不写竖直边框(Split 自带那根线,§4.2)。
49
+
50
+ ### 模板 C:详情 / 向导页(长内容自身滚)
51
+
52
+ ```tsx
53
+ <AppShell>
54
+ <AppShell.Header title="值映射详情" toolbar={<AppButton tone="secondary" shape="ghost" onClick={back}>返回</AppButton>} />
55
+ <AppShell.Body mode="scroll">
56
+ <AppSection title="基本信息"><AppForm>…</AppForm></AppSection>
57
+ </AppShell.Body>
58
+ </AppShell>
59
+ ```
60
+
61
+ 筛选字段 >3 或跨区域时,在 `AppShell.Header` **之前**加 `AppShell.Filter`(页面级筛选唯一位置)。
62
+ `AppShell.Toolbar` 仅保留兼容,新页面不要用。
63
+
64
+ ---
65
+
66
+ ## 二、表格(`AppTable`)
67
+
68
+ ```tsx
69
+ const cols: AppTableColumn<Row>[] = [
70
+ { key: 'code', title: '编码', width: 160, render: (row) => <span>{row.code}</span> },
71
+ { key: 'name', title: '名称', width: 2 }, // 数字 = 宽度权重
72
+ { key: 'op', title: '操作', width: 120, align: 'center', render: (row) => (
73
+ <AppButton shape="link" onClick={() => view(row)}>查看</AppButton>) },
74
+ ]
75
+
76
+ <AppTable
77
+ framed
78
+ rows={rows}
79
+ columns={cols}
80
+ rowKey="id"
81
+ rowHighlightKey={selected?.id} // 只染行、不加勾选列
82
+ loading={loading}
83
+ error={error} // 加载失败传 error,不要塞 empty
84
+ empty="暂无数据"
85
+ emptyActionText="新建" onEmptyAction={openCreate}
86
+ onRowClick={(row) => (selected = row)}
87
+ onRowDoubleClick={(row) => confirm(row)}
88
+ headerHeight={56} // 双行表头时才传
89
+ rowHeight={32}
90
+ />
91
+ ```
92
+
93
+ - `render` / `rowClass` / 行事件收到的都是**传入 `rows` 的那个业务对象**,可直接回写 `row.字段`(§9)。
94
+ - 多选:`selection={{ mode: 'multiple', selected, onSelectedChange }}`;传 `selected` 即受控(上报增量)、不传则非受控(上报全量,翻页自然清空)。
95
+ - 表头高度、行高一旦用常量传,宿主的容器高度算法要用**同源常量**(见契约 §9 与 `AppTable` 源码注释)。
96
+ - 不要给 `AppTable` 外套 `height:auto` 的 div(表头会跟着 Body 一起滚,§5)。
97
+
98
+ ---
99
+
100
+ ## 三、三态(§4.6)
101
+
102
+ `AppTable` / `AppTree` 三态内建,页面只给文案与动作:
103
+
104
+ ```tsx
105
+ <AppTable
106
+ rows={rows} columns={cols}
107
+ loading={loading}
108
+ error={loadError} // 字符串原文会自动作副文案
109
+ errorActionText="重试" onErrorAction={refetch}
110
+ empty="暂无值映射"
111
+ emptyActionText="新建" onEmptyAction={openCreate}
112
+ />
113
+ ```
114
+
115
+ - 优先级 `loading > error > empty`;**错误别塞进 empty**(§5)。
116
+ - 动作要文案 + 回调**都传**才渲染;同区域工具条里已有入口(如「新建」)时,空态不要再放一颗。
117
+
118
+ ---
119
+
120
+ ## 四、表单(§4.7)
121
+
122
+ ```tsx
123
+ <AppForm labelAlign="left" labelWidth={120} columns={2}>
124
+ <AppForm.Section title="基本信息">
125
+ <AppForm.Item label="编码" required error={errors.code} hint="唯一标识">
126
+ <AppInput modelValue={form.code} onChange={(v) => (form.code = v)} />
127
+ </AppForm.Item>
128
+ <AppForm.Item label="缓存策略">
129
+ <AppSelect options={cacheOptions} modelValue={form.cache} onChange={(v) => (form.cache = v)} />
130
+ </AppForm.Item>
131
+ </AppForm.Section>
132
+ <AppForm.Section title="来源列" v-slots={{ extra: () => <span>{n} / 10</span> }}>
133
+ <AppTable framed rows={rows} columns={cols} rowKey="id" />
134
+ </AppForm.Section>
135
+ </AppForm>
136
+ ```
137
+
138
+ - **只读摘要行**:`<AppForm.Item label="编码" text={record.code} />`(传 `text` 即只读文本行,空值自动显示 `—`,label 自动弱化)。
139
+ - 只读但控件位要放徽标/链接:用 `readonly` 而不是 `text`(内容仍走默认插槽)。
140
+ - 整块内容(文本域、卡片组、表格、纯说明行)在 `columns={2}` 下要加 `fullWidth`,否则被压成半宽。
141
+ - 不要自绘 label 列、必填星号、错误行;不要在应用侧改控件高度(会让同行 label 错位)。
142
+ - 表单内分组**只能**用 `AppForm.Section`(自绘 sub-title 会导致组内行距丢失)。
143
+
144
+ ---
145
+
146
+ ## 五、弹窗(§4.10、§4.13)
147
+
148
+ ```tsx
149
+ <AppDialog
150
+ open={visible}
151
+ title="新增分类"
152
+ width={640}
153
+ footer={() => (
154
+ <>
155
+ <AppButton tone="secondary" onClick={close}>取消</AppButton>
156
+ <AppButton tone="primary" loading={saving} onClick={submit}>确定</AppButton>
157
+ </>
158
+ )}
159
+ onUpdate:open={(v) => (visible = v)}>
160
+ <AppPanel padding={{ y: 12, x: 20 }}>
161
+ <AppPanel.Body padding={{ y: 24, x: 100 }}>…表单…</AppPanel.Body>
162
+ </AppPanel>
163
+ </AppDialog>
164
+ ```
165
+
166
+ - 页脚只给按钮,容器(右对齐 / 内距 / 常驻)由组件补。
167
+ - 需要「定高 + 内部滚动」:`fitContent={false}` + `height`(缺省 `fitContent` 为 true,不关掉 `height` 会被忽略)。
168
+ - 关闭拦截用 `beforeClose`(返回 `false` 阻止)。
169
+ - 弹窗内区域没有外层给内距 → 用 `AppPanel` 的 `padding` 维度,**不要**为此挂应用侧「只写 padding」的类。
170
+ - 确认/提示/报错一律 `messageBox.confirm()` / `notify.success()`,不要自绘确认弹窗:
171
+
172
+ ```tsx
173
+ const ok = await messageBox.confirm({ title: '确认删除?', description: '删除后不可恢复' })
174
+ if (ok) await remove(row.id)
175
+ notify.error('保存失败:' + msg, { timeout: 6000 })
176
+ ```
177
+
178
+ 文案传纯文本(`\n` 换行,内部转义);右上角 ✕ / Esc 只关窗、Promise 不落定,按「未确认」处理。
179
+
180
+ ---
181
+
182
+ ## 六、树 / 卡片单选 / 步骤条
183
+
184
+ ```tsx
185
+ // 树:受控展开,数据刷新不丢展开态(§9)
186
+ <AppTree nodes={nodes} expandedKeys={expandedKeys} selectedKey={selectedKey}
187
+ onExpandChange={(k) => (expandedKeys = k)} onSelect={(k) => (selectedKey = k)} />
188
+
189
+ // 卡片单选:选项内容长时用卡片,不要用下拉(§4.9)
190
+ <AppRadioCard modelValue={form.type}
191
+ items={[{ value: 'A', label: '值映射设置', description: '手工维护来源/目标向量' },
192
+ { value: 'B', label: '数据库表', description: '绑定 msu 与数据对象' }]}
193
+ onChange={(v) => (form.type = v)} />
194
+
195
+ // 步骤条:0 基;点击只上报,须回写 modelValue(§4.8)
196
+ <AppSteps items={stepItems} modelValue={step}
197
+ onBeforeChange={(i) => i <= maxVisited}
198
+ onChange={(i) => (step = i)} />
199
+ ```
200
+
201
+ 树的缩进与行高由组件算,**不要**在应用侧改行内 padding 或写死缩进像素。
202
+
203
+ ---
204
+
205
+ ## 七、布局(§4.12)
206
+
207
+ 方向按「它排什么」定:**排「行」用 `Row`(竖排;传 `columns` 则一行分 n 列)**、**排「列」用 `Column`(水平容器)**。
208
+ `Row` 在 CSS 里是 `flex-direction: column`,与属性名相反是刻意的。
209
+
210
+ ```tsx
211
+ // 竖排一组「行」,行内横排
212
+ <AppLayout.Row gap="lg">
213
+ <AppLayout.Column gap="sm"><AppBadge tone="success" shape="soft">通过</AppBadge><span>编码唯一性</span></AppLayout.Column>
214
+ <AppLayout.Column gap="sm"><AppBadge tone="success" shape="soft">通过</AppBadge><span>列结构一致性</span></AppLayout.Column>
215
+ </AppLayout.Row>
216
+
217
+ // 一行分两列(窄容器自动回落一列)
218
+ <AppLayout.Row columns={2} gap="lg">
219
+ <AppLayout.Column gap="sm"><span>缓存方式</span><span>按需缓存</span></AppLayout.Column>
220
+ <AppLayout.Column span={2}>整行内容</AppLayout.Column>
221
+ </AppLayout.Row>
222
+ ```
223
+
224
+ - `gap` 只有档位:`none` / `sm` 4 / `md` 8(默认)/ `lg` 12 / `xl` 16。
225
+ - 横向的「一组」一律 `Column`;「一条条往下」用 `Row`(父容器已有 gap 时不要叠)。
226
+ - 手写 `display:flex` 只留给一次性特例(固定高度、自身滚动、复杂定位)。
227
+
228
+ ---
229
+
230
+ ## 八、区块容器怎么选
231
+
232
+ | 我要的是 | 用 |
233
+ |---|---|
234
+ | 内容自己滚 / 有区域级筛选、操作、分页 | `AppPanel`(三件:Header / Body / Footer) |
235
+ | 只是「给一段内容起个名」 | `AppSection`(标题 + 内容,含组间距) |
236
+ | 页面级两栏 | `AppShell.Split` |
237
+ | 表单里并排字段 | `AppForm columns={2}` |
238
+ | 表单内分组 | `AppForm.Section`(与 `AppSection` 同一件) |
239
+
240
+ 拿 `AppPanel` 当「带标题的卡片」会把 48px 区域头与滚动契约带进不需要的地方(§5、§4.11)。