vue2-components-plus 1.0.28 → 1.0.31

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.
@@ -0,0 +1,432 @@
1
+ # NsForm / NsFormTitle 使用说明(面向 AI 代码生成)
2
+
3
+ 本文档只描述当前仓库已经实现的表单能力,目的是让 AI 生成的代码能够直接贴进项目运行,而不是发明不存在的字段或方法。
4
+
5
+ ## 1. 组件关系
6
+
7
+ | 模块 | 路径 | 作用 |
8
+ |---|---|---|
9
+ | `NsForm` | `packages/components/NsForm/DynamicForm.vue` | 动态字段渲染器 |
10
+ | `NsFormTitle` | `packages/components/NsForm/DynamicFormTitle.vue` | 分组标题容器 |
11
+ | `FormFieldRenderer` | `packages/components/NsForm/FormFieldRenderer.js` | 单字段组件渲染器 |
12
+ | 导出入口 | `packages/components/NsForm/index.js` | 组件与工具函数导出 |
13
+
14
+ ## 2. 使用定位
15
+
16
+ - 外层业务页面通常仍然要写 `el-form`
17
+ - `NsForm` 负责渲染字段和表单项
18
+ - 校验由外层 `el-form` + 内层自动生成的 `el-form-item` 共同完成
19
+ - 数据源核心是 `rows`
20
+
21
+ ## 3. 快速示例
22
+
23
+ ```vue
24
+ <template>
25
+ <el-form ref="shellForm" :model="state" label-position="top">
26
+ <NsFormTitle title="基础信息">
27
+ <NsForm
28
+ ref="formRef"
29
+ :rows="state.rows"
30
+ formPropKey="rows"
31
+ model="vertical"
32
+ labelWidth="120"
33
+ gapH="16px"
34
+ gapV="12px"
35
+ />
36
+ </NsFormTitle>
37
+ </el-form>
38
+ </template>
39
+
40
+ <script setup>
41
+ import { reactive, ref } from 'vue'
42
+
43
+ const formRef = ref(null)
44
+
45
+ const state = reactive({
46
+ rows: [
47
+ [
48
+ {
49
+ key: 'name',
50
+ label: '姓名',
51
+ value: '',
52
+ component: 'ElInput',
53
+ params: {
54
+ clearable: true,
55
+ },
56
+ },
57
+ {
58
+ key: 'age',
59
+ label: '年龄',
60
+ value: '',
61
+ component: 'ElInput',
62
+ params: {
63
+ 'v-length.range': { min: 1, max: 120, int: true },
64
+ },
65
+ },
66
+ ],
67
+ ],
68
+ })
69
+ </script>
70
+ ```
71
+
72
+ ## 4. `NsForm` Props
73
+
74
+ | 属性 | 类型 | 默认值 | 说明 |
75
+ |---|---|---|---|
76
+ | `model` | `String` | `''` | 布局模式,包含 `vertical` 或 `table` 时生效 |
77
+ | `readOnly` | `Boolean` | `false` | 是否进入只读展示 |
78
+ | `labelWidth` | `String` | `'80px'` | 普通标签宽度 |
79
+ | `superLabelWidth` | `String` | `'30px'` | 分组父标签宽度 |
80
+ | `labelColor` | `String` | `'#0A7BFF'` | 标签颜色 |
81
+ | `gapH` | `String` | `'10px'` | 列间距 |
82
+ | `gapV` | `String` | `'10px'` | 行间距 |
83
+ | `height` | `String` | `'32px'` | 值区最小高度 |
84
+ | `rows` | `Array` | `[]` | 动态字段配置 |
85
+ | `backgroundColor` | `String` | `''` | 字段背景色 |
86
+ | `valueEmptyTag` | `String` | `'--'` | 空值占位 |
87
+ | `formPropKey` | `String` | `'rows'` | 生成内部 `el-form-item.prop` 的前缀 |
88
+ | `hasPoint` | `Boolean` | `false` | 是否统一显示红色星号 |
89
+
90
+ ## 5. `rows` 数据结构
91
+
92
+ ### 5.1 总体结构
93
+
94
+ `rows` 是二维数组:
95
+
96
+ ```js
97
+ const rows = [
98
+ [fieldA, fieldB],
99
+ [fieldC],
100
+ ]
101
+ ```
102
+
103
+ - 第一层数组表示“行”
104
+ - 第二层数组表示“当前行中的字段”
105
+ - 每个字段节点都可以是普通字段,也可以是带 `children` 的分组字段
106
+
107
+ ### 5.2 字段节点 `Field`
108
+
109
+ | 字段 | 类型 | 必填 | 说明 |
110
+ |---|---|---|---|
111
+ | `key` | `String` | 强烈建议必填 | 字段唯一键,数据读写都依赖它 |
112
+ | `label` | `String` | 否 | 标签文本 |
113
+ | `value` | `Any` | 否 | 当前值 |
114
+ | `defaultValue` | `Any` | 否 | 重置时回退值;不传时首次以当前 `value` 快照为默认值 |
115
+ | `component` | `String \| Component` | 否 | 字段组件,例如 `ElInput` |
116
+ | `params` | `Object` | 否 | 透传给组件的配置对象 |
117
+ | `events` | `Object` | 否 | 透传给组件的事件对象 |
118
+ | `slots` | `Object` | 否 | 透传给组件的插槽函数 |
119
+ | `style` | `Object` | 否 | 组件样式 |
120
+ | `required` | `Boolean` | 否 | 显式必填标记 |
121
+ | `hideLabel` | `Boolean` | 否 | 是否隐藏标签 |
122
+ | `span` | `Number \| String` | 否 | 宽度配置 |
123
+ | `children` | `Array<Field>` | 否 | 分组字段 |
124
+ | `valueEmptyTag` | `String` | 否 | 当前字段空值占位 |
125
+ | `readOnlyUseComponent` | `Boolean` | 否 | 只读时是否仍渲染组件而不是文本 |
126
+ | `delValue` | `Array` | 否 | 上传删除记录等扩展数据 |
127
+ | `ref` | `Any` | 自动写入 | 渲染完成后自动记录组件实例 |
128
+
129
+ ## 6. `span` 布局规则
130
+
131
+ | `span` 值 | 当前实现行为 |
132
+ |---|---|
133
+ | `0` | 当前字段隐藏 |
134
+ | `1 ~ 24` | 按 24 栅格换算宽度 |
135
+ | `> 24` 的数字 | 视为像素宽度 |
136
+ | `'50%'` 这类百分比字符串 | 直接按百分比宽度渲染 |
137
+ | 不传 | 当前行所有非隐藏字段均分宽度 |
138
+
139
+ ## 7. `params` 的特殊约定
140
+
141
+ `params` 不是简单透传,它有几类保留键。
142
+
143
+ | 键 | 当前实现 |
144
+ |---|---|
145
+ | `rules` | 给外层 `el-form-item` 使用 |
146
+ | `style` | 与 `field.style` 合并 |
147
+ | `options` | `ElSelect` / `ElRadioGroup` / `ElCheckboxGroup` 生成选项 |
148
+ | `v-*` | 生成指令,例如 `v-length.range` |
149
+
150
+ ### 7.1 指令透传示例
151
+
152
+ ```js
153
+ {
154
+ key: 'age',
155
+ component: 'ElInput',
156
+ params: {
157
+ 'v-length.range': { min: 1, max: 120, int: true },
158
+ },
159
+ }
160
+ ```
161
+
162
+ ### 7.2 `options` 生成规则
163
+
164
+ 以下组件会根据 `params.options` 自动渲染子选项:
165
+
166
+ - `ElSelect`
167
+ - `ElRadioGroup`
168
+ - `ElCheckboxGroup`
169
+
170
+ 每个选项结构为:
171
+
172
+ ```js
173
+ { label: '启用', value: 1, disabled: false }
174
+ ```
175
+
176
+ ## 8. 插槽与事件机制
177
+
178
+ ### 8.1 插槽
179
+
180
+ `NsForm` 本体没有组件级插槽,插槽要写在字段节点的 `slots` 里。
181
+
182
+ ```js
183
+ field.slots = {
184
+ default: ({ field, value }) => h('span', value),
185
+ tip: () => h('div', '上传说明'),
186
+ }
187
+ ```
188
+
189
+ 当前签名会收到:
190
+
191
+ | 参数 | 说明 |
192
+ |---|---|
193
+ | `field` | 当前字段配置对象 |
194
+ | `value` | 当前字段值 |
195
+
196
+ ### 8.2 事件
197
+
198
+ - `field.events` 会透传到组件
199
+ - 对非上传组件,`input` 事件会被包装:
200
+ - 先更新 `field.value`
201
+ - 再执行你自己写的 `events.input`
202
+ - 上传组件 `ElUpload` 不会自动注入 `value` prop
203
+
204
+ ## 9. 只读模式真实行为
205
+
206
+ 当 `readOnly=true` 且字段未设置 `readOnlyUseComponent` 时,字段会改为文本展示。
207
+
208
+ ### 9.1 文本展示优先级
209
+
210
+ 1. `field.params.formatter(field.value, field)`
211
+ 2. 上传组件:拼接文件名
212
+ 3. `ElSwitch`:显示 `activeText / inactiveText` 或 `是 / 否`
213
+ 4. 选择类组件:根据 `params.options` 反查 label
214
+ 5. `ElCascader`:根据层级 options 解析路径文本
215
+ 6. 普通数组:使用中文逗号连接
216
+ 7. 普通值:直接显示
217
+ 8. 空值:显示 `field.valueEmptyTag` 或全局 `valueEmptyTag`
218
+
219
+ ### 9.2 AI 生成代码注意
220
+
221
+ - 只读展示有特殊格式需求时,优先写 `params.formatter`
222
+ - 想在只读态仍保留控件外观,显式设置 `readOnlyUseComponent: true`
223
+
224
+ ## 10. 实例方法
225
+
226
+ 通过 `ref="formRef"` 可调用以下方法。
227
+
228
+ | 方法 | 参数 | 返回 | 说明 |
229
+ |---|---|---|---|
230
+ | `getFormKvData()` | - | `Object` | 提取全部字段键值 |
231
+ | `resetForm(triggerEvents=false)` | `Boolean` | `void` | 重置为默认值 |
232
+ | `setFormData(data)` | `Object` | `void` | 按 key 回填 |
233
+ | `getFormNodeByKey(key)` | `String` | `Field \| null` | 获取字段节点 |
234
+ | `getFormNodeRefByKey(key)` | `String` | `Any` | 获取字段组件实例 |
235
+ | `initDefaultValues()` | - | `void` | 为尚未定义 `defaultValue` 的字段补齐默认值快照 |
236
+
237
+ ### 10.1 关键细节
238
+
239
+ - `getFormKvData()` 会把上传字段的 `value + delValue` 合并后返回
240
+ - `resetForm(true)` 只在值真实变化时触发字段的 `events.change` 与 `events.input`
241
+ - `resetForm()` 会清空 `delValue`
242
+ - 如果字段 `params.fileList` 是数组,`resetForm()` 会同步清空它
243
+ - `setFormData(data)` 对 `ElCascader` 的逗号字符串会自动拆成数组
244
+ - `setFormData(data)` 对上传组件会同步写入 `params.fileList`
245
+
246
+ ### 10.2 `initDefaultValues()` 的真实限制
247
+
248
+ 这一点必须写清楚,否则 AI 很容易误用。
249
+
250
+ - 它不会强制覆盖已有 `defaultValue`
251
+ - 它的作用是“补齐缺失的默认值快照”
252
+ - 如果你想重新定义默认值,应手动修改字段上的 `defaultValue`
253
+
254
+ ## 11. `NsFormTitle`
255
+
256
+ ### 11.1 Props
257
+
258
+ | 属性 | 类型 | 默认值 | 说明 |
259
+ |---|---|---|---|
260
+ | `title` | `String` | `''` | 标题文本 |
261
+
262
+ ### 11.2 插槽
263
+
264
+ | 插槽 | 说明 |
265
+ |---|---|
266
+ | `title` | 自定义标题区 |
267
+ | `default` | 承载 `NsForm` 或其它内容 |
268
+
269
+ ## 12. 导出能力
270
+
271
+ `packages/components/NsForm/index.js` 还导出了下面这些工具函数。
272
+
273
+ | 导出项 | 说明 |
274
+ |---|---|
275
+ | `getAllFormKvData(rows)` | 不依赖组件实例,直接从 `rows` 取值 |
276
+ | `getAllFormNodeByKey(rows, key)` | 从 `rows` 查字段节点 |
277
+ | `getAllFormNodeRefByKey(rows, key)` | 从 `rows` 查字段 ref |
278
+ | `useFileUpload` | 上传辅助 hook |
279
+
280
+ 另外,`NsForm.install` 会顺带注册 directives,因此单独安装 `NsForm` 时也能使用 `v-sline`、`v-length` 等表单相关指令。
281
+
282
+ ## 13. Demo 应覆盖的能力
283
+
284
+ `src/views/FormDemo.vue` 建议覆盖以下类型,AI 生成页面时也应尽量齐全:
285
+
286
+ | 场景 | 建议能力 |
287
+ |---|---|
288
+ | 基础字段 | `ElInput`、`ElSelect`、`ElSwitch` |
289
+ | 分组字段 | `children` |
290
+ | 上传字段 | `value + params.fileList + delValue` |
291
+ | 联动字段 | `events.change` |
292
+ | 只读切换 | `readOnly` + `params.formatter` |
293
+ | 数据操作 | `getFormKvData / setFormData / resetForm` |
294
+ | 节点访问 | `getFormNodeByKey / getFormNodeRefByKey` |
295
+
296
+ ## 14. AI 生成代码规则
297
+
298
+ - 页面外层始终使用 `el-form`
299
+ - 每个字段都尽量提供稳定 `key`
300
+ - 选项类组件统一使用 `params.options`
301
+ - 自定义指令统一放在 `params['v-xxx']`
302
+ - 上传场景必须同步维护 `value` 和 `params.fileList`
303
+ - 联动逻辑优先写在 `field.events.change`
304
+ - 不要把 `initDefaultValues()` 当成“强制重建全部默认值”的方法
305
+
306
+ ## 15. 推荐 Prompt
307
+
308
+ ```text
309
+ 请生成 Vue2.7 + script setup 的 NsForm 页面,要求:
310
+ 1) 外层使用 el-form;
311
+ 2) rows 至少包含普通输入、选择项、children 分组、上传字段、联动字段;
312
+ 3) 每个字段遵循 key、label、value、component、params、events 结构;
313
+ 4) 演示 params.options、params.rules、params.formatter、params['v-length.range'];
314
+ 5) 演示 getFormKvData、setFormData、resetForm(true)、getFormNodeByKey、getFormNodeRefByKey;
315
+ 6) 演示 readOnly 切换;
316
+ 7) 代码风格与当前仓库一致,不使用 TS,不虚构不存在的方法。
317
+ ```
318
+
319
+ ## 16. 标准模板
320
+
321
+ ```vue
322
+ <template>
323
+ <el-form ref="shellForm" :model="state" label-position="top">
324
+ <NsFormTitle title="基础信息">
325
+ <NsForm
326
+ ref="formRef"
327
+ :rows="state.rows"
328
+ formPropKey="rows"
329
+ model="vertical"
330
+ :readOnly="readOnly"
331
+ labelWidth="120"
332
+ gapH="16px"
333
+ gapV="12px"
334
+ />
335
+ </NsFormTitle>
336
+
337
+ <div style="margin-top: 20px;">
338
+ <el-button type="primary" @click="submit">提交</el-button>
339
+ <el-button @click="fillData">回填</el-button>
340
+ <el-button @click="resetForm">重置</el-button>
341
+ <el-button @click="toggleReadonly">切换只读</el-button>
342
+ </div>
343
+ </el-form>
344
+ </template>
345
+
346
+ <script setup>
347
+ import { reactive, ref, nextTick } from 'vue'
348
+
349
+ const shellForm = ref(null)
350
+ const formRef = ref(null)
351
+ const readOnly = ref(false)
352
+
353
+ const state = reactive({
354
+ rows: [
355
+ [
356
+ {
357
+ key: 'name',
358
+ label: '姓名',
359
+ value: '',
360
+ component: 'ElInput',
361
+ params: {
362
+ clearable: true,
363
+ rules: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
364
+ },
365
+ },
366
+ {
367
+ key: 'age',
368
+ label: '年龄',
369
+ value: '',
370
+ component: 'ElInput',
371
+ params: {
372
+ clearable: true,
373
+ 'v-length.range': { min: 1, max: 120, int: true },
374
+ },
375
+ },
376
+ ],
377
+ [
378
+ {
379
+ key: 'status',
380
+ label: '状态',
381
+ value: 1,
382
+ component: 'ElSelect',
383
+ params: {
384
+ clearable: true,
385
+ options: [
386
+ { label: '启用', value: 1 },
387
+ { label: '禁用', value: 0 },
388
+ ],
389
+ formatter: (value) => (value === 1 ? '启用' : '禁用'),
390
+ },
391
+ },
392
+ {
393
+ key: 'avatar',
394
+ label: '头像',
395
+ value: [],
396
+ component: 'ElUpload',
397
+ params: {
398
+ action: '#',
399
+ autoUpload: false,
400
+ fileList: [],
401
+ },
402
+ },
403
+ ],
404
+ ],
405
+ })
406
+
407
+ const submit = async () => {
408
+ await shellForm.value.validate()
409
+ console.log(formRef.value.getFormKvData())
410
+ }
411
+
412
+ const fillData = () => {
413
+ formRef.value.setFormData({
414
+ name: '张三',
415
+ age: '28',
416
+ status: 1,
417
+ avatar: [{ name: '头像.png', url: 'https://example.com/avatar.png' }],
418
+ })
419
+ }
420
+
421
+ const resetForm = () => {
422
+ formRef.value.resetForm(true)
423
+ nextTick(() => {
424
+ shellForm.value.clearValidate()
425
+ })
426
+ }
427
+
428
+ const toggleReadonly = () => {
429
+ readOnly.value = !readOnly.value
430
+ }
431
+ </script>
432
+ ```
@@ -3,7 +3,7 @@
3
3
  <el-card shadow="never" class="demo-card">
