@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,6 @@
2
2
 
3
3
  > 见 SKILL.md 主文件(约束 + 按钮规则 + Mock 规范等共用规则)。
4
4
 
5
-
6
5
  > 适用场景:通过 BaseQuery 选定主记录(如炉号、生产计划号),展示可编辑的 BaseForm 字段区 + BaseTable 明细行,**无分页**。
7
6
  > 典型于生产域实绩录入(转炉实绩、精炼实绩、连铸实绩等)。
8
7
  > 参考实现:`src/views/produce/production-omom/lgsj/mmsm-convert-progress/`
@@ -23,16 +22,19 @@ import {
23
22
  BaseQueryItemDesc,
24
23
  ActionButtonDesc,
25
24
  TableColumnDesc,
26
- BusLogicDataType
25
+ BusLogicDataType,
27
26
  } from "@/types/page";
28
27
  import type { BaseFormItemDesc } from "@jhlc/common-core/src/components/form/common/type";
29
28
  import c_spliterTitle from "@/components/local/c_spliterTitle/index.vue";
30
29
  import { getAction, postAction } from "@jhlc/common-core/src/api/action";
31
30
  import { debounce } from "lodash-es";
31
+ import { defineColumns } from "@agile-team/wk-skills-ui/runtime";
32
+
33
+ export const BOTTOM_TABLE_CID = "[pageAbbr]-[base36Timestamp]";
32
34
 
33
35
  export const API_CONFIG = {
34
- getByKey: "/[服务缩写]/[资源名]/getBy[Key]", // 按主键查询(炉号/计划号/熔炼号)
35
- saveOrUpdate: "/[服务缩写]/[资源名]/saveOrUpdate" // 保存实绩
36
+ getByKey: "/[服务缩写]/[资源名]/getBy[Key]", // 按主键查询(炉号/计划号/熔炼号)
37
+ saveOrUpdate: "/[服务缩写]/[资源名]/saveOrUpdate", // 保存实绩
36
38
  } as const;
37
39
 
38
40
  // ─────────────── 查询区 ───────────────
@@ -44,8 +46,8 @@ export const queryItems: BaseQueryItemDesc<any>[] = [
44
46
  {
45
47
  name: "[keyField]",
46
48
  label: "[主键名]",
47
- placeholder: "请输入[主键名]"
48
- }
49
+ placeholder: "请输入[主键名]",
50
+ },
49
51
  // 如需关联 Picker,使用 componentVNode 渲染(参考 mmsm-convert-progress PlanMainPicker)
50
52
  ];
51
53
 
@@ -54,7 +56,7 @@ export const select = async () => {
54
56
  const res = await getAction(API_CONFIG.getByKey, queryParam.value);
55
57
  form.value = {
56
58
  ...queryParam.value,
57
- ...(res.data?.[主数据字段] || {})
59
+ ...(res.data?.[主数据字段] || {}),
58
60
  };
59
61
  bottomTableData.value = res.data?.[明细字段] || [];
60
62
  };
@@ -85,7 +87,7 @@ export const formItems: BaseFormItemDesc<any>[] = [
85
87
  label: "",
86
88
  labelWidth: "0px",
87
89
  span: 4,
88
- componentVNode: () => h(c_spliterTitle, { title: "[分区名称]" })
90
+ componentVNode: () => h(c_spliterTitle, { title: "[分区名称]" }),
89
91
  },
90
92
  // 普通文本
91
93
  { label: "[字段名]", name: "[fieldName]", placeholder: "请输入[字段名]" },
@@ -96,22 +98,22 @@ export const formItems: BaseFormItemDesc<any>[] = [
96
98
  placeholder: "请选择[字典字段]",
97
99
  logicType: BusLogicDataType.dict,
98
100
  logicValue: "[dictCode]",
99
- required: true
101
+ required: true,
100
102
  },
101
103
  // 时间
102
104
  {
103
105
  label: "[时间字段]",
104
106
  name: "[timeField]",
105
107
  placeholder: "请选择[时间字段]",
106
- logicType: BusLogicDataType.datetime
108
+ logicType: BusLogicDataType.datetime,
107
109
  },
108
110
  // 数值
