@agile-team/wl-skills-kit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +328 -0
  3. package/bin/wl-skills.js +104 -0
  4. package/files/.github/copilot-instructions.md +211 -0
  5. package/files/.github/docs/SYS_MENU_INFO.md +247 -0
  6. package/files/.github/docs/menu-sync-design.md +265 -0
  7. package/files/.github/docs/use-skill.md +379 -0
  8. package/files/.github/docs/wl-skills-kit.md +266 -0
  9. package/files/.github/skills/api-contract/SKILL.md +247 -0
  10. package/files/.github/skills/convention-extract/SKILL.md +355 -0
  11. package/files/.github/skills/menu-sync/SKILL.md +255 -0
  12. package/files/.github/skills/menu-sync/env/guide.md +73 -0
  13. package/files/.github/skills/page-codegen/SKILL.md +825 -0
  14. package/files/.github/skills/page-codegen/TPL-CHANGE-HISTORY.md +281 -0
  15. package/files/.github/skills/page-codegen/TPL-DETAIL-TABS.md +1112 -0
  16. package/files/.github/skills/page-codegen/TPL-DRIVEN.md +124 -0
  17. package/files/.github/skills/page-codegen/TPL-FORM-ROUTE.md +441 -0
  18. package/files/.github/skills/page-codegen/TPL-LIST.md +196 -0
  19. package/files/.github/skills/page-codegen/TPL-MASTER-DETAIL.md +153 -0
  20. package/files/.github/skills/page-codegen/TPL-OPERATION-STATION.md +442 -0
  21. package/files/.github/skills/page-codegen/TPL-RECORD-FORM.md +376 -0
  22. package/files/.github/skills/page-codegen/TPL-TREE-LIST.md +191 -0
  23. package/files/.github/skills/prototype-scan/SKILL.md +414 -0
  24. package/files/demo/README.md +44 -0
  25. package/files/demo/produce/aiflow/mmwr-customer-apply-add/api.md +54 -0
  26. package/files/demo/produce/aiflow/mmwr-customer-apply-add/data.ts +346 -0
  27. package/files/demo/produce/aiflow/mmwr-customer-apply-add/index.scss +1 -0
  28. package/files/demo/produce/aiflow/mmwr-customer-apply-add/index.vue +28 -0
  29. package/files/demo/produce/aiflow/mmwr-customer-apply-add-form/data.ts +115 -0
  30. package/files/demo/produce/aiflow/mmwr-customer-apply-add-form/index.scss +44 -0
  31. package/files/demo/produce/aiflow/mmwr-customer-apply-add-form/index.vue +43 -0
  32. package/files/demo/produce/aiflow/mmwr-customer-apply-change/data.ts +338 -0
  33. package/files/demo/produce/aiflow/mmwr-customer-apply-change/index.scss +1 -0
  34. package/files/demo/produce/aiflow/mmwr-customer-apply-change/index.vue +28 -0
  35. package/files/demo/produce/aiflow/mmwr-customer-apply-change-form/data.ts +115 -0
  36. package/files/demo/produce/aiflow/mmwr-customer-apply-change-form/index.scss +44 -0
  37. package/files/demo/produce/aiflow/mmwr-customer-apply-change-form/index.vue +43 -0
  38. package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/data.ts +196 -0
  39. package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/index.scss +150 -0
  40. package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/index.vue +79 -0
  41. package/files/demo/produce/aiflow/mmwr-customer-archive/api.md +88 -0
  42. package/files/demo/produce/aiflow/mmwr-customer-archive/data.ts +601 -0
  43. package/files/demo/produce/aiflow/mmwr-customer-archive/index.scss +1 -0
  44. package/files/demo/produce/aiflow/mmwr-customer-archive/index.vue +64 -0
  45. package/files/demo/produce/aiflow/mmwr-customer-detail/api.md +67 -0
  46. package/files/demo/produce/aiflow/mmwr-customer-detail/data.ts +286 -0
  47. package/files/demo/produce/aiflow/mmwr-customer-detail/index.scss +139 -0
  48. package/files/demo/produce/aiflow/mmwr-customer-detail/index.vue +318 -0
  49. package/files/demo/produce/aiflow/mmwr-temp-customer-archive/api.md +98 -0
  50. package/files/demo/produce/aiflow/mmwr-temp-customer-archive/data.ts +543 -0
  51. package/files/demo/produce/aiflow/mmwr-temp-customer-archive/index.scss +1 -0
  52. package/files/demo/produce/aiflow/mmwr-temp-customer-archive/index.vue +52 -0
  53. package/files/demo/sale/demo/add-demo/data.ts +518 -0
  54. package/files/demo/sale/demo/add-demo/index.scss +207 -0
  55. package/files/demo/sale/demo/add-demo/index.vue +167 -0
  56. package/files/demo/sale/demo/billet-flame-cut-plan/data.ts +524 -0
  57. package/files/demo/sale/demo/billet-flame-cut-plan/index.scss +155 -0
  58. package/files/demo/sale/demo/billet-flame-cut-plan/index.vue +117 -0
  59. package/files/demo/sale/demo/domestic-trade-order/data.ts +308 -0
  60. package/files/demo/sale/demo/domestic-trade-order/index.scss +99 -0
  61. package/files/demo/sale/demo/domestic-trade-order/index.vue +77 -0
  62. package/files/demo/sale/demo/heat-batch-return/data.ts +367 -0
  63. package/files/demo/sale/demo/heat-batch-return/index.scss +100 -0
  64. package/files/demo/sale/demo/heat-batch-return/index.vue +170 -0
  65. package/files/demo/sale/demo/heat-batch-return/meltDialog.vue +320 -0
  66. package/files/demo/sale/demo/metallurgical-spec/data.ts +825 -0
  67. package/files/demo/sale/demo/metallurgical-spec/index.scss +264 -0
  68. package/files/demo/sale/demo/metallurgical-spec/index.vue +309 -0
  69. package/files/docs/jh-date-range.md +257 -0
  70. package/files/docs/jh-date.md +222 -0
  71. package/files/docs/jh-dept-picker.md +190 -0
  72. package/files/docs/jh-drag-row.md +590 -0
  73. package/files/docs/jh-file-upload.md +216 -0
  74. package/files/docs/jh-pagination.md +505 -0
  75. package/files/docs/jh-picker.md +218 -0
  76. package/files/docs/jh-select.md +148 -0
  77. package/files/docs/jh-text.md +248 -0
  78. package/files/docs/jh-user-picker.md +197 -0
  79. package/files/docs/page-query-hook-best-practices.md +362 -0
  80. package/files/docs/request.md +925 -0
  81. package/files/src/components/global/C_ParentView/index.vue +3 -0
  82. package/files/src/components/global/C_RightToolbar/index.vue +459 -0
  83. package/files/src/components/global/C_Splitter/index.vue +195 -0
  84. package/files/src/components/global/C_SvgIcon/index.vue +61 -0
  85. package/files/src/components/global/C_SvgIcon/svgicon.js +10 -0
  86. package/files/src/components/global/C_TagStatus/README.md +264 -0
  87. package/files/src/components/global/C_TagStatus/config.ts +192 -0
  88. package/files/src/components/global/C_TagStatus/index.vue +127 -0
  89. package/files/src/components/global/C_TagStatus/types.ts +64 -0
  90. package/files/src/components/global/C_Tree/README.md +153 -0
  91. package/files/src/components/global/C_Tree/index.scss +42 -0
  92. package/files/src/components/global/C_Tree/index.vue +119 -0
  93. package/files/src/components/global/C_Tree/types.ts +59 -0
  94. package/files/src/components/local/c_formModal/README.md +235 -0
  95. package/files/src/components/local/c_formModal/data.ts +95 -0
  96. package/files/src/components/local/c_formModal/index.scss +8 -0
  97. package/files/src/components/local/c_formModal/index.vue +107 -0
  98. package/files/src/components/local/c_formSections/README.md +496 -0
  99. package/files/src/components/local/c_formSections/data.ts +175 -0
  100. package/files/src/components/local/c_formSections/index.scss +280 -0
  101. package/files/src/components/local/c_formSections/index.vue +429 -0
  102. package/files/src/components/local/c_listModal/data.ts +41 -0
  103. package/files/src/components/local/c_listModal/index.vue +136 -0
  104. package/files/src/components/local/c_spliterTitle/index.scss +25 -0
  105. package/files/src/components/local/c_spliterTitle/index.vue +21 -0
  106. package/files/src/components/remote/AGGrid/README.md +530 -0
  107. package/files/src/components/remote/BaseForm/README.md +508 -0
  108. package/files/src/components/remote/BaseQuery/README.md +865 -0
  109. package/files/src/components/remote/BaseTable/README.md +941 -0
  110. package/files/src/components/remote/BaseToolbar/README.md +496 -0
  111. package/files/src/types/page.ts +24 -0
  112. package/package.json +31 -0
