@agile-team/wl-skills-kit 2.2.0 → 2.3.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.
Files changed (101) hide show
  1. package/CHANGELOG.md +33 -22
  2. package/README.md +11 -103
  3. package/bin/wl-skills.js +2 -42
  4. package/files/.github/guides/README.md +13 -13
  5. package/files/.github/guides/architecture.md +555 -555
  6. package/files/.github/guides/usage.md +176 -173
  7. package/files/.github/reports/README.md +65 -65
  8. package/files/.github/reports/SYS_DICT_INFO.md +50 -50
  9. package/files/.github/reports/SYS_MENU_INFO.md +247 -247
  10. package/files/.github/reports/SYS_PERMISSION_INFO.md +20 -20
  11. package/files/.github/reports//347/273/204/344/273/266/346/217/220/345/217/226/345/273/272/350/256/256.md +33 -33
  12. package/files/.github/reports//350/247/204/350/214/203/345/256/241/346/237/245/346/212/245/345/221/212.md +44 -44
  13. package/files/.github/skills/_compat/README.md +108 -108
  14. package/files/.github/skills/_compat/headers/agents.txt +8 -8
  15. package/files/.github/skills/_compat/headers/claude-code.txt +7 -7
  16. package/files/.github/skills/_compat/headers/cline.txt +7 -7
  17. package/files/.github/skills/_compat/headers/cursor-mdc.txt +16 -16
  18. package/files/.github/skills/_compat/headers/cursor-rules.txt +7 -7
  19. package/files/.github/skills/_compat/headers/github-copilot.txt +1 -1
  20. package/files/.github/skills/_compat/headers/kiro.txt +10 -10
  21. package/files/.github/skills/_compat/headers/trae.txt +11 -11
  22. package/files/.github/skills/_compat/headers/windsurf.txt +7 -7
  23. package/files/.github/skills/_registry.md +81 -81
  24. package/files/.github/skills/core/api-contract/SKILL.md +344 -344
  25. package/files/.github/skills/core/api-contract/USAGE.md +110 -110
  26. package/files/.github/skills/core/convention-audit/SKILL.md +189 -189
  27. package/files/.github/skills/core/convention-audit/USAGE.md +99 -99
  28. package/files/.github/skills/core/page-codegen/SKILL.md +973 -973
  29. package/files/.github/skills/core/page-codegen/USAGE.md +102 -102
  30. package/files/.github/skills/core/page-codegen/templates/_index.md +46 -46
  31. package/files/.github/skills/core/page-codegen/templates/domains/_CONTRIBUTING.md +107 -107
  32. package/files/.github/skills/core/page-codegen/templates/domains/produce/TPL-OPERATION-STATION.md +442 -442
  33. package/files/.github/skills/core/page-codegen/templates/domains/sale/README.md +26 -26
  34. package/files/.github/skills/core/page-codegen/templates/universal/TPL-CHANGE-HISTORY.md +276 -276
  35. package/files/.github/skills/core/page-codegen/templates/universal/TPL-DETAIL-TABS.md +1145 -1145
  36. package/files/.github/skills/core/page-codegen/templates/universal/TPL-DRIVEN.md +124 -124
  37. package/files/.github/skills/core/page-codegen/templates/universal/TPL-FORM-ROUTE.md +436 -436
  38. package/files/.github/skills/core/page-codegen/templates/universal/TPL-LIST.md +191 -191
  39. package/files/.github/skills/core/page-codegen/templates/universal/TPL-MASTER-DETAIL.md +148 -148
  40. package/files/.github/skills/core/page-codegen/templates/universal/TPL-RECORD-FORM.md +376 -376
  41. package/files/.github/skills/core/page-codegen/templates/universal/TPL-TREE-LIST.md +186 -186
  42. package/files/.github/skills/core/prototype-scan/SKILL.md +498 -498
  43. package/files/.github/skills/core/prototype-scan/USAGE.md +95 -95
  44. package/files/.github/skills/core/template-extract/SKILL.md +139 -139
  45. package/files/.github/skills/core/template-extract/USAGE.md +93 -93
  46. package/files/.github/skills/domain/README.md +51 -51
  47. package/files/.github/skills/sync/env.local.json +2 -1
  48. package/files/.github/skills/sync/menu-sync/SKILL.md +263 -263
  49. package/files/.github/skills/sync/menu-sync/USAGE.md +104 -104
  50. package/files/.github/skills/sync/menu-sync/env/env.local.json +7 -7
  51. package/files/.github/skills/sync/menu-sync/env/guide.md +99 -99
  52. package/files/.github/skills/sync/permission-sync/SKILL.draft.md +91 -91
  53. package/files/.github/standards/01-toolchain.md +57 -57
  54. package/files/.github/standards/02-code-structure.md +111 -111
  55. package/files/.github/standards/03-comments.md +53 -53
  56. package/files/.github/standards/04-coding-basics.md +33 -33
  57. package/files/.github/standards/05-logging.md +38 -38
  58. package/files/.github/standards/06-security.md +44 -44
  59. package/files/.github/standards/07-config.md +52 -52
  60. package/files/.github/standards/08-git.md +60 -60
  61. package/files/.github/standards/09-typescript.md +71 -71
  62. package/files/.github/standards/10-pinia.md +57 -57
  63. package/files/.github/standards/11-form-validation.md +81 -81
  64. package/files/.github/standards/12-base-table.md +153 -153
  65. package/files/.github/standards/13-platform-components.md +123 -123
  66. package/files/.github/standards/index.md +89 -89
  67. package/files/demo/produce/aiflow/mmwr-customer-apply-add/api.md +1 -1
  68. package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/data.ts +196 -196
  69. package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/index.scss +150 -150
  70. package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/index.vue +79 -79
  71. package/files/docs/jh-date-range.md +257 -257
  72. package/files/docs/jh-date.md +222 -222
  73. package/files/docs/jh-dept-picker.md +190 -190
  74. package/files/docs/jh-drag-row.md +590 -590
  75. package/files/docs/jh-file-upload.md +216 -216
  76. package/files/docs/jh-picker.md +218 -218
  77. package/files/docs/jh-select.md +148 -148
  78. package/files/docs/jh-text.md +248 -248
  79. package/files/docs/jh-user-picker.md +197 -197
  80. package/files/docs/request.md +24 -9
  81. package/files/src/components/global/C_RightToolbar/data.ts +228 -0
  82. package/files/src/components/global/C_RightToolbar/index.scss +44 -0
  83. package/files/src/components/global/C_RightToolbar/index.vue +34 -336
  84. package/files/src/components/global/C_Splitter/index.scss +61 -0
  85. package/files/src/components/global/C_Splitter/index.vue +2 -64
  86. package/files/src/components/global/C_SvgIcon/index.scss +15 -0
  87. package/files/src/components/global/C_SvgIcon/index.vue +20 -50
  88. package/files/src/components/global/C_TagStatus/index.scss +20 -0
  89. package/files/src/components/global/C_TagStatus/index.vue +1 -22
  90. package/files/src/components/global/C_Tree/data.ts +61 -0
  91. package/files/src/components/global/C_Tree/index.vue +12 -53
  92. package/files/src/components/local/c_listModal/index.scss +4 -0
  93. package/files/src/components/local/c_listModal/index.vue +1 -1
  94. package/mcp/api/client.js +76 -0
  95. package/mcp/api/dictApi.js +40 -0
  96. package/mcp/api/menuApi.js +32 -0
  97. package/mcp/config.js +47 -0
  98. package/mcp/server.js +210 -0
  99. package/mcp/tools/dictSync.js +173 -0
  100. package/mcp/tools/menuSync.js +96 -0
  101. package/package.json +6 -9