4
4
  <div slot="header" class="demo-card__header">
5
5
  <div>
6
- <div class="demo-card__title">NsForm 动态表单预览(&lt;script setup&gt; 版)</div>
6
+ <div class="demo-card__title">表单场景能力面板</div>
7
7
  <div class="demo-card__desc">覆盖校验、回填、只读、级联、上传和动态表单项等常见场景,使用 &lt;script setup&gt; 写法。</div>
8
8
 
9
9
  </div>
@@ -24,6 +24,9 @@
24
24
  <el-button type="primary" @click="getFormData">获取表单数据</el-button>
25
25
  <el-button @click="loadDetailData()">模拟详情回填</el-button>
26
26
  <el-button @click="resetFormData()">重置表单</el-button>
27
+ <el-button @click="inspectFormNode">查看节点配置</el-button>
28
+ <el-button @click="inspectFormNodeRef">查看节点实例</el-button>
29
+ <el-button @click="rebuildDefaultValues">重建默认值快照</el-button>
27
30
  <el-button @click="toggleReadOnly">切换只读</el-button>
28
31
  <el-button @click="notifyInnerButton">触发自定义事件</el-button>
29
32
  <el-button v-if="insideDialog" type="danger" plain @click="emit('close')">从内容区关闭弹窗</el-button>
