@agile-team/wl-skills-kit 2.11.1 → 2.11.2

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 (85) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +1 -1
  3. package/bin/wl-skills.js +27 -3
  4. package/files/.wl-skills/docs/jh-pagination.md +505 -505
  5. package/files/.wl-skills/docs/request.md +940 -940
  6. package/files/.wl-skills/guides/architecture.md +1 -1
  7. package/files/.wl-skills/skills/core/convention-audit/SKILL.md +3 -3
  8. package/files/.wl-skills/skills/core/spec-doc-parse/SKILL.md +332 -332
  9. package/files/.wl-skills/skills/core/spec-doc-parse/USAGE.md +97 -97
  10. package/files/.wl-skills/skills/sync/permission-sync/USAGE.md +107 -107
  11. package/files/.wl-skills/src/components/global/C_ParentView/index.vue +3 -3
  12. package/files/.wl-skills/src/components/global/C_RightToolbar/index.vue +157 -157
  13. package/files/.wl-skills/src/components/global/C_SvgIcon/index.vue +31 -31
  14. package/files/.wl-skills/src/components/global/C_SvgIcon/svgicon.js +10 -10
  15. package/files/.wl-skills/src/components/global/C_TagStatus/README.md +264 -264
  16. package/files/.wl-skills/src/components/global/C_TagStatus/config.ts +192 -192
  17. package/files/.wl-skills/src/components/global/C_TagStatus/index.vue +106 -106
  18. package/files/.wl-skills/src/components/global/C_TagStatus/types.ts +64 -64
  19. package/files/.wl-skills/src/components/global/C_Tree/README.md +153 -153
  20. package/files/.wl-skills/src/components/global/C_Tree/index.scss +42 -42
  21. package/files/.wl-skills/src/components/global/C_Tree/index.vue +78 -78
  22. package/files/.wl-skills/src/components/global/C_Tree/types.ts +59 -59
  23. package/files/.wl-skills/src/components/local/c_formModal/README.md +235 -235
  24. package/files/.wl-skills/src/components/local/c_formModal/data.ts +95 -95
  25. package/files/.wl-skills/src/components/local/c_formModal/index.scss +8 -8
  26. package/files/.wl-skills/src/components/local/c_formModal/index.vue +107 -107
  27. package/files/.wl-skills/src/components/local/c_formSections/data.ts +175 -175
  28. package/files/.wl-skills/src/components/local/c_formSections/index.scss +280 -280
  29. package/files/.wl-skills/src/components/local/c_formSections/index.vue +429 -429
  30. package/files/.wl-skills/src/components/local/c_listModal/data.ts +41 -41
  31. package/files/.wl-skills/src/components/local/c_listModal/index.vue +136 -136
  32. package/files/.wl-skills/src/components/local/c_spliterTitle/index.scss +25 -25
  33. package/files/.wl-skills/src/components/local/c_spliterTitle/index.vue +21 -21
  34. package/files/.wl-skills/src/components/remote/AGGrid/README.md +530 -530
  35. package/files/.wl-skills/src/components/remote/BaseForm/README.md +508 -508
  36. package/files/.wl-skills/src/components/remote/BaseQuery/README.md +865 -865
  37. package/files/.wl-skills/src/components/remote/BaseTable/README.md +941 -941
  38. package/files/.wl-skills/src/components/remote/BaseToolbar/README.md +496 -496
  39. package/files/.wl-skills/src/types/page.ts +24 -24
  40. package/files/.wl-skills/standards/04-coding-basics.md +39 -1
  41. package/files/.wl-skills/standards/09-typescript.md +26 -3
  42. package/files/.wl-skills/standards/index.md +2 -2
  43. package/files/.wl-skills/templates/README.md +44 -44
  44. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/api.md +54 -54
  45. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/data.ts +346 -346
  46. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/index.scss +1 -1
  47. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/index.vue +28 -28
  48. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add-form/data.ts +115 -115
  49. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add-form/index.scss +44 -44
  50. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add-form/index.vue +43 -43
  51. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change/data.ts +338 -338
  52. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change/index.scss +1 -1
  53. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change/index.vue +28 -28
  54. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change-form/data.ts +115 -115
  55. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change-form/index.scss +44 -44
  56. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change-form/index.vue +43 -43
  57. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/api.md +88 -88
  58. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/data.ts +601 -601
  59. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/index.scss +1 -1
  60. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/index.vue +64 -64
  61. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/api.md +67 -67
  62. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/data.ts +286 -286
  63. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/index.scss +139 -139
  64. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/index.vue +318 -318
  65. package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/api.md +98 -98
  66. package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/data.ts +543 -543
  67. package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/index.scss +1 -1
  68. package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/index.vue +52 -52
  69. package/files/.wl-skills/templates/sale/demo/add-demo/data.ts +518 -518
  70. package/files/.wl-skills/templates/sale/demo/billet-flame-cut-plan/data.ts +524 -524
  71. package/files/.wl-skills/templates/sale/demo/billet-flame-cut-plan/index.scss +154 -154
  72. package/files/.wl-skills/templates/sale/demo/billet-flame-cut-plan/index.vue +117 -117
  73. package/files/.wl-skills/templates/sale/demo/domestic-trade-order/data.ts +308 -308
  74. package/files/.wl-skills/templates/sale/demo/domestic-trade-order/index.scss +99 -99
  75. package/files/.wl-skills/templates/sale/demo/domestic-trade-order/index.vue +77 -77
  76. package/files/.wl-skills/templates/sale/demo/heat-batch-return/data.ts +367 -367
  77. package/files/.wl-skills/templates/sale/demo/heat-batch-return/index.scss +100 -100
  78. package/files/.wl-skills/templates/sale/demo/heat-batch-return/index.vue +170 -170
  79. package/files/.wl-skills/templates/sale/demo/heat-batch-return/meltDialog.vue +320 -320
  80. package/files/.wl-skills/templates/sale/demo/metallurgical-spec/data.ts +824 -824
  81. package/lib/ast-rules.js +304 -9
  82. package/mcp/config.js +46 -46
  83. package/mcp/registry.js +6 -1
  84. package/mcp/tools/projectTools.js +9 -1
  85. package/package.json +2 -2