109
111
  {
110
112
  label: "[数值字段]",
111
113
  name: "[numField]",
112
114
  placeholder: "请输入[数值字段]",
113
- logicType: BusLogicDataType.number
114
- }
115
+ logicType: BusLogicDataType.number,
116
+ },
115
117
  ];
116
118
 
117
119
  /** 工具栏(需传入 formRef 以触发校验) */
@@ -126,13 +128,15 @@ export const toolbars = (formRef: any): ActionButtonDesc[] => [
126
128
  ElMessageBox.confirm("确定保存吗?", "提示", {
127
129
  confirmButtonText: "确定",
128
130
  cancelButtonText: "取消",
129
- type: "warning"
131
+ type: "warning",
130
132
  }).then(async () => {
131
- const res = await postAction(API_CONFIG.saveOrUpdate, { ...form.value });
133
+ const res = await postAction(API_CONFIG.saveOrUpdate, {
134
+ ...form.value,
135
+ });
132
136
  ElMessage.success(res?.message || "保存成功");
133
137
  });
134
138
  });
135
- }, 600)
139
+ }, 600),
136
140
  },
137
141
  {
138
142
  label: "重置",
@@ -141,8 +145,8 @@ export const toolbars = (formRef: any): ActionButtonDesc[] => [
141
145
  onClick: () => {
142
146
  resetForm();
143
147
  formRef?.resetFields();
144
- }
145
- }
148
+ },
149
+ },
146
150
  ];
147
151
 
148
152
  // ─────────────── 明细表格区 ───────────────
@@ -150,17 +154,18 @@ export const toolbars = (formRef: any): ActionButtonDesc[] => [
150
154
  export const bottomTableData = ref<any[]>([]);
151
155
 
152
156
  /** 明细表格列配置 */
153
- export const bottomTableColumns: TableColumnDesc<any>[] = [
154
- { type: "index", width: 55 },
157
+ export const bottomTableColumns: TableColumnDesc<any>[] = defineColumns([
158
+ { type: "index", label: "序号", width: 60, align: "center" },
155
159
  {
156
160
  label: "[列名]",
157
161
  name: "[fieldName]",
162
+ cid: `${BOTTOM_TABLE_CID}-[fieldName]`,
158
163
  minWidth: 100,
159
164
  sortable: true,
160
- filterable: true
161
- }
165
+ filterable: true,
166
+ },
162
167
  // 按原型顺序添加明细列,通常为只读(无操作列)
163
- ];
168
+ ] as any) as TableColumnDesc<any>[];
164
169
  ```
165
170
 
166
171
  #### index.vue
@@ -190,6 +195,8 @@ export const bottomTableColumns: TableColumnDesc<any>[] = [
190
195
  />
191
196
  <!-- 明细表格(无分页) -->
192
197
  <BaseTable
198
+ render-type="agGrid"
199
+ :cid="BOTTOM_TABLE_CID"
193
200
  :data="bottomTableData"
194
201
  :columns="bottomTableColumns"
195
202
  :height="300"
@@ -209,7 +216,8 @@ import {
209
216
  reset,
210
217
  bottomTableData,
211
218
  bottomTableColumns,
212
- resetForm
219
+ BOTTOM_TABLE_CID,
220
+ resetForm,
213
221
  } from "./data";
214
222
 
215
223
  const formRef = ref<any>(null);
@@ -248,14 +256,14 @@ onMounted(() => {
248
256
 
249
257
  #### 关键约束
250
258
 
251
- | 约束 | 说明 |
252
- |------|------|
253
- | **不用 AbstractPageQueryHook** | 直接导出 `ref` + 函数,无 `createPage()` 包装 |
254
- | **无分页** | `bottomTableData` 绑定全量数据,不加 `jh-pagination` |
255
- | **auto-select: false** | BaseQuery 查询是"选择主记录",不自动触发,用户主动点击 |
256
- | **toolbars(formRef)** | template 中调用函数传入 `formRef`,不是直接绑定数组 |
257
- | **debounce 保存** | 防止连点,来自 `lodash-es`,600ms |
258
- | **c_spliterTitle 分区** | 表单字段按业务分组,每组前插分隔标题 |
259
+ | 约束 | 说明 |
260
+ | ------------------------------ | ------------------------------------------------------ |
261
+ | **不用 AbstractPageQueryHook** | 直接导出 `ref` + 函数,无 `createPage()` 包装 |
262
+ | **无分页** | `bottomTableData` 绑定全量数据,不加 `jh-pagination` |
263
+ | **auto-select: false** | BaseQuery 查询是"选择主记录",不自动触发,用户主动点击 |
264
+ | **toolbars(formRef)** | template 中调用函数传入 `formRef`,不是直接绑定数组 |
265
+ | **debounce 保存** | 防止连点,来自 `lodash-es`,600ms |
266
+ | **c_spliterTitle 分区** | 表单字段按业务分组,每组前插分隔标题 |
259
267
 
260
268
  #### Mock 文件要点
261
269
 
@@ -312,20 +320,29 @@ export default mockApi;
312
320
  defineExpose({ loadData, collectFormData, validate, loadDiffData, clearDiffData })
313
321
  ```
