@agile-team/wl-skills-kit 2.3.4 → 2.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +13 -7
- package/files/.cursor/mcp.json +8 -8
- package/files/.github/guides/README.md +13 -13
- package/files/.github/guides/architecture.md +555 -555
- package/files/.github/guides/mcp-setup.md +109 -109
- package/files/.github/guides/usage.md +184 -184
- package/files/.github/reports/README.md +65 -65
- package/files/.github/reports/SYS_DICT_INFO.md +50 -50
- package/files/.github/reports/SYS_MENU_INFO.md +247 -247
- package/files/.github/reports/SYS_PERMISSION_INFO.md +20 -20
- 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
- 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
- package/files/.github/skills/_compat/README.md +108 -108
- package/files/.github/skills/_compat/headers/agents.txt +8 -8
- package/files/.github/skills/_compat/headers/claude-code.txt +7 -7
- package/files/.github/skills/_compat/headers/cline.txt +7 -7
- package/files/.github/skills/_compat/headers/cursor-mdc.txt +16 -16
- package/files/.github/skills/_compat/headers/cursor-rules.txt +7 -7
- package/files/.github/skills/_compat/headers/github-copilot.txt +1 -1
- package/files/.github/skills/_compat/headers/kiro.txt +10 -10
- package/files/.github/skills/_compat/headers/qoder.txt +8 -8
- package/files/.github/skills/_compat/headers/trae.txt +11 -11
- package/files/.github/skills/_compat/headers/windsurf.txt +7 -7
- package/files/.github/skills/_registry.md +81 -81
- package/files/.github/skills/core/api-contract/SKILL.md +344 -344
- package/files/.github/skills/core/api-contract/USAGE.md +110 -110
- package/files/.github/skills/core/convention-audit/SKILL.md +189 -189
- package/files/.github/skills/core/convention-audit/USAGE.md +99 -99
- package/files/.github/skills/core/page-codegen/SKILL.md +973 -973
- package/files/.github/skills/core/page-codegen/USAGE.md +102 -102
- package/files/.github/skills/core/page-codegen/templates/_index.md +46 -46
- package/files/.github/skills/core/page-codegen/templates/domains/_CONTRIBUTING.md +107 -107
- package/files/.github/skills/core/page-codegen/templates/domains/produce/TPL-OPERATION-STATION.md +442 -442
- package/files/.github/skills/core/page-codegen/templates/domains/sale/README.md +26 -26
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-CHANGE-HISTORY.md +276 -276
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-DETAIL-TABS.md +1145 -1145
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-DRIVEN.md +309 -309
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-FORM-ROUTE.md +436 -436
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-LIST.md +191 -191
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-MASTER-DETAIL.md +148 -148
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-RECORD-FORM.md +376 -376
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-TREE-LIST.md +186 -186
- package/files/.github/skills/core/prototype-scan/SKILL.md +498 -498
- package/files/.github/skills/core/prototype-scan/USAGE.md +95 -95
- package/files/.github/skills/core/template-extract/SKILL.md +139 -139
- package/files/.github/skills/core/template-extract/USAGE.md +93 -93
- package/files/.github/skills/domain/README.md +51 -51
- package/files/.github/skills/sync/menu-sync/SKILL.md +263 -263
- package/files/.github/skills/sync/menu-sync/USAGE.md +104 -104
- package/files/.github/skills/sync/menu-sync/env/env.local.json +7 -7
- package/files/.github/skills/sync/menu-sync/env/guide.md +99 -99
- package/files/.github/skills/sync/permission-sync/SKILL.draft.md +91 -91
- package/files/.github/standards/01-toolchain.md +57 -57
- package/files/.github/standards/02-code-structure.md +111 -111
- package/files/.github/standards/03-comments.md +53 -53
- package/files/.github/standards/04-coding-basics.md +33 -33
- package/files/.github/standards/05-logging.md +38 -38
- package/files/.github/standards/06-security.md +44 -44
- package/files/.github/standards/07-config.md +52 -52
- package/files/.github/standards/08-git.md +60 -60
- package/files/.github/standards/09-typescript.md +71 -71
- package/files/.github/standards/10-pinia.md +57 -57
- package/files/.github/standards/11-form-validation.md +81 -81
- package/files/.github/standards/12-base-table.md +153 -153
- package/files/.github/standards/13-platform-components.md +123 -123
- package/files/.github/standards/index.md +89 -89
- package/files/.kiro/settings/mcp.json +8 -8
- package/files/.mcp.json +8 -8
- package/files/.vscode/mcp.json +9 -9
- package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/data.ts +196 -196
- package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/index.scss +150 -150
- package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/index.vue +79 -79
- package/files/docs/jh-date-range.md +257 -257
- package/files/docs/jh-date.md +222 -222
- package/files/docs/jh-dept-picker.md +190 -190
- package/files/docs/jh-drag-row.md +590 -590
- package/files/docs/jh-file-upload.md +216 -216
- package/files/docs/jh-picker.md +218 -218
- package/files/docs/jh-select.md +148 -148
- package/files/docs/jh-text.md +248 -248
- package/files/docs/jh-user-picker.md +197 -197
- package/files/src/components/global/C_RightToolbar/data.ts +228 -228
- package/files/src/components/global/C_RightToolbar/index.scss +44 -44
- package/files/src/components/global/C_Splitter/index.scss +61 -61
- package/files/src/components/global/C_SvgIcon/index.scss +15 -15
- package/files/src/components/global/C_TagStatus/index.scss +20 -20
- package/files/src/components/global/C_Tree/data.ts +61 -61
- package/files/src/components/local/c_listModal/index.scss +4 -4
- package/package.json +1 -1
|
@@ -2,440 +2,440 @@
|
|
|
2
2
|
|
|
3
3
|
> 见 SKILL.md 主文件(约束 + 按钮规则 + Mock 规范等共用规则)。
|
|
4
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
|
-
|
|
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
441
|
---
|