@@ -1,973 +1,973 @@
1
- ---
2
- name: page-codegen
3
- description: "Use when: generating complete Vue 3 page code (index.vue + data.ts + modal components + api.md + pages.ts registration) from a prototype page inventory and API contract, strictly following the cx-ui-produce project conventions. Read SKILL.md first (rules+constraints), then read the matching TPL-*.md for the template code. Triggers on: generate page, create page, code generation, 生成页面, 页面代码, 代码生成, vue页面, 按原型生成, 口述需求, 建个页面, 写个页面, 帮我生成, natural language page generation."
4
- ---
5
-
6
- # Skill: 页面代码生成(page-codegen)
7
-
8
- 基于《页面清单》+ 原型信息,生成符合项目规范的完整 Vue 3 页面代码。
9
-
10
- ---
11
-
12
- ## Pre-flight 规范声明(执行前必须输出)
13
-
14
- ```
15
- 🚀 已触发技能 page-codegen/SKILL.md → 页面代码生成:4文件 + 模板调度 + 前置检查
16
- ✅ 已读取 templates/_index.md → 模板注册表,匹配 → {TPL路径}
17
- ✅ 已读取 templates/{universal|domains/xxx}/TPL-XXX.md → {当前模板说明}
18
- ✅ 已读取 standards/index.md → 规范门控(任务类型 A:生成新页面)
19
- ✅ 已读取 standards/02-code-structure.md → 4文件原则 + 三段式 + script 9段顺序
20
- ✅ 已读取 standards/12-base-table.md → AGGrid必用 + cid命名规范
21
- ✅ 已读取 standards/13-platform-components.md → 平台组件对照表 + docs前置读取清单
22
- ✅ 已读取 docs/{涉及的jh-*文档} → 当前页涉及组件的使用规范
23
- ✅ 工具链检测:.prettierrc.js ✓ eslint.config.ts ✓ .husky/ ✓ [全部就绪]
24
- ✅ cid 已生成:{value}({首字母缩写说明})
25
- ```
26
-
27
- **工具链失败时(红叉 + 暂停)**:
28
-
29
- ```
30
- ❌ 工具链检测失败:未找到 .prettierrc.js / eslint.config.ts / .husky/
31
- → 请执行:npx @robot-admin/git-standards init
32
- → 或联系 CHENY(工号 409322)解决
33
- → ⛔ 代码生成已暂停,修复后重新触发
34
- ```
35
-
36
- **生成完成摘要(生成结束后输出)**:
37
-
38
- ```
39
- 📦 本次生成完成
40
- ────────────────────────────────────────────────
41
- ✅ src/views/.../{页面}/index.vue
42
- ✅ src/views/.../{页面}/data.ts
43
- ✅ src/views/.../{页面}/index.scss
44
- ✅ src/views/.../{页面}/api.md
45
- ✅ reports/SYS_MENU_INFO.md → 已追加菜单条目
46
- ────────────────────────────────────────────────
47
- 📌 后续步骤:
48
- 1. 在 router/pages.ts 注册路由
49
- 2. 提交:git cz(禁止直接 git commit)
50
- 3. 可选:触发 convention-audit 扫描本次生成文件
51
- ────────────────────────────────────────────────
52
- ```
53
-
54
- ---
55
-
56
- ## 前置检查
57
-
58
- ```
59
- □ 页面中文名:
60
- □ 交互模式:LIST / MASTER_DETAIL / TREE_LIST / DETAIL_TABS / FORM_ROUTE / CHANGE_HISTORY / RECORD_FORM / OPERATION_STATION / TEMPLATE_DRIVEN
61
- □ page-spec JSON:(必须存在,由 prototype-scan 输出)
62
- □ 文件路径:src/views/[域]/[模块]/[子模块]/[kebab-case-目录名]/
63
- □ pages.ts 注册名:["kebab-目录名", "中文名"]
64
- □ 服务缩写:[pm / mmwr / sale / ...]
65
- □ 资源名(CamelCase):
66
- ```
67
-
68
- > **重要**:查询字段、表格列、按钮列表不再手动罗列,直接从 page-spec JSON 中读取。
69
- > 如果没有 page-spec JSON,必须先执行 prototype-scan Skill 生成。
70
- >
71
- > **模式 0 快捷路径**:当用户直接口述需求(如"帮我生成一个客户管理页面")而未提供 page-spec JSON 时,AI 内部自动调用 prototype-scan 模式 0 构建 page-spec JSON,然后继续执行代码生成,无需用户提供任何文件。
72
-
73
- ---
74
-
75
- ## 生成产物(默认 4 文件)
76
-
77
- ```
78
- src/views/[域]/[模块]/[子模块]/[kebab-case-目录名]/
79
- ├── index.vue ← 页面入口(纯模板 + 解构)
80
- ├── data.ts ← 业务逻辑(AbstractPageQueryHook 类 / 直接导出 ref+函数)
81
- ├── index.scss ← 页面样式
82
- └── api.md ← 接口约定(按 api-contract Skill 模板生成)
83
- ```
84
-
85
- 弹窗组件处理策略:
86
-
87
- - **通用弹窗**(新增/编辑表单,2+ 页面可复用)→ 提取到 `src/components/local/c_xxxModal/`
88
- - **极个性弹窗**(仅单页面使用,c_modal 无法满足)→ 放在页面 `components/xxxModal.vue`
89
-
90
- 附加输出:
91
-
92
- - `pages.ts` 注册片段
93
- - **`reports/SYS_MENU_INFO.md`** — 集中式菜单配置,**追加写入**(见下方 §SYS_MENU_INFO 生成规则)
94
- - `mock/[页面kebab-name].ts`(项目根目录 `mock/` 下,`vite-plugin-mock` 自动加载,与 api.md 的 URL 和字段完全一致)
95
-
96
- ---
97
-
98
- ## 约束(严格遵守)
99
-
100
- ### 必须
101
-
102
- 1. data.ts 使用 `class extends AbstractPageQueryHook`,通过 `queryDef()` / `toolbarDef()` / `columnsDef()` 配置。**仅适用于 LIST / MASTER_DETAIL / TREE_LIST 三种列表型页面**。其余模板不用此基类:DETAIL_TABS(直接导出 reactive+ref)、FORM_ROUTE(useXxx composable)、CHANGE_HISTORY(composable+mock)、RECORD_FORM(直接 ref+函数)、OPERATION_STATION(多个 createXxxPage)、TEMPLATE_DRIVEN(仅 config 对象)
103
- 2. index.vue 只有模板 + `createPage()` 解构 + `onMounted`,不写业务逻辑。**例外**:DETAIL_TABS / FORM_ROUTE / CHANGE_HISTORY 的 index.vue 可包含表单状态管理;OPERATION_STATION 包含 computed/watch/多列表协调逻辑
104
- 3. 最外层 class:`app-container app-page-container`
105
- 4. 样式用 `@import "./index.scss"`
106
- 5. API 用 `getAction` / `postAction` from `@jhlc/common-core/src/api/action`
107
- 6. 字典字段用 `logicType: BusLogicDataType.dict, logicValue: "dictCode"`
108
- 7. 同时生成 api.md(基于 api-contract Skill 模板)
109
- 8. 提供 pages.ts 注册片段
110
- 9. 同时在 `mock/` 目录下生成对应的 mock 文件(`MockMethod[]` + mockjs,URL 和字段与 api.md 一致,URL 必须带 `/dev-api` 前缀)
111
- 10. **查询字段顺序**:`queryDef()` 中字段顺序必须与 page-spec `query` 数组顺序严格一致(即原型从左到右、从上到下)
112
- 11. **表格列顺序**:`columnsDef()` 中列顺序必须与 page-spec `columns` 数组顺序严格一致(`selection` + `index` 在最前,其余按原型表头从左到右)
113
- 12. **按钮顺序与颜色**:`toolbarDef()` 中按钮顺序和 `name`(颜色)必须与 page-spec `toolbar` 数组严格一致(`primary`=蓝底, `danger`=红色, `warning`=橙色, `default`=灰色; `plain: true`=线框)。**"新增"类按钮永远排第一**(如"新增"、"新增申请"),这是产品通用规范
114
- 13. **操作列按钮**:`columnsDef()` 操作列的 `operations` 数组必须与 page-spec `operations` 数组**严格一一对应**,不可遗漏也**不可自行添加**(如原型没有"查看"按钮就不能加"查看")
115
- 14. **Tab 标签**:当 page-spec `features.tabSwitch === true` 时,必须在 index.vue 中生成 Tab 组件,tabs 与 `features.tabItems` 一一对应
116
- 15. **按钮文字保真**:使用原型中的原始文字(如"新增申请"不可简化为"新增","变更申请"不可简化为"变更")
117
- 16. **可点击列(蓝色链接列)**:原型中蓝色凸显的列(如客户编码、申请编码等编码/编号类字段)必须实现为可点击链接,使用 `defaultSlot` + `h()` 渲染蓝色链接样式,点击后查看详情(调 `getById` 后展示或路由跳转)
118
- 17. **按钮颜色映射**:按钮的 `type` 属性决定颜色,须根据原型按钮颜色或按钮语义映射(见下方 §按钮颜色映射表)
119
- 18. **按钮必须可交互**:所有按钮的 `onClick` 必须有真实处理逻辑,禁止空函数 `() => {}`。通用交互实现见下方 §按钮交互实现规则
120
- 19. **未知交互兜底**:当原型未提供交互细节、且无法从通用模式推断时,`onClick` 中使用 `ElMessage.info("需业务确认交互逻辑")` 作为占位
121
- 20. **生成后依赖自检**:代码生成完成后,检查 `package.json` 是否已安装生成代码所需的依赖(`mockjs`、`vite-plugin-mock`、`lodash-es`、`xlsx` 等),若缺失则提示用户执行安装命令。同时检查 `vite.config.ts` 是否已注册 `viteMockServe`、`mock/` 目录是否存在
122
-
123
- ### 禁止事项(严格遵守)
124
-
125
- 1. **❌ 禁止手写弹窗**:不可在页面 `components/` 下用 `el-dialog` + `el-form` + `el-row/col` 手写弹窗。必须使用 `c_formModal`(`src/components/local/c_formModal/`),通过 `modalConfig` 配置驱动。**例外**:纯只读详情弹窗(`jh-dialog` + `BaseForm :disabled="true"`)可不用 `c_formModal`,如工艺参数查看(参考 mmwr-process-parameters)
126
- 2. **❌ 禁止在弹窗中使用原生 Element Plus 组件**:不可使用 `el-select`、`el-input`、`el-date-picker` 等原生组件,必须使用 `jh-select`、`jh-date`、`jh-user-picker` 等平台组件(通过 `BaseFormItemDesc` 的 `component` 属性配置)
127
- 3. **❌ 禁止在 BaseToolbar 内使用 slot**:`BaseToolbar` 组件**不支持任何 slot**(源码中无 `<slot>` 标签),放入的内容会被丢弃不渲染。Tab/视角切换等额外 UI 必须放在 BaseToolbar **外部**
128
- 4. **❌ 禁止用 el-radio-group 做 Tab/视角切换**:所有 Tab 式切换(视角切换、数据过滤 Tab、功能 Tab)**必须使用 `el-tabs`**(参考 `mmwr-steel-stripping-operations`)。不可用 `el-radio-group` + 手动 `handleViewChange` / `handleTabChange`
129
- 5. **❌ 禁止 Mock 端点只返回成功不修改数据**:mock 文件中每个端点的 `response` 必须实际修改 `dataPool`(splice/assign/修改字段),否则 `this.select()` 刷新后数据不变。详见 §Mock 端点最佳实践
130
- 6. **❌ 禁止遗留未使用的 import**:data.ts 中不要导入未使用的模块(如仅用 `postAction` 时不导入 `getAction`)
131
- 7. **❌ 禁止操作列自编按钮**:操作列的 `operations` 数组必须与原型操作列按钮**严格一致**,不可凭空添加原型中不存在的按钮(如原型只有"编辑"+"删除",不可自行加"查看")
132
- 8. **❌ 状态类列必须 `fixed: "right"` + 色块渲染**:启用状态、停用时间、转化状态、客户状态、审批状态、核实状态等靠近操作列的状态类列必须设置 `fixed: "right"`,与操作列一起固定在表格右侧。**且状态列必须用 `defaultSlot` + `h(ElTag)` 渲染彩色标签**,不可纯文本显示(详见 §状态列色块渲染模式)
133
- 9. **❌ 禁止操作按钮标签自编**:操作列按钮 `label` 必须与原型严格一致(如原型写"修改"不可改成"编辑",写"作废"不可改成"删除"),且 onClick 逻辑必须匹配语义("作废"调 cancel API,不是 remove)
134
- 10. **❌ 禁止平台组件遗漏 `label=""`**:在 el-form-item 内使用 `jh-select`、`jh-date`、`jh-file-upload` 时,**必须传 `label=""`** 隐藏组件自身标签(否则会渲染"下拉选择框:"、"日期:"等多余文字)
135
- 11. **❌ 禁止表单控件宽度不统一**:`jh-select`、`jh-date`、`el-input-number`、`jh-file-upload` 默认宽度可能与 `el-input` 不一致,必须在 scoped style 中用 `:deep()` 统一设为 `width: 100%`(详见 §表单页 UI 细节规范)
136
- 12. **❌ 禁止表单页无滚动**:独立路由表单页内容超出视口时必须可滚动,`.app-page-container` 须设 `overflow-y: auto`(**不要加 `height: 100%`,全局已有 `height: calc(100vh - 100px)`,叠加会导致双滚动条**)
137
- 13. **❌ 禁止内联 style 散落**:所有页面/组件样式统一写在 `index.scss` 中(便于复用和移动),不可在 template 中大量使用内联 `style="..."`
138
-
139
- ### c_formModal 使用规范
140
-
141
- > 项目已有 `src/components/local/c_formModal/` 通用表单弹窗组件,支持 add/edit/view 三模式。
142
- > 所有标准 CRUD 弹窗**必须使用此组件**,不可重复编写。
143
-
144
- **data.ts 中定义 modalConfig:**
145
-
146
- ```typescript
147
- import type { BaseFormItemDesc } from "@jhlc/common-core/src/components/form/common/type";
148
-
149
- export const modalConfig = {
150
- titlePrefix: "客户", // 标题前缀:新增客户 / 编辑客户 / 查看客户
151
- width: "850px",
152
- columns: 2,
153
- labelWidth: "110px",
154
- formItems: [
155
- { name: "code", label: "编码", disabled: true, placeholder: "系统自动生成" },
156
- { name: "name", label: "名称", required: true, placeholder: "请输入" },
157
- // 下拉用 jh-select 组件
158
- {
159
- name: "type",
160
- label: "类型",
161
- component: () => ({ tag: "jh-select", items: OPTS.type })
162
- }
163
- ] as BaseFormItemDesc<any>[],
164
- api: {
165
- getById: API_CONFIG.getById,
166
- save: API_CONFIG.save,
167
- update: API_CONFIG.update
168
- }
169
- };
170
- ```
171
-
172
- **index.vue 中使用:**
173
-
174
- ```vue
175
- <c_formModal ref="editModalRef" v-bind="modalConfig" @ok="select" />
176
-
177
- <script setup lang="ts">
178
- import { createPage, modalConfig } from "./data";
179
- import c_formModal from "@/components/local/c_formModal/index.vue";
180
- </script>
181
- ```
182
-
183
- **调用方式**(在 data.ts 中):
184
- - 新增:`_editModalRef?.value?.open()`
185
- - 编辑:`_editModalRef?.value?.edit(row.id)`
186
- - 查看:`_editModalRef?.value?.view(row.id)`
187
-
188
- ---
189
-
190
- ### 可点击列(蓝色链接列)
191
-
192
- 原型中以蓝色文字凸显的列(通常是编码、编号类字段)表示"可点击查看详情"。
193
-
194
- **识别规则**:
195
- - 原型中蓝色/带下划线的列文字 → 必须实现为可点击
196
- - 常见目标列:客户编码、申请编码、订单编号、合同编号、计划编号等"XX编码/编号"字段
197
-
198
- **实现方式**:在 `columnsDef()` 中使用 `defaultSlot` + `h()` 渲染蓝色链接:
199
-
200
- ```typescript
201
- import { h } from "vue";
202
-
203
- // 在 columnsDef() 中:
204
- {
205
- label: "客户编码",
206
- name: "customerCode",
207
- minWidth: 120,
208
- defaultSlot: ({ row }: any) => {
209
- return h(
210
- "span",
211
- {
212
- style: "color: #409eff; cursor: pointer; text-decoration: underline;",
213
- onClick: () => handleCodeClick(row)
214
- },
215
- row.customerCode
216
- );
217
- }
218
- }
219
- ```
220
-
221
- **点击处理逻辑**(按优先级选择):
222
- 1. 有编辑弹窗 → `_editModalRef?.value?.open(row.id, "view")` (查看模式打开同一弹窗)
223
- 2. 如果有详情路由 → `navigateToForm({ id: row.id, mode: "view" })`
224
- 3. Mock 阶段暂无详情页 → `ElMessage.info(\`查看详情: ${row.fieldValue}\`)`
225
-
226
- **handleCodeClick 推荐实现**:
227
-
228
- ```typescript
229
- let _editModalRef: any = null;
230
-
231
- function handleCodeClick(row: any) {
232
- _editModalRef?.value?.open(row.id, "view");
233
- }
234
- ```
235
-
236
- > 注意:`_editModalRef` 在 `createPage(editModalRef?)` 中赋值,详见 §弹窗模板
237
-
238
- ---
239
-
240
- ### FORM_ROUTE 表单页(路由跳转式表单)
241
-
242
- > 当表单足够复杂(如多 Tab、多子表、独立布局)时,使用**独立路由**替代弹窗(c_formModal)。
243
- >
244
- > **导航方式选择**(按场景区分):
245
- >
246
- > | 场景 | 方式 | 原因 |
247
- > |---|---|---|
248
- > | **菜单页 → 隐藏页**(如列表→表单) | `envConfig()?.router` + `location.href` | 需要父壳刷新菜单高亮 |
249
- > | **隐藏页 → 隐藏页**(如表单→变更历史) | `envConfig()?.router` + `location.href` | `router.push()` 跳过 shell 的 `generateCurrentRoute`,导致 "Invalid route component" 报错 |
250
- > | **返回上一页** | `useRouter().back()` | 任何页面均可用 |
251
-
252
- #### 路由路径命名规则
253
-
254
- | 目录名(kebab-case) | 路由路径(camelCase) |
255
- | ---------------------------------- | -------------------------------------------- |
256
- | `mmwr-customer-apply-add-form` | `/aiflow/mmwrCustomerApplyAddForm` |
257
- | `mmwr-customer-apply-change-form` | `/aiflow/mmwrCustomerApplyChangeForm` |
258
-
259
- **规则**:`/[子模块名-camelCase]/[完整页面目录名转PascalCase]`
260
- - 子模块名取 pages.ts 的 key,如 `aiflow`
261
- - 页面目录名整体转 PascalCase(含 `mmwr` 前缀),如 `mmwr-customer-apply-add-form` → `mmwrCustomerApplyAddForm`
262
-
263
- #### 标准实现(data.ts)
264
-
265
- ```typescript
266
- // ✅ 正确:用 envConfig
267
- import envConfig from "@jhlc/common-core/src/store/env-config";
268
-
269
- // 在 createPage() 外部定义,避免每次调用都重新创建
270
- const FORM_ROUTE = "/aiflow/mmwrCustomerApplyAddForm";
271
-
272
- 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;
281
- }
282
-
283
- export function createPage() {
284
- // ... 不在 createPage 内部声明 router
285
- const Page = new (class extends AbstractPageQueryHook {
286
- // ...
287
- toolbarDef(): ActionButtonDesc[] {
288
- return [
289
- {
290
- name: "primary",
291
- label: "新增申请",
292
- onClick: () => navigateToForm() // 无参:新增
293
- }
294
- ];
295
- }
296
- columnsDef(): TableColumnDesc<any>[] {
297
- return [
298
- // ...
299
- {
300
- label: "操作",
301
- operations: [
302
- {
303
- label: "编辑",
304
- onClick: (row: any) => navigateToForm({ id: row.id }) // 带 id:编辑
305
- }
306
- ]
307
- }
308
- ];
309
- }
310
- })();
311
- return Page.create() as any;
312
- }
313
- ```
314
-
315
- > **❌ 禁止**:
316
- > - `router.push({ path: "/mmwr-xxx-form" })`(kebab-case 路径错误)
317
- > - 在**菜单可见页面**(如列表页 data.ts 的 `navigateToForm`)中使用 `router.push()`(父壳无法刷新菜单高亮)
318
- >
319
- > **✅ 允许**:
320
- > - `useRouter().back()`(表单页"取消"按钮返回上一页时可用)
321
- >
322
- > ⚠️ **所有前进导航(包括隐藏页→隐藏页)必须用 `location.href`**。`router.push()` 会跳过 shell 的 `generateCurrentRoute`,在 dev 模式下触发 "Invalid route component" 错误(已在 `mmwrCustomerApplyChangeHistory` 实测验证)。
323
-
324
- ---
325
-
326
- ### 按钮颜色映射表
327
-
328
- > **原型颜色优先**:当原型明确展示按钮颜色时,**必须以原型为准**,不可用语义推断覆盖。下方语义推断仅在原型未标颜色时使用。
329
-
330
- | 原型按钮颜色 | `name` 值 | `plain` | 说明 |
331
- | --- | --- | --- | --- |
332
- | 蓝色填充(深蓝底白字) | `"primary"` | 不设 | 主操作-填充(新增申请、启用) |
333
- | 蓝色线框(蓝边框蓝字) | `"primary"` | `true` | 次要操作-线框(变更申请) |
334
- | 红色填充(红底白字) | `"danger"` | 不设 | 危险-填充(删除、批量删除) |
335
- | 红色线框(红边框红字) | `"danger"` | `true` | 危险-线框(审批驳回、作废) |
336
- | 橙色填充(橙底白字) | `"warning"` | 不设 | 警告-填充(停用) |
337
- | 橙色线框(橙边框橙字) | `"warning"` | `true` | 警告-线框(撤回、退回、回收) |
338
- | 绿色线框(绿边框绿字) | `"success"` | `true` | 正向确认-线框(审批通过、转化、认领) |
339
- | 灰色线框(灰边框灰字) | 不设 name | `true` | 中性操作-线框(导出、导入、批量修改) |
340
-
341
- > **`name` vs `type` 属性**:`name` 为按钮提供默认的颜色(`type`)和图标(`icon`);`type` 可单独覆盖颜色,两者可共存,`type` 优先级更高。工具栏按钮优先使用 `name`,只在需要与 `name` 默认颜色不同时才加 `type` 覆盖。
342
-
343
- **语义自动推断**(仅当原型未标颜色时使用,原型明确颜色时以原型为准):
344
- - 新增/新增申请/保存 → `name: "primary"`(蓝色填充)
345
- - 变更申请 → `plain: true`(灰色线框)
346
- - 提交 → `name: "primary", plain: true`
347
- - 审批通过/认领/转化 → `name: "success", plain: true`
348
- - 删除/批量删除 → `name: "danger"`(红色填充)
349
- - 审批驳回/作废 → `name: "danger", plain: true`
350
- - 启用 → `name: "primary"`(蓝色填充)
351
- - 停用 → `name: "warning"`(橙色填充)
352
- - 撤回/退回/回收 → `name: "warning", plain: true`
353
- - 导出/导入/批量修改 → `plain: true`(灰色线框)
354
-
355
- ---
356
-
357
- ### 按钮交互实现规则
358
-
359
- 所有按钮 `onClick` 必须实现真实交互逻辑,按按钮语义选择以下模式:
360
-
361
- | 按钮语义 | 交互实现 |
362
- | --- | --- |
363
- | **新增/新增申请** | `_editModalRef?.value?.open()` |
364
- | **删除** | 校验选中 → `this.removeBatch()` |
365
- | **提交** | 校验选中 → `ElMessageBox.confirm → postAction(API_CONFIG.submit, { ids })` |
366
- | **审批通过** | 校验选中 → `ElMessageBox.confirm → postAction(API_CONFIG.update, { ids, approvalStatus: "审批完成" })` |
367
- | **审批驳回** | 校验选中 → `ElMessageBox.confirm → postAction(API_CONFIG.update, { ids, approvalStatus: "驳回" })` |
368
- | **启用/停用** | 校验选中 → `ElMessageBox.confirm → postAction(API_CONFIG.enable/disable, { ids })` |
369
- | **撤回** | 校验选中 → `ElMessageBox.confirm → postAction(API_CONFIG.withdraw, { ids })` |
370
- | **导出** | 客户端 XLSX 生成(见下方 §导出/导入实现模式) |
371
- | **导入** | 文件选择器 → XLSX 解析 → postAction 批量导入(见下方 §导出/导入实现模式) |
372
- | **其他** | `ElMessage.info("需业务确认交互逻辑")` |
373
-
374
- **获取选中行的通用模式**:
375
-
376
- ```typescript
377
- const rows = this.tableRef.value?.getSelectionRows();
378
- if (!rows?.length) {
379
- ElMessage.warning("请先选择数据");
380
- return;
381
- }
382
- const ids = rows.map((r: any) => r.id);
383
- ```
384
-
385
- **操作列按钮交互**:
386
- - 编辑 → `_editModalRef?.value?.open(row.id)`
387
- - 删除 → `this.remove(row.id)`(基类内置方法,自带确认弹窗)
388
- - 查看 → `_editModalRef?.value?.view(row.id)`
389
-
390
- ---
391
-
392
- ### 操作列条件显示模式(`show` 属性)
393
-
394
- > 原型中操作列可能在**不同行**显示**不同按钮**(如已核实行显示"修改+作废",未核实行显示"编辑+删除")。
395
- > 此时需取**所有按钮的并集**,通过 `show: (row) => boolean` 按行条件显示。
396
-
397
- **判断依据**:如果原型操作列中,不同行的按钮文字/数量不同,则属于条件操作。
398
-
399
- **标准实现**(框架原生 `show` 属性):
400
-
401
- ```typescript
402
- {
403
- label: "操作",
404
- width: 140,
405
- 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
- ]
432
- }
433
- ```
434
-
435
- **关键规则**:
436
- 1. **width** 按并集中同时显示的最大按钮数计算(2 个≈140,3 个≈200)
437
- 2. **按钮 label** 必须与原型中每行实际显示的文字严格一致
438
- 3. **按钮语义→API 对应**:"作废"→cancel API,"删除"→remove API,不可混用
439
-
440
- ---
441
-
442
- ### 状态列色块渲染模式
443
-
444
- > 所有"XX状态"类列**必须用 `defaultSlot` + `h(ElTag)` 渲染彩色标签**,不可纯文本显示。
445
-
446
- **标准实现模式:**
447
-
448
- 1. **文件顶部定义映射表 + 渲染函数**(与 `import` 同级):
449
- ```typescript
450
- import { h, resolveComponent } from "vue";
451
-
452
- /** 状态色块映射 */
453
- const STATUS_TAG_MAP: Record<string, Record<string, string>> = {
454
- convertStatus: { "已转化": "success", "未转化": "info" },
455
- customerStatus: { "临时客户": "warning", "正式客户": "success" },
456
- verifyStatus: { "已核实": "success", "未核实": "info" },
457
- enableStatus: { "已启用": "success", "已停用": "danger" },
458
- approvalStatus: { "开立审批中": "", "审批完成": "success", "驳回": "danger", "流程终止": "info" }
459
- };
460
- function renderStatusTag(row: any, field: string) {
461
- const val = row[field];
462
- const type = STATUS_TAG_MAP[field]?.[val];
463
- if (type === undefined) return val;
464
- return h(resolveComponent("ElTag") as any, { type: type || "", effect: "light", size: "small" }, () => val);
465
- }
466
- ```
467
-
468
- 2. **列定义中使用 `defaultSlot`**:
469
- ```typescript
470
- { label: "转化状态", name: "convertStatus", minWidth: 100, fixed: "right",
471
- defaultSlot: ({ row }: any) => renderStatusTag(row, "convertStatus") },
472
- ```
473
-
474
- **颜色映射规则**(按语义):
475
- | 语义 | ElTag type | 效果 |
476
- |------|-----------|------|
477
- | 成功/已完成/已启用/已核实/已转化/正式 | `success` | 绿色 |
478
- | 警告/临时/待处理 | `warning` | 橙色 |
479
- | 危险/已停用/驳回/已作废 | `danger` | 红色 |
480
- | 默认/进行中/审批中 | `""` | 蓝灰 |
481
- | 信息/未处理/未核实/未转化/终止 | `info` | 灰色 |
482
-
483
- **注意**:当映射值中包含空字符串 `""` 时(如"开立审批中"),`renderStatusTag` 中判断条件必须用 `type === undefined` 而非 `!type`,否则空字符串会被跳过不渲染标签。
484
-
485
- ---
486
-
487
- ### 视角切换(viewSwitch)与 Tab 切换(tabSwitch)
488
-
489
- #### viewSwitch — 同数据不同列(如"管理视角 / 使用视角")
490
-
491
- > 列定义放在 `class` **外部**作为独立 export 函数;`columnsDef()` 返回其中一个提供默认的 `columns` ref;`index.vue` 自行管理 `activeView`,用 `v-if` 切换 `BaseTable`。
492
-
493
- 外部列函数无法用 `this` 调用 Page 方法,需要**模块级变量**引用:
494
-
495
- ```typescript
496
- // 模块顶部:外部列函数通过此变量回调 Page 的 select()/remove()
497
- let Page: any = null;
498
-
499
- export function managementColumns(): TableColumnDesc<any>[] {
500
- return [
501
- // ...
502
- { label: "操作", fixed: "right", width: 100, operations: [
503
- { name: "remove", label: "删除", onClick: (row: any) => Page?.remove(row.id) }
504
- ]}
505
- ];
506
- }
507
- export function usageColumns(): TableColumnDesc<any>[] {
508
- return [ /* 使用视角列... */ ];
509
- }
510
-
511
- export function createPage(editModalRef?: any) {
512
- const inst = new (class extends AbstractPageQueryHook {
513
- columnsDef() { return managementColumns(); } // 提供 columns ref 默认值
514
- })();
515
- Page = inst;
516
- return (inst as any).create() as any;
517
- }
518
- ```
519
-
520
- `index.vue` 核心片段:
521
-
522
- ```vue
523
- <el-tabs v-model="activeView">
524
- <el-tab-pane label="管理视角" name="management">
525
- <BaseTable v-if="activeView === 'management'" ref="tableRef"
526
- :data="list" :columns="mgmtCols" showToolbar />
527
- </el-tab-pane>
528
- <el-tab-pane label="使用视角" name="usage">
529
- <BaseTable v-if="activeView === 'usage'" ref="tableRef"
530
- :data="list" :columns="useCols" showToolbar />
531
- </el-tab-pane>
532
- </el-tabs>
533
-
534
- <script setup lang="ts">
535
- import { createPage, managementColumns, usageColumns } from "./data";
536
- const editModalRef = ref();
537
- const activeView = ref("management");
538
- const mgmtCols = managementColumns();
539
- const useCols = usageColumns();
540
- const Page = createPage(editModalRef);
541
- const { tableRef, page, queryParam, list, queryItems, toolbars, select } = Page;
542
- </script>
543
- ```
544
-
545
- #### tabSwitch — 同列不同数据(如"临时客户 / 正式客户 / 公海池")
546
-
547
- > `createPage()` 在 `return` 前把 `activeTab` + `handleTabChange` 挂到结果对象,`index.vue` 解构后直接绑定。
548
-
549
- **data.ts(`createPage()` 末尾,return 之前)**:
550
-
551
- ```typescript
552
- const activeTab = ref<"temp" | "formal" | "pool">("temp");
553
- const handleTabChange = (val: typeof activeTab.value) => {
554
- activeTab.value = val;
555
- result.queryParam.value.tabType = val;
556
- result.select();
557
- };
558
- result.activeTab = activeTab;
559
- result.handleTabChange = handleTabChange;
560
- return result;
561
- ```
562
-
563
- **index.vue 核心片段**:
564
-
565
- ```vue
566
- <el-tabs v-model="activeTab" @tab-change="handleTabChange">
567
- <el-tab-pane label="临时客户" name="temp" />
568
- <el-tab-pane label="正式客户" name="formal" />
569
- <el-tab-pane label="公海池" name="pool" />
570
- </el-tabs>
571
-
572
- <script setup lang="ts">
573
- const Page = createPage(editModalRef);
574
- const { tableRef, page, queryParam, list, queryItems, toolbars, select,
575
- activeTab, handleTabChange } = Page;
576
- </script>
577
- ```
578
-
579
- ---
580
-
581
- ### 导出/导入实现模式
582
-
583
- > 使用 `xlsx` 库进行客户端 Excel 生成与解析,不依赖后端文件流。
584
-
585
- **导出(data.ts 顶部需 `import * as XLSX from "xlsx"`)**:
586
-
587
- ```typescript
588
- {
589
- label: "导出",
590
- plain: true,
591
- onClick: async () => {
592
- const data = this.list.value;
593
- if (!data?.length) { ElMessage.warning("无数据可导出"); return; }
594
- const exportData = data.map((row: any) => ({
595
- "列中文名1": row.fieldName1,
596
- "列中文名2": row.fieldName2
597
- }));
598
- const ws = XLSX.utils.json_to_sheet(exportData);
599
- const wb = XLSX.utils.book_new();
600
- XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
601
- XLSX.writeFile(wb, "导出文件名.xlsx");
602
- ElMessage.success("导出成功");
603
- }
604
- }
605
- ```
606
-
607
- **导入(需 mock 提供 import 端点)**:
608
-
609
- ```typescript
610
- {
611
- label: "导入",
612
- plain: true,
613
- onClick: () => {
614
- const input = document.createElement("input");
615
- input.type = "file";
616
- input.accept = ".xlsx,.xls";
617
- input.onchange = async (e: any) => {
618
- const file = e.target.files?.[0];
619
- if (!file) return;
620
- try {
621
- const buf = await file.arrayBuffer();
622
- const wb = XLSX.read(buf, { type: "array" });
623
- const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]) as any[];
624
- if (!rows.length) { ElMessage.warning("文件无有效数据"); return; }
625
- await postAction(API_CONFIG.import, { rows });
626
- ElMessage.success(`导入成功 ${rows.length} 条`);
627
- this.select();
628
- } catch { ElMessage.error("导入失败,请检查文件格式"); }
629
- };
630
- input.click();
631
- }
632
- }
633
- ```
634
-
635
- ### Mock 端点最佳实践
636
-
637
- > **核心原则**:Mock 模式下所有操作必须能完整走通,不可出现接口报错。
638
- > data.ts 中每个 `postAction(API_CONFIG.xxx, ...)` 调用,mock 文件中都必须有对应端点。
639
-
640
- **1. 所有端点必须修改 dataPool**
641
-
642
- mock 端点不能只返回 `{ code: 200 }` — 必须实际修改内存中的 `dataPool` 数据,否则 `this.select()` 刷新后数据不变。
643
-
644
- ```typescript
645
- // ✅ 正确:启用端点修改 dataPool 中的 enableStatus
646
- {
647
- url: "/dev-api/sale/xxx/enable",
648
- method: "post",
649
- response: ({ body }: any) => {
650
- const ids = body?.ids || [];
651
- ids.forEach((id: string) => {
652
- const item = dataPool.find((d) => d.id === id);
653
- if (item) item.enableStatus = "已启用";
654
- });
655
- return { code: 200, msg: "启用成功", data: null };
656
- }
657
- }
658
-
659
- // ❌ 错误:只返回成功,不修改数据
660
- {
661
- url: "/dev-api/sale/xxx/enable",
662
- method: "post",
663
- response: () => ({ code: 200, msg: "启用成功", data: null })
664
- }
665
- ```
666
-
667
- **2. 常见操作的 Mock 修改模式**
668
-
669
- | 操作 | dataPool 修改方式 |
670
- | --- | --- |
671
- | 删除 | `dataPool.splice(idx, 1)` |
672
- | 新增 | `dataPool.unshift({ ...genRecord(), ...body, id: Random.id() })` |
673
- | 编辑 | `Object.assign(dataPool[idx], body)` |
674
- | 启用/停用 | 修改 `item.enableStatus` |
675
- | 提交/审批 | 修改 `item.approvalStatus` |
676
- | 作废 | `dataPool.splice(idx, 1)` 或修改状态 |
677
- | 分配/认领 | 修改 `item.businessPerson` |
678
-
679
- **3. 端点覆盖检查**
680
-
681
- 生成完成后,逐个对比 `API_CONFIG` 的所有 key 与 mock 文件中的 `url`,确保一一对应、零遗漏。
682
-
683
- ### 禁止
684
-
685
- > 以下为精简速查清单,详细说明见上方 §禁止事项(严格遵守)。
686
-
687
- - ❌ index.vue 中写业务逻辑(逻辑全在 data.ts)
688
- - ❌ 使用 Vuex(用 Pinia)
689
- - ❌ `::v-deep` / `/deep/`(用 `:deep()`)
690
- - ❌ 直接用 axios(用 getAction/postAction)
691
- - ❌ 手写查询表单/工具栏/分页(用 BaseQuery/BaseToolbar/jh-pagination)
692
- - ❌ 使用 `useTableDelete`(用 `this.remove(row.id)`)
693
- - ❌ 用 `{ ...instance }` 展开 `create()` 返回值
694
- - ❌ Mock 端点不修改 dataPool、字段名不对齐 columnsDef
695
- - ❌ data.ts 导入未使用的模块
696
- - ❌ 用 `el-radio-group` 做 Tab/视角切换(统一用 `el-tabs`)
697
-
698
- ---
699
-
700
- ## 表单页 UI 细节规范(FORM_TAB / 独立路由表单页)
701
-
702
- > 适用于使用 el-form + el-row/col 布局的复杂表单页(如客户申请新增/变更)。
703
- > 所有样式规则**写在组件或页面的 index.scss** 中,便于未来复用和移动,避免内联 style 散落。
704
-
705
- ### 1. 平台组件 label 隐藏
706
-
707
- `jh-select`、`jh-date`、`jh-file-upload` 等平台组件自带 `label` prop(默认会渲染"下拉选择框:"、"日期:"等文字)。
708
- **在 el-form-item 内使用时,必须传 `label=""` 隐藏组件自身标签**,避免与 el-form-item 的 label 重复。
709
-
710
- ```vue
711
- <!-- ✅ 正确 -->
712
- <el-form-item label="审批产品别">
713
- <jh-select v-model="form.productLine" dict="product_line" label="" placeholder="请选择" />
714
- </el-form-item>
715
- <el-form-item label="成立时间">
716
- <jh-date v-model="form.establishDate" label="" placeholder="请选择" />
717
- </el-form-item>
718
- <el-form-item label="营业执照">
719
- <jh-file-upload v-model="form.businessLicense" label="" :disabled="isView" />
720
- </el-form-item>
721
-
722
- <!-- ❌ 错误:不传 label="",组件内部会额外渲染 "下拉选择框:" / "日期:" 等文字 -->
723
- <jh-select v-model="form.productLine" dict="product_line" />
724
- <jh-date v-model="form.establishDate" />
725
- ```
726
-
727
- ### 2. 表单控件宽度统一
728
-
729
- `jh-select`、`jh-date`、`el-input-number`、`jh-file-upload` 默认宽度可能与 `el-input` 不一致。
730
- 在组件 scoped style 中统一设置 `width: 100%`:
731
-
732
- ```scss
733
- :deep(.jh-select),
734
- :deep(.jh-date),
735
- :deep(.el-input-number) {
736
- width: 100%;
737
- }
738
- :deep(.jh-select .el-input),
739
- :deep(.jh-date .el-input) {
740
- width: 100%;
741
- }
742
- :deep(.jh-file-upload) {
743
- width: 100%;
744
- }
745
- ```
746
-
747
- ### 3. 页面滚动
748
-
749
- 独立表单页内容通常超出视口高度。全局 `.app-page-container` 已设 `height: calc(100vh - 100px); overflow: hidden`(列表页靠表格内部滚动),**表单页必须覆盖 `overflow` 为 `auto`**:
750
-
751
- > ⚠️ 不要加 `height: 100%`,否则会产生双滚动条(与全局 height 冲突)。
752
-
753
- ```scss
754
- .app-page-container {
755
- overflow-y: auto;
756
- padding-bottom: 24px;
757
- }
758
- ```
759
-
760
- ### 4. 只看必填项
761
-
762
- 通过在最外层容器加 CSS class 切换,利用 Element Plus 的 `.is-required` 自动标记来隐藏非必填项。
763
-
764
- **关键**:不能只隐藏 `el-form-item`(外层 `el-col` 仍占栅格空间→留白),必须隐藏整个 `el-col` 并让剩余列自动重排。
765
-
766
- **组件 props**:接收 `onlyRequired` Boolean prop
767
- **模板**:`:class="{ 'only-required': onlyRequired }"`
768
- **样式**(使用 `:has()` 选择器,Chrome 105+):
769
-
770
- ```scss
771
- &.only-required {
772
- /* 隐藏包含非必填字段的整个 el-col */
773
- :deep(.el-col:has(> .el-form-item:not(.is-required))) {
774
- display: none !important;
775
- }
776
- /* 让可见列自动重排(4列/行) */
777
- :deep(.el-row) {
778
- flex-wrap: wrap;
779
- }
780
- :deep(.el-col) {
781
- flex: 0 0 25% !important;
782
- max-width: 25% !important;
783
- }
784
- }
785
- ```
786
-
787
- ### 5. 状态信息区域放置
788
-
789
- 状态信息(创建时间、修改时间、核实状态等只读字段)**仅在"基本信息"Tab 内展示**(业务表格下方),不放在 el-tabs 外部——否则切换到其他 Tab 时仍然可见,与原型不符。
790
-
791
- ### 6. 文件上传预览
792
-
793
- 使用 `jh-file-upload` 时,默认 `list-type="picture"` 会将已上传文件显示在组件下方。如需在框内预览(卡片样式),设 `list-type="picture-card"` + `:limit="1"`:
794
-
795
- ```vue
796
- <jh-file-upload v-model="form.businessLicense" label="" list-type="picture-card" :limit="1" />
797
- ```
798
-
799
- ### 7. 企业核实 Drawer
800
-
801
- 客户名称输入框右侧加搜索图标,点击打开 `el-drawer` 展示工商信息(天眼查/企查查),使用 `el-descriptions` 两列表格布局。mock 阶段先用静态数据,后续对接 API。
802
-
803
- ### 8. 按钮位置
804
-
805
- 表单页操作按钮(保存、取消等)**左对齐**,放在 `.page-toolbar` 区域(标题行下方、tabs 上方):
806
-
807
- ```vue
808
- <div class="page-header">
809
- <span class="page-title">客户申请详情</span>
810
- <span class="page-tag page-tag--add">新增</span>
811
- <span class="page-tag page-tag--status">未审核</span>
812
- <el-checkbox v-model="onlyRequired" class="only-required-check">只看必填项</el-checkbox>
813
- </div>
814
- <div class="page-toolbar">
815
- <el-button type="danger" @click="handleSaveAndChange">保存并变更</el-button>
816
- <el-button type="warning" @click="handleSave">保存</el-button>
817
- <el-button @click="handleCancel">取消</el-button>
818
- </div>
819
- ```
820
-
821
- ---
822
-
823
- ### 导入路径规范(@/types/page 桶文件)
824
-
825
- > `src/types/page.ts` 是类型桶文件(barrel export),统一重导出 `@jhlc/common-core` 中的常用类型和基类。
826
- > **所有 data.ts 文件必须从 `@/types/page` 导入,禁止直接引用 `@jhlc/common-core/src/...` 的深层路径。**
827
-
828
- ```typescript
829
- // ✅ 正确:从桶文件导入
830
- import {
831
- AbstractPageQueryHook,
832
- BaseQueryItemDesc,
833
- ActionButtonDesc,
834
- TableColumnDesc,
835
- BusLogicDataType
836
- } from "@/types/page";
837
-
838
- // ❌ 错误:直接从 common-core 深层路径导入
839
- import { AbstractPageQueryHook } from "@jhlc/common-core/src/page-hooks/page-query-hook.ts";
840
- import { BaseQueryItemDesc } from "@jhlc/common-core/src/components/form/base-query/type.ts";
841
- ```
842
-
843
- | 导出名 | 说明 |
844
- | ------------------------ | -------------------------- |
845
- | `AbstractPageQueryHook` | 列表页基类 |
846
- | `BaseQueryItemDesc` | 查询表单字段描述类型 |
847
- | `ActionButtonDesc` | 工具栏/操作列按钮描述类型 |
848
- | `TableColumnDesc` | 表格列描述类型 |
849
- | `BusLogicDataType` | 业务逻辑类型枚举(如 dict)|
850
-
851
- > **例外**:`BaseFormItemDesc`(弹窗表单字段类型)仍直接从 common-core 导入:
852
- > `import type { BaseFormItemDesc } from "@jhlc/common-core/src/components/form/common/type";`
853
- > 因为 `src/types/page.ts` 当前未导出该类型。
854
-
855
- ---
856
-
857
- ## api.md 生成时序
858
-
859
- > **api.md 在页面代码之前生成**(Step 2: api-contract → Step 3: page-codegen)。
860
- > page-codegen 读取已生成的 api.md 中的 URL 和字段定义,确保 `API_CONFIG`、mock、data.ts 与接口约定一致。
861
- > 未来使用真实 API 设计文档时,api.md 由后端提供或 api-contract Skill 从设计文档提取,page-codegen 直接消费。
862
-
863
- ---
864
-
865
- ## SYS_MENU_INFO 生成规则(所有模板通用)
866
-
867
- page-codegen 生成页面代码后,**必须追加写入菜单配置信息到 `reports/SYS_MENU_INFO.md`**。
868
-
869
- ### 写入策略(默认追加,不覆盖)
870
-
871
- - **默认为追加模式**:保留已有内容,在末尾追加本次生成的菜单。避免覆盖团队之前累积的菜单记录。
872
- - **如需重置**:用户明确说“覆盖”才走覆盖逻辑。
873
-
874
- > AI 询问示例(仅当用户意图不明时):`本次生成了 N 个页面的菜单配置,默认追加到 reports/SYS_MENU_INFO.md,是否需要覆盖已有内容?`
875
-
876
- ### 生成模板
877
-
878
- 每个页面生成一个菜单条目,格式如下:
879
-
880
- ```markdown
881
- ## [序号]. [菜单名称]
882
-
883
- | 字段 | 填写值 |
884
- | ------------ | ------ |
885
- | 类型 Tab | 选择 **菜单** |
886
- | 上级目录 | `[父目录名]` |
887
- | 应用选择 | `[应用名]` |
888
- | 使用缓存 | ◉ 使用 |
889
- | 显示排序 | `[序号]` |
890
- | 菜单路径 | `[camelCase目录名]` |
891
- | 菜单名称 | `[中文名]` |
892
- | 名称编码后缀 | `[菜单路径拼音小写]` |
893
- | 组件路径 | `[域]/[模块]/[子模块]/[kebab-目录名]/index.vue` |
894
- | 权限标识 | `[camelCase目录名]` |
895
- | 是否隐藏 | **否** / **是** |
896
- ```
897
-
898
- ### 字段生成规则
899
-
900
- | 字段 | 来源 | 规则 |
901
- |------|------|------|
902
- | 菜单路径 | page-spec.kebabName | kebab-case → camelCase(`mmwr-customer-archive` → `mmwrCustomerArchive`) |
903
- | 菜单名称 | page-spec.pageName | 直接使用中文名 |
904
- | 组件路径 | pages.ts 注册路径 | `[域]/[模块]/[子模块]/[kebab-目录名]/index.vue` |
905
- | 权限标识 | 同菜单路径 | camelCase |
906
- | 是否隐藏 | page-spec.features.hiddenMenu | `true` → 是,`false` → 否 |
907
- | 上级目录 | 用户指定 / page-spec 推断 | 如果用户在原型扫描阶段指定了上级目录,使用该值 |
908
- | 应用选择 | pages.ts 域名 | `produce` → 生产,`sale` → 销售 |
909
- | 显示排序 | 页面在模块内的序号 | 从 1 开始递增 |
910
-
911
- ### 隐藏页面判断规则
912
-
913
- 以下页面类型应设置 `是否隐藏: 是`:
914
- - 目录名含 `-form`(独立路由表单页)
915
- - 目录名含 `-detail`(详情页)
916
- - 目录名含 `-history`(历史查询页)
917
- - page-spec.features.hiddenMenu === true
918
-
919
- ### SYS_MENU_INFO.md 文件结构
920
-
921
- ```markdown
922
- # 系统菜单配置 — [模块名称]([域] / [子模块路径])
923
-
924
- > 对应系统管理 → 菜单管理 → 新增菜单,每栏直接复制粘贴。
925
- > **操作顺序:先建目录(第 0 步),再逐个添加菜单。**
926
- >
927
- > **pages.ts 注册位置**:`vite/plugins/shared/pages.ts` → `[模块变量名]` → `[子模块key]`
928
-
929
- ## 第 0 步:新建目录(如需要)
930
-
931
- | 字段 | 值 |
932
- | -------- | -- |
933
- | 上级目录 | `[上级目录名]` |
934
- | 菜单名称 | `[目录名]` |
935
- | 显示排序 | `[序号]` |
936
-
937
- ## 第 1 步:[页面名称]
938
-
939
- [菜单条目表格]
940
-
941
- > pages.ts 对应:`["[kebab-name]", "[中文名]"]`
942
-
943
- ## 第 2 步:[页面名称]
944
- ...
945
- ```
946
-
947
- ### 与 menu-sync 的衔接
948
-
949
- SYS_MENU_INFO.md 是 menu-sync Skill 的输入数据源:
950
- - **自动创建**:用户说"帮我创建菜单" → menu-sync 读取 SYS_MENU_INFO.md → 调 API 逐条创建
951
- - **手动创建**:用户也可直接按 SYS_MENU_INFO.md 的表格在系统管理后台手动创建菜单
952
- - 两种方式等价,菜单创建后通过 `组件路径` 字段与 pages.ts 注册的文件路径关联
953
-
954
- ---
955
-
956
- ## 代码模板索引
957
-
958
- > 各模板完整代码见对应独立文件,按需读取。主文件(SKILL.md)包含前置检查、约束、按钮规则、Mock规范等所有共用规则。
959
-
960
- | 交互模式 | 文件 | 适用场景 | 典型参考页面 |
961
- |---|---|---|---|
962
- | LIST | templates/universal/TPL-LIST.md | 标准查询+工具栏+表格+分页 | mmwr-customer-archive |
963
- | MASTER_DETAIL | templates/universal/TPL-MASTER-DETAIL.md | jh-drag-row 主从表,双击联动 | ompt-ht-plan-order |
964
- | TREE_LIST | templates/universal/TPL-TREE-LIST.md | 左侧 C_Tree + 右侧列表 | — |
965
- | DETAIL_TABS | templates/universal/TPL-DETAIL-TABS.md | C_Splitter 上Tab表单+下子表 | add-demo / domestic-trade-order |
966
- | FORM_ROUTE | templates/universal/TPL-FORM-ROUTE.md | 复杂表单独立路由(非弹窗) | mmwr-customer-apply-add-form |
967
- | CHANGE_HISTORY | templates/universal/TPL-CHANGE-HISTORY.md | 左历史时间线+右变更详情 | mmwr-customer-apply-change-history |
968
- | RECORD_FORM | templates/universal/TPL-RECORD-FORM.md | BaseQuery选主记录+Form+Table无分页 | mmsm-convert-progress |
969
- | OPERATION_STATION | templates/domains/produce/TPL-OPERATION-STATION.md | 工序站点操作(待处理↔已处理+操作表单) | mmwr-rolling-management |
970
-
971
- > **配置驱动模板页**(ResultQueryTemplate / FinishingAchievementTemplate 等):见 templates/universal/TPL-DRIVEN.md,仅需生成 config 对象,不套用以上模板。
972
- > **领域模板查询**:完整路径以 `templates/_index.md` 注册表为准,新增领域模板见 `templates/domains/_CONTRIBUTING.md`。
973
-
1
+ ---
2
+ name: page-codegen
3
+ description: "Use when: generating complete Vue 3 page code (index.vue + data.ts + modal components + api.md + pages.ts registration) from a prototype page inventory and API contract, strictly following the cx-ui-produce project conventions. Read SKILL.md first (rules+constraints), then read the matching TPL-*.md for the template code. Triggers on: generate page, create page, code generation, 生成页面, 页面代码, 代码生成, vue页面, 按原型生成, 口述需求, 建个页面, 写个页面, 帮我生成, natural language page generation."
4
+ ---
5
+
6
+ # Skill: 页面代码生成(page-codegen)
7
+
8
+ 基于《页面清单》+ 原型信息,生成符合项目规范的完整 Vue 3 页面代码。
9
+
10
+ ---
11
+
12
+ ## Pre-flight 规范声明(执行前必须输出)
13
+
14
+ ```
15
+ 🚀 已触发技能 page-codegen/SKILL.md → 页面代码生成:4文件 + 模板调度 + 前置检查
16
+ ✅ 已读取 templates/_index.md → 模板注册表,匹配 → {TPL路径}
17
+ ✅ 已读取 templates/{universal|domains/xxx}/TPL-XXX.md → {当前模板说明}
18
+ ✅ 已读取 standards/index.md → 规范门控(任务类型 A:生成新页面)
19
+ ✅ 已读取 standards/02-code-structure.md → 4文件原则 + 三段式 + script 9段顺序
20
+ ✅ 已读取 standards/12-base-table.md → AGGrid必用 + cid命名规范
21
+ ✅ 已读取 standards/13-platform-components.md → 平台组件对照表 + docs前置读取清单
22
+ ✅ 已读取 docs/{涉及的jh-*文档} → 当前页涉及组件的使用规范
23
+ ✅ 工具链检测:.prettierrc.js ✓ eslint.config.ts ✓ .husky/ ✓ [全部就绪]
24
+ ✅ cid 已生成:{value}({首字母缩写说明})
25
+ ```
26
+
27
+ **工具链失败时(红叉 + 暂停)**:
28
+
29
+ ```
30
+ ❌ 工具链检测失败:未找到 .prettierrc.js / eslint.config.ts / .husky/
31
+ → 请执行:npx @robot-admin/git-standards init
32
+ → 或联系 CHENY(工号 409322)解决
33
+ → ⛔ 代码生成已暂停,修复后重新触发
34
+ ```
35
+
36
+ **生成完成摘要(生成结束后输出)**:
37
+
38
+ ```
39
+ 📦 本次生成完成
40
+ ────────────────────────────────────────────────
41
+ ✅ src/views/.../{页面}/index.vue
42
+ ✅ src/views/.../{页面}/data.ts
43
+ ✅ src/views/.../{页面}/index.scss
44
+ ✅ src/views/.../{页面}/api.md
45
+ ✅ reports/SYS_MENU_INFO.md → 已追加菜单条目
46
+ ────────────────────────────────────────────────
47
+ 📌 后续步骤:
48
+ 1. 在 router/pages.ts 注册路由
49
+ 2. 提交:git cz(禁止直接 git commit)
50
+ 3. 可选:触发 convention-audit 扫描本次生成文件
51
+ ────────────────────────────────────────────────
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 前置检查
57
+
58
+ ```
59
+ □ 页面中文名:
60
+ □ 交互模式:LIST / MASTER_DETAIL / TREE_LIST / DETAIL_TABS / FORM_ROUTE / CHANGE_HISTORY / RECORD_FORM / OPERATION_STATION / TEMPLATE_DRIVEN
61
+ □ page-spec JSON:(必须存在,由 prototype-scan 输出)
62
+ □ 文件路径:src/views/[域]/[模块]/[子模块]/[kebab-case-目录名]/
63
+ □ pages.ts 注册名:["kebab-目录名", "中文名"]
64
+ □ 服务缩写:[pm / mmwr / sale / ...]
65
+ □ 资源名(CamelCase):
66
+ ```
67
+
68
+ > **重要**:查询字段、表格列、按钮列表不再手动罗列,直接从 page-spec JSON 中读取。
69
+ > 如果没有 page-spec JSON,必须先执行 prototype-scan Skill 生成。
70
+ >
71
+ > **模式 0 快捷路径**:当用户直接口述需求(如"帮我生成一个客户管理页面")而未提供 page-spec JSON 时,AI 内部自动调用 prototype-scan 模式 0 构建 page-spec JSON,然后继续执行代码生成,无需用户提供任何文件。
72
+
73
+ ---
74
+
75
+ ## 生成产物(默认 4 文件)
76
+
77
+ ```
78
+ src/views/[域]/[模块]/[子模块]/[kebab-case-目录名]/
79
+ ├── index.vue ← 页面入口(纯模板 + 解构)
80
+ ├── data.ts ← 业务逻辑(AbstractPageQueryHook 类 / 直接导出 ref+函数)
81
+ ├── index.scss ← 页面样式
82
+ └── api.md ← 接口约定(按 api-contract Skill 模板生成)
83
+ ```
84
+
85
+ 弹窗组件处理策略:
86
+
87
+ - **通用弹窗**(新增/编辑表单,2+ 页面可复用)→ 提取到 `src/components/local/c_xxxModal/`
88
+ - **极个性弹窗**(仅单页面使用,c_modal 无法满足)→ 放在页面 `components/xxxModal.vue`
89
+
90
+ 附加输出:
91
+
92
+ - `pages.ts` 注册片段
93
+ - **`reports/SYS_MENU_INFO.md`** — 集中式菜单配置,**追加写入**(见下方 §SYS_MENU_INFO 生成规则)
94
+ - `mock/[页面kebab-name].ts`(项目根目录 `mock/` 下,`vite-plugin-mock` 自动加载,与 api.md 的 URL 和字段完全一致)
95
+
96
+ ---
97
+
98
+ ## 约束(严格遵守)
99
+
100
+ ### 必须
101
+
102
+ 1. data.ts 使用 `class extends AbstractPageQueryHook`,通过 `queryDef()` / `toolbarDef()` / `columnsDef()` 配置。**仅适用于 LIST / MASTER_DETAIL / TREE_LIST 三种列表型页面**。其余模板不用此基类:DETAIL_TABS(直接导出 reactive+ref)、FORM_ROUTE(useXxx composable)、CHANGE_HISTORY(composable+mock)、RECORD_FORM(直接 ref+函数)、OPERATION_STATION(多个 createXxxPage)、TEMPLATE_DRIVEN(仅 config 对象)
103
+ 2. index.vue 只有模板 + `createPage()` 解构 + `onMounted`,不写业务逻辑。**例外**:DETAIL_TABS / FORM_ROUTE / CHANGE_HISTORY 的 index.vue 可包含表单状态管理;OPERATION_STATION 包含 computed/watch/多列表协调逻辑
104
+ 3. 最外层 class:`app-container app-page-container`
105
+ 4. 样式用 `@import "./index.scss"`
106
+ 5. API 用 `getAction` / `postAction` from `@jhlc/common-core/src/api/action`
107
+ 6. 字典字段用 `logicType: BusLogicDataType.dict, logicValue: "dictCode"`
108
+ 7. 同时生成 api.md(基于 api-contract Skill 模板)
109
+ 8. 提供 pages.ts 注册片段
110
+ 9. 同时在 `mock/` 目录下生成对应的 mock 文件(`MockMethod[]` + mockjs,URL 和字段与 api.md 一致,URL 必须带 `/dev-api` 前缀)
111
+ 10. **查询字段顺序**:`queryDef()` 中字段顺序必须与 page-spec `query` 数组顺序严格一致(即原型从左到右、从上到下)
112
+ 11. **表格列顺序**:`columnsDef()` 中列顺序必须与 page-spec `columns` 数组顺序严格一致(`selection` + `index` 在最前,其余按原型表头从左到右)
113
+ 12. **按钮顺序与颜色**:`toolbarDef()` 中按钮顺序和 `name`(颜色)必须与 page-spec `toolbar` 数组严格一致(`primary`=蓝底, `danger`=红色, `warning`=橙色, `default`=灰色; `plain: true`=线框)。**"新增"类按钮永远排第一**(如"新增"、"新增申请"),这是产品通用规范
114
+ 13. **操作列按钮**:`columnsDef()` 操作列的 `operations` 数组必须与 page-spec `operations` 数组**严格一一对应**,不可遗漏也**不可自行添加**(如原型没有"查看"按钮就不能加"查看")
115
+ 14. **Tab 标签**:当 page-spec `features.tabSwitch === true` 时,必须在 index.vue 中生成 Tab 组件,tabs 与 `features.tabItems` 一一对应
116
+ 15. **按钮文字保真**:使用原型中的原始文字(如"新增申请"不可简化为"新增","变更申请"不可简化为"变更")
117
+ 16. **可点击列(蓝色链接列)**:原型中蓝色凸显的列(如客户编码、申请编码等编码/编号类字段)必须实现为可点击链接,使用 `defaultSlot` + `h()` 渲染蓝色链接样式,点击后查看详情(调 `getById` 后展示或路由跳转)
118
+ 17. **按钮颜色映射**:按钮的 `type` 属性决定颜色,须根据原型按钮颜色或按钮语义映射(见下方 §按钮颜色映射表)
119
+ 18. **按钮必须可交互**:所有按钮的 `onClick` 必须有真实处理逻辑,禁止空函数 `() => {}`。通用交互实现见下方 §按钮交互实现规则
120
+ 19. **未知交互兜底**:当原型未提供交互细节、且无法从通用模式推断时,`onClick` 中使用 `ElMessage.info("需业务确认交互逻辑")` 作为占位
121
+ 20. **生成后依赖自检**:代码生成完成后,检查 `package.json` 是否已安装生成代码所需的依赖(`mockjs`、`vite-plugin-mock`、`lodash-es`、`xlsx` 等),若缺失则提示用户执行安装命令。同时检查 `vite.config.ts` 是否已注册 `viteMockServe`、`mock/` 目录是否存在
122
+
123
+ ### 禁止事项(严格遵守)
124
+
125
+ 1. **❌ 禁止手写弹窗**:不可在页面 `components/` 下用 `el-dialog` + `el-form` + `el-row/col` 手写弹窗。必须使用 `c_formModal`(`src/components/local/c_formModal/`),通过 `modalConfig` 配置驱动。**例外**:纯只读详情弹窗(`jh-dialog` + `BaseForm :disabled="true"`)可不用 `c_formModal`,如工艺参数查看(参考 mmwr-process-parameters)
126
+ 2. **❌ 禁止在弹窗中使用原生 Element Plus 组件**:不可使用 `el-select`、`el-input`、`el-date-picker` 等原生组件,必须使用 `jh-select`、`jh-date`、`jh-user-picker` 等平台组件(通过 `BaseFormItemDesc` 的 `component` 属性配置)
127
+ 3. **❌ 禁止在 BaseToolbar 内使用 slot**:`BaseToolbar` 组件**不支持任何 slot**(源码中无 `<slot>` 标签),放入的内容会被丢弃不渲染。Tab/视角切换等额外 UI 必须放在 BaseToolbar **外部**
128
+ 4. **❌ 禁止用 el-radio-group 做 Tab/视角切换**:所有 Tab 式切换(视角切换、数据过滤 Tab、功能 Tab)**必须使用 `el-tabs`**(参考 `mmwr-steel-stripping-operations`)。不可用 `el-radio-group` + 手动 `handleViewChange` / `handleTabChange`
129
+ 5. **❌ 禁止 Mock 端点只返回成功不修改数据**:mock 文件中每个端点的 `response` 必须实际修改 `dataPool`(splice/assign/修改字段),否则 `this.select()` 刷新后数据不变。详见 §Mock 端点最佳实践
130
+ 6. **❌ 禁止遗留未使用的 import**:data.ts 中不要导入未使用的模块(如仅用 `postAction` 时不导入 `getAction`)
131
+ 7. **❌ 禁止操作列自编按钮**:操作列的 `operations` 数组必须与原型操作列按钮**严格一致**,不可凭空添加原型中不存在的按钮(如原型只有"编辑"+"删除",不可自行加"查看")
132
+ 8. **❌ 状态类列必须 `fixed: "right"` + 色块渲染**:启用状态、停用时间、转化状态、客户状态、审批状态、核实状态等靠近操作列的状态类列必须设置 `fixed: "right"`,与操作列一起固定在表格右侧。**且状态列必须用 `defaultSlot` + `h(ElTag)` 渲染彩色标签**,不可纯文本显示(详见 §状态列色块渲染模式)
133
+ 9. **❌ 禁止操作按钮标签自编**:操作列按钮 `label` 必须与原型严格一致(如原型写"修改"不可改成"编辑",写"作废"不可改成"删除"),且 onClick 逻辑必须匹配语义("作废"调 cancel API,不是 remove)
134
+ 10. **❌ 禁止平台组件遗漏 `label=""`**:在 el-form-item 内使用 `jh-select`、`jh-date`、`jh-file-upload` 时,**必须传 `label=""`** 隐藏组件自身标签(否则会渲染"下拉选择框:"、"日期:"等多余文字)
135
+ 11. **❌ 禁止表单控件宽度不统一**:`jh-select`、`jh-date`、`el-input-number`、`jh-file-upload` 默认宽度可能与 `el-input` 不一致,必须在 scoped style 中用 `:deep()` 统一设为 `width: 100%`(详见 §表单页 UI 细节规范)
136
+ 12. **❌ 禁止表单页无滚动**:独立路由表单页内容超出视口时必须可滚动,`.app-page-container` 须设 `overflow-y: auto`(**不要加 `height: 100%`,全局已有 `height: calc(100vh - 100px)`,叠加会导致双滚动条**)
137
+ 13. **❌ 禁止内联 style 散落**:所有页面/组件样式统一写在 `index.scss` 中(便于复用和移动),不可在 template 中大量使用内联 `style="..."`
138
+
139
+ ### c_formModal 使用规范
140
+
141
+ > 项目已有 `src/components/local/c_formModal/` 通用表单弹窗组件,支持 add/edit/view 三模式。
142
+ > 所有标准 CRUD 弹窗**必须使用此组件**,不可重复编写。
143
+
144
+ **data.ts 中定义 modalConfig:**
145
+
146
+ ```typescript
147
+ import type { BaseFormItemDesc } from "@jhlc/common-core/src/components/form/common/type";
148
+
149
+ export const modalConfig = {
150
+ titlePrefix: "客户", // 标题前缀:新增客户 / 编辑客户 / 查看客户
151
+ width: "850px",
152
+ columns: 2,
153
+ labelWidth: "110px",
154
+ formItems: [
155
+ { name: "code", label: "编码", disabled: true, placeholder: "系统自动生成" },
156
+ { name: "name", label: "名称", required: true, placeholder: "请输入" },
157
+ // 下拉用 jh-select 组件
158
+ {
159
+ name: "type",
160
+ label: "类型",
161
+ component: () => ({ tag: "jh-select", items: OPTS.type })
162
+ }
163
+ ] as BaseFormItemDesc<any>[],
164
+ api: {
165
+ getById: API_CONFIG.getById,
166
+ save: API_CONFIG.save,
167
+ update: API_CONFIG.update
168
+ }
169
+ };
170
+ ```
171
+
172
+ **index.vue 中使用:**
173
+
174
+ ```vue
175
+ <c_formModal ref="editModalRef" v-bind="modalConfig" @ok="select" />
176
+
177
+ <script setup lang="ts">
178
+ import { createPage, modalConfig } from "./data";
179
+ import c_formModal from "@/components/local/c_formModal/index.vue";
180
+ </script>
181
+ ```
182
+
183
+ **调用方式**(在 data.ts 中):
184
+ - 新增:`_editModalRef?.value?.open()`
185
+ - 编辑:`_editModalRef?.value?.edit(row.id)`
186
+ - 查看:`_editModalRef?.value?.view(row.id)`
187
+
188
+ ---
189
+
190
+ ### 可点击列(蓝色链接列)
191
+
192
+ 原型中以蓝色文字凸显的列(通常是编码、编号类字段)表示"可点击查看详情"。
193
+
194
+ **识别规则**:
195
+ - 原型中蓝色/带下划线的列文字 → 必须实现为可点击
196
+ - 常见目标列:客户编码、申请编码、订单编号、合同编号、计划编号等"XX编码/编号"字段
197
+
198
+ **实现方式**:在 `columnsDef()` 中使用 `defaultSlot` + `h()` 渲染蓝色链接:
199
+
200
+ ```typescript
201
+ import { h } from "vue";
202
+
203
+ // 在 columnsDef() 中:
204
+ {
205
+ label: "客户编码",
206
+ name: "customerCode",
207
+ minWidth: 120,
208
+ defaultSlot: ({ row }: any) => {
209
+ return h(
210
+ "span",
211
+ {
212
+ style: "color: #409eff; cursor: pointer; text-decoration: underline;",
213
+ onClick: () => handleCodeClick(row)
214
+ },
215
+ row.customerCode
216
+ );
217
+ }
218
+ }
219
+ ```
220
+
221
+ **点击处理逻辑**(按优先级选择):
222
+ 1. 有编辑弹窗 → `_editModalRef?.value?.open(row.id, "view")` (查看模式打开同一弹窗)
223
+ 2. 如果有详情路由 → `navigateToForm({ id: row.id, mode: "view" })`
224
+ 3. Mock 阶段暂无详情页 → `ElMessage.info(\`查看详情: ${row.fieldValue}\`)`
225
+
226
+ **handleCodeClick 推荐实现**:
227
+
228
+ ```typescript
229
+ let _editModalRef: any = null;
230
+
231
+ function handleCodeClick(row: any) {
232
+ _editModalRef?.value?.open(row.id, "view");
233
+ }
234
+ ```
235
+
236
+ > 注意:`_editModalRef` 在 `createPage(editModalRef?)` 中赋值,详见 §弹窗模板
237
+
238
+ ---
239
+
240
+ ### FORM_ROUTE 表单页(路由跳转式表单)
241
+
242
+ > 当表单足够复杂(如多 Tab、多子表、独立布局)时,使用**独立路由**替代弹窗(c_formModal)。
243
+ >
244
+ > **导航方式选择**(按场景区分):
245
+ >
246
+ > | 场景 | 方式 | 原因 |
247
+ > |---|---|---|
248
+ > | **菜单页 → 隐藏页**(如列表→表单) | `envConfig()?.router` + `location.href` | 需要父壳刷新菜单高亮 |
249
+ > | **隐藏页 → 隐藏页**(如表单→变更历史) | `envConfig()?.router` + `location.href` | `router.push()` 跳过 shell 的 `generateCurrentRoute`,导致 "Invalid route component" 报错 |
250
+ > | **返回上一页** | `useRouter().back()` | 任何页面均可用 |
251
+
252
+ #### 路由路径命名规则
253
+
254
+ | 目录名(kebab-case) | 路由路径(camelCase) |
255
+ | ---------------------------------- | -------------------------------------------- |
256
+ | `mmwr-customer-apply-add-form` | `/aiflow/mmwrCustomerApplyAddForm` |
257
+ | `mmwr-customer-apply-change-form` | `/aiflow/mmwrCustomerApplyChangeForm` |
258
+
259
+ **规则**:`/[子模块名-camelCase]/[完整页面目录名转PascalCase]`
260
+ - 子模块名取 pages.ts 的 key,如 `aiflow`
261
+ - 页面目录名整体转 PascalCase(含 `mmwr` 前缀),如 `mmwr-customer-apply-add-form` → `mmwrCustomerApplyAddForm`
262
+
263
+ #### 标准实现(data.ts)
264
+
265
+ ```typescript
266
+ // ✅ 正确:用 envConfig
267
+ import envConfig from "@jhlc/common-core/src/store/env-config";
268
+
269
+ // 在 createPage() 外部定义,避免每次调用都重新创建
270
+ const FORM_ROUTE = "/aiflow/mmwrCustomerApplyAddForm";
271
+
272
+ 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;
281
+ }
282
+
283
+ export function createPage() {
284
+ // ... 不在 createPage 内部声明 router
285
+ const Page = new (class extends AbstractPageQueryHook {
286
+ // ...
287
+ toolbarDef(): ActionButtonDesc[] {
288
+ return [
289
+ {
290
+ name: "primary",
291
+ label: "新增申请",
292
+ onClick: () => navigateToForm() // 无参:新增
293
+ }
294
+ ];
295
+ }
296
+ columnsDef(): TableColumnDesc<any>[] {
297
+ return [
298
+ // ...
299
+ {
300
+ label: "操作",
301
+ operations: [
302
+ {
303
+ label: "编辑",
304
+ onClick: (row: any) => navigateToForm({ id: row.id }) // 带 id:编辑
305
+ }
306
+ ]
307
+ }
308
+ ];
309
+ }
310
+ })();
311
+ return Page.create() as any;
312
+ }
313
+ ```
314
+
315
+ > **❌ 禁止**:
316
+ > - `router.push({ path: "/mmwr-xxx-form" })`(kebab-case 路径错误)
317
+ > - 在**菜单可见页面**(如列表页 data.ts 的 `navigateToForm`)中使用 `router.push()`(父壳无法刷新菜单高亮)
318
+ >
319
+ > **✅ 允许**:
320
+ > - `useRouter().back()`(表单页"取消"按钮返回上一页时可用)
321
+ >
322
+ > ⚠️ **所有前进导航(包括隐藏页→隐藏页)必须用 `location.href`**。`router.push()` 会跳过 shell 的 `generateCurrentRoute`,在 dev 模式下触发 "Invalid route component" 错误(已在 `mmwrCustomerApplyChangeHistory` 实测验证)。
323
+
324
+ ---
325
+
326
+ ### 按钮颜色映射表
327
+
328
+ > **原型颜色优先**:当原型明确展示按钮颜色时,**必须以原型为准**,不可用语义推断覆盖。下方语义推断仅在原型未标颜色时使用。
329
+
330
+ | 原型按钮颜色 | `name` 值 | `plain` | 说明 |
331
+ | --- | --- | --- | --- |
332
+ | 蓝色填充(深蓝底白字) | `"primary"` | 不设 | 主操作-填充(新增申请、启用) |
333
+ | 蓝色线框(蓝边框蓝字) | `"primary"` | `true` | 次要操作-线框(变更申请) |
334
+ | 红色填充(红底白字) | `"danger"` | 不设 | 危险-填充(删除、批量删除) |
335
+ | 红色线框(红边框红字) | `"danger"` | `true` | 危险-线框(审批驳回、作废) |
336
+ | 橙色填充(橙底白字) | `"warning"` | 不设 | 警告-填充(停用) |
337
+ | 橙色线框(橙边框橙字) | `"warning"` | `true` | 警告-线框(撤回、退回、回收) |
338
+ | 绿色线框(绿边框绿字) | `"success"` | `true` | 正向确认-线框(审批通过、转化、认领) |
339
+ | 灰色线框(灰边框灰字) | 不设 name | `true` | 中性操作-线框(导出、导入、批量修改) |
340
+
341
+ > **`name` vs `type` 属性**:`name` 为按钮提供默认的颜色(`type`)和图标(`icon`);`type` 可单独覆盖颜色,两者可共存,`type` 优先级更高。工具栏按钮优先使用 `name`,只在需要与 `name` 默认颜色不同时才加 `type` 覆盖。
342
+
343
+ **语义自动推断**(仅当原型未标颜色时使用,原型明确颜色时以原型为准):
344
+ - 新增/新增申请/保存 → `name: "primary"`(蓝色填充)
345
+ - 变更申请 → `plain: true`(灰色线框)
346
+ - 提交 → `name: "primary", plain: true`
347
+ - 审批通过/认领/转化 → `name: "success", plain: true`
348
+ - 删除/批量删除 → `name: "danger"`(红色填充)
349
+ - 审批驳回/作废 → `name: "danger", plain: true`
350
+ - 启用 → `name: "primary"`(蓝色填充)
351
+ - 停用 → `name: "warning"`(橙色填充)
352
+ - 撤回/退回/回收 → `name: "warning", plain: true`
353
+ - 导出/导入/批量修改 → `plain: true`(灰色线框)
354
+
355
+ ---
356
+
357
+ ### 按钮交互实现规则
358
+
359
+ 所有按钮 `onClick` 必须实现真实交互逻辑,按按钮语义选择以下模式:
360
+
361
+ | 按钮语义 | 交互实现 |
362
+ | --- | --- |
363
+ | **新增/新增申请** | `_editModalRef?.value?.open()` |
364
+ | **删除** | 校验选中 → `this.removeBatch()` |
365
+ | **提交** | 校验选中 → `ElMessageBox.confirm → postAction(API_CONFIG.submit, { ids })` |
366
+ | **审批通过** | 校验选中 → `ElMessageBox.confirm → postAction(API_CONFIG.update, { ids, approvalStatus: "审批完成" })` |
367
+ | **审批驳回** | 校验选中 → `ElMessageBox.confirm → postAction(API_CONFIG.update, { ids, approvalStatus: "驳回" })` |
368
+ | **启用/停用** | 校验选中 → `ElMessageBox.confirm → postAction(API_CONFIG.enable/disable, { ids })` |
369
+ | **撤回** | 校验选中 → `ElMessageBox.confirm → postAction(API_CONFIG.withdraw, { ids })` |
370
+ | **导出** | 客户端 XLSX 生成(见下方 §导出/导入实现模式) |
371
+ | **导入** | 文件选择器 → XLSX 解析 → postAction 批量导入(见下方 §导出/导入实现模式) |
372
+ | **其他** | `ElMessage.info("需业务确认交互逻辑")` |
373
+
374
+ **获取选中行的通用模式**:
375
+
376
+ ```typescript
377
+ const rows = this.tableRef.value?.getSelectionRows();
378
+ if (!rows?.length) {
379
+ ElMessage.warning("请先选择数据");
380
+ return;
381
+ }
382
+ const ids = rows.map((r: any) => r.id);
383
+ ```
384
+
385
+ **操作列按钮交互**:
386
+ - 编辑 → `_editModalRef?.value?.open(row.id)`
387
+ - 删除 → `this.remove(row.id)`(基类内置方法,自带确认弹窗)
388
+ - 查看 → `_editModalRef?.value?.view(row.id)`
389
+
390
+ ---
391
+
392
+ ### 操作列条件显示模式(`show` 属性)
393
+
394
+ > 原型中操作列可能在**不同行**显示**不同按钮**(如已核实行显示"修改+作废",未核实行显示"编辑+删除")。
395
+ > 此时需取**所有按钮的并集**,通过 `show: (row) => boolean` 按行条件显示。
396
+
397
+ **判断依据**:如果原型操作列中,不同行的按钮文字/数量不同,则属于条件操作。
398
+
399
+ **标准实现**(框架原生 `show` 属性):
400
+
401
+ ```typescript
402
+ {
403
+ label: "操作",
404
+ width: 140,
405
+ 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
+ ]
432
+ }
433
+ ```
434
+
435
+ **关键规则**:
436
+ 1. **width** 按并集中同时显示的最大按钮数计算(2 个≈140,3 个≈200)
437
+ 2. **按钮 label** 必须与原型中每行实际显示的文字严格一致
438
+ 3. **按钮语义→API 对应**:"作废"→cancel API,"删除"→remove API,不可混用
439
+
440
+ ---
441
+
442
+ ### 状态列色块渲染模式
443
+
444
+ > 所有"XX状态"类列**必须用 `defaultSlot` + `h(ElTag)` 渲染彩色标签**,不可纯文本显示。
445
+
446
+ **标准实现模式:**
447
+
448
+ 1. **文件顶部定义映射表 + 渲染函数**(与 `import` 同级):
449
+ ```typescript
450
+ import { h, resolveComponent } from "vue";
451
+
452
+ /** 状态色块映射 */
453
+ const STATUS_TAG_MAP: Record<string, Record<string, string>> = {
454
+ convertStatus: { "已转化": "success", "未转化": "info" },
455
+ customerStatus: { "临时客户": "warning", "正式客户": "success" },
456
+ verifyStatus: { "已核实": "success", "未核实": "info" },
457
+ enableStatus: { "已启用": "success", "已停用": "danger" },
458
+ approvalStatus: { "开立审批中": "", "审批完成": "success", "驳回": "danger", "流程终止": "info" }
459
+ };
460
+ function renderStatusTag(row: any, field: string) {
461
+ const val = row[field];
462
+ const type = STATUS_TAG_MAP[field]?.[val];
463
+ if (type === undefined) return val;
464
+ return h(resolveComponent("ElTag") as any, { type: type || "", effect: "light", size: "small" }, () => val);
465
+ }
466
+ ```
467
+
468
+ 2. **列定义中使用 `defaultSlot`**:
469
+ ```typescript
470
+ { label: "转化状态", name: "convertStatus", minWidth: 100, fixed: "right",
471
+ defaultSlot: ({ row }: any) => renderStatusTag(row, "convertStatus") },
472
+ ```
473
+
474
+ **颜色映射规则**(按语义):
475
+ | 语义 | ElTag type | 效果 |
476
+ |------|-----------|------|
477
+ | 成功/已完成/已启用/已核实/已转化/正式 | `success` | 绿色 |
478
+ | 警告/临时/待处理 | `warning` | 橙色 |
479
+ | 危险/已停用/驳回/已作废 | `danger` | 红色 |
480
+ | 默认/进行中/审批中 | `""` | 蓝灰 |
481
+ | 信息/未处理/未核实/未转化/终止 | `info` | 灰色 |
482
+
483
+ **注意**:当映射值中包含空字符串 `""` 时(如"开立审批中"),`renderStatusTag` 中判断条件必须用 `type === undefined` 而非 `!type`,否则空字符串会被跳过不渲染标签。
484
+
485
+ ---
486
+
487
+ ### 视角切换(viewSwitch)与 Tab 切换(tabSwitch)
488
+
489
+ #### viewSwitch — 同数据不同列(如"管理视角 / 使用视角")
490
+
491
+ > 列定义放在 `class` **外部**作为独立 export 函数;`columnsDef()` 返回其中一个提供默认的 `columns` ref;`index.vue` 自行管理 `activeView`,用 `v-if` 切换 `BaseTable`。
492
+
493
+ 外部列函数无法用 `this` 调用 Page 方法,需要**模块级变量**引用:
494
+
495
+ ```typescript
496
+ // 模块顶部:外部列函数通过此变量回调 Page 的 select()/remove()
497
+ let Page: any = null;
498
+
499
+ export function managementColumns(): TableColumnDesc<any>[] {
500
+ return [
501
+ // ...
502
+ { label: "操作", fixed: "right", width: 100, operations: [
503
+ { name: "remove", label: "删除", onClick: (row: any) => Page?.remove(row.id) }
504
+ ]}
505
+ ];
506
+ }
507
+ export function usageColumns(): TableColumnDesc<any>[] {
508
+ return [ /* 使用视角列... */ ];
509
+ }
510
+
511
+ export function createPage(editModalRef?: any) {
512
+ const inst = new (class extends AbstractPageQueryHook {
513
+ columnsDef() { return managementColumns(); } // 提供 columns ref 默认值
514
+ })();
515
+ Page = inst;
516
+ return (inst as any).create() as any;
517
+ }
518
+ ```
519
+
520
+ `index.vue` 核心片段:
521
+
522
+ ```vue
523
+ <el-tabs v-model="activeView">
524
+ <el-tab-pane label="管理视角" name="management">
525
+ <BaseTable v-if="activeView === 'management'" ref="tableRef"
526
+ :data="list" :columns="mgmtCols" showToolbar />
527
+ </el-tab-pane>
528
+ <el-tab-pane label="使用视角" name="usage">
529
+ <BaseTable v-if="activeView === 'usage'" ref="tableRef"
530
+ :data="list" :columns="useCols" showToolbar />
531
+ </el-tab-pane>
532
+ </el-tabs>
533
+
534
+ <script setup lang="ts">
535
+ import { createPage, managementColumns, usageColumns } from "./data";
536
+ const editModalRef = ref();
537
+ const activeView = ref("management");
538
+ const mgmtCols = managementColumns();
539
+ const useCols = usageColumns();
540
+ const Page = createPage(editModalRef);
541
+ const { tableRef, page, queryParam, list, queryItems, toolbars, select } = Page;
542
+ </script>
543
+ ```
544
+
545
+ #### tabSwitch — 同列不同数据(如"临时客户 / 正式客户 / 公海池")
546
+
547
+ > `createPage()` 在 `return` 前把 `activeTab` + `handleTabChange` 挂到结果对象,`index.vue` 解构后直接绑定。
548
+
549
+ **data.ts(`createPage()` 末尾,return 之前)**:
550
+
551
+ ```typescript
552
+ const activeTab = ref<"temp" | "formal" | "pool">("temp");
553
+ const handleTabChange = (val: typeof activeTab.value) => {
554
+ activeTab.value = val;
555
+ result.queryParam.value.tabType = val;
556
+ result.select();
557
+ };
558
+ result.activeTab = activeTab;
559
+ result.handleTabChange = handleTabChange;
560
+ return result;
561
+ ```
562
+
563
+ **index.vue 核心片段**:
564
+
565
+ ```vue
566
+ <el-tabs v-model="activeTab" @tab-change="handleTabChange">
567
+ <el-tab-pane label="临时客户" name="temp" />
568
+ <el-tab-pane label="正式客户" name="formal" />
569
+ <el-tab-pane label="公海池" name="pool" />
570
+ </el-tabs>
571
+
572
+ <script setup lang="ts">
573
+ const Page = createPage(editModalRef);
574
+ const { tableRef, page, queryParam, list, queryItems, toolbars, select,
575
+ activeTab, handleTabChange } = Page;
576
+ </script>
577
+ ```
578
+
579
+ ---
580
+
581
+ ### 导出/导入实现模式
582
+
583
+ > 使用 `xlsx` 库进行客户端 Excel 生成与解析,不依赖后端文件流。
584
+
585
+ **导出(data.ts 顶部需 `import * as XLSX from "xlsx"`)**:
586
+
587
+ ```typescript
588
+ {
589
+ label: "导出",
590
+ plain: true,
591
+ onClick: async () => {
592
+ const data = this.list.value;
593
+ if (!data?.length) { ElMessage.warning("无数据可导出"); return; }
594
+ const exportData = data.map((row: any) => ({
595
+ "列中文名1": row.fieldName1,
596
+ "列中文名2": row.fieldName2
597
+ }));
598
+ const ws = XLSX.utils.json_to_sheet(exportData);
599
+ const wb = XLSX.utils.book_new();
600
+ XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
601
+ XLSX.writeFile(wb, "导出文件名.xlsx");
602
+ ElMessage.success("导出成功");
603
+ }
604
+ }
605
+ ```
606
+
607
+ **导入(需 mock 提供 import 端点)**:
608
+
609
+ ```typescript
610
+ {
611
+ label: "导入",
612
+ plain: true,
613
+ onClick: () => {
614
+ const input = document.createElement("input");
615
+ input.type = "file";
616
+ input.accept = ".xlsx,.xls";
617
+ input.onchange = async (e: any) => {
618
+ const file = e.target.files?.[0];
619
+ if (!file) return;
620
+ try {
621
+ const buf = await file.arrayBuffer();
622
+ const wb = XLSX.read(buf, { type: "array" });
623
+ const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]) as any[];
624
+ if (!rows.length) { ElMessage.warning("文件无有效数据"); return; }
625
+ await postAction(API_CONFIG.import, { rows });
626
+ ElMessage.success(`导入成功 ${rows.length} 条`);
627
+ this.select();
628
+ } catch { ElMessage.error("导入失败,请检查文件格式"); }
629
+ };
630
+ input.click();
631
+ }
632
+ }
633
+ ```
634
+
635
+ ### Mock 端点最佳实践
636
+
637
+ > **核心原则**:Mock 模式下所有操作必须能完整走通,不可出现接口报错。
638
+ > data.ts 中每个 `postAction(API_CONFIG.xxx, ...)` 调用,mock 文件中都必须有对应端点。
639
+
640
+ **1. 所有端点必须修改 dataPool**
641
+
642
+ mock 端点不能只返回 `{ code: 2000 }` — 必须实际修改内存中的 `dataPool` 数据,否则 `this.select()` 刷新后数据不变。
643
+
644
+ ```typescript
645
+ // ✅ 正确:启用端点修改 dataPool 中的 enableStatus
646
+ {
647
+ url: "/dev-api/sale/xxx/enable",
648
+ method: "post",
649
+ response: ({ body }: any) => {
650
+ const ids = body?.ids || [];
651
+ ids.forEach((id: string) => {
652
+ const item = dataPool.find((d) => d.id === id);
653
+ if (item) item.enableStatus = "已启用";
654
+ });
655
+ return { code: 2000, message: "启用成功", data: null };
656
+ }
657
+ }
658
+
659
+ // ❌ 错误:只返回成功,不修改数据
660
+ {
661
+ url: "/dev-api/sale/xxx/enable",
662
+ method: "post",
663
+ response: () => ({ code: 2000, message: "启用成功", data: null })
664
+ }
665
+ ```
666
+
667
+ **2. 常见操作的 Mock 修改模式**
668
+
669
+ | 操作 | dataPool 修改方式 |
670
+ | --- | --- |
671
+ | 删除 | `dataPool.splice(idx, 1)` |
672
+ | 新增 | `dataPool.unshift({ ...genRecord(), ...body, id: Random.id() })` |
673
+ | 编辑 | `Object.assign(dataPool[idx], body)` |
674
+ | 启用/停用 | 修改 `item.enableStatus` |
675
+ | 提交/审批 | 修改 `item.approvalStatus` |
676
+ | 作废 | `dataPool.splice(idx, 1)` 或修改状态 |
677
+ | 分配/认领 | 修改 `item.businessPerson` |
678
+
679
+ **3. 端点覆盖检查**
680
+
681
+ 生成完成后,逐个对比 `API_CONFIG` 的所有 key 与 mock 文件中的 `url`,确保一一对应、零遗漏。
682
+
683
+ ### 禁止
684
+
685
+ > 以下为精简速查清单,详细说明见上方 §禁止事项(严格遵守)。
686
+
687
+ - ❌ index.vue 中写业务逻辑(逻辑全在 data.ts)
688
+ - ❌ 使用 Vuex(用 Pinia)
689
+ - ❌ `::v-deep` / `/deep/`(用 `:deep()`)
690
+ - ❌ 直接用 axios(用 getAction/postAction)
691
+ - ❌ 手写查询表单/工具栏/分页(用 BaseQuery/BaseToolbar/jh-pagination)
692
+ - ❌ 使用 `useTableDelete`(用 `this.remove(row.id)`)
693
+ - ❌ 用 `{ ...instance }` 展开 `create()` 返回值
694
+ - ❌ Mock 端点不修改 dataPool、字段名不对齐 columnsDef
695
+ - ❌ data.ts 导入未使用的模块
696
+ - ❌ 用 `el-radio-group` 做 Tab/视角切换(统一用 `el-tabs`)
697
+
698
+ ---
699
+
700
+ ## 表单页 UI 细节规范(FORM_TAB / 独立路由表单页)
701
+
702
+ > 适用于使用 el-form + el-row/col 布局的复杂表单页(如客户申请新增/变更)。
703
+ > 所有样式规则**写在组件或页面的 index.scss** 中,便于未来复用和移动,避免内联 style 散落。
704
+
705
+ ### 1. 平台组件 label 隐藏
706
+
707
+ `jh-select`、`jh-date`、`jh-file-upload` 等平台组件自带 `label` prop(默认会渲染"下拉选择框:"、"日期:"等文字)。
708
+ **在 el-form-item 内使用时,必须传 `label=""` 隐藏组件自身标签**,避免与 el-form-item 的 label 重复。
709
+
710
+ ```vue
711
+ <!-- ✅ 正确 -->
712
+ <el-form-item label="审批产品别">
713
+ <jh-select v-model="form.productLine" dict="product_line" label="" placeholder="请选择" />
714
+ </el-form-item>
715
+ <el-form-item label="成立时间">
716
+ <jh-date v-model="form.establishDate" label="" placeholder="请选择" />
717
+ </el-form-item>
718
+ <el-form-item label="营业执照">
719
+ <jh-file-upload v-model="form.businessLicense" label="" :disabled="isView" />
720
+ </el-form-item>
721
+
722
+ <!-- ❌ 错误:不传 label="",组件内部会额外渲染 "下拉选择框:" / "日期:" 等文字 -->
723
+ <jh-select v-model="form.productLine" dict="product_line" />
724
+ <jh-date v-model="form.establishDate" />
725
+ ```
726
+
727
+ ### 2. 表单控件宽度统一
728
+
729
+ `jh-select`、`jh-date`、`el-input-number`、`jh-file-upload` 默认宽度可能与 `el-input` 不一致。
730
+ 在组件 scoped style 中统一设置 `width: 100%`:
731
+
732
+ ```scss
733
+ :deep(.jh-select),
734
+ :deep(.jh-date),
735
+ :deep(.el-input-number) {
736
+ width: 100%;
737
+ }
738
+ :deep(.jh-select .el-input),
739
+ :deep(.jh-date .el-input) {
740
+ width: 100%;
741
+ }
742
+ :deep(.jh-file-upload) {
743
+ width: 100%;
744
+ }
745
+ ```
746
+
747
+ ### 3. 页面滚动
748
+
749
+ 独立表单页内容通常超出视口高度。全局 `.app-page-container` 已设 `height: calc(100vh - 100px); overflow: hidden`(列表页靠表格内部滚动),**表单页必须覆盖 `overflow` 为 `auto`**:
750
+
751
+ > ⚠️ 不要加 `height: 100%`,否则会产生双滚动条(与全局 height 冲突)。
752
+
753
+ ```scss
754
+ .app-page-container {
755
+ overflow-y: auto;
756
+ padding-bottom: 24px;
757
+ }
758
+ ```
759
+
760
+ ### 4. 只看必填项
761
+
762
+ 通过在最外层容器加 CSS class 切换,利用 Element Plus 的 `.is-required` 自动标记来隐藏非必填项。
763
+
764
+ **关键**:不能只隐藏 `el-form-item`(外层 `el-col` 仍占栅格空间→留白),必须隐藏整个 `el-col` 并让剩余列自动重排。
765
+
766
+ **组件 props**:接收 `onlyRequired` Boolean prop
767
+ **模板**:`:class="{ 'only-required': onlyRequired }"`
768
+ **样式**(使用 `:has()` 选择器,Chrome 105+):
769
+
770
+ ```scss
771
+ &.only-required {
772
+ /* 隐藏包含非必填字段的整个 el-col */
773
+ :deep(.el-col:has(> .el-form-item:not(.is-required))) {
774
+ display: none !important;
775
+ }
776
+ /* 让可见列自动重排(4列/行) */
777
+ :deep(.el-row) {
778
+ flex-wrap: wrap;
779
+ }
780
+ :deep(.el-col) {
781
+ flex: 0 0 25% !important;
782
+ max-width: 25% !important;
783
+ }
784
+ }
785
+ ```
786
+
787
+ ### 5. 状态信息区域放置
788
+
789
+ 状态信息(创建时间、修改时间、核实状态等只读字段)**仅在"基本信息"Tab 内展示**(业务表格下方),不放在 el-tabs 外部——否则切换到其他 Tab 时仍然可见,与原型不符。
790
+
791
+ ### 6. 文件上传预览
792
+
793
+ 使用 `jh-file-upload` 时,默认 `list-type="picture"` 会将已上传文件显示在组件下方。如需在框内预览(卡片样式),设 `list-type="picture-card"` + `:limit="1"`:
794
+
795
+ ```vue
796
+ <jh-file-upload v-model="form.businessLicense" label="" list-type="picture-card" :limit="1" />
797
+ ```
798
+
799
+ ### 7. 企业核实 Drawer
800
+
801
+ 客户名称输入框右侧加搜索图标,点击打开 `el-drawer` 展示工商信息(天眼查/企查查),使用 `el-descriptions` 两列表格布局。mock 阶段先用静态数据,后续对接 API。
802
+
803
+ ### 8. 按钮位置
804
+
805
+ 表单页操作按钮(保存、取消等)**左对齐**,放在 `.page-toolbar` 区域(标题行下方、tabs 上方):
806
+
807
+ ```vue
808
+ <div class="page-header">
809
+ <span class="page-title">客户申请详情</span>
810
+ <span class="page-tag page-tag--add">新增</span>
811
+ <span class="page-tag page-tag--status">未审核</span>
812
+ <el-checkbox v-model="onlyRequired" class="only-required-check">只看必填项</el-checkbox>
813
+ </div>
814
+ <div class="page-toolbar">
815
+ <el-button type="danger" @click="handleSaveAndChange">保存并变更</el-button>
816
+ <el-button type="warning" @click="handleSave">保存</el-button>
817
+ <el-button @click="handleCancel">取消</el-button>
818
+ </div>
819
+ ```
820
+
821
+ ---
822
+
823
+ ### 导入路径规范(@/types/page 桶文件)
824
+
825
+ > `src/types/page.ts` 是类型桶文件(barrel export),统一重导出 `@jhlc/common-core` 中的常用类型和基类。
826
+ > **所有 data.ts 文件必须从 `@/types/page` 导入,禁止直接引用 `@jhlc/common-core/src/...` 的深层路径。**
827
+
828
+ ```typescript
829
+ // ✅ 正确:从桶文件导入
830
+ import {
831
+ AbstractPageQueryHook,
832
+ BaseQueryItemDesc,
833
+ ActionButtonDesc,
834
+ TableColumnDesc,
835
+ BusLogicDataType
836
+ } from "@/types/page";
837
+
838
+ // ❌ 错误:直接从 common-core 深层路径导入
839
+ import { AbstractPageQueryHook } from "@jhlc/common-core/src/page-hooks/page-query-hook.ts";
840
+ import { BaseQueryItemDesc } from "@jhlc/common-core/src/components/form/base-query/type.ts";
841
+ ```
842
+
843
+ | 导出名 | 说明 |
844
+ | ------------------------ | -------------------------- |
845
+ | `AbstractPageQueryHook` | 列表页基类 |
846
+ | `BaseQueryItemDesc` | 查询表单字段描述类型 |
847
+ | `ActionButtonDesc` | 工具栏/操作列按钮描述类型 |
848
+ | `TableColumnDesc` | 表格列描述类型 |
849
+ | `BusLogicDataType` | 业务逻辑类型枚举(如 dict)|
850
+
851
+ > **例外**:`BaseFormItemDesc`(弹窗表单字段类型)仍直接从 common-core 导入:
852
+ > `import type { BaseFormItemDesc } from "@jhlc/common-core/src/components/form/common/type";`
853
+ > 因为 `src/types/page.ts` 当前未导出该类型。
854
+
855
+ ---
856
+
857
+ ## api.md 生成时序
858
+
859
+ > **api.md 在页面代码之前生成**(Step 2: api-contract → Step 3: page-codegen)。
860
+ > page-codegen 读取已生成的 api.md 中的 URL 和字段定义,确保 `API_CONFIG`、mock、data.ts 与接口约定一致。
861
+ > 未来使用真实 API 设计文档时,api.md 由后端提供或 api-contract Skill 从设计文档提取,page-codegen 直接消费。
862
+
863
+ ---
864
+
865
+ ## SYS_MENU_INFO 生成规则(所有模板通用)
866
+
867
+ page-codegen 生成页面代码后,**必须追加写入菜单配置信息到 `reports/SYS_MENU_INFO.md`**。
868
+
869
+ ### 写入策略(默认追加,不覆盖)
870
+
871
+ - **默认为追加模式**:保留已有内容,在末尾追加本次生成的菜单。避免覆盖团队之前累积的菜单记录。
872
+ - **如需重置**:用户明确说“覆盖”才走覆盖逻辑。
873
+
874
+ > AI 询问示例(仅当用户意图不明时):`本次生成了 N 个页面的菜单配置,默认追加到 reports/SYS_MENU_INFO.md,是否需要覆盖已有内容?`
875
+
876
+ ### 生成模板
877
+
878
+ 每个页面生成一个菜单条目,格式如下:
879
+
880
+ ```markdown
881
+ ## [序号]. [菜单名称]
882
+
883
+ | 字段 | 填写值 |
884
+ | ------------ | ------ |
885
+ | 类型 Tab | 选择 **菜单** |
886
+ | 上级目录 | `[父目录名]` |
887
+ | 应用选择 | `[应用名]` |
888
+ | 使用缓存 | ◉ 使用 |
889
+ | 显示排序 | `[序号]` |
890
+ | 菜单路径 | `[camelCase目录名]` |
891
+ | 菜单名称 | `[中文名]` |
892
+ | 名称编码后缀 | `[菜单路径拼音小写]` |
893
+ | 组件路径 | `[域]/[模块]/[子模块]/[kebab-目录名]/index.vue` |
894
+ | 权限标识 | `[camelCase目录名]` |
895
+ | 是否隐藏 | **否** / **是** |
896
+ ```
897
+
898
+ ### 字段生成规则
899
+
900
+ | 字段 | 来源 | 规则 |
901
+ |------|------|------|
902
+ | 菜单路径 | page-spec.kebabName | kebab-case → camelCase(`mmwr-customer-archive` → `mmwrCustomerArchive`) |
903
+ | 菜单名称 | page-spec.pageName | 直接使用中文名 |
904
+ | 组件路径 | pages.ts 注册路径 | `[域]/[模块]/[子模块]/[kebab-目录名]/index.vue` |
905
+ | 权限标识 | 同菜单路径 | camelCase |
906
+ | 是否隐藏 | page-spec.features.hiddenMenu | `true` → 是,`false` → 否 |
907
+ | 上级目录 | 用户指定 / page-spec 推断 | 如果用户在原型扫描阶段指定了上级目录,使用该值 |
908
+ | 应用选择 | pages.ts 域名 | `produce` → 生产,`sale` → 销售 |
909
+ | 显示排序 | 页面在模块内的序号 | 从 1 开始递增 |
910
+
911
+ ### 隐藏页面判断规则
912
+
913
+ 以下页面类型应设置 `是否隐藏: 是`:
914
+ - 目录名含 `-form`(独立路由表单页)
915
+ - 目录名含 `-detail`(详情页)
916
+ - 目录名含 `-history`(历史查询页)
917
+ - page-spec.features.hiddenMenu === true
918
+
919
+ ### SYS_MENU_INFO.md 文件结构
920
+
921
+ ```markdown
922
+ # 系统菜单配置 — [模块名称]([域] / [子模块路径])
923
+
924
+ > 对应系统管理 → 菜单管理 → 新增菜单,每栏直接复制粘贴。
925
+ > **操作顺序:先建目录(第 0 步),再逐个添加菜单。**
926
+ >
927
+ > **pages.ts 注册位置**:`vite/plugins/shared/pages.ts` → `[模块变量名]` → `[子模块key]`
928
+
929
+ ## 第 0 步:新建目录(如需要)
930
+
931
+ | 字段 | 值 |
932
+ | -------- | -- |
933
+ | 上级目录 | `[上级目录名]` |
934
+ | 菜单名称 | `[目录名]` |
935
+ | 显示排序 | `[序号]` |
936
+
937
+ ## 第 1 步:[页面名称]
938
+
939
+ [菜单条目表格]
940
+
941
+ > pages.ts 对应:`["[kebab-name]", "[中文名]"]`
942
+
943
+ ## 第 2 步:[页面名称]
944
+ ...
945
+ ```
946
+
947
+ ### 与 menu-sync 的衔接
948
+
949
+ SYS_MENU_INFO.md 是 menu-sync Skill 的输入数据源:
950
+ - **自动创建**:用户说"帮我创建菜单" → menu-sync 读取 SYS_MENU_INFO.md → 调 API 逐条创建
951
+ - **手动创建**:用户也可直接按 SYS_MENU_INFO.md 的表格在系统管理后台手动创建菜单
952
+ - 两种方式等价,菜单创建后通过 `组件路径` 字段与 pages.ts 注册的文件路径关联
953
+
954
+ ---
955
+
956
+ ## 代码模板索引
957
+
958
+ > 各模板完整代码见对应独立文件,按需读取。主文件(SKILL.md)包含前置检查、约束、按钮规则、Mock规范等所有共用规则。
959
+
960
+ | 交互模式 | 文件 | 适用场景 | 典型参考页面 |
961
+ |---|---|---|---|
962
+ | LIST | templates/universal/TPL-LIST.md | 标准查询+工具栏+表格+分页 | mmwr-customer-archive |
963
+ | MASTER_DETAIL | templates/universal/TPL-MASTER-DETAIL.md | jh-drag-row 主从表,双击联动 | ompt-ht-plan-order |
964
+ | TREE_LIST | templates/universal/TPL-TREE-LIST.md | 左侧 C_Tree + 右侧列表 | — |
965
+ | DETAIL_TABS | templates/universal/TPL-DETAIL-TABS.md | C_Splitter 上Tab表单+下子表 | add-demo / domestic-trade-order |
966
+ | FORM_ROUTE | templates/universal/TPL-FORM-ROUTE.md | 复杂表单独立路由(非弹窗) | mmwr-customer-apply-add-form |
967
+ | CHANGE_HISTORY | templates/universal/TPL-CHANGE-HISTORY.md | 左历史时间线+右变更详情 | mmwr-customer-apply-change-history |
968
+ | RECORD_FORM | templates/universal/TPL-RECORD-FORM.md | BaseQuery选主记录+Form+Table无分页 | mmsm-convert-progress |
969
+ | OPERATION_STATION | templates/domains/produce/TPL-OPERATION-STATION.md | 工序站点操作(待处理↔已处理+操作表单) | mmwr-rolling-management |
970
+
971
+ > **配置驱动模板页**(ResultQueryTemplate / FinishingAchievementTemplate 等):见 templates/universal/TPL-DRIVEN.md,仅需生成 config 对象,不套用以上模板。
972
+ > **领域模板查询**:完整路径以 `templates/_index.md` 注册表为准,新增领域模板见 `templates/domains/_CONTRIBUTING.md`。
973
+