314
322
 
315
- | 方法 | 说明 |
316
- |------|------|
323
+ | 方法 | 说明 |
324
+ | ------------------------ | ---------------------------------------- |
317
325
  | `loadDiffData(prevData)` | 接收旧版数据,组件内部对比并渲染差异指示 |
318
- | `clearDiffData()` | 清除比对状态 |
326
+ | `clearDiffData()` | 清除比对状态 |
319
327
 
320
328
  **组件内部实现要点**:
321
329
 
322
330
  1. **表单字段 diff**:用 `div.diff-field-col` 包裹 `jh-select` + `<span class="diff-old-value">`,旧值出现在字段**下方**(不破坏原布局):
331
+
323
332
  ```html
324
333
  <el-form-item label="纳税类型" prop="taxCategory">
325
334
  <div class="diff-field-col">
326
- <jh-select v-model="basicInfo.taxCategory" dict="tax_category" label="" placeholder="请选择" />
327
- <span v-if="diffBasicInfo && diffBasicInfo.taxCategory !== basicInfo.taxCategory"
328
- class="diff-old-value">{{ diffBasicInfo.taxCategory }}</span>
335
+ <jh-select
336
+ v-model="basicInfo.taxCategory"
337
+ dict="tax_category"
338
+ label=""
339
+ placeholder="请选择"
340
+ />
341
+ <span
342
+ v-if="diffBasicInfo && diffBasicInfo.taxCategory !== basicInfo.taxCategory"
343
+ class="diff-old-value"
344
+ >{{ diffBasicInfo.taxCategory }}</span
345
+ >
329
346
  </div>
330
347
  </el-form-item>
331
348
  ```