@@ -1,429 +1,429 @@
1
- <!--
2
- * @Author: ChenYu ycyplus@gmail.com
3
- * @Date: 2026-01-01 13:30:00
4
- * @LastEditors: ChenYu ycyplus@gmail.com
5
- * @LastEditTime: 2026-01-01 16:30:00
6
- * @FilePath: \cx-ui-sale\src\components\local\c_formSections\index.vue
7
- * @Description: 表单区块组件 - 集成楼层导航、工具栏、布局切换等功能
8
- * Copyright (c) 2026 by CHENY, All Rights Reserved 😎.
9
- -->
10
- <template>
11
- <div class="c-form-sections-wrapper">
12
- <!-- 顶部标题栏 -->
13
- <div v-if="showHeader" class="c-form-header">
14
- <span class="header-title">{{ headerTitle }}</span>
15
- <div class="header-right">
16
- <!-- 布局切换器 -->
17
- <div v-if="showLayoutSwitch" class="layout-switcher">
18
- <span
19
- v-for="option in internalLayoutOptions"
20
- :key="option.columns"
21
- class="layout-icon"
22
- :class="{ active: currentLayout === option.columns }"
23
- :title="option.title"
24
- @click="currentLayout = option.columns"
25
- >
26
- {{ option.icon }}
27
- </span>
28
- </div>
29
- <el-divider
30
- v-if="showLayoutSwitch && headerActions.length > 0"
31
- direction="vertical"
32
- />
33
- <!-- 顶部操作按钮 -->
34
- <template v-for="action in headerActions" :key="action.label">
35
- <el-button
36
- :type="action.type || ''"
37
- size="small"
38
- :disabled="action.disabled"
39
- @click="action.onClick"
40
- >
41
- <el-icon v-if="action.icon">
42
- <component :is="action.icon" />
43
- </el-icon>
44
- {{ action.label }}
45
- </el-button>
46
- </template>
47
- </div>
48
- </div>
49
-
50
- <!-- 工具栏 -->
51
- <div v-if="showToolbar" class="c-form-toolbar">
52
- <div class="toolbar-left">
53
- <!-- 必填字段过滤开关 -->
54
- <span v-if="showRequiredFilter" class="required-toggle">
55
- <span class="label">仅显示必填字段</span>
56
- <el-switch v-model="showRequiredOnly" size="small" />
57
- </span>
58
- <!-- 工具栏左侧插槽 -->
59
- <slot name="toolbar-left"></slot>
60
- </div>
61
- <div class="toolbar-right">
62
- <!-- 工具栏右侧插槽 -->
63
- <slot name="toolbar-right"></slot>
64
- </div>
65
- </div>
66
-
67
- <!-- 主体内容区域 -->
68
- <div class="c-form-content">
69
- <!-- 左侧楼层导航 -->
70
- <el-tabs
71
- v-if="showNavTabs"
72
- :tab-position="navTabsPosition"
73
- v-model="activeNavTab"
74
- @tab-click="handleNavTabClick"
75
- class="nav-tabs"
76
- >
77
- <el-tab-pane
78
- v-for="tab in navTabs"
79
- :key="tab.name"
80
- :label="tab.label"
81
- :name="tab.name"
82
- />
83
- </el-tabs>
84
-
85
- <!-- 表单区域 -->
86
- <div class="form-container" ref="formContainerRef">
87
- <el-form
88
- :label-width="labelWidth"
89
- :label-position="labelPosition"
90
- :model="form"
91
- :rules="rules"
92
- >
93
- <el-collapse v-model="activeNamesModel">
94
- <!-- 循环渲染折叠面板 -->
95
- <el-collapse-item
96
- v-for="section in visibleSections"
97
- :key="section.name"
98
- :name="section.name"
99
- :id="section.id"
100
- >
101
- <template #title>
102
- <span class="section-title">{{ section.title }}</span>
103
- <el-divider />
104
- </template>
105
-
106
- <!-- 特殊区块 - 使用具名插槽 -->
107
- <slot
108
- v-if="section.isSpecial"
109
- :name="`special-${section.name}`"
110
- :section="section"
111
- >
112
- <el-row :gutter="gutter">
113
- <el-col :span="24">
114
- <el-empty description="请通过插槽自定义特殊区块内容" />
115
- </el-col>
116
- </el-row>
117
- </slot>
118
-
119
- <!-- 普通区块 - 循环渲染字段 -->
120
- <el-row v-else :gutter="gutter">
121
- <el-col
122
- v-for="field in section.fieldsConfig"
123
- :key="field.prop"
124
- :span="currentFieldSpan"
125
- >
126
- <el-form-item
127
- :label="field.label"
128
- :prop="field.prop"
129
- :required="field.required"
130
- >
131
- <!-- 下拉选择框 -->
132
- <el-select
133
- v-if="field.type === 'select'"
134
- v-model="form[field.prop]"
135
- :placeholder="field.placeholder || '请选择'"
136
- :clearable="field.clearable !== false"
137
- >
138
- <el-option
139
- v-for="opt in field.options"
140
- :key="opt.value"
141
- :label="opt.label"
142
- :value="opt.value"
143
- />
144
- </el-select>
145
-
146
- <!-- 多行文本框 -->
147
- <el-input
148
- v-else-if="field.type === 'textarea'"
149
- v-model="form[field.prop]"
150
- type="textarea"
151
- :rows="field.rows || 3"
152
- :placeholder="field.placeholder || '请输入'"
153
- />
154
-
155
- <!-- 日期选择器 -->
156
- <el-date-picker
157
- v-else-if="field.type === 'date'"
158
- v-model="form[field.prop]"
159
- type="date"
160
- :placeholder="field.placeholder || '选择日期'"
161
- style="width: 100%"
162
- />
163
-
164
- <!-- 日期时间选择器 -->
165
- <el-date-picker
166
- v-else-if="field.type === 'datetime'"
167
- v-model="form[field.prop]"
168
- type="datetime"
169
- :placeholder="field.placeholder || '选择日期时间'"
170
- style="width: 100%"
171
- />
172
-
173
- <!-- 数字输入框 -->
174
- <el-input-number
175
- v-else-if="field.type === 'number'"
176
- v-model="form[field.prop]"
177
- :min="field.min"
178
- :max="field.max"
179
- :precision="field.precision"
180
- :step="field.step || 1"
181
- style="width: 100%"
182
- />
183
-
184
- <!-- 自定义字段 - 使用作用域插槽 -->
185
- <slot
186
- v-else-if="field.type === 'custom'"
187
- :name="`field-${field.prop}`"
188
- :field="field"
189
- :form="form"
190
- >
191
- <el-input
192
- v-model="form[field.prop]"
193
- :placeholder="field.placeholder || '请输入'"
194
- />
195
- </slot>
196
-
197
- <!-- 默认单行文本输入框 -->
198
- <el-input
199
- v-else
200
- v-model="form[field.prop]"
201
- :placeholder="field.placeholder || '请输入'"
202
- />
203
- </el-form-item>
204
- </el-col>
205
- </el-row>
206
- </el-collapse-item>
207
- </el-collapse>
208
- </el-form>
209
- </div>
210
- </div>
211
- </div>
212
- </template>
213
-
214
- <script setup lang="ts">
215
- import { ref, computed } from "vue";
216
- import type {
217
- SectionConfig,
218
- FormDataType,
219
- NavTabConfig,
220
- HeaderAction,
221
- LayoutOption
222
- } from "./data";
223
-
224
- /** 组件 Props */
225
- interface Props {
226
- // ===== 基础配置 =====
227
- /** 表单区块配置数组 */
228
- sections: SectionConfig[];
229
- /** 表单数据对象 */
230
- form: FormDataType;
231
- /** 激活的折叠面板 name 数组 */
232
- activeNames?: string[];
233
- /** 表单验证规则 */
234
- rules?: any;
235
- /** 表单标签宽度 */
236
- labelWidth?: string;
237
- /** 表单标签位置 */
238
- labelPosition?: "left" | "right" | "top";
239
- /** 栅格间距 */
240
- gutter?: number;
241
- /** 每个字段占据的栅格数(外部手动控制时使用,会覆盖内部布局切换) */
242
- fieldSpan?: number;
243
-
244
- // ===== 顶部标题栏配置 =====
245
- /** 是否显示顶部标题栏 */
246
- showHeader?: boolean;
247
- /** 标题栏标题文本 */
248
- headerTitle?: string;
249
- /** 顶部操作按钮 */
250
- headerActions?: HeaderAction[];
251
-
252
- // ===== 工具栏配置 =====
253
- /** 是否显示工具栏 */
254
- showToolbar?: boolean;
255
- /** 是否显示必填字段过滤开关 */
256
- showRequiredFilter?: boolean;
257
-
258
- // ===== 布局切换配置 =====
259
- /** 是否显示布局切换器 */
260
- showLayoutSwitch?: boolean;
261
- /** 默认布局列数 */
262
- defaultLayout?: 2 | 3 | 4 | 5;
263
- /** 可选布局列数 */
264
- layoutOptions?: Array<2 | 3 | 4 | 5>;
265
-
266
- // ===== 楼层导航配置 =====
267
- /** 是否显示楼层导航 */
268
- showNavTabs?: boolean;
269
- /** 楼层导航位置 */
270
- navTabsPosition?: "left" | "right";
271
- /** 楼层导航配置(不传则自动从 sections 生成) */
272
- navTabs?: NavTabConfig[];
273
- }
274
-
275
- const props = withDefaults(defineProps<Props>(), {
276
- activeNames: () => [],
277
- labelWidth: "100px",
278
- labelPosition: "right",
279
- gutter: 20,
280
- fieldSpan: undefined,
281
-
282
- showHeader: false,
283
- headerTitle: "主档维护",
284
- headerActions: () => [],
285
-
286
- showToolbar: false,
287
- showRequiredFilter: false,
288
-
289
- showLayoutSwitch: false,
290
- defaultLayout: 5,
291
- layoutOptions: () => [2, 3, 4, 5],
292
-
293
- showNavTabs: false,
294
- navTabsPosition: "left",
295
- navTabs: () => []
296
- });
297
-
298
- /** 组件 Emits */
299
- interface Emits {
300
- (e: "update:activeNames", value: string[]): void;
301
- }
302
-
303
- const emit = defineEmits<Emits>();
304
-
305
- // ===== 内部状态 =====
306
- /** 仅显示必填字段开关 */
307
- const showRequiredOnly = ref(false);
308
-
309
- /** 当前布局列数 */
310
- const currentLayout = ref<2 | 3 | 4 | 5>(props.defaultLayout);
311
-
312
- /** 当前激活的导航标签 */
313
- const activeNavTab = ref("");
314
-
315
- /** 表单容器引用 */
316
- const formContainerRef = ref<HTMLElement | null>(null);
317
-
318
- // ===== 计算属性 =====
319
- /** 激活的折叠面板(v-model) */
320
- const activeNamesModel = computed({
321
- get: () => props.activeNames,
322
- set: (val) => emit("update:activeNames", val)
323
- });
324
-
325
- /**
326
- * 🆕 内部布局选项配置
327
- */
328
- const internalLayoutOptions = computed<LayoutOption[]>(() => {
329
- return props.layoutOptions.map((col) => ({
330
- columns: col,
331
- icon: "|".repeat(col),
332
- title: `${col}列布局`
333
- }));
334
- });
335
-
336
- /**
337
- * 🆕 当前字段 span 值
338
- * 优先使用外部传入的 fieldSpan,否则根据内部布局计算
339
- */
340
- const currentFieldSpan = computed(() => {
341
- if (props.fieldSpan !== undefined) {
342
- return props.fieldSpan;
343
- }
344
- return Math.floor(24 / currentLayout.value);
345
- });
346
-
347
- /**
348
- * 🆕 过滤后的区块(根据必填字段过滤)
349
- */
350
- const filteredSections = computed(() => {
351
- if (!showRequiredOnly.value) {
352
- return props.sections;
353
- }
354
-
355
- return props.sections.map((section) => {
356
- if (section.isSpecial) {
357
- return section;
358
- }
359
- return {
360
- ...section,
361
- fieldsConfig: section.fieldsConfig.filter((field) => field.required)
362
- };
363
- });
364
- });
365
-
366
- /**
367
- * 过滤后的可见区块
368
- * 根据 section.visible 函数和字段过滤结果判断是否显示
369
- */
370
- const visibleSections = computed(() => {
371
- return filteredSections.value.filter((section) => {
372
- // 特殊区块:必填模式下隐藏
373
- if (section.isSpecial) {
374
- return !showRequiredOnly.value;
375
- }
376
- // 普通区块:有字段才显示
377
- const hasFields = section.fieldsConfig.length > 0;
378
- const visibleByFunc = section.visible ? section.visible() : true;
379
- return hasFields && visibleByFunc;
380
- });
381
- });
382
-
383
- /**
384
- * 🆕 内部导航标签配置
385
- * 如果外部传入则使用外部的,否则自动从 sections 生成
386
- */
387
- const internalNavTabs = computed<NavTabConfig[]>(() => {
388
- if (props.navTabs && props.navTabs.length > 0) {
389
- return props.navTabs;
390
- }
391
- // 自动从 sections 生成
392
- return props.sections.map((section) => ({
393
- name: section.name,
394
- label: section.title,
395
- sectionName: section.name
396
- }));
397
- });
398
-
399
- // ===== 方法 =====
400
- /** 处理楼层导航点击 */
401
- const handleNavTabClick = (tab: any) => {
402
- const tabName = tab.paneName || tab.props?.name;
403
- const tabConfig = internalNavTabs.value.find((t) => t.name === tabName);
404
-
405
- if (!tabConfig?.sectionName) return;
406
-
407
- // 展开对应的 collapse section
408
- if (!activeNamesModel.value.includes(tabConfig.sectionName)) {
409
- activeNamesModel.value = [...activeNamesModel.value, tabConfig.sectionName];
410
- }
411
-
412
- // 滚动到对应区域
413
- setTimeout(() => {
414
- const section = props.sections.find(
415
- (s) => s.name === tabConfig.sectionName
416
- );
417
- if (section?.id) {
418
- const sectionEl = document.getElementById(section.id);
419
- if (sectionEl) {
420
- sectionEl.scrollIntoView({ behavior: "smooth", block: "start" });
421
- }
422
- }
423
- }, 150);
424
- };
425
- </script>
426
-
427
- <style scoped lang="scss">
428
- @import "./index.scss";
429
- </style>
1
+ <!--
2
+ * @Author: ChenYu ycyplus@gmail.com
3
+ * @Date: 2026-01-01 13:30:00
4
+ * @LastEditors: ChenYu ycyplus@gmail.com
5
+ * @LastEditTime: 2026-01-01 16:30:00
6
+ * @FilePath: \cx-ui-sale\src\components\local\c_formSections\index.vue
7
+ * @Description: 表单区块组件 - 集成楼层导航、工具栏、布局切换等功能
8
+ * Copyright (c) 2026 by CHENY, All Rights Reserved 😎.
9
+ -->
10
+ <template>
11
+ <div class="c-form-sections-wrapper">
12
+ <!-- 顶部标题栏 -->
13
+ <div v-if="showHeader" class="c-form-header">
14
+ <span class="header-title">{{ headerTitle }}</span>
15
+ <div class="header-right">
16
+ <!-- 布局切换器 -->
17
+ <div v-if="showLayoutSwitch" class="layout-switcher">
18
+ <span
19
+ v-for="option in internalLayoutOptions"
20
+ :key="option.columns"
21
+ class="layout-icon"
22
+ :class="{ active: currentLayout === option.columns }"
23
+ :title="option.title"
24
+ @click="currentLayout = option.columns"
25
+ >
26
+ {{ option.icon }}
27
+ </span>
28
+ </div>
29
+ <el-divider
30
+ v-if="showLayoutSwitch && headerActions.length > 0"
31
+ direction="vertical"
32
+ />
33
+ <!-- 顶部操作按钮 -->
34
+ <template v-for="action in headerActions" :key="action.label">
35
+ <el-button
36
+ :type="action.type || ''"
37
+ size="small"
38
+ :disabled="action.disabled"
39
+ @click="action.onClick"
40
+ >
41
+ <el-icon v-if="action.icon">
42
+ <component :is="action.icon" />
43
+ </el-icon>
44
+ {{ action.label }}
45
+ </el-button>
46
+ </template>
47
+ </div>
48
+ </div>
49
+
50
+ <!-- 工具栏 -->
51
+ <div v-if="showToolbar" class="c-form-toolbar">
52
+ <div class="toolbar-left">
53
+ <!-- 必填字段过滤开关 -->
54
+ <span v-if="showRequiredFilter" class="required-toggle">
55
+ <span class="label">仅显示必填字段</span>
56
+ <el-switch v-model="showRequiredOnly" size="small" />
57
+ </span>
58
+ <!-- 工具栏左侧插槽 -->
59
+ <slot name="toolbar-left"></slot>
60
+ </div>
61
+ <div class="toolbar-right">
62
+ <!-- 工具栏右侧插槽 -->
63
+ <slot name="toolbar-right"></slot>
64
+ </div>
65
+ </div>
66
+
67
+ <!-- 主体内容区域 -->
68
+ <div class="c-form-content">
69
+ <!-- 左侧楼层导航 -->
70
+ <el-tabs
71
+ v-if="showNavTabs"
72
+ :tab-position="navTabsPosition"
73
+ v-model="activeNavTab"
74
+ @tab-click="handleNavTabClick"
75
+ class="nav-tabs"
76
+ >
77
+ <el-tab-pane
78
+ v-for="tab in navTabs"
79
+ :key="tab.name"
80
+ :label="tab.label"
81
+ :name="tab.name"
82
+ />
83
+ </el-tabs>
84
+
85
+ <!-- 表单区域 -->
86
+ <div class="form-container" ref="formContainerRef">
87
+ <el-form
88
+ :label-width="labelWidth"
89
+ :label-position="labelPosition"
90
+ :model="form"
91
+ :rules="rules"
92
+ >
93
+ <el-collapse v-model="activeNamesModel">
94
+ <!-- 循环渲染折叠面板 -->
95
+ <el-collapse-item
96
+ v-for="section in visibleSections"
97
+ :key="section.name"
98
+ :name="section.name"
99
+ :id="section.id"
100
+ >
101
+ <template #title>
102
+ <span class="section-title">{{ section.title }}</span>
103
+ <el-divider />
104
+ </template>
105
+
106
+ <!-- 特殊区块 - 使用具名插槽 -->
107
+ <slot
108
+ v-if="section.isSpecial"
109
+ :name="`special-${section.name}`"
110
+ :section="section"
111
+ >
112
+ <el-row :gutter="gutter">
113
+ <el-col :span="24">
114
+ <el-empty description="请通过插槽自定义特殊区块内容" />
115
+ </el-col>
116
+ </el-row>
117
+ </slot>
118
+
119
+ <!-- 普通区块 - 循环渲染字段 -->
120
+ <el-row v-else :gutter="gutter">
121
+ <el-col
122
+ v-for="field in section.fieldsConfig"
123
+ :key="field.prop"
124
+ :span="currentFieldSpan"
125
+ >
126
+ <el-form-item
127
+ :label="field.label"
128
+ :prop="field.prop"
129
+ :required="field.required"
130
+ >
131
+ <!-- 下拉选择框 -->
132
+ <el-select
133
+ v-if="field.type === 'select'"
134
+ v-model="form[field.prop]"
135
+ :placeholder="field.placeholder || '请选择'"
136
+ :clearable="field.clearable !== false"
137
+ >
138
+ <el-option
139
+ v-for="opt in field.options"
140
+ :key="opt.value"
141
+ :label="opt.label"
142
+ :value="opt.value"
143
+ />
144
+ </el-select>
145
+
146
+ <!-- 多行文本框 -->
147
+ <el-input
148
+ v-else-if="field.type === 'textarea'"
149
+ v-model="form[field.prop]"
150
+ type="textarea"
151
+ :rows="field.rows || 3"
152
+ :placeholder="field.placeholder || '请输入'"
153
+ />
154
+
155
+ <!-- 日期选择器 -->
156
+ <el-date-picker
157
+ v-else-if="field.type === 'date'"
158
+ v-model="form[field.prop]"
159
+ type="date"
160
+ :placeholder="field.placeholder || '选择日期'"
161
+ style="width: 100%"
162
+ />
163
+
164
+ <!-- 日期时间选择器 -->
165
+ <el-date-picker
166
+ v-else-if="field.type === 'datetime'"
167
+ v-model="form[field.prop]"
168
+ type="datetime"
169
+ :placeholder="field.placeholder || '选择日期时间'"
170
+ style="width: 100%"
171
+ />
172
+
173
+ <!-- 数字输入框 -->
174
+ <el-input-number
175
+ v-else-if="field.type === 'number'"
176
+ v-model="form[field.prop]"
177
+ :min="field.min"
178
+ :max="field.max"
179
+ :precision="field.precision"
180
+ :step="field.step || 1"
181
+ style="width: 100%"
182
+ />
183
+
184
+ <!-- 自定义字段 - 使用作用域插槽 -->
185
+ <slot
186
+ v-else-if="field.type === 'custom'"
187
+ :name="`field-${field.prop}`"
188
+ :field="field"
189
+ :form="form"
190
+ >
191
+ <el-input
192
+ v-model="form[field.prop]"
193
+ :placeholder="field.placeholder || '请输入'"
194
+ />
195
+ </slot>
196
+
197
+ <!-- 默认单行文本输入框 -->
198
+ <el-input
199
+ v-else
200
+ v-model="form[field.prop]"
201
+ :placeholder="field.placeholder || '请输入'"
202
+ />
203
+ </el-form-item>
204
+ </el-col>
205
+ </el-row>
206
+ </el-collapse-item>
207
+ </el-collapse>
208
+ </el-form>
209
+ </div>
210
+ </div>
211
+ </div>
212
+ </template>
213
+
214
+ <script setup lang="ts">
215
+ import { ref, computed } from "vue";
216
+ import type {
217
+ SectionConfig,
218
+ FormDataType,
219
+ NavTabConfig,
220
+ HeaderAction,
221
+ LayoutOption
222
+ } from "./data";
223
+
224
+ /** 组件 Props */
225
+ interface Props {
226
+ // ===== 基础配置 =====
227
+ /** 表单区块配置数组 */
228
+ sections: SectionConfig[];
229
+ /** 表单数据对象 */
230
+ form: FormDataType;
231
+ /** 激活的折叠面板 name 数组 */
232
+ activeNames?: string[];
233
+ /** 表单验证规则 */
234
+ rules?: any;
235
+ /** 表单标签宽度 */
236
+ labelWidth?: string;
237
+ /** 表单标签位置 */
238
+ labelPosition?: "left" | "right" | "top";
239
+ /** 栅格间距 */
240
+ gutter?: number;
241
+ /** 每个字段占据的栅格数(外部手动控制时使用,会覆盖内部布局切换) */
242
+ fieldSpan?: number;
243
+
244
+ // ===== 顶部标题栏配置 =====
245
+ /** 是否显示顶部标题栏 */
246
+ showHeader?: boolean;
247
+ /** 标题栏标题文本 */
248
+ headerTitle?: string;
249
+ /** 顶部操作按钮 */
250
+ headerActions?: HeaderAction[];
251
+
252
+ // ===== 工具栏配置 =====
253
+ /** 是否显示工具栏 */
254
+ showToolbar?: boolean;
255
+ /** 是否显示必填字段过滤开关 */
256
+ showRequiredFilter?: boolean;
257
+
258
+ // ===== 布局切换配置 =====
259
+ /** 是否显示布局切换器 */
260
+ showLayoutSwitch?: boolean;
261
+ /** 默认布局列数 */
262
+ defaultLayout?: 2 | 3 | 4 | 5;
263
+ /** 可选布局列数 */
264
+ layoutOptions?: Array<2 | 3 | 4 | 5>;
265
+
266
+ // ===== 楼层导航配置 =====
267
+ /** 是否显示楼层导航 */
268
+ showNavTabs?: boolean;
269
+ /** 楼层导航位置 */
270
+ navTabsPosition?: "left" | "right";
271
+ /** 楼层导航配置(不传则自动从 sections 生成) */
272
+ navTabs?: NavTabConfig[];
273
+ }
274
+
275
+ const props = withDefaults(defineProps<Props>(), {
276
+ activeNames: () => [],
277
+ labelWidth: "100px",
278
+ labelPosition: "right",
279
+ gutter: 20,
280
+ fieldSpan: undefined,
281
+
282
+ showHeader: false,
283
+ headerTitle: "主档维护",
284
+ headerActions: () => [],
285
+
286
+ showToolbar: false,
287
+ showRequiredFilter: false,
288
+
289
+ showLayoutSwitch: false,
290
+ defaultLayout: 5,
291
+ layoutOptions: () => [2, 3, 4, 5],
292
+
293
+ showNavTabs: false,
294
+ navTabsPosition: "left",
295
+ navTabs: () => []
296
+ });
297
+
298
+ /** 组件 Emits */
299
+ interface Emits {
300
+ (e: "update:activeNames", value: string[]): void;
301
+ }
302
+
303
+ const emit = defineEmits<Emits>();
304
+
305
+ // ===== 内部状态 =====
306
+ /** 仅显示必填字段开关 */
307
+ const showRequiredOnly = ref(false);
308
+
309
+ /** 当前布局列数 */
310
+ const currentLayout = ref<2 | 3 | 4 | 5>(props.defaultLayout);
311
+
312
+ /** 当前激活的导航标签 */
313
+ const activeNavTab = ref("");
314
+
315
+ /** 表单容器引用 */
316
+ const formContainerRef = ref<HTMLElement | null>(null);
317
+
318
+ // ===== 计算属性 =====
319
+ /** 激活的折叠面板(v-model) */
320
+ const activeNamesModel = computed({
321
+ get: () => props.activeNames,
322
+ set: (val) => emit("update:activeNames", val)
323
+ });
324
+
325
+ /**
326
+ * 🆕 内部布局选项配置
327
+ */
328
+ const internalLayoutOptions = computed<LayoutOption[]>(() => {
329
+ return props.layoutOptions.map((col) => ({
330
+ columns: col,
331
+ icon: "|".repeat(col),
332
+ title: `${col}列布局`
333
+ }));
334
+ });
335
+
336
+ /**
337
+ * 🆕 当前字段 span 值
338
+ * 优先使用外部传入的 fieldSpan,否则根据内部布局计算
339
+ */
340
+ const currentFieldSpan = computed(() => {
341
+ if (props.fieldSpan !== undefined) {
342
+ return props.fieldSpan;
343
+ }
344
+ return Math.floor(24 / currentLayout.value);
345
+ });
346
+
347
+ /**
348
+ * 🆕 过滤后的区块(根据必填字段过滤)
349
+ */
350
+ const filteredSections = computed(() => {
351
+ if (!showRequiredOnly.value) {
352
+ return props.sections;
353
+ }
354
+
355
+ return props.sections.map((section) => {
356
+ if (section.isSpecial) {
357
+ return section;
358
+ }
359
+ return {
360
+ ...section,
361
+ fieldsConfig: section.fieldsConfig.filter((field) => field.required)
362
+ };
363
+ });
364
+ });
365
+
366
+ /**
367
+ * 过滤后的可见区块
368
+ * 根据 section.visible 函数和字段过滤结果判断是否显示
369
+ */
370
+ const visibleSections = computed(() => {
371
+ return filteredSections.value.filter((section) => {
372
+ // 特殊区块:必填模式下隐藏
373
+ if (section.isSpecial) {
374
+ return !showRequiredOnly.value;
375
+ }
376
+ // 普通区块:有字段才显示
377
+ const hasFields = section.fieldsConfig.length > 0;
378
+ const visibleByFunc = section.visible ? section.visible() : true;
379
+ return hasFields && visibleByFunc;
380
+ });
381
+ });
382
+
383
+ /**
384
+ * 🆕 内部导航标签配置
385
+ * 如果外部传入则使用外部的,否则自动从 sections 生成
386
+ */
387
+ const internalNavTabs = computed<NavTabConfig[]>(() => {
388
+ if (props.navTabs && props.navTabs.length > 0) {
389
+ return props.navTabs;
390
+ }
391
+ // 自动从 sections 生成
392
+ return props.sections.map((section) => ({
393
+ name: section.name,
394
+ label: section.title,
395
+ sectionName: section.name
396
+ }));
397
+ });
398
+
399
+ // ===== 方法 =====
400
+ /** 处理楼层导航点击 */
401
+ const handleNavTabClick = (tab: any) => {
402
+ const tabName = tab.paneName || tab.props?.name;
403
+ const tabConfig = internalNavTabs.value.find((t) => t.name === tabName);
404
+
405
+ if (!tabConfig?.sectionName) return;
406
+
407
+ // 展开对应的 collapse section
408
+ if (!activeNamesModel.value.includes(tabConfig.sectionName)) {
409
+ activeNamesModel.value = [...activeNamesModel.value, tabConfig.sectionName];
410
+ }
411
+
412
+ // 滚动到对应区域
413
+ setTimeout(() => {
414
+ const section = props.sections.find(
415
+ (s) => s.name === tabConfig.sectionName
416
+ );
417
+ if (section?.id) {
418
+ const sectionEl = document.getElementById(section.id);
419
+ if (sectionEl) {
420
+ sectionEl.scrollIntoView({ behavior: "smooth", block: "start" });
421
+ }
422
+ }
423
+ }, 150);
424
+ };
425
+ </script>
426
+
427
+ <style scoped lang="scss">
428
+ @import "./index.scss";
429
+ </style>