@@ -0,0 +1,124 @@
1
+ # TEMPLATE_DRIVEN:配置驱动模板页(识别参考)
2
+
3
+ > ⚠️ **本文件为识别参考,不是代码生成模板。**
4
+ > 配置驱动页面已由现有业务模板组件封装,AI **只需生成 config 配置对象**,不得套用 TPL-A 至 TPL-G 的结构重新手写页面逻辑。
5
+
6
+ ---
7
+
8
+ ## 什么是配置驱动模板页
9
+
10
+ 这类页面的 `index.vue` 极为简单,只有 1-3 行:
11
+
12
+ ```vue
13
+ <template>
14
+ <div class="app-container app-page-container">
15
+ <XxxTemplate :config="xxxConfig" />
16
+ </div>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import { xxxConfig } from "./data";
21
+ </script>
22
+ ```
23
+
24
+ 所有业务逻辑(查询、表格列、导出、翻译等)全部由模板组件内部处理,`data.ts` 只需导出一个 `config` 对象。
25
+
26
+ ---
27
+
28
+ ## 已知配置驱动模板一览
29
+
30
+ > ⚠️ **以下模板是基于高度相似的特定业务场景提炼的**,仅适用于列出的具体页面类型。不得因为"看起来相似"就套用到其他业务场景,否则会出现功能异常。
31
+
32
+ ### 1. ResultQueryTemplate(轧钢实绩查询类)
33
+
34
+ **适用范围**:`production-mmwr/sjtj/` 下的各工序实绩查询页(查询+表格+导出,无新增/编辑/删除)
35
+
36
+ **识别特征**:
37
+ - 原型中只有查询区 + 数据表格 + 导出按钮
38
+ - 无 CRUD 操作,纯查询
39
+ - 字段较多,表格列数 10+
40
+
41
+ **生成规则**:
42
+
43
+ ```typescript
44
+ // data.ts — 只导出一个 config 对象
45
+ import type { ResultQueryConfig } from "@/components/template/ResultQueryTemplate/types";
46
+ import { BusLogicDataType } from "@/types/page";
47
+
48
+ export const [pageName]Config: ResultQueryConfig = {
49
+ api: {
50
+ list: "/[服务缩写]/[资源名]/list",
51
+ export: "/[服务缩写]/[资源名]/export" // 有导出时才加
52
+ },
53
+ queryItems: [
54
+ // 同 Template A 的 queryDef 格式
55
+ { name: "[field]", label: "[中文名]", placeholder: "请输入" },
56
+ {
57
+ name: "[dateField]",
58
+ type: "range",
59
+ startName: "[startDate]",
60
+ endName: "[endDate]",
61
+ label: "[日期名]",
62
+ logicType: BusLogicDataType.date,
63
+ rangeSeparator: "至"
64
+ }
65
+ ],
66
+ columns: [
67
+ // 同 Template A 的 columnsDef 格式(无 selection/index/操作列)
68
+ { label: "[列名]", name: "[fieldName]", minWidth: 120 }
69
+ ]
70
+ };
71
+ ```
72
+
73
+ ```vue
74
+ <!-- index.vue -->
75
+ <template>
76
+ <ResultQueryTemplate :config="[pageName]Config" />
77
+ </template>
78
+
79
+ <script setup lang="ts">
80
+ import ResultQueryTemplate from "@/components/template/ResultQueryTemplate/index.vue";
81
+ import { [pageName]Config } from "./data";
82
+ </script>
83
+ ```
84
+
85
+ ---
86
+
87
+ ### 2. FinishingAchievementTemplate(精整实绩类)
88
+
89
+ **适用范围**:`production-mmwr/jzsj/` 下的各精整工序实绩管理页(喷砂、倒棱、矫直、剥皮、检验、酸洗、包装)
90
+
91
+ **识别特征**:
92
+ - 原型标题含"实绩"二字且在精整工序列表中
93
+ - 有固定的查询区 + 实绩录入 + 汇总数据结构
94
+ - 字段结构高度统一(同一套模板 7 个页面共用)
95
+
96
+ **生成规则**:参考 `src/components/template/FinishingAchievementTemplate/` 内的 `types.ts` 定义,只生成对应的 config 配置。
97
+
98
+ ---
99
+
100
+ ### 3. ApplicationDeterminationTemplate(申请判定类)
101
+
102
+ **适用范围**:`production-mmwr/` 下的申请判定类页面(如 mmwr-application-determination-jz、mmwr-application-determination-sj)
103
+
104
+ **识别特征**:原型中有"申请"列表 + "判定"操作的双状态工作流页面
105
+
106
+ ---
107
+
108
+ ### 4. SamplingCommissionTemplate(取样委托类)
109
+
110
+ **适用范围**:`production-mmwr/jhgl/` 下的取样委托管理页(如 mmwr-sampling-commission-jz、mmwr-sampling-commission-sj)
111
+
112
+ **识别特征**:原型中有"委托单"列表 + 取样记录结构
113
+
114
+ ---
115
+
116
+ ## AI 操作规范
117
+
118
+ 1. **识别阶段**:在 page-spec 前置检查时,若页面落入以上任一范围,将交互模式标记为 `TEMPLATE_DRIVEN` 并注明具体模板名
119
+ 2. **生成阶段**:只生成 `data.ts`(config 对象)+ `index.vue`(2-3行)+ `index.scss`(空或极少样式)
120
+ 3. **禁止行为**:
121
+ - ❌ 不得手写 `createPage()` + `AbstractPageQueryHook` 结构
122
+ - ❌ 不得添加独立的 `BaseQuery` / `BaseTable` / `jh-pagination`(模板内部已处理)
123
+ - ❌ 不得将其他业务场景的页面识别为 TEMPLATE_DRIVEN 并套用以上 config 格式
124
+ 4. **不确定时**:如果不确定是否适用配置驱动模板,使用 Template A(LIST)代替,更安全
@@ -0,0 +1,441 @@
1
+ # FORM_ROUTE:复杂表单独立路由页
2
+
3
+ > 见 SKILL.md 主文件(约束 + 按钮规则 + Mock 规范等共用规则)。
4
+
5
+
6
+ > 复杂表单(多 Tab、多子表、独立布局)使用独立路由而非弹窗。
7
+ > 表单页 `data.ts` **不继承 `AbstractPageQueryHook`**,改为导出 `useXxx` Composable。
8
+ > 需在 `pages.ts` 单独注册路由,路径规则见"FORM_ROUTE 表单页"章节。
9
+
10
+ #### data.ts
11
+
12
+ ```typescript
13
+ import { getAction, postAction } from "@jhlc/common-core/src/api/action";
14
+ import { ElMessage } from "element-plus";
15
+ import { useRouter } from "vue-router"; // ✅ 仅用于 router.back()
16
+
17
+ export const API_CONFIG = {
18
+ getById: "/[服务缩写]/[资源名]/getById",
19
+ save: "/[服务缩写]/[资源名]/save",
20
+ submit: "/[服务缩写]/[资源名]/submit"
21
+ } as const;
22
+
23
+ export function use[PageName]Form(tabsRef: any) {
24
+ const router = useRouter();
25
+ const loading = ref(false);
26
+ const isEdit = ref(false);
27
+ const currentId = ref<string>("");
28
+
29
+ async function loadDetail(id: string) {
30
+ loading.value = true;
31
+ isEdit.value = true;
32
+ currentId.value = id;
33
+ try {
34
+ const res = await getAction(API_CONFIG.getById, { id });
35
+ if (res?.data) tabsRef.value?.loadData(res.data);
36
+ } finally {
37
+ loading.value = false;
38
+ }
39
+ }
40
+
41
+ async function handleSave() {
42
+ const valid = await tabsRef.value?.validate();
43
+ if (!valid) { ElMessage.warning("请完善必填项"); return; }
44
+ loading.value = true;
45
+ try {
46
+ const formData = tabsRef.value?.collectFormData();
47
+ const payload = isEdit.value ? { ...formData, id: currentId.value } : formData;
48
+ const res = await postAction(API_CONFIG.save, payload);
49
+ if (res?.code === 200) {
50
+ ElMessage.success("保存成功");
51
+ if (!isEdit.value && res.data?.id) {
52
+ currentId.value = res.data.id;
53
+ isEdit.value = true;
54
+ }
55
+ }
56
+ } finally {
57
+ loading.value = false;
58
+ }
59
+ }
60
+
61
+ function handleCancel() {
62
+ router.back(); // ✅ back() 允许,不影响菜单激活
63
+ }
64
+
65
+ return { loading, isEdit, loadDetail, handleSave, handleCancel };
66
+ }
67
+ ```
68
+
69
+ #### index.vue
70
+
71
+ ```vue
72
+ <template>
73
+ <div class="app-container app-page-container" v-loading="loading">
74
+ <div class="page-header">
75
+ <span class="page-title">[页面标题]</span>
76
+ <span class="page-tag page-tag--add">新增</span>
77
+ <el-checkbox v-model="onlyRequired" class="only-required-check">只看必填项</el-checkbox>
78
+ </div>
79
+ <div class="page-toolbar">
80
+ <el-button type="primary" @click="handleSave">保存</el-button>
81
+ <el-button @click="handleCancel">取消</el-button>
82
+ </div>
83
+ <c_[业务名]Tabs ref="tabsRef" mode="add" :only-required="onlyRequired" />
84
+ </div>
85
+ </template>
86
+
87
+ <script setup lang="ts">
88
+ import { useRoute } from "vue-router";
89
+ import { use[PageName]Form } from "./data";
90
+ import c_[业务名]Tabs from "@/components/local/c_[业务名]Tabs/index.vue";
91
+
92
+ const tabsRef = ref();
93
+ const route = useRoute();
94
+ const onlyRequired = ref(false);
95
+ const { loading, loadDetail, handleSave, handleCancel } = use[PageName]Form(tabsRef);
96
+
97
+ onMounted(() => {
98
+ const id = route.query.id as string;
99
+ if (id) loadDetail(id);
100
+ });
101
+ </script>
102
+
103
+ <style scoped lang="scss">
104
+ @import "./index.scss";
105
+ </style>
106
+ ```
107
+
108
+ ---
109
+
110
+ ### Template C: FLAT_DETAIL 平铺详情页
111
+
112
+ > 适用场景:单页平铺 Section 式详情/编辑页面(无 Tab 组件),如「临时客户档案详情」。
113
+ > 与 Template B 的区别:不使用 `c_[业务名]Tabs`,而是直接在 `el-form` 中按 Section 分块铺设表单字段。
114
+
115
+ #### C-1 data.ts 模板
116
+
117
+ ```typescript
118
+ import { getAction, postAction } from "@jhlc/common-core/src/api/action";
119
+ import { ElMessage, ElMessageBox } from "element-plus";
120
+ import { useRouter } from "vue-router";
121
+
122
+ export const API_CONFIG = {
123
+ getById: "/sale/[业务名]/getById",
124
+ save: "/sale/[业务名]/save"
125
+ // ...其他业务操作
126
+ } as const;
127
+
128
+ export const OPTS = {
129
+ // 下拉选项集合
130
+ // [字段名]: [{ label: "显示文本", value: "值" }]
131
+ };
132
+
133
+ export interface [PageName]Form {
134
+ id: string;
135
+ // ...所有字段
136
+ }
137
+
138
+ /** 开发期 Mock 数据 */
139
+ export function createMockData(): [PageName]Form {
140
+ return {
141
+ id: "mock-001"
142
+ // ...所有字段的模拟值
143
+ };
144
+ }
145
+
146
+ export function use[PageName]Detail() {
147
+ const router = useRouter();
148
+ const loading = ref(false);
149
+ const form = reactive<[PageName]Form>(createMockData());
150
+
151
+ async function loadDetail(id: string) {
152
+ loading.value = true;
153
+ try {
154
+ const res = await getAction(API_CONFIG.getById, { id });
155
+ if (res?.data) Object.assign(form, res.data);
156
+ } finally {
157
+ loading.value = false;
158
+ }
159
+ }
160
+
161
+ async function handleSave() {
162
+ loading.value = true;
163
+ try {
164
+ const res = await postAction(API_CONFIG.save, { ...form });
165
+ if (res?.code === 200) ElMessage.success("保存成功");
166
+ } finally {
167
+ loading.value = false;
168
+ }
169
+ }
170
+
171
+ function handleCancel() { router.back(); }
172
+
173
+ return { loading, form, loadDetail, handleSave, handleCancel };
174
+ }
175
+ ```
176
+
177
+ #### C-2 index.vue 模板
178
+
179
+ ```vue
180
+ <template>
181
+ <div class="app-container [page-class]" v-loading="loading">
182
+ <!-- 标题栏 -->
183
+ <div class="title-bar">
184
+ <span class="customer-name">{{ form.[标题字段] }}</span>
185
+ <el-tag type="warning" effect="plain" size="small">{{ form.[状态字段] }}</el-tag>
186
+ </div>
187
+
188
+ <!-- 工具栏 -->
189
+ <div class="page-toolbar">
190
+ <el-button type="primary" @click="handleSave">保存</el-button>
191
+ <!-- ...其他按钮 -->
192
+ <el-button @click="handleCancel">返回</el-button>
193
+ </div>
194
+
195
+ <el-form :model="form" label-position="top" class="detail-form">
196
+ <!-- 头部信息网格 -->
197
+ <div class="header-info">
198
+ <el-row :gutter="12">
199
+ <el-col :span="4">
200
+ <el-form-item label="[字段名]">
201
+ <el-input v-model="form.[字段]" disabled />
202
+ </el-form-item>
203
+ </el-col>
204
+ <!-- ...更多头部字段 -->
205
+ </el-row>
206
+ </div>
207
+
208
+ <!-- Section: 按业务分块,每个 Section 一个 .form-section -->
209
+ <div class="form-section">
210
+ <div class="section-title">[分区名称]</div>
211
+ <el-row :gutter="12">
212
+ <el-col :span="[n]">
213
+ <el-form-item label="[字段名]">
214
+ <el-input v-model="form.[字段]" />
215
+ </el-form-item>
216
+ </el-col>
217
+ <!-- ...更多字段 -->
218
+ </el-row>
219
+ </div>
220
+
221
+ <!-- 子表格 Section(如跟进记录) -->
222
+ <div class="form-section">
223
+ <div class="section-title">[表格标题]</div>
224
+ <el-table :data="form.[列表字段]" border size="small">
225
+ <el-table-column type="index" label="序号" width="55" align="center" />
226
+ <!-- ...更多列 -->
227
+ <el-table-column label="操作" width="100" fixed="right">
228
+ <template #default="{ $index }">
229
+ <el-button type="primary" link size="small">编辑</el-button>
230
+ <el-button type="danger" link size="small" @click="removeRecord($index)">删除</el-button>
231
+ </template>
232
+ </el-table-column>
233
+ </el-table>
234
+ <div class="add-row-btn" @click="addRecord">+ 新增行</div>
235
+ </div>
236
+ </el-form>
237
+ </div>
238
+ </template>
239
+
240
+ <script setup lang="ts">
241
+ import { useRoute } from "vue-router";
242
+ import { use[PageName]Detail, OPTS } from "./data";
243
+
244
+ const route = useRoute();
245
+ const { loading, form, loadDetail, handleSave, handleCancel } = use[PageName]Detail();
246
+
247
+ onMounted(() => {
248
+ const id = route.query.id as string;
249
+ if (id) loadDetail(id);
250
+ });
251
+ </script>
252
+
253
+ <style scoped lang="scss">
254
+ @import "./index.scss";
255
+ </style>
256
+ ```
257
+
258
+ #### C-3 index.scss 要点
259
+
260
+ ```scss
261
+ .[page-class] {
262
+ padding: 0 !important;
263
+ display: flex;
264
+ flex-direction: column;
265
+ overflow: hidden;
266
+
267
+ .title-bar { /* 标题 + 状态 Tag,灰色背景 */ }
268
+ .page-toolbar { /* 按钮行,白底,底部边框 */ }
269
+ .detail-form { flex: 1; overflow-y: auto; padding: 0 16px 16px; }
270
+ .header-info { padding: 12px 0 4px; border-bottom: 1px solid #f0f2f5; }
271
+ .form-section { margin-top: 16px;
272
+ .section-title { border-left: 3px solid var(--el-color-primary); padding-left: 10px; font-weight: 600; }
273
+ }
274
+ .add-row-btn { color: #409eff; cursor: pointer; margin-top: 8px; }
275
+ .el-form-item { margin-bottom: 10px; }
276
+ }
277
+ ```
278
+
279
+ ---
280
+
281
+ ## Template D: MULTI_TABLE — 多表联动实绩页
282
+
283
+ > 适用场景:多个 BaseTable 上下/左右联动,选中上表行驱动下表查询。
284
+ > 典型页面:精整实绩(抛丸/倒棱/矫直/酸洗/剥皮/检验/包装)、加热管理(装炉/出炉)、剔钢操作。
285
+ >
286
+ > 项目中已有两种落地方式:
287
+ > - **配置驱动模板组件**:`FinishingAchievementTemplate`(7 个精整页面共用)
288
+ > - **独立页面编排**:`mmwr-heating-management`、`mmwr-steel-stripping-operations`
289
+
290
+ ### D-0 核心特征
291
+
292
+ | 特征 | 说明 |
293
+ |---|---|
294
+ | **多 AbstractPageQueryHook 实例** | 每个表格区域一个实例,各自管理 `list/page/queryParam/columns` |
295
+ | **主从联动** | 选中上表行 → 调用下表实例的 `selectByPlan(row)` 驱动查询 |
296
+ | **可拖拽分隔** | `<jh-drag-row :top-height="N">` 上下分隔,可嵌套 |
297
+ | **Tab 切换** | `<el-tabs type="border-card">` 或 `<jh-tabs>` 切换录入/查询视角 |
298
+ | **操作区** | 在上下表之间放置 `BaseForm` + 按钮,或 `BaseToolbar` |
299
+ | **懒加载** | Tab 切换时才加载对应数据,避免首次全量查询 |
300
+
301
+ ### D-1 判断何时使用配置驱动 vs 独立编排
302
+
303
+ | 条件 | 方式 |
304
+ |---|---|
305
+ | 3+ 页面布局完全相同,仅 API/工序代码/列不同 | 提取 `src/components/template/XxxTemplate/`,页面仅传 config |
306
+ | 页面布局有显著差异(不同 Tab 结构、不同表数量) | 独立页面,在 data.ts 中定义多个 `createXxxPage()` |
307
+
308
+ ### D-2 配置驱动模板组件结构(参考 FinishingAchievementTemplate)
309
+
310
+ ```
311
+ src/components/template/[TemplateName]/
312
+ ├── index.vue ← 模板组件(接收 config prop)
313
+ ├── data.ts ← createXxxPage() 工厂函数
314
+ ├── types.ts ← 配置类型定义(ApiConfig, ColumnsConfig, UiConfig 等)
315
+ ├── index.scss ← 模板样式
316
+ └── README.md ← 使用说明
317
+
318
+ src/views/.../[page-name]/
319
+ ├── index.vue ← <TemplateName :config="xxxConfig" />(极简)
320
+ └── data.ts ← export const xxxConfig: FinishingAchievementConfig = { ... }
321
+ ```
322
+
323
+ **types.ts 要点**:
324
+ ```typescript
325
+ export interface XxxTemplateConfig {
326
+ api: Record<string, string>; // 各表格 API 端点
327
+ processCode: string; // 工序标识,用于查询参数
328
+ query?: { plan?: { items: BaseQueryItemDesc<any>[]; defaultParams?: Record<string, any> } };
329
+ columns?: { planColumns: TableColumnDesc<any>[]; detailColumns: TableColumnDesc<any>[] };
330
+ ui?: Partial<UiConfig>; // 可选 UI 覆盖(Tab 标题、区域标题等)
331
+ }
332
+ ```
333
+
334
+ **页面 data.ts 要点**(仅配置,不写逻辑):
335
+ ```typescript
336
+ import type { XxxTemplateConfig } from "@/components/template/XxxTemplate/types";
337
+
338
+ export const xxxConfig: XxxTemplateConfig = {
339
+ api: { planList: "/mmwr/...", materialList: "/mmwr/..." },
340
+ processCode: "PW",
341
+ query: { plan: { items: [...], defaultParams: { firstProcess: "D", subBacklogCode: "PW" } } }
342
+ };
343
+ ```
344
+
345
+ ### D-3 独立编排页面结构(参考 mmwr-steel-stripping-operations)
346
+
347
+ **data.ts 要点**(多个 createPage 工厂函数):
348
+ ```typescript
349
+ // 上表
350
+ export function createEntryPage() {
351
+ return new (class extends AbstractPageQueryHook {
352
+ constructor() { super({ url: { list: API_CONFIG.planList }, page: { current: 1, size: 10 } }); }
353
+ queryDef() { return [...]; }
354
+ toolbarDef() { return []; }
355
+ columnsDef() { return [...]; }
356
+ })();
357
+ }
358
+
359
+ // 下表(主从联动)
360
+ export function createEntryBottomPage(rejectForm: any) {
361
+ const Page = new (class extends AbstractPageQueryHook {
362
+ constructor() { super({ url: { list: API_CONFIG.detailList } }); }
363
+ queryDef() { return []; }
364
+ toolbarDef() { return [...]; }
365
+ columnsDef() { return [...]; }
366
+ // 关键:由上表行驱动查询
367
+ async selectByPlan(planRow: any) {
368
+ this.queryParam.value.loNo = planRow.loNo;
369
+ this.queryParam.value.lotNo = planRow.lotNo;
370
+ await this.select();
371
+ }
372
+ })();
373
+ return Page;
374
+ }
375
+ ```
376
+
377
+ **index.vue 要点**:
378
+ ```vue
379
+ <template>
380
+ <div class="app-container app-page-container [page-class]">
381
+ <el-tabs v-model="activeTab" type="border-card">
382
+ <el-tab-pane label="录入" name="entry">
383
+ <jh-drag-row :top-height="420">
384
+ <template #top>
385
+ <BaseQuery :form="..." :items="..." @select="..." @reset="..." />
386
+ <BaseTable ref="..." :data="..." :columns="..." highlight-current-row @current-change="handleRowClick" />
387
+ <jh-pagination ... />
388
+ </template>
389
+ <template #bottom>
390
+ <BaseToolbar v-if="selectedRow" :items="..." />
391
+ <el-empty v-if="!selectedRow" description="请先在上方列表中选择一行数据" />
392
+ <BaseTable v-else ref="..." :data="..." :columns="..." />
393
+ <jh-pagination ... />
394
+ </template>
395
+ </jh-drag-row>
396
+ </el-tab-pane>
397
+ <el-tab-pane label="查询" name="query" lazy>
398
+ <!-- 标准 LIST 模式 -->
399
+ </el-tab-pane>
400
+ </el-tabs>
401
+ </div>
402
+ </template>
403
+
404
+ <script setup lang="ts">
405
+ import { createEntryPage, createEntryBottomPage, createQueryPage } from "./data";
406
+
407
+ const activeTab = ref("entry");
408
+ const selectedRow = ref(null);
409
+
410
+ const EntryPage = createEntryPage();
411
+ const { tableRef, page, queryParam, list, queryItems, columns, select } = EntryPage;
412
+
413
+ const BottomPage = createEntryBottomPage();
414
+ const { list: bottomList, columns: bottomColumns, select: bottomSelect, selectByPlan } = BottomPage;
415
+
416
+ const handleRowClick = (row: any) => {
417
+ selectedRow.value = row;
418
+ if (row) selectByPlan(row);
419
+ };
420
+
421
+ onMounted(() => select());
422
+ watch(activeTab, (tab) => { if (tab === "query") QueryPage.select(); });
423
+ </script>
424
+ ```
425
+
426
+ ### D-4 index.scss 要点
427
+
428
+ ```scss
429
+ .[page-class] {
430
+ .section-header { display: flex; align-items: center; gap: 6px; margin: 8px 0; }
431
+ .section-header .title-bar { width: 3px; height: 14px; background: var(--el-color-primary); border-radius: 1px; }
432
+ .section-header .section-title { font-size: 14px; font-weight: 600; margin: 0; }
433
+ .empty-tip { padding: 40px 0; }
434
+ .operation-area { padding: 8px 0; }
435
+ .operation-buttons { display: flex; gap: 8px; margin: 8px 0; }
436
+ .results-container { display: flex; gap: 16px; /* 左右分栏时 */ }
437
+ .results-container .section { flex: 1; min-width: 0; }
438
+ }
439
+ ```
440
+
441
+ ---