@@ -333,6 +350,7 @@ defineExpose({ loadData, collectFormData, validate, loadDiffData, clearDiffData
333
350
  > **数据约定**:`diffBasicInfo` 中存储显示标签(如 `"小规模纳税人"`),不存储 dict code,与 `basicInfo` 保持一致格式,才能正确比对和展示。
334
351
 
335
352
  2. **表格行 diff**:使用 `computed` 在每条主数据行后插入带 `_isDiffRow: true` 标记的旧版行:
353
+
336
354
  ```typescript
337
355
  const displayList = computed(() => {
338
356
  if (!diffList.value) return mainList.value;
@@ -341,8 +359,11 @@ const displayList = computed(() => {
341
359
  result.push({ ...row, _seq: i + 1 });
342
360
  const old = diffList.value![i];
343
361
  if (old) {
344
- const changed = Object.keys(old).filter(k => !k.startsWith('_') && String(old[k]) !== String(row[k]));
345
- if (changed.length) result.push({ ...old, _isDiffRow: true, _changedFields: changed });
362
+ const changed = Object.keys(old).filter(
363
+ (k) => !k.startsWith("_") && String(old[k]) !== String(row[k]),
364
+ );
365
+ if (changed.length)
366
+ result.push({ ...old, _isDiffRow: true, _changedFields: changed });
346
367
  }
347
368
  });
348
369
  return result;
@@ -352,25 +373,49 @@ const displayList = computed(() => {
352
373
  3. **单元格级高亮**:每个 view 模式列的 `<span>` 加上 `diffCellClass(row, 'fieldName')`。
353
374
 
354
375
  4. **CSS 样式**(在组件 scoped style 中):
376
+
355
377
  ```scss
356
378
  /* 表单字段 diff 包装器:列方向 flex,使旧值出现在 jh-select 下方 */
357
379
  .diff-field-col {
358
- display: flex; flex-direction: column; width: 100%;
359
- :deep(.el-select) { width: 100% !important; }
380
+ display: flex;
381
+ flex-direction: column;
382
+ width: 100%;
383
+ :deep(.el-select) {
384
+ width: 100% !important;
385
+ }
360
386
  }
361
387
  /* 表单字段旧值:在字段下方,● 前缀不加删除线,文字橙色 + 删除线 */
362
388
  .diff-old-value {
363
- display: block; font-size: 12px; color: #e6a23c;
364
- text-decoration: line-through; margin-top: 2px; line-height: 1.4;
365
- &::before { content: "● "; text-decoration: none; display: inline-block; }
389
+ display: block;
390
+ font-size: 12px;
391
+ color: #e6a23c;
392
+ text-decoration: line-through;
393
+ margin-top: 2px;
394
+ line-height: 1.4;
395
+ &::before {
396
+ content: "● ";
397
+ text-decoration: none;
398
+ display: inline-block;
399
+ }
366
400
  }
367
401
  /* 表格对比行:已变更字段 —— 橙色 + 删除线 */
368
- .diff-changed { color: #e6a23c !important; text-decoration: line-through; }
369
- .diff-row-marker { color: #e6a23c; font-size: 12px; }
402
+ .diff-changed {
403
+ color: #e6a23c !important;
404
+ text-decoration: line-through;
405
+ }
406
+ .diff-row-marker {
407
+ color: #e6a23c;
408
+ font-size: 12px;
409
+ }
370
410
  /* 表格对比行:整行浅红背景 + 未变字段灰色,已变字段橙色覆盖 */
371
411
  :deep(.el-table .is-diff-row) {
372
412
  background-color: #fef0f0 !important;
373
- td { background-color: #fef0f0 !important; color: #c0c4cc; }
374
- .diff-changed { color: #e6a23c !important; }
413
+ td {
414
+ background-color: #fef0f0 !important;
415
+ color: #c0c4cc;
416
+ }
417
+ .diff-changed {
418
+ color: #e6a23c !important;
419
+ }
375
420
  }
376
421
  ```
@@ -2,7 +2,6 @@
2
2
 
3
3
  > 见 SKILL.md 主文件(约束 + 按钮规则 + Mock 规范等共用规则)。
4
4
 
5
-
6
5
  #### index.vue
7
6
 
8
7
  ```vue
@@ -24,7 +23,14 @@
24
23
  @reset="select"
25
24
  />
26
25
  <BaseToolbar :items="toolbars" />
27
- <BaseTable ref="tableRef" :data="list" :columns="columns" showToolbar />
26
+ <BaseTable
27
+ ref="tableRef"
28
+ render-type="agGrid"
29
+ :cid="TABLE_CID"
30
+ :data="list"
31
+ :columns="columns"
32
+ showToolbar
33
+ />
28
34
  <jh-pagination
29
35
  v-show="page.total && page.total > 0"
30
36
  :total="page.total || 0"
@@ -39,7 +45,7 @@
39
45
  </template>
40
46
 
41
47
  <script setup lang="ts">
42
- import { createPage, loadTree } from "./data";
48
+ import { createPage, loadTree, TABLE_CID } from "./data";
43
49
 
44
50
  const Page = createPage();
45
51
  const {
@@ -52,7 +58,7 @@ const {
52
58
  columns,
53
59
  toolbars,
54
60
  select,
55
- handleNodeClick
61
+ handleNodeClick,
56
62
  } = Page;
57
63
 
58
64
  onMounted(() => {
@@ -74,9 +80,13 @@ import {
74
80
  BaseQueryItemDesc,
75
81
  ActionButtonDesc,
76
82
  TableColumnDesc,
77
- BusLogicDataType
83
+ BusLogicDataType,
78
84
  } from "@/types/page";
79
85
  import { getAction, postAction } from "@jhlc/common-core/src/api/action";
86
+ import { ElMessage } from "element-plus";
87
+ import { defineColumns, renderOps } from "@agile-team/wk-skills-ui/runtime";
88
+
89
+ export const TABLE_CID = "[pageAbbr]-[base36Timestamp]";
80
90
 
81
91
  export const API_CONFIG = {
82
92
  tree: "/[服务缩写]/[树资源]/tree",
@@ -84,7 +94,7 @@ export const API_CONFIG = {
84
94
  remove: "/[服务缩写]/[主资源]/remove",
85
95
  getById: "/[服务缩写]/[主资源]/getById",
86
96
  save: "/[服务缩写]/[主资源]/save",
87
- update: "/[服务缩写]/[主资源]/update"
97
+ update: "/[服务缩写]/[主资源]/update",
88
98
  } as const;
89
99
 
90
100
  // ===== 树形数据 =====
@@ -106,7 +116,7 @@ export function createPage(editModalRef?: any) {
106
116
 
107
117
  queryDef(): BaseQueryItemDesc<any>[] {
108
118
  return [
109
- { name: "[fieldName]", label: "[中文名]", placeholder: "请输入" }
119
+ { name: "[fieldName]", label: "[中文名]", placeholder: "请输入" },
110
120
  ];
111
121
  }
112
122
 
@@ -115,7 +125,7 @@ export function createPage(editModalRef?: any) {
115
125
  {
116
126
  name: "primary",
117
127
  label: "新增",
118
- onClick: () => _editModalRef?.value?.open()
128
+ onClick: () => _editModalRef?.value?.open(),
119
129
  },
120
130
  {
121
131
  name: "danger",
@@ -124,34 +134,44 @@ export function createPage(editModalRef?: any) {
124
134
  const rows = this.tableRef.value?.getSelectionRows();
125
135
  if (!rows?.length) return ElMessage.warning("请先选择数据");
126
136
  this.removeBatch();
127
- }
128
- }
137
+ },
138
+ },
129
139
  ];
130
140
  }
131
141
 
132
142
  columnsDef(): TableColumnDesc<any>[] {
133
- return [
134
- { type: "selection" },
135
- { type: "index" },
136
- { label: "[字段名]", name: "[fieldName]", minWidth: 120 },
143
+ return defineColumns([
144
+ {
145
+ type: "selection",
146
+ width: 55,
147
+ fixed: "left",
148
+ align: "center",
149
+ headerAlign: "center",
150
+ },
151
+ { type: "index", label: "序号", width: 60, align: "center" },
152
+ {
153
+ label: "[字段名]",
154
+ name: "[fieldName]",
155
+ cid: `${TABLE_CID}-[fieldName]`,
156
+ minWidth: 120,
157
+ },
137
158
  {
138
159
  label: "操作",
160
+ name: "_action",
161
+ cid: `${TABLE_CID}-action`,
139
162
  width: 140,
140
163
  fixed: "right",
141
- operations: [
142
- {
143
- name: "edit",
144
- label: "编辑",
145
- onClick: (row: any) => _editModalRef?.value?.edit(row.id)
146
- },
147
- {
148
- name: "remove",
149
- label: "删除",
150
- onClick: (row: any) => this.remove(row.id)
151
- }
152
- ]
153
- }
154
- ];
164
+ align: "center",
165
+ defaultSlot: ({ row }: any) =>
166
+ renderOps([
167
+ {
168
+ type: "edit",
169
+ onClick: () => _editModalRef?.value?.edit(row.id),
170
+ },
171
+ { type: "del", onClick: () => this.remove(row.id) },
172
+ ]),
173
+ },
174
+ ] as any) as TableColumnDesc<any>[];
155
175
  }
156
176
  })();
157
177
 
@@ -168,7 +188,7 @@ export function createPage(editModalRef?: any) {
168
188
  return {
169
189
  ...created,
170
190
  treeData,
171
- handleNodeClick
191
+ handleNodeClick,
172
192
  };
173
193
  }
174
194
  ```
@@ -108,6 +108,22 @@ AI 根据提取的信息,内部构建 page-spec JSON(**不输出给用户**
108
108
 
109
109
  ## 步骤
110
110
 
111
+ > ### ⚠️ Axure 原型文件访问前置说明
112
+ >
113
+ > Axure 导出的 HTML 包含一个 `index.html` 入口,但**不能直接用 `open_browser_page(index.html)` 访问**:
114
+ >
115
+ > | 访问方式 | 结果 | 推荐 |
116
+ > |---------|------|------|
117
+ > | `open_browser_page(index.html)` | 被重定向到 `resources/chrome/chrome.html`(扩展安装引导页),侧边栏不渲染 | ❌ 永久不可用 |
118
+ > | `open_browser_page(具体功能页.html)` | 页面内容正常,`read_page` 可读完整 DOM 快照 | ✅ **首选** |
119
+ > | `read_file(xxx.html)` | 直接读 HTML 源码,用正则提取文本/label | ✅ 推荐(补细节用)|
120
+ >
121
+ > **根本原因**:VS Code 集成浏览器是独立 Playwright/Chromium 进程,**不加载用户 Chrome 的任何扩展**,即使用户本地已安装 Axure RP 扩展也无效。此行为不随环境变化。
122
+ >
123
+ > **补充**:浏览器面板显示 `(not visible)` 仅表示该标签不在前台,不代表无法访问,`screenshot_page`/`read_page` 照样可用。
124
+ >
125
+ > **菜单树**应从 `系统整体框架.html`(或类似全局框架页)的 DOM 文本节点提取,不依赖侧边栏渲染。
126
+
111
127
  ### 1. 全量扫描 HTML
112
128
 
113
129
  遍历所有 `.html` 文件,提取:
@@ -19,9 +19,10 @@ description: "Use when: creating system menus for newly generated pages, batch r
19
19
 
20
20
  | 数据 | 来源 | 说明 |
21
21
  | ------------------------------------------------ | -------------------------- | --------------------------------------- |
22
- | 菜单名称、路径、组件、权限、隐藏、排序、应用编码 | `reports/SYS_MENU_INFO.md` | 由 page-codegen 追加写入,AI 直接读取 |
23
- | `parentMenuNameCode` | API 自动查询 | AI 调 children 接口获取,无需手填 |
24
- | **gatewayPath、parentMenuId、sysAppNo、token** | `env.local.json` | 每套环境不同,唯一需要手动维护的 4 个值 |
22
+ | 菜单名称、路径、组件、权限、隐藏、排序、应用编码 | `.github/reports/SYS_MENU_INFO*.md` | 由 page-codegen 追加写入,AI 直接读取 |
23
+ | `parentMenuNameCode` | `wls_menu_query` 查询菜单树 | 从父级节点获取,无需手填 |
24
+ | **gatewayPath、parentMenuId、sysAppNo、token** | `env.local.json` | 可通过 token/接口辅助提取 |
25
+ | **domainId** | 用户确认 / 菜单后台 Network | 当前权限下无法总是自动获取,需确认 |
25
26
 
26
27
  ### 配置文件(统一维护,菜单/字典/权限共用)
27
28
 
@@ -33,7 +34,8 @@ description: "Use when: creating system menus for newly generated pages, batch r
33
34
  "sysAppNo": "应用编码(从已有菜单的sysAppNo字段获取,非明文)",
34
35
  "token": "Bearer Token(不含bearer前缀)",
35
36
  "menu": {
36
- "parentMenuId": "父级菜单ID"
37
+ "parentMenuId": "父级菜单ID",
38
+ "domainId": "应用域ID"
37
39
  }
38
40
  }
39
41
  ```
@@ -47,9 +49,15 @@ menu-sync 读取规则:`parentMenuId` 优先从 `menu.parentMenuId` 读取,
47
49
 
48
50
  1. **首次**:按 `env/guide.md` 填写 `skills/sync/env.local.json` 的字段
49
51
  2. **之后**:直接对 AI 说「帮我创建菜单」/「同步菜单」/「补菜单」
50
- 3. AI 自动执行:读 `reports/SYS_MENU_INFO.md` → 读 `env.local.json` → 查父级已有子节点逐条对比去重 `/system/menu/save` → 输出 created/skipped 结果表
52
+ 3. AI 自动执行:读 `.github/reports/SYS_MENU_INFO*.md` → 读 `env.local.json` → 优先调用 `wls_menu_sync_from_report` 一步完成确定性同步;如需手动拆分,则 `wls_menu_query` 查 domain 菜单树 upsert 一级目录 用返回 id upsert 二级页面菜单 → 输出 created/updated/skipped 结果表
51
53
  4. **全程无需手动执行任何命令**
52
54
 
55
+ SYS_MENU_INFO.md 是 menu-sync Skill 的输入数据源:
56
+ - **自动创建**:用户说"帮我创建菜单" → menu-sync 调用 `wls_menu_sync_from_report` 读取 SYS_MENU_INFO.md → 调 API 按目录与页面顺序创建/更新
57
+ - **手动创建**:用户也可直接按 SYS_MENU_INFO.md 的表格在系统管理后台手动创建菜单
58
+ - 两种方式等价,菜单创建后通过 `组件路径` 字段与 pages.ts 注册的文件路径关联
59
+ - **自动创建顺序**:优先使用 `wls_menu_sync_from_report`;手动拆分时必须先调用 `wls_menu_query` 获取当前 domain 菜单树,再 `wls_menu_upsert` 创建/更新一级目录(type=M),拿到目录 id 后再创建二级菜单(type=C)。不得把二级页面全部直接挂到根 `parentMenuId`。
60
+
53
61
  ---
54
62
 
55
63
  ## 方案演进路线
@@ -90,18 +98,23 @@ menu-sync 读取规则:`parentMenuId` 优先从 `menu.parentMenuId` 读取,
90
98
 
91
99
  ### 执行流程
92
100
 
93
- #### Step 1: 查询父级下已有菜单(防重复)
101
+ #### Step 1: 查询当前 domain 菜单树(防重复 + 取父级信息)
94
102
 
95
103
  ```
96
- GET {gatewayPath}/system/menu/children?current=1&size=100&menuId={parentMenuId}
97
- Headers:
98
- authorization: bearer {token}
99
- Sysappno: {sysAppNo}
104
+ 工具:wls_menu_query
105
+ 读取:env.local.json → menu.domainId
106
+ 返回:当前应用域完整菜单树
100
107
  ```
101
108
 
102
- #### Step 2: 逐条创建菜单
109
+ > 推荐:具备 MCP 时优先调用 `wls_menu_sync_from_report`,它会自动读取最新 `SYS_MENU_INFO*.md`、查询菜单树、复用/更新目录,再把二级菜单挂到对应目录下。可先传 `dryRun: true` 做预览。
110
+
111
+ #### Step 2: 先创建一级目录(type=M)
112
+
113
+ 对于 `SYS_MENU_INFO` 中的一级目录,按 `menuName/path` 在父级 `parentMenuId` 下去重;不存在则创建,存在则复用 id。
114
+
115
+ #### Step 3: 再创建二级页面菜单(type=C)
103
116
 
104
- 对于每条待创建的菜单,先检查是否与已有菜单重名(`menuName` `path` 相同),重复则跳过。
117
+ 对于每条页面菜单,`parentId` 必须使用上一步对应目录的 id,禁止全部挂到根 `parentMenuId`。
105
118
 
106
119
  > **响应码说明**:后端成功响应为 `code: 2000`(非标准 HTTP 200),判断成功应检查 `response.body.code === 2000` 或 `message` 包含"成功"。
107
120
 
@@ -201,11 +214,12 @@ pages.ts 条目:
201
214
 
202
215
  ### 树形菜单处理
203
216
 
204
- 如果需要先创建目录再创建子菜单:
217
+ 如果 `SYS_MENU_INFO` 包含一级目录和二级菜单,必须按以下顺序处理:
205
218
 
206
219
  1. 先以 `type: "M"` 创建目录
207
220
  2. 保存成功后取返回的 `data.id`
208
221
  3. 以新 `id` 作为 `parentId` 继续创建子菜单
222
+ 4. 若目录已存在,复用已存在目录 id,不重复创建
209
223
 
210
224
  ---
211
225