@agile-team/wl-skills-kit 2.4.2 → 2.5.1

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **读者**:团队技术负责人 / wl-skills-kit 维护者 / 对体系设计感兴趣的团队成员
4
4
  > **更新方式**:重大架构变更后追加对应章节,旧章节原文保留(历史可溯)
5
- > **当前版本**:v2.4.2(2026-05-05
5
+ > **当前版本**:v2.5.0(2026-05-07
6
6
 
7
7
  ---
8
8
 
@@ -46,6 +46,7 @@ npx @agile-team/wl-skills-kit
46
46
  "审计 src/views/sale 这个目录的代码规范"
47
47
  "提取 mmwr-rolling-management 这个页面作为模板"
48
48
  "同步菜单到后端"
49
+ "帮我做个台账页面,后端没好先 mock,菜单也加上"
49
50
  ```
50
51
 
51
52
  AI 会自动识别意图,触发对应的 Skill。
@@ -54,17 +55,17 @@ AI 会自动识别意图,触发对应的 Skill。
54
55
 
55
56
  ## 9 个 Skill 速览
56
57
 
57
- | Skill | 触发关键词 | 用途 |
58
- | ------------------ | ------------------------------ | ---------------------------- |
59
- | `prototype-scan` | 扫描原型 / 解析原型 / 口述需求 | 原型 / 详设 → page-spec JSON |
60
- | `api-contract` | 接口约定 / api.md / 字段定义 | 生成接口约定文档 |
61
- | `page-codegen` | 生成页面 / 帮我生成 | 生成页面骨架 + 菜单注册 |
58
+ | Skill | 触发关键词 | 用途 |
59
+ | ------------------ | ------------------------------ | ---------------------------------------------------- |
60
+ | `prototype-scan` | 扫描原型 / 解析原型 / 口述需求 | 原型 / 详设 → page-spec JSON |
61
+ | `api-contract` | 接口约定 / api.md / 字段定义 | 生成接口约定文档 |
62
+ | `page-codegen` | 生成页面 / 帮我生成 | 生成页面骨架 + 菜单注册 |
62
63
  | `menu-sync` | 创建菜单 / 同步菜单 | 菜单数据同步到后端(MCP 自动 / prompt 手动两种模式) |
63
- | `dict-sync` | 同步字典 / 创建字典 / 字典审计 | 字典基线同步到后端(MCP 自动 / prompt 手动两种模式) |
64
- | `convention-audit` | 规范审计 / 代码审计 | 13 条规范扫描 + 偏差报告 |
65
- | `template-extract` | 提取模板 / 抄取模板 | 从现有页面沉淠领域专属模板 |
66
- | `permission-sync` | 创建角色 / 角色授权 / 同步权限 | 角色+授权+动作权限同步(MCP) |
67
- | `code-fix` | 自动修复 / 整改偏差 / 规范整改 | 受控自动修复审计报告中的偏差 |
64
+ | `dict-sync` | 同步字典 / 创建字典 / 字典审计 | 字典基线同步到后端(MCP 自动 / prompt 手动两种模式) |
65
+ | `convention-audit` | 规范审计 / 代码审计 | 13 条规范扫描 + 偏差报告 |
66
+ | `template-extract` | 提取模板 / 抄取模板 | 从现有页面沉淠领域专属模板 |
67
+ | `permission-sync` | 创建角色 / 角色授权 / 同步权限 | 角色+授权+动作权限同步(MCP) |
68
+ | `code-fix` | 自动修复 / 整改偏差 / 规范整改 | 受控自动修复审计报告中的偏差 |
68
69
 
69
70
  完整调度规则见 `.github/skills/_registry.md`。
70
71
 
@@ -105,9 +106,28 @@ AI 会自动识别意图,触发对应的 Skill。
105
106
  ```
106
107
 
107
108
  > **说明**:每一步都可以单独触发,也可以按用户意图自动接续。
109
+ >
108
110
  > - `dict-sync`:首次使用先跑 **pull 模式**(「刷新字典基线」)建立本地基线,再跑 push 模式同步差异。
109
111
  > - `code-fix`:只修复 🟡/🟢 偏差;🔴 严重偏差必须人工或 page-codegen 处理。每条修复前强制 diff 预览确认。
110
112
 
113
+ ### 页面生成最终标准
114
+
115
+ `page-codegen` 生成的业务列表/台账/主从/树表页面必须满足:
116
+
117
+ - `AbstractPageQueryHook + BaseQuery + BaseToolbar + BaseTable + jh-pagination`
118
+ - `BaseTable` 必须 `render-type="agGrid"`
119
+ - 每个 `BaseTable` 必须有全局唯一 `cid`
120
+ - 列定义必须使用 `@agile-team/wk-skills-ui/runtime` 的 `defineColumns()`
121
+ - 操作列必须使用 `renderOps()`,禁止旧式 `operations: []`
122
+ - mock-first:后端未就绪时生成 `mock/*.ts`,关闭 mock 后业务代码不需要改
123
+
124
+ 生成后建议运行:
125
+
126
+ ```bash
127
+ wl-skills validate-page src/views/<页面目录>
128
+ wl-skills doctor-ui
129
+ ```
130
+
111
131
  ---
112
132
 
113
133
  ## 常见问题
@@ -146,6 +166,8 @@ A: 执行 `npx @agile-team/wl-skills-kit clean`。会移除所有 AI 辅助文
146
166
  ```bash
147
167
  npx @agile-team/wl-skills-kit check # 环境/MCP/manifest 体检
148
168
  npx @agile-team/wl-skills-kit diff # 查看与当前包版本差异
169
+ npx @agile-team/wl-skills-kit validate-page src/views/xxx # 单页/目录强校验
170
+ npx @agile-team/wl-skills-kit doctor-ui # wk-skills-ui 接入体检
149
171
  npx @agile-team/wl-skills-kit update # 增量更新
150
172
  npx @agile-team/wl-skills-kit clean # 清理 AI 文件,保留 src/components + src/types
151
173
  npx @agile-team/wl-skills-kit export # 导出菜单/字典/权限基线 xlsx
@@ -38,17 +38,17 @@ skills/
38
38
 
39
39
  ## 启用 Skill 路由表
40
40
 
41
- | Skill 名 | 状态 | 路径 | 触发关键词 |
42
- | ---------------- | ---------- | -------------------------------------------- | -------------------------------------------------------------------------- |
43
- | prototype-scan | ✅ 启用 | `skills/core/prototype-scan/SKILL.md` | 扫描原型 / 解析原型 / 页面清单 / 详设文档 / 口述需求 / 建个页面 / 写个页面 |
44
- | api-contract | ✅ 启用 | `skills/core/api-contract/SKILL.md` | 接口约定 / api.md / 字段定义 / 前后端对齐 / 接口设计 |
45
- | page-codegen | ✅ 启用 | `skills/core/page-codegen/SKILL.md` | 生成页面 / 创建页面 / 代码生成 / vue页面 / 按原型生成 / 帮我生成 |
46
- | convention-audit | ✅ 启用 | `skills/core/convention-audit/SKILL.md` | 规范审计 / 代码审计 / 规范检查 / 对齐规范 / 规范偏差 / 接手新项目 / 存量代码分析 / 项目体检 |
47
- | template-extract | ✅ 启用 | `skills/core/template-extract/SKILL.md` | 提取模板 / 抽取模板 / 沉淀模板 / 模板贡献 |
48
- | menu-sync | ✅ 启用 | `skills/sync/menu-sync/SKILL.md` | 创建菜单 / 注册菜单 / 同步菜单 / 补菜单 |
49
- | dict-sync | ✅ 启用 | `skills/sync/dict-sync/SKILL.md` | 同步字典 / 创建字典 / 刷新字典基线 / 字典对比 / 字典审计 |
50
- | permission-sync | ✅ 启用 | `skills/sync/permission-sync/SKILL.md` | 创建角色 / 角色管理 / 角色授权 / 给角色分配菜单 / 挂动作 / 添加动作按钮 / 同步权限 / 权限码注册 |
51
- | code-fix | ✅ 启用 | `skills/ops/code-fix/SKILL.md` | 自动修复 / 整改偏差 / 修复报告 / 规范整改 / 修复偏差 / code fix |
41
+ | Skill 名 | 状态 | 路径 | 触发关键词 |
42
+ | ---------------- | ------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
43
+ | prototype-scan | ✅ 启用 | `skills/core/prototype-scan/SKILL.md` | 扫描原型 / 解析原型 / 页面清单 / 详设文档 / 口述需求 / 建个页面 / 写个页面 / 根据截图 / 根据原型 |
44
+ | api-contract | ✅ 启用 | `skills/core/api-contract/SKILL.md` | 接口约定 / api.md / 字段定义 / 前后端对齐 / 接口设计 |
45
+ | page-codegen | ✅ 启用 | `skills/core/page-codegen/SKILL.md` | 生成页面 / 创建页面 / 代码生成 / vue页面 / 按原型生成 / 帮我生成 / 列表页 / 管理页 / 台账 / mock / 假数据 / 先能跑 / AGGrid / skills-ui |
46
+ | convention-audit | ✅ 启用 | `skills/core/convention-audit/SKILL.md` | 规范审计 / 代码审计 / 规范检查 / 对齐规范 / 规范偏差 / 接手新项目 / 存量代码分析 / 项目体检 |
47
+ | template-extract | ✅ 启用 | `skills/core/template-extract/SKILL.md` | 提取模板 / 抽取模板 / 沉淀模板 / 模板贡献 |
48
+ | menu-sync | ✅ 启用 | `skills/sync/menu-sync/SKILL.md` | 创建菜单 / 注册菜单 / 同步菜单 / 补菜单 / 页面点击进不来 / 菜单打不开 |
49
+ | dict-sync | ✅ 启用 | `skills/sync/dict-sync/SKILL.md` | 同步字典 / 创建字典 / 刷新字典基线 / 字典对比 / 字典审计 |
50
+ | permission-sync | ✅ 启用 | `skills/sync/permission-sync/SKILL.md` | 创建角色 / 角色管理 / 角色授权 / 给角色分配菜单 / 挂动作 / 添加动作按钮 / 同步权限 / 权限码注册 |
51
+ | code-fix | ✅ 启用 | `skills/ops/code-fix/SKILL.md` | 自动修复 / 整改偏差 / 修复报告 / 规范整改 / 修复偏差 / code fix |
52
52
 
53
53
  ---
54
54
 
@@ -58,6 +58,8 @@ skills/
58
58
  2. SKILL.md 中标注的 `必读 standards` 按 `standards/index.md` 任务类型映射加载
59
59
  3. 如用户表达连续交付/智能体/全流程意图,同时读取 `_pipeline.md` 获取 Skill I/O 与 `next_suggest`
60
60
  4. 在 SKILL.md 指示下输出 **Pre-flight 声明**(强制约定式输出)
61
+ 5. 页面生成任务必须额外读取 `standards/12-base-table.md`,并执行 `BaseTable + render-type="agGrid" + cid + defineColumns + renderOps` 硬约束
62
+ 6. 若消息包含“风格 / skills-ui / 状态标签 / 操作列 / AGGrid”,同时建议运行 `wl-skills doctor-ui`
61
63
 
62
64
  ---
63
65
 
@@ -75,9 +77,9 @@ skills/
75
77
 
76
78
  每个启用 Skill 同目录下都有 `USAGE.md`:
77
79
 
78
- | 文件 | 读者 | 用途 |
79
- | --------------- | -------------- | ------------------------------------------ |
80
- | `SKILL.md` | AI 编辑器/模型 | 触发规则、流程步骤、Pre-flight 声明 |
81
- | `USAGE.md` | 团队成员 | 示例对话、踩坑、FAQ、快速上手 |
80
+ | 文件 | 读者 | 用途 |
81
+ | --------------- | ------------------------- | -------------------------------------------------------- |
82
+ | `SKILL.md` | AI 编辑器/模型 | 触发规则、流程步骤、Pre-flight 声明 |
83
+ | `USAGE.md` | 团队成员 | 示例对话、踩坑、FAQ、快速上手 |
82
84
  | `_pipeline.md` | AI 编辑器/模型 + 团队成员 | Skill 间 I/O 契约、next_suggest、Agent Pipeline 调度协议 |
83
- | `*.MAINTAIN.md` | kit 维护者 | 维护要点(在 `kit-internal/skills/` 目录) |
85
+ | `*.MAINTAIN.md` | kit 维护者 | 维护要点(在 `kit-internal/skills/` 目录) |
@@ -46,8 +46,9 @@ description: "Use when: generating complete Vue 3 page code (index.vue + data.ts
46
46
  ────────────────────────────────────────────────
47
47
  📌 后续步骤:
48
48
  1. 在 router/pages.ts 注册路由
49
- 2. 提交:git cz(禁止直接 git commit)
50
- 3. 可选:触发 convention-audit 扫描本次生成文件
49
+ 2. 若本页 hiddenMenu=true 在 src/util/navigate-hidden.ts 的 HIDDEN_ROUTE_MAP 追加一行
50
+ 3. 提交:git cz(禁止直接 git commit)
51
+ 4. 可选:触发 convention-audit 扫描本次生成文件
51
52
  ────────────────────────────────────────────────
52
53
  ```
53
54
 
@@ -119,6 +120,15 @@ src/views/[域]/[模块]/[子模块]/[kebab-case-目录名]/
119
120
  18. **按钮必须可交互**:所有按钮的 `onClick` 必须有真实处理逻辑,禁止空函数 `() => {}`。通用交互实现见下方 §按钮交互实现规则
120
121
  19. **未知交互兜底**:当原型未提供交互细节、且无法从通用模式推断时,`onClick` 中使用 `ElMessage.info("需业务确认交互逻辑")` 作为占位
121
122
  20. **生成后依赖自检**:代码生成完成后,检查 `package.json` 是否已安装生成代码所需的依赖(`mockjs`、`vite-plugin-mock`、`lodash-es`、`xlsx` 等),若缺失则提示用户执行安装命令。同时检查 `vite.config.ts` 是否已注册 `viteMockServe`、`mock/` 目录是否存在
123
+ 21. **默认 Mock First**:新生成页面默认必须走 `vite-plugin-mock`。必须生成 `mock/[页面kebab-name].ts`,并确保 `API_CONFIG` 中每个 URL 都有对应 mock 端点;只有当用户明确要求关闭 mock 或 `.env.dev` 中 `ENV_MOCK=false` 时,才允许直接联调真实接口。
124
+ 22. **Mock URL 必须匹配真实请求**:`API_CONFIG` 保持真实接口路径(如 `/mdata/mdataModel/list`),mock 文件端点必须带 Vite 代理前缀(如 `/dev-api/mdata/mdataModel/list`),这样关闭 mock 后无需修改业务代码。
125
+ 23. **页面初始数据必须由 mock 提供**:列表页 `onMounted(() => select())` 后必须能显示模拟数据,不允许生成空白页等待后端接口;`list` 端点返回 `{ code: 2000, data: { records, total, size, current } }`。
126
+ 24. **必须使用 wk-skills-ui runtime 风格**:当项目安装了 `@agile-team/wk-skills-ui` 时,列表列定义必须使用 `defineColumns()`,操作列必须使用 `renderOps()`,状态/字典列优先使用 runtime 渲染器或 `logicType=dict` 自动映射;不可退回默认纯文本/空函数风格。
127
+ 25. **wk-skills-ui 接入自检**:生成页面前检查项目是否已接入 `@agile-team/wk-skills-ui` 样式与 runtime。若未接入,先提示并补齐:`@use '@agile-team/wk-skills-ui/styles' as *;`、`installCommonPreset()`、必要的 design tokens 引入;否则页面风格不会自动生效。
128
+ 26. **pages.ts 分组注册**:多页面模块必须按当前业务目录分组写入 `vite/plugins/shared/pages.ts`,使用 `gProd(module, { subModule: [[page, label]] })` 结构,不允许把所有页面扁平追加到一个数组。
129
+ 27. **BaseTable 强制 AGGrid**:所有业务主列表/台账/主从表/树表/详情子表的 `BaseTable` 必须显式写 `render-type="agGrid"`,并绑定全局唯一 `cid`。弹窗小表格可豁免,但必须在生成摘要中说明理由。
130
+ 28. **cid 必须可追踪**:每个页面导出 `TABLE_CID = "{pageAbbr}-{base36Timestamp}"`;多表页面使用 `BOTTOM_TABLE_CID` / `ITEM_TABLE_CID`,列级 `cid` 必须使用 `${TABLE_CID}-fieldName` 前缀。
131
+ 29. **skills-ui 只能融合,不可生搬硬套**:不得照搬 `wk-skills-ui/templates/list-page` 中的原生 `el-form/usePageHook/el-pagination` 通用写法;本项目必须保留 `AbstractPageQueryHook + BaseQuery + BaseToolbar + BaseTable + jh-pagination` 平台骨架,只融合 `defineColumns/renderOps/tokens/preset`。
122
132
 
123
133
  ### 禁止事项(严格遵守)
124
134
 
@@ -135,6 +145,11 @@ src/views/[域]/[模块]/[子模块]/[kebab-case-目录名]/
135
145
  11. **❌ 禁止表单控件宽度不统一**:`jh-select`、`jh-date`、`el-input-number`、`jh-file-upload` 默认宽度可能与 `el-input` 不一致,必须在 scoped style 中用 `:deep()` 统一设为 `width: 100%`(详见 §表单页 UI 细节规范)
136
146
  12. **❌ 禁止表单页无滚动**:独立路由表单页内容超出视口时必须可滚动,`.app-page-container` 须设 `overflow-y: auto`(**不要加 `height: 100%`,全局已有 `height: calc(100vh - 100px)`,叠加会导致双滚动条**)
137
147
  13. **❌ 禁止内联 style 散落**:所有页面/组件样式统一写在 `index.scss` 中(便于复用和移动),不可在 template 中大量使用内联 `style="..."`
148
+ 14. **❌ 禁止生成无 mock 的页面**:只写 `API_CONFIG` 但不写 `mock/*.ts` 属于生成失败。
149
+ 15. **❌ 禁止生成空 onClick**:`onClick: () => {}` 属于生成失败;未知逻辑也必须用 `ElMessage.info(...)` 明示。
150
+ 16. **❌ 禁止忽略 wk-skills-ui**:项目已安装 `@agile-team/wk-skills-ui` 时,不使用 `defineColumns/renderOps` 属于生成失败。
151
+ 17. **❌ 禁止 BaseTable 非 AGGrid**:业务列表中 `<BaseTable>` 未写 `render-type="agGrid"` 或缺少 `cid/:cid` 属于生成失败。
152
+ 18. **❌ 禁止列缺 cid**:AGGrid 表格的数据列/操作列缺少列级 `cid` 属于生成失败。
138
153
 
139
154
  ### c_formModal 使用规范
140
155
 
@@ -245,9 +260,10 @@ function handleCodeClick(row: any) {
245
260
  >
246
261
  > | 场景 | 方式 | 原因 |
247
262
  > |---|---|---|
248
- > | **菜单页 → 隐藏页**(如列表→表单) | `envConfig()?.router` + `location.href` | 需要父壳刷新菜单高亮 |
249
- > | **隐藏页 → 隐藏页**(如表单→变更历史) | `envConfig()?.router` + `location.href` | `router.push()` 跳过 shell 的 `generateCurrentRoute`,导致 "Invalid route component" 报错 |
263
+ > | **菜单页 → 隐藏页 / 隐藏页 → 隐藏页** | `navigateHidden(path, query?)` from `src/util/navigate-hidden.ts` | 懒注册 + router.push,无整页刷新;内部自动兜底 location.href 防白屏 |
250
264
  > | **返回上一页** | `useRouter().back()` | 任何页面均可用 |
265
+ >
266
+ > ⚠️ `navigateHidden` 依赖 `src/util/navigate-hidden.ts` 的 `HIDDEN_ROUTE_MAP`。**每新增一个隐藏页,必须在该 Map 里追加一行**,否则兜底会退化为整页刷新。
251
267
 
252
268
  #### 路由路径命名规则
253
269
 
@@ -260,66 +276,94 @@ function handleCodeClick(row: any) {
260
276
  - 子模块名取 pages.ts 的 key,如 `aiflow`
261
277
  - 页面目录名整体转 PascalCase(含 `mmwr` 前缀),如 `mmwr-customer-apply-add-form` → `mmwrCustomerApplyAddForm`
262
278
 
263
- #### 标准实现(data.ts
279
+ #### navigate-hidden.ts 标准实现(首次使用时创建,后续只追加 Map 条目)
264
280
 
265
281
  ```typescript
266
- // ✅ 正确:用 envConfig
282
+ // src/util/navigate-hidden.ts
267
283
  import envConfig from "@jhlc/common-core/src/store/env-config";
284
+ import { ElMessage } from "element-plus";
285
+
286
+ /**
287
+ * 隐藏页路由懒注册表
288
+ * 每新增一个 hiddenMenu=true 的页面,在此追加一行
289
+ */
290
+ const HIDDEN_ROUTE_MAP: Record<string, () => Promise<any>> = {
291
+ // "/aiflow/mmwrCustomerApplyAddForm": () => import("@/views/produce/aiflow/mmwr-customer-apply-add-form/index.vue"),
292
+ };
293
+
294
+ export async function navigateHidden(path: string, query?: Record<string, string>) {
295
+ const router = envConfig()?.router;
296
+ if (!router) { ElMessage.error("路由未初始化,请刷新页面重试"); return; }
297
+
298
+ const matched = router.resolve({ path }).matched;
299
+ if (!matched.length || matched[0].path === "/:pathMatch(.*)*") {
300
+ const loader = HIDDEN_ROUTE_MAP[path];
301
+ if (!loader) {
302
+ // 降级兜底:路由 Map 未配置时整页跳转,不白屏
303
+ location.href = router.resolve({ path, query } as any).href;
304
+ return;
305
+ }
306
+ router.addRoute({ path, component: loader });
307
+ }
308
+ await router.push({ path, query } as any);
309
+ }
310
+ ```
311
+
312
+ #### 调用侧标准实现(data.ts)
313
+
314
+ ```typescript
315
+ // ✅ 正确:用 navigateHidden
316
+ import { navigateHidden } from "@/util/navigate-hidden";
268
317
 
269
318
  // 在 createPage() 外部定义,避免每次调用都重新创建
270
319
  const FORM_ROUTE = "/aiflow/mmwrCustomerApplyAddForm";
271
320
 
272
321
  function navigateToForm(query?: Record<string, string>) {
273
- const router = envConfig()?.router;
274
- if (!router) {
275
- ElMessage.error("路由未初始化,请刷新页面重试");
276
- return;
277
- }
278
- const target: any = { path: FORM_ROUTE };
279
- if (query) target.query = query;
280
- location.href = router.resolve(target).href;
322
+ navigateHidden(FORM_ROUTE, query);
281
323
  }
282
324
 
283
325
  export function createPage() {
284
- // ... 不在 createPage 内部声明 router
285
326
  const Page = new (class extends AbstractPageQueryHook {
286
- // ...
287
327
  toolbarDef(): ActionButtonDesc[] {
288
328
  return [
289
329
  {
290
330
  name: "primary",
291
331
  label: "新增申请",
292
- onClick: () => navigateToForm() // 无参:新增
332
+ onClick: () => navigateToForm() // 无参:新增
293
333
  }
294
334
  ];
295
335
  }
296
336
  columnsDef(): TableColumnDesc<any>[] {
297
- return [
337
+ return defineColumns([
298
338
  // ...
299
339
  {
300
340
  label: "操作",
301
- operations: [
302
- {
303
- label: "编辑",
304
- onClick: (row: any) => navigateToForm({ id: row.id }) // 带 id:编辑
305
- }
306
- ]
341
+ name: "_action",
342
+ cid: `${TABLE_CID}-action`,
343
+ fixed: "right",
344
+ align: "center",
345
+ defaultSlot: ({ row }: any) =>
346
+ renderOps([
347
+ { type: "edit", onClick: () => navigateToForm({ id: row.id }) } // 带 id:编辑
348
+ ])
307
349
  }
308
- ];
350
+ ] as any) as TableColumnDesc<any>[];
309
351
  }
310
352
  })();
311
353
  return Page.create() as any;
312
354
  }
313
355
  ```
314
356
 
315
- > **❌ 禁止**:
316
- > - `router.push({ path: "/mmwr-xxx-form" })`(kebab-case 路径错误)
317
- > - 在**菜单可见页面**(如列表页 data.ts `navigateToForm`)中使用 `router.push()`(父壳无法刷新菜单高亮)
357
+ > **✅ 正确做法**:
358
+ > - 跳转隐藏页 → `navigateHidden(path, query?)`(懒注册 + router.push,无刷新,内部兜底防白屏)
359
+ > - 返回上一页 `useRouter().back()`
318
360
  >
319
- > **✅ 允许**:
320
- > - `useRouter().back()`(表单页"取消"按钮返回上一页时可用)
361
+ > **❌ 禁止**:
362
+ > - 直接 `router.push({ path: "..." })` — 主应用过滤了 hidden 路由,路由未注册直接 push 会白屏或报 "Invalid route component"
363
+ > - 直接 `location.href = router.resolve(...).href` — 触发整页重载,有加载动画刷新感;`navigateHidden` 内部已兜底,**外部调用侧禁止直接使用**
364
+ > - kebab-case 路径(`/mmwr-xxx-form`)— 路由路径必须是 camelCase
321
365
  >
322
- > ⚠️ **所有前进导航(包括隐藏页→隐藏页)必须用 `location.href`**。`router.push()` 会跳过 shell 的 `generateCurrentRoute`,在 dev 模式下触发 "Invalid route component" 错误(已在 `mmwrCustomerApplyChangeHistory` 实测验证)。
366
+ > ⚠️ **新增隐藏页时必须同步维护 `src/util/navigate-hidden.ts` 的 `HIDDEN_ROUTE_MAP`**,否则 `navigateHidden` 降级为整页刷新,失去无刷新优势。
323
367
 
324
368
  ---
325
369
 
@@ -401,34 +445,38 @@ const ids = rows.map((r: any) => r.id);
401
445
  ```typescript
402
446
  {
403
447
  label: "操作",
448
+ name: "_action",
449
+ cid: `${TABLE_CID}-action`,
404
450
  width: 140,
405
451
  fixed: "right",
406
- operations: [
407
- {
408
- name: "edit",
409
- label: "修改",
410
- show: (row: any) => row.verifyStatus === "已核实",
411
- onClick: (row: any) => _editModalRef?.value?.edit(row.id)
412
- },
413
- {
414
- name: "danger",
415
- label: "作废",
416
- show: (row: any) => row.verifyStatus === "已核实",
417
- onClick: (row: any) => { /* cancel API */ }
418
- },
419
- {
420
- name: "edit",
421
- label: "编辑",
422
- show: (row: any) => row.verifyStatus !== "已核实",
423
- onClick: (row: any) => _editModalRef?.value?.edit(row.id)
424
- },
425
- {
426
- name: "remove",
427
- label: "删除",
428
- show: (row: any) => row.verifyStatus !== "已核实",
429
- onClick: (row: any) => { /* remove API */ }
430
- }
431
- ]
452
+ align: "center",
453
+ defaultSlot: ({ row }: any) =>
454
+ renderOps([
455
+ {
456
+ type: "edit",
457
+ label: "修改",
458
+ show: () => row.verifyStatus === "已核实",
459
+ onClick: () => _editModalRef?.value?.edit(row.id)
460
+ },
461
+ {
462
+ type: "danger",
463
+ label: "作废",
464
+ show: () => row.verifyStatus === "已核实",
465
+ onClick: () => handleCancel(row)
466
+ },
467
+ {
468
+ type: "edit",
469
+ label: "编辑",
470
+ show: () => row.verifyStatus !== "已核实",
471
+ onClick: () => _editModalRef?.value?.edit(row.id)
472
+ },
473
+ {
474
+ type: "del",
475
+ label: "删除",
476
+ show: () => row.verifyStatus !== "已核实",
477
+ onClick: () => Page?.remove(row.id)
478
+ }
479
+ ])
432
480
  }
433
481
  ```
434
482
 
@@ -497,15 +545,24 @@ function renderStatusTag(row: any, field: string) {
497
545
  let Page: any = null;
498
546
 
499
547
  export function managementColumns(): TableColumnDesc<any>[] {
500
- return [
548
+ return defineColumns([
501
549
  // ...
502
- { label: "操作", fixed: "right", width: 100, operations: [
503
- { name: "remove", label: "删除", onClick: (row: any) => Page?.remove(row.id) }
504
- ]}
505
- ];
550
+ {
551
+ label: "操作",
552
+ name: "_action",
553
+ cid: `${TABLE_CID}-management-action`,
554
+ fixed: "right",
555
+ width: 100,
556
+ align: "center",
557
+ defaultSlot: ({ row }: any) =>
558
+ renderOps([
559
+ { type: "del", label: "删除", onClick: () => Page?.remove(row.id) }
560
+ ])
561
+ }
562
+ ] as any) as TableColumnDesc<any>[];
506
563
  }
507
564
  export function usageColumns(): TableColumnDesc<any>[] {
508
- return [ /* 使用视角列... */ ];
565
+ return defineColumns([ /* 使用视角列... */ ] as any) as TableColumnDesc<any>[];
509
566
  }
510
567
 
511
568
  export function createPage(editModalRef?: any) {
@@ -866,6 +923,50 @@ import { BaseQueryItemDesc } from "@jhlc/common-core/src/components/form/base-qu
866
923
 
867
924
  page-codegen 生成页面代码后,**必须追加写入菜单配置信息到 `reports/SYS_MENU_INFO.md`**。
868
925
 
926
+ ## pages.ts 分组注册规则(Module Federation 子应用)
927
+
928
+ 多页面子应用必须使用与 `wl-mdata` 验证一致的分组模式,禁止扁平追加:
929
+
930
+ ```typescript
931
+ import type { SharedPageItem } from "./utils";
932
+
933
+ type PageTuple = [string, string];
934
+ type SubModuleMap = Record<string, PageTuple[]>;
935
+
936
+ const gProd = (module: string, subModules: SubModuleMap): SharedPageItem[] =>
937
+ Object.entries(subModules).flatMap(([subModule, pages]) =>
938
+ pages.map(([page, label]) => ({
939
+ name: `${module}/${subModule}/${page}/index.vue`,
940
+ label
941
+ }))
942
+ );
943
+
944
+ const mdataModule = gProd("mdata", {
945
+ model: [
946
+ ["mdata-model-config", "主数据模型配置"],
947
+ ["mdata-attr-mapping", "属性映射管理"]
948
+ ],
949
+ integration: [
950
+ ["mdata-integ-system", "集成系统管理"]
951
+ ]
952
+ });
953
+
954
+ export const list: SharedPageItem[] = mdataModule;
955
+ export default list;
956
+ ```
957
+
958
+ 生成规则:
959
+
960
+ | 字段 | 规则 |
961
+ | --- | --- |
962
+ | `module` | 来自项目启动参数 `--module=xxx`,如 `mdata` |
963
+ | `subModule` | 来自页面目录第二级,如 `src/views/mdata/model/...` → `model` |
964
+ | `page` | 页面 kebab 目录名,如 `mdata-model-config` |
965
+ | `name` | `${module}/${subModule}/${page}/index.vue` |
966
+ | `label` | 页面中文名 |
967
+
968
+ > menu-sync 的 `component` 必须与 `pages.ts` 生成的 `name` 完全一致,否则菜单能创建但点击无法加载页面。
969
+
869
970
  ### 写入策略(默认追加,不覆盖)
870
971
 
871
972
  - **默认为追加模式**:保留已有内容,在末尾追加本次生成的菜单。避免覆盖团队之前累积的菜单记录。
@@ -875,24 +976,27 @@ page-codegen 生成页面代码后,**必须追加写入菜单配置信息到 `
875
976
 
876
977
  ### 生成模板
877
978
 
878
- 每个页面生成一个菜单条目,格式如下:
979
+ 多级菜单必须先写目录(`type=M`),再在目录下写页面菜单(`type=C`)。格式如下:
879
980
 
880
981
  ```markdown
881
- ## [序号]. [菜单名称]
882
-
883
- | 字段 | 填写值 |
884
- | ------------ | ------ |
885
- | 类型 Tab | 选择 **菜单** |
886
- | 上级目录 | `[父目录名]` |
887
- | 应用选择 | `[应用名]` |
888
- | 使用缓存 | 使用 |
889
- | 显示排序 | `[序号]` |
890
- | 菜单路径 | `[camelCase目录名]` |
891
- | 菜单名称 | `[中文名]` |
892
- | 名称编码后缀 | `[菜单路径拼音小写]` |
893
- | 组件路径 | `[域]/[模块]/[子模块]/[kebab-目录名]/index.vue` |
894
- | 权限标识 | `[camelCase目录名]` |
895
- | 是否隐藏 | **否** / **是** |
982
+ # 系统菜单配置 — [项目名] [业务模块]
983
+
984
+ > **module 命名**:`[module]`
985
+ > **父级菜单**:来自 `.github/skills/sync/env.local.json` `menu.parentMenuId`
986
+
987
+ ## 一级目录(type=M)
988
+
989
+ | # | 菜单名 | path | orderNum | 备注 |
990
+ | - | ------ | ---- | -------- | ---- |
991
+ | 1 | [目录中文名] | `[目录pathCamel]` | 1 | 含 N 个子菜单 |
992
+
993
+ ## 二级菜单(type=C)
994
+
995
+ ### 1. [目录中文名] 子菜单
996
+
997
+ | 菜单名 | path | component | permission |
998
+ | ------ | ---- | --------- | ---------- |
999
+ | [页面中文名] | `[pageCamel]` | `[module]/[subModule]/[pageKebab]/index.vue` | `[module]:[pageCamel]:list` |
896
1000
  ```
897
1001
 
898
1002
  ### 字段生成规则
@@ -901,8 +1005,8 @@ page-codegen 生成页面代码后,**必须追加写入菜单配置信息到 `
901
1005
  |------|------|------|
902
1006
  | 菜单路径 | page-spec.kebabName | kebab-case → camelCase(`mmwr-customer-archive` → `mmwrCustomerArchive`) |
903
1007
  | 菜单名称 | page-spec.pageName | 直接使用中文名 |
904
- | 组件路径 | pages.ts 注册路径 | `[]/[模块]/[子模块]/[kebab-目录名]/index.vue` |
905
- | 权限标识 | 同菜单路径 | camelCase |
1008
+ | 组件路径 | pages.ts 注册路径 | `[module]/[subModule]/[kebab-目录名]/index.vue` |
1009
+ | 权限标识 | module + 菜单路径 | `[module]:[pageCamel]:list` |
906
1010
  | 是否隐藏 | page-spec.features.hiddenMenu | `true` → 是,`false` → 否 |
907
1011
  | 上级目录 | 用户指定 / page-spec 推断 | 如果用户在原型扫描阶段指定了上级目录,使用该值 |
908
1012
  | 应用选择 | pages.ts 域名 | `produce` → 生产,`sale` → 销售 |
@@ -919,29 +1023,27 @@ page-codegen 生成页面代码后,**必须追加写入菜单配置信息到 `
919
1023
  ### SYS_MENU_INFO.md 文件结构
920
1024
 
921
1025
  ```markdown
922
- # 系统菜单配置 — [模块名称]([域] / [子模块路径]
1026
+ # 系统菜单配置 — [项目名] [模块名]
923
1027
 
924
- > 对应系统管理 → 菜单管理 → 新增菜单,每栏直接复制粘贴。
925
- > **操作顺序:先建目录(第 0 步),再逐个添加菜单。**
926
- >
927
- > **pages.ts 注册位置**:`vite/plugins/shared/pages.ts` → `[模块变量名]` → `[子模块key]`
1028
+ > **父级菜单**:`[parentMenuId]`
1029
+ > **应用编码**:`[sysAppNo]`
1030
+ > **module 命名**:`[module]`
928
1031
 
929
- ## 第 0 步:新建目录(如需要)
1032
+ ## 一级目录(type=M)
930
1033
 
931
- | 字段 | |
932
- | -------- | -- |
933
- | 上级目录 | `[上级目录名]` |
934
- | 菜单名称 | `[目录名]` |
935
- | 显示排序 | `[序号]` |
1034
+ | # | 菜单名 | path | orderNum | 备注 |
1035
+ | - | ------ | ---- | -------- | ---- |
1036
+ | 1 | [目录名] | `[目录pathCamel]` | 1 | 含 N 个子菜单 |
936
1037
 
937
- ## 第 1 步:[页面名称]
1038
+ ## 二级菜单(type=C)
938
1039
 
939
- [菜单条目表格]
1040
+ ### 1. [目录名] 子菜单
940
1041
 
941
- > pages.ts 对应:`["[kebab-name]", "[中文名]"]`
1042
+ | 菜单名 | path | component | permission |
1043
+ | ------ | ---- | --------- | ---------- |
1044
+ | [页面名称] | `[pageCamel]` | `[module]/[subModule]/[pageKebab]/index.vue` | `[module]:[pageCamel]:list` |
942
1045
 
943
- ## 2 步:[页面名称]
944
- ...
1046
+ > pages.ts 对应:`gProd("[module]", { [subModule]: [["[pageKebab]", "[页面名称]"]] })`
945
1047
  ```
946
1048
 
947
1049
  ### 与 menu-sync 的衔接
@@ -950,6 +1052,7 @@ SYS_MENU_INFO.md 是 menu-sync Skill 的输入数据源:
950
1052
  - **自动创建**:用户说"帮我创建菜单" → menu-sync 读取 SYS_MENU_INFO.md → 调 API 逐条创建
951
1053
  - **手动创建**:用户也可直接按 SYS_MENU_INFO.md 的表格在系统管理后台手动创建菜单
952
1054
  - 两种方式等价,菜单创建后通过 `组件路径` 字段与 pages.ts 注册的文件路径关联
1055
+ - **自动创建顺序**:必须先调用 `wls_menu_query` 获取当前 domain 菜单树,再 `wls_menu_upsert` 创建/更新一级目录(type=M),拿到目录 id 后再创建二级菜单(type=C)。不得把二级页面全部直接挂到根 `parentMenuId`。
953
1056
 
954
1057
  ---
955
1058