@@ -218,6 +221,38 @@ function createRows() {
218
221
  },
219
222
  },
220
223
  ],
224
+ [
225
+ {
226
+ label: '推理策略',
227
+ span: 24,
228
+ children: [
229
+ {
230
+ key: 'inferMode',
231
+ label: '推理模式',
232
+ value: 'fast',
233
+ component: 'ElSelect',
234
+ params: {
235
+ clearable: true,
236
+ options: [
237
+ { label: '快速', value: 'fast' },
238
+ { label: '平衡', value: 'balanced' },
239
+ { label: '高精度', value: 'accurate' },
240
+ ],
241
+ },
242
+ },
243
+ {
244
+ key: 'batchSize',
245
+ label: '批量大小',
246
+ value: '1',
247
+ component: 'ElInput',
248
+ params: {
249
+ clearable: true,
250
+ 'v-length.range': { min: 1, max: 16, int: true },
251
+ },
252
+ },
253
+ ],
254
+ },
255
+ ],
221
256
  ]
222
257
  }
223
258
 
@@ -466,6 +501,8 @@ function createDetailData() {
466
501
  modelName: 'helmet-detector-v2',
467
502
  confidence: '0.72',
468
503
  iou: '0.33',
504
+ inferMode: 'accurate',
505
+ batchSize: '4',
469
506
  timeInterval: '3',
470
507
  stuck_threshold: ['component', 'table'],
471
508
  maxRetries: '5',
@@ -685,6 +722,40 @@ const notifyInnerButton = () => {
685
722
  emit('btnClick', collectFormData())
686
723
  proxy.$message.success('已触发自定义事件')
687
724
  }
725
+
726
+ const inspectFormNode = () => {
727
+ if (!row1Ref.value || typeof row1Ref.value.getFormNodeByKey !== 'function') {
728
+ proxy.$message.warning('表单实例尚未就绪')
729
+ return
730
+ }
731
+ const node = row1Ref.value.getFormNodeByKey('modelName')
732
+ proxy.$message.info(node ? '已找到字段节点:' + node.label : '未找到字段节点 modelName')
733
+ }
734
+
735
+ const inspectFormNodeRef = () => {
736
+ if (!row1Ref.value || typeof row1Ref.value.getFormNodeRefByKey !== 'function') {
737
+ proxy.$message.warning('表单实例尚未就绪')
738
+ return
739
+ }
740
+ const nodeRef = row1Ref.value.getFormNodeRefByKey('modelName')
741
+ if (!nodeRef) {
742
+ proxy.$message.warning('未找到字段实例 modelName')
743
+ return
744
+ }
745
+ if (typeof nodeRef.focus === 'function') {
746
+ nodeRef.focus()
747
+ }
748
+ proxy.$message.success('已获取 modelName 对应组件实例')
749
+ }
750
+
751
+ const rebuildDefaultValues = () => {
752
+ getFormRefs().forEach((refInstance) => {
753
+ if (refInstance && typeof refInstance.initDefaultValues === 'function') {
754
+ refInstance.initDefaultValues()
755
+ }
756
+ })
757
+ proxy.$message.success('已重建默认值快照')
758
+ }
688
759
 
689
760
  const normalizeUploadList = (fileList) =>
690
761
  (fileList || []).map((item) => {
@@ -770,6 +841,9 @@ defineExpose({
770
841
  resetFormData,
771
842
  loadDetailData,
772
843
  showToast,
844
+ inspectFormNode,
845
+ inspectFormNodeRef,
846
+ rebuildDefaultValues,
773
847
  })
774
848
  </script>
775
849