jianghu-ui 1.0.7 → 1.0.8
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/dist/jianghu-ui.css +76 -84
- package/dist/jianghu-ui.js +1 -1
- package/package.json +1 -1
- package/src/components/JhDrawer/JhDrawer.vue +7 -1
- package/src/components/JhDrawerForm/JhDrawerForm.stories.js +161 -0
- package/src/components/JhForm/JhForm.vue +837 -273
- package/src/components/JhFormFields/JhFormFields.vue +9 -2
|
@@ -5,61 +5,431 @@
|
|
|
5
5
|
:class="formClasses"
|
|
6
6
|
v-bind="mergedFormProps"
|
|
7
7
|
v-on="formListeners"
|
|
8
|
+
@submit.prevent
|
|
8
9
|
>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
10
|
+
<!-- 标题区域 -->
|
|
11
|
+
<div v-if="title || $slots.title" class="jh-form-header">
|
|
12
|
+
<slot name="title">
|
|
13
|
+
<div class="jh-form-title">
|
|
14
|
+
{{ title }}
|
|
15
|
+
<v-tooltip v-if="tooltip" bottom>
|
|
16
|
+
<template v-slot:activator="{ on, attrs }">
|
|
17
|
+
<v-icon small class="ml-1" v-bind="attrs" v-on="on">mdi-help-circle-outline</v-icon>
|
|
18
|
+
</template>
|
|
19
|
+
<span>{{ tooltip }}</span>
|
|
20
|
+
</v-tooltip>
|
|
21
|
+
</div>
|
|
22
|
+
</slot>
|
|
23
|
+
<div v-if="description" class="jh-form-description">{{ description }}</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<!-- 字段内容区域 -->
|
|
27
|
+
<v-row :class="rowClass" :dense="dense" v-bind="gridRowProps">
|
|
28
|
+
<template v-for="(field, index) in visibleFields">
|
|
29
|
+
<!-- 分组标题 -->
|
|
30
|
+
<v-col v-if="field.type === 'group'" :key="`group-${index}`" cols="12">
|
|
31
|
+
<div class="jh-form-group-title" :class="field.titleClass">
|
|
32
|
+
<v-divider v-if="field.divider && index > 0" class="mb-4"></v-divider>
|
|
33
|
+
<h3 v-if="field.title" class="text-h6 mb-2">{{ field.title }}</h3>
|
|
34
|
+
<p v-if="field.description" class="text-caption grey--text mb-3">{{ field.description }}</p>
|
|
35
|
+
</div>
|
|
36
|
+
</v-col>
|
|
37
|
+
|
|
38
|
+
<!-- 普通字段 -->
|
|
39
|
+
<v-col
|
|
40
|
+
v-else
|
|
41
|
+
:key="field.key"
|
|
42
|
+
:cols="getFieldCols(field)"
|
|
43
|
+
:sm="getFieldSm(field)"
|
|
44
|
+
:md="getFieldMd(field)"
|
|
45
|
+
:lg="getFieldLg(field)"
|
|
46
|
+
:xl="getFieldXl(field)"
|
|
47
|
+
:class="getFieldColClass(field)"
|
|
48
|
+
>
|
|
49
|
+
<!-- 字段包装器 -->
|
|
50
|
+
<div :class="getFieldWrapperClass(field)">
|
|
51
|
+
<!-- 字段标签 (水平布局) -->
|
|
52
|
+
<div
|
|
53
|
+
v-if="field.label && showLabels && (internalLayout === 'horizontal' || field.layout === 'horizontal')"
|
|
54
|
+
:class="getHorizontalLabelClass(field)"
|
|
55
|
+
:style="getHorizontalLabelStyle(field)"
|
|
56
|
+
>
|
|
57
|
+
<span v-if="(field.required && showRequiredMark)" class="error--text mr-1">*</span>
|
|
58
|
+
{{ field.label }}
|
|
59
|
+
<v-tooltip v-if="field.tooltip" bottom>
|
|
60
|
+
<template v-slot:activator="{ on, attrs }">
|
|
61
|
+
<v-icon small class="ml-1" v-bind="attrs" v-on="on">mdi-help-circle-outline</v-icon>
|
|
62
|
+
</template>
|
|
63
|
+
<span>{{ field.tooltip }}</span>
|
|
64
|
+
</v-tooltip>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- 字段输入区域 -->
|
|
68
|
+
<div :class="getFieldInputClass(field)">
|
|
69
|
+
<!-- 字段标签 (垂直/行内布局) -->
|
|
70
|
+
<div
|
|
71
|
+
v-if="field.label && showLabels && internalLayout !== 'horizontal' && field.layout !== 'horizontal'"
|
|
72
|
+
:class="getVerticalLabelClass(field)"
|
|
73
|
+
>
|
|
74
|
+
<span v-if="field.required && showRequiredMark" class="error--text">*</span>
|
|
75
|
+
{{ field.label }}
|
|
76
|
+
<v-tooltip v-if="field.tooltip" bottom>
|
|
77
|
+
<template v-slot:activator="{ on, attrs }">
|
|
78
|
+
<v-icon small class="ml-1" v-bind="attrs" v-on="on">mdi-help-circle-outline</v-icon>
|
|
79
|
+
</template>
|
|
80
|
+
<span>{{ field.tooltip }}</span>
|
|
81
|
+
</v-tooltip>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<!-- 只读模式展示 -->
|
|
85
|
+
<div v-if="isFieldReadonly(field)" class="jh-form-readonly-text">
|
|
86
|
+
{{ getReadonlyValue(field) }}
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<!-- 表单字段 -->
|
|
90
|
+
<template v-else>
|
|
91
|
+
<!-- 文本输入框 -->
|
|
92
|
+
<v-text-field
|
|
93
|
+
v-if="field.type === 'text' || !field.type"
|
|
94
|
+
:class="inputClass"
|
|
95
|
+
:dense="getDense(field)"
|
|
96
|
+
:single-line="getSingleLine(field)"
|
|
97
|
+
:filled="getFilled(field)"
|
|
98
|
+
:outlined="getOutlined(field)"
|
|
99
|
+
:value="getFieldValue(field.key)"
|
|
100
|
+
@input="handleInput(field.key, $event)"
|
|
101
|
+
:rules="getRules(field)"
|
|
102
|
+
:disabled="getFieldDisabled(field)"
|
|
103
|
+
:readonly="field.readonly"
|
|
104
|
+
:placeholder="field.placeholder"
|
|
105
|
+
:prefix="field.prefix"
|
|
106
|
+
:suffix="field.suffix"
|
|
107
|
+
:hide-details="field.hideDetails || hideDetails"
|
|
108
|
+
v-bind="field.props"
|
|
109
|
+
@change="handleChange(field.key, $event)"
|
|
110
|
+
@blur="handleBlur(field.key, $event)"
|
|
111
|
+
></v-text-field>
|
|
112
|
+
|
|
113
|
+
<!-- 文本域 -->
|
|
114
|
+
<v-textarea
|
|
115
|
+
v-else-if="field.type === 'textarea'"
|
|
116
|
+
:class="inputClass"
|
|
117
|
+
:dense="getDense(field)"
|
|
118
|
+
:filled="getFilled(field)"
|
|
119
|
+
:outlined="getOutlined(field)"
|
|
120
|
+
:value="getFieldValue(field.key)"
|
|
121
|
+
@input="handleInput(field.key, $event)"
|
|
122
|
+
:rules="getRules(field)"
|
|
123
|
+
:disabled="getFieldDisabled(field)"
|
|
124
|
+
:readonly="field.readonly"
|
|
125
|
+
:placeholder="field.placeholder"
|
|
126
|
+
:rows="field.rows || 3"
|
|
127
|
+
:hide-details="field.hideDetails || hideDetails"
|
|
128
|
+
v-bind="field.props"
|
|
129
|
+
@change="handleChange(field.key, $event)"
|
|
130
|
+
@blur="handleBlur(field.key, $event)"
|
|
131
|
+
></v-textarea>
|
|
132
|
+
|
|
133
|
+
<!-- 数字输入框 -->
|
|
134
|
+
<v-text-field
|
|
135
|
+
v-else-if="field.type === 'number'"
|
|
136
|
+
:class="inputClass"
|
|
137
|
+
type="number"
|
|
138
|
+
:dense="getDense(field)"
|
|
139
|
+
:single-line="getSingleLine(field)"
|
|
140
|
+
:filled="getFilled(field)"
|
|
141
|
+
:outlined="getOutlined(field)"
|
|
142
|
+
:value="getFieldValue(field.key)"
|
|
143
|
+
@input="handleInput(field.key, $event)"
|
|
144
|
+
:rules="getRules(field)"
|
|
145
|
+
:disabled="getFieldDisabled(field)"
|
|
146
|
+
:readonly="field.readonly"
|
|
147
|
+
:placeholder="field.placeholder"
|
|
148
|
+
:hide-details="field.hideDetails || hideDetails"
|
|
149
|
+
v-bind="field.props"
|
|
150
|
+
@change="handleChange(field.key, $event)"
|
|
151
|
+
@blur="handleBlur(field.key, $event)"
|
|
152
|
+
></v-text-field>
|
|
153
|
+
|
|
154
|
+
<!-- 下拉选择框 -->
|
|
155
|
+
<v-select
|
|
156
|
+
v-else-if="field.type === 'select'"
|
|
157
|
+
:class="inputClass"
|
|
158
|
+
:dense="getDense(field)"
|
|
159
|
+
:filled="getFilled(field)"
|
|
160
|
+
:outlined="getOutlined(field)"
|
|
161
|
+
:value="getFieldValue(field.key)"
|
|
162
|
+
@input="handleInput(field.key, $event)"
|
|
163
|
+
:items="getFieldOptions(field)"
|
|
164
|
+
:rules="getRules(field)"
|
|
165
|
+
:disabled="getFieldDisabled(field)"
|
|
166
|
+
:readonly="field.readonly"
|
|
167
|
+
:placeholder="field.placeholder"
|
|
168
|
+
:item-text="field.itemText || 'text'"
|
|
169
|
+
:item-value="field.itemValue || 'value'"
|
|
170
|
+
:multiple="field.multiple"
|
|
171
|
+
:chips="field.chips"
|
|
172
|
+
:hide-details="field.hideDetails || hideDetails"
|
|
173
|
+
small-chips
|
|
174
|
+
v-bind="field.props"
|
|
175
|
+
@change="handleChange(field.key, $event)"
|
|
176
|
+
@blur="handleBlur(field.key, $event)"
|
|
177
|
+
></v-select>
|
|
178
|
+
|
|
179
|
+
<!-- 自动完成 -->
|
|
180
|
+
<v-autocomplete
|
|
181
|
+
v-else-if="field.type === 'autocomplete'"
|
|
182
|
+
:class="inputClass"
|
|
183
|
+
:dense="getDense(field)"
|
|
184
|
+
:filled="getFilled(field)"
|
|
185
|
+
:outlined="getOutlined(field)"
|
|
186
|
+
:value="getFieldValue(field.key)"
|
|
187
|
+
@input="handleInput(field.key, $event)"
|
|
188
|
+
:items="getFieldOptions(field)"
|
|
189
|
+
:rules="getRules(field)"
|
|
190
|
+
:disabled="getFieldDisabled(field)"
|
|
191
|
+
:readonly="field.readonly"
|
|
192
|
+
:placeholder="field.placeholder"
|
|
193
|
+
:item-text="field.itemText || 'text'"
|
|
194
|
+
:item-value="field.itemValue || 'value'"
|
|
195
|
+
:hide-details="field.hideDetails || hideDetails"
|
|
196
|
+
v-bind="field.props"
|
|
197
|
+
@change="handleChange(field.key, $event)"
|
|
198
|
+
@blur="handleBlur(field.key, $event)"
|
|
199
|
+
></v-autocomplete>
|
|
200
|
+
|
|
201
|
+
<!-- 日期选择器 -->
|
|
202
|
+
<v-menu
|
|
203
|
+
v-else-if="field.type === 'date'"
|
|
204
|
+
v-model="dateMenus[field.key]"
|
|
205
|
+
:close-on-content-click="false"
|
|
206
|
+
transition="scale-transition"
|
|
207
|
+
offset-y
|
|
208
|
+
min-width="290px"
|
|
209
|
+
>
|
|
210
|
+
<template v-slot:activator="{ on, attrs }">
|
|
211
|
+
<v-text-field
|
|
212
|
+
:class="inputClass"
|
|
213
|
+
:dense="getDense(field)"
|
|
214
|
+
:filled="getFilled(field)"
|
|
215
|
+
:outlined="getOutlined(field)"
|
|
216
|
+
:value="getFieldValue(field.key)"
|
|
217
|
+
:rules="getRules(field)"
|
|
218
|
+
:disabled="getFieldDisabled(field)"
|
|
219
|
+
:placeholder="field.placeholder"
|
|
220
|
+
:hide-details="field.hideDetails || hideDetails"
|
|
221
|
+
readonly
|
|
222
|
+
v-bind="attrs"
|
|
223
|
+
v-on="on"
|
|
224
|
+
></v-text-field>
|
|
225
|
+
</template>
|
|
226
|
+
<v-date-picker
|
|
227
|
+
:value="getFieldValue(field.key)"
|
|
228
|
+
@input="dateMenus[field.key] = false; handleInput(field.key, $event)"
|
|
229
|
+
:locale="field.locale || 'zh-cn'"
|
|
230
|
+
v-bind="field.pickerProps"
|
|
231
|
+
></v-date-picker>
|
|
232
|
+
</v-menu>
|
|
233
|
+
|
|
234
|
+
<!-- 时间选择器 -->
|
|
235
|
+
<v-menu
|
|
236
|
+
v-else-if="field.type === 'time'"
|
|
237
|
+
v-model="timeMenus[field.key]"
|
|
238
|
+
:close-on-content-click="false"
|
|
239
|
+
transition="scale-transition"
|
|
240
|
+
offset-y
|
|
241
|
+
min-width="290px"
|
|
242
|
+
>
|
|
243
|
+
<template v-slot:activator="{ on, attrs }">
|
|
244
|
+
<v-text-field
|
|
245
|
+
:class="inputClass"
|
|
246
|
+
:dense="getDense(field)"
|
|
247
|
+
:filled="getFilled(field)"
|
|
248
|
+
:outlined="getOutlined(field)"
|
|
249
|
+
:value="getFieldValue(field.key)"
|
|
250
|
+
:rules="getRules(field)"
|
|
251
|
+
:disabled="getFieldDisabled(field)"
|
|
252
|
+
:placeholder="field.placeholder"
|
|
253
|
+
:hide-details="field.hideDetails || hideDetails"
|
|
254
|
+
readonly
|
|
255
|
+
v-bind="attrs"
|
|
256
|
+
v-on="on"
|
|
257
|
+
></v-text-field>
|
|
258
|
+
</template>
|
|
259
|
+
<v-time-picker
|
|
260
|
+
:value="getFieldValue(field.key)"
|
|
261
|
+
@input="timeMenus[field.key] = false; handleInput(field.key, $event)"
|
|
262
|
+
format="24hr"
|
|
263
|
+
v-bind="field.pickerProps"
|
|
264
|
+
></v-time-picker>
|
|
265
|
+
</v-menu>
|
|
266
|
+
|
|
267
|
+
<!-- 颜色选择器 -->
|
|
268
|
+
<v-menu
|
|
269
|
+
v-else-if="field.type === 'color'"
|
|
270
|
+
v-model="colorMenus[field.key]"
|
|
271
|
+
:close-on-content-click="false"
|
|
272
|
+
transition="scale-transition"
|
|
273
|
+
offset-y
|
|
274
|
+
min-width="320px"
|
|
275
|
+
>
|
|
276
|
+
<template v-slot:activator="{ on, attrs }">
|
|
277
|
+
<v-text-field
|
|
278
|
+
:class="inputClass"
|
|
279
|
+
:dense="getDense(field)"
|
|
280
|
+
:filled="getFilled(field)"
|
|
281
|
+
:outlined="getOutlined(field)"
|
|
282
|
+
:value="getFieldValue(field.key)"
|
|
283
|
+
:rules="getRules(field)"
|
|
284
|
+
:disabled="getFieldDisabled(field)"
|
|
285
|
+
:placeholder="field.placeholder"
|
|
286
|
+
:hide-details="field.hideDetails || hideDetails"
|
|
287
|
+
readonly
|
|
288
|
+
v-on="on"
|
|
289
|
+
v-bind="field.props"
|
|
290
|
+
>
|
|
291
|
+
<template v-slot:append>
|
|
292
|
+
<span
|
|
293
|
+
class="jh-color-preview"
|
|
294
|
+
:style="{ backgroundColor: getFieldValue(field.key) || field.defaultValue || '#ffffff' }"
|
|
295
|
+
></span>
|
|
296
|
+
</template>
|
|
297
|
+
</v-text-field>
|
|
298
|
+
</template>
|
|
299
|
+
<v-color-picker
|
|
300
|
+
:value="getFieldValue(field.key) || field.defaultValue || '#000000'"
|
|
301
|
+
flat
|
|
302
|
+
:hide-mode-switch="field.hideModeSwitch !== false"
|
|
303
|
+
@input="handleColorInput(field, $event)"
|
|
304
|
+
v-bind="field.pickerProps"
|
|
305
|
+
></v-color-picker>
|
|
306
|
+
</v-menu>
|
|
307
|
+
|
|
308
|
+
<!-- 滑块 -->
|
|
309
|
+
<v-slider
|
|
310
|
+
v-else-if="field.type === 'slider'"
|
|
311
|
+
:class="inputClass"
|
|
312
|
+
:value="getFieldValue(field.key)"
|
|
313
|
+
@input="handleInput(field.key, $event)"
|
|
314
|
+
:rules="getRules(field)"
|
|
315
|
+
:disabled="getFieldDisabled(field)"
|
|
316
|
+
:readonly="field.readonly"
|
|
317
|
+
:min="field.min !== undefined ? field.min : 0"
|
|
318
|
+
:max="field.max !== undefined ? field.max : 100"
|
|
319
|
+
:step="field.step !== undefined ? field.step : 1"
|
|
320
|
+
:thumb-label="field.thumbLabel"
|
|
321
|
+
:ticks="field.ticks"
|
|
322
|
+
:tick-size="field.tickSize"
|
|
323
|
+
:color="field.color || 'primary'"
|
|
324
|
+
:hide-details="field.hideDetails || hideDetails"
|
|
325
|
+
v-bind="field.props"
|
|
326
|
+
></v-slider>
|
|
327
|
+
|
|
328
|
+
<!-- 区间滑块 -->
|
|
329
|
+
<v-range-slider
|
|
330
|
+
v-else-if="field.type === 'range-slider'"
|
|
331
|
+
:class="inputClass"
|
|
332
|
+
:value="getFieldValue(field.key)"
|
|
333
|
+
@input="handleInput(field.key, $event)"
|
|
334
|
+
:rules="getRules(field)"
|
|
335
|
+
:disabled="getFieldDisabled(field)"
|
|
336
|
+
:readonly="field.readonly"
|
|
337
|
+
:min="field.min !== undefined ? field.min : 0"
|
|
338
|
+
:max="field.max !== undefined ? field.max : 100"
|
|
339
|
+
:step="field.step !== undefined ? field.step : 1"
|
|
340
|
+
:thumb-label="field.thumbLabel"
|
|
341
|
+
:ticks="field.ticks"
|
|
342
|
+
:tick-size="field.tickSize"
|
|
343
|
+
:color="field.color || 'primary'"
|
|
344
|
+
:hide-details="field.hideDetails || hideDetails"
|
|
345
|
+
v-bind="field.props"
|
|
346
|
+
></v-range-slider>
|
|
347
|
+
|
|
348
|
+
<!-- 开关 -->
|
|
349
|
+
<v-switch
|
|
350
|
+
v-else-if="field.type === 'switch'"
|
|
351
|
+
:input-value="getFieldValue(field.key)"
|
|
352
|
+
@change="handleChange(field.key, $event)"
|
|
353
|
+
:label="field.switchLabel"
|
|
354
|
+
:disabled="getFieldDisabled(field)"
|
|
355
|
+
:readonly="field.readonly"
|
|
356
|
+
:color="field.color || 'success'"
|
|
357
|
+
:hide-details="field.hideDetails || hideDetails"
|
|
358
|
+
v-bind="field.props"
|
|
359
|
+
></v-switch>
|
|
360
|
+
|
|
361
|
+
<!-- 复选框 -->
|
|
362
|
+
<v-checkbox
|
|
363
|
+
v-else-if="field.type === 'checkbox'"
|
|
364
|
+
:input-value="getFieldValue(field.key)"
|
|
365
|
+
@change="handleChange(field.key, $event)"
|
|
366
|
+
:label="field.checkboxLabel"
|
|
367
|
+
:disabled="getFieldDisabled(field)"
|
|
368
|
+
:readonly="field.readonly"
|
|
369
|
+
:color="field.color || 'success'"
|
|
370
|
+
:hide-details="field.hideDetails || hideDetails"
|
|
371
|
+
v-bind="field.props"
|
|
372
|
+
></v-checkbox>
|
|
373
|
+
|
|
374
|
+
<!-- 单选按钮组 -->
|
|
375
|
+
<v-radio-group
|
|
376
|
+
v-else-if="field.type === 'radio'"
|
|
377
|
+
:value="getFieldValue(field.key)"
|
|
378
|
+
@change="handleChange(field.key, $event)"
|
|
379
|
+
:rules="getRules(field)"
|
|
380
|
+
:disabled="getFieldDisabled(field)"
|
|
381
|
+
:readonly="field.readonly"
|
|
382
|
+
:row="field.row !== false"
|
|
383
|
+
:hide-details="field.hideDetails || hideDetails"
|
|
384
|
+
v-bind="field.props"
|
|
385
|
+
>
|
|
386
|
+
<v-radio
|
|
387
|
+
v-for="option in getFieldOptions(field)"
|
|
388
|
+
:key="option.value"
|
|
389
|
+
:label="option.text"
|
|
390
|
+
:value="option.value"
|
|
391
|
+
:color="field.color || 'success'"
|
|
392
|
+
></v-radio>
|
|
393
|
+
</v-radio-group>
|
|
394
|
+
|
|
395
|
+
<!-- 自定义字段插槽 -->
|
|
396
|
+
<slot
|
|
397
|
+
v-else-if="field.type === 'slot'"
|
|
398
|
+
:name="`field-${field.key}`"
|
|
399
|
+
:field="field"
|
|
400
|
+
:value="getFieldValue(field.key)"
|
|
401
|
+
:formData="formData"
|
|
402
|
+
:updateField="updateField"
|
|
403
|
+
></slot>
|
|
404
|
+
</template>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
</v-col>
|
|
36
408
|
</template>
|
|
37
|
-
</jh-form-fields>
|
|
38
409
|
|
|
39
|
-
|
|
40
|
-
<
|
|
410
|
+
<!-- 操作按钮区域 (Grid布局时) -->
|
|
411
|
+
<template v-if="isGridLayout">
|
|
41
412
|
<v-col cols="12" class="jh-form-actions-col">
|
|
42
|
-
<slot name="actions" :formData="formData" :validate="validate" :resetForm="resetForm"></slot>
|
|
413
|
+
<slot name="actions" :formData="formData" :validate="validate" :resetForm="resetForm" :reset="reset"></slot>
|
|
43
414
|
</v-col>
|
|
44
|
-
</
|
|
45
|
-
</
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
415
|
+
</template>
|
|
416
|
+
</v-row>
|
|
417
|
+
|
|
418
|
+
<!-- 操作按钮区域 (非Grid布局时) -->
|
|
419
|
+
<div v-if="!isGridLayout" class="jh-form-actions-wrapper">
|
|
420
|
+
<slot name="actions" :formData="formData" :validate="validate" :resetForm="resetForm" :reset="reset"></slot>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
<!-- 底部插槽 -->
|
|
424
|
+
<slot name="footer" :formData="formData"></slot>
|
|
49
425
|
</v-form>
|
|
50
426
|
</template>
|
|
51
427
|
|
|
52
428
|
<script>
|
|
53
|
-
import JhFormFields from '../JhFormFields/JhFormFields.vue';
|
|
54
|
-
|
|
55
429
|
export default {
|
|
56
430
|
name: 'JhForm',
|
|
57
431
|
inheritAttrs: false,
|
|
58
432
|
|
|
59
|
-
components: {
|
|
60
|
-
JhFormFields,
|
|
61
|
-
},
|
|
62
|
-
|
|
63
433
|
props: {
|
|
64
434
|
// 表单字段配置
|
|
65
435
|
fields: {
|
|
@@ -67,7 +437,13 @@ export default {
|
|
|
67
437
|
default: () => [],
|
|
68
438
|
},
|
|
69
439
|
|
|
70
|
-
//
|
|
440
|
+
// v-model 绑定的值
|
|
441
|
+
value: {
|
|
442
|
+
type: Object,
|
|
443
|
+
default: () => ({}),
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
// 初始表单数据 (当不使用 v-model 时使用)
|
|
71
447
|
initialData: {
|
|
72
448
|
type: Object,
|
|
73
449
|
default: () => ({}),
|
|
@@ -76,7 +452,7 @@ export default {
|
|
|
76
452
|
// 表单引用名称
|
|
77
453
|
formRef: {
|
|
78
454
|
type: String,
|
|
79
|
-
default: '
|
|
455
|
+
default: 'form',
|
|
80
456
|
},
|
|
81
457
|
|
|
82
458
|
// 懒加载验证
|
|
@@ -98,13 +474,6 @@ export default {
|
|
|
98
474
|
default: true,
|
|
99
475
|
},
|
|
100
476
|
|
|
101
|
-
// 标签位置 ('top' | 'left') - 已废弃,使用 layout 替代
|
|
102
|
-
labelPosition: {
|
|
103
|
-
type: String,
|
|
104
|
-
default: 'top',
|
|
105
|
-
validator: (v) => ['top', 'left'].includes(v),
|
|
106
|
-
},
|
|
107
|
-
|
|
108
477
|
// 标签宽度 (horizontal 布局时生效)
|
|
109
478
|
labelWidth: {
|
|
110
479
|
type: [Number, String],
|
|
@@ -166,7 +535,7 @@ export default {
|
|
|
166
535
|
// 隐藏详情信息
|
|
167
536
|
hideDetails: {
|
|
168
537
|
type: [Boolean, String],
|
|
169
|
-
default:
|
|
538
|
+
default: false,
|
|
170
539
|
},
|
|
171
540
|
|
|
172
541
|
// 自定义标签样式类
|
|
@@ -246,6 +615,37 @@ export default {
|
|
|
246
615
|
type: Object,
|
|
247
616
|
default: () => ({}),
|
|
248
617
|
},
|
|
618
|
+
|
|
619
|
+
// 分组标题
|
|
620
|
+
title: {
|
|
621
|
+
type: String,
|
|
622
|
+
default: ''
|
|
623
|
+
},
|
|
624
|
+
// 分组描述
|
|
625
|
+
description: {
|
|
626
|
+
type: String,
|
|
627
|
+
default: ''
|
|
628
|
+
},
|
|
629
|
+
// 标题旁的 tooltip
|
|
630
|
+
tooltip: {
|
|
631
|
+
type: String,
|
|
632
|
+
default: ''
|
|
633
|
+
},
|
|
634
|
+
// 是否显示边框
|
|
635
|
+
bordered: {
|
|
636
|
+
type: Boolean,
|
|
637
|
+
default: false
|
|
638
|
+
},
|
|
639
|
+
// 压缩外边距/间距
|
|
640
|
+
dense: {
|
|
641
|
+
type: Boolean,
|
|
642
|
+
default: false
|
|
643
|
+
},
|
|
644
|
+
// 字段依赖配置
|
|
645
|
+
dependencies: {
|
|
646
|
+
type: Array,
|
|
647
|
+
default: () => []
|
|
648
|
+
},
|
|
249
649
|
},
|
|
250
650
|
|
|
251
651
|
data() {
|
|
@@ -254,6 +654,12 @@ export default {
|
|
|
254
654
|
// 表单提交状态
|
|
255
655
|
submitLoading: false,
|
|
256
656
|
submitError: null,
|
|
657
|
+
|
|
658
|
+
// 字段状态
|
|
659
|
+
dateMenus: {},
|
|
660
|
+
timeMenus: {},
|
|
661
|
+
colorMenus: {},
|
|
662
|
+
dependencyWatchers: [],
|
|
257
663
|
};
|
|
258
664
|
},
|
|
259
665
|
|
|
@@ -262,6 +668,7 @@ export default {
|
|
|
262
668
|
formClasses() {
|
|
263
669
|
return {
|
|
264
670
|
'jh-form': true,
|
|
671
|
+
'jh-form--bordered': this.bordered,
|
|
265
672
|
[`jh-form--${this.layout}`]: true,
|
|
266
673
|
'jh-form--readonly': this.readonly,
|
|
267
674
|
'jh-form--disabled': this.disabled,
|
|
@@ -272,18 +679,40 @@ export default {
|
|
|
272
679
|
isGridLayout() {
|
|
273
680
|
return this.grid || this.layout === 'grid';
|
|
274
681
|
},
|
|
682
|
+
|
|
683
|
+
// 内部布局模式
|
|
684
|
+
internalLayout() {
|
|
685
|
+
return this.isGridLayout ? 'vertical' : this.layout;
|
|
686
|
+
},
|
|
275
687
|
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
688
|
+
// 需要显示的字段
|
|
689
|
+
visibleFields() {
|
|
690
|
+
// 先生成 normalizedFields (处理 col 配置)
|
|
691
|
+
const fields = this.normalizedFields;
|
|
692
|
+
|
|
693
|
+
return fields.filter(field => {
|
|
694
|
+
if (typeof field.visible === 'function') {
|
|
695
|
+
return field.visible(this.formData);
|
|
696
|
+
}
|
|
697
|
+
if (field.visible !== undefined) {
|
|
698
|
+
return field.visible;
|
|
699
|
+
}
|
|
700
|
+
return true;
|
|
701
|
+
});
|
|
702
|
+
},
|
|
281
703
|
|
|
704
|
+
// 标准化字段配置 (处理 Grid 列配置)
|
|
705
|
+
normalizedFields() {
|
|
282
706
|
return this.fields.map(field => {
|
|
283
707
|
if (field.type === 'group') {
|
|
284
708
|
return { ...field };
|
|
285
709
|
}
|
|
286
710
|
|
|
711
|
+
// 如果不是 grid 布局,且没有显式指定 cols,则保持原样 (render 时会默认占满或 auto)
|
|
712
|
+
if (!this.isGridLayout && !field.cols) {
|
|
713
|
+
return { ...field };
|
|
714
|
+
}
|
|
715
|
+
|
|
287
716
|
const bindings = this.getColBindings(field);
|
|
288
717
|
const colConfig = {};
|
|
289
718
|
|
|
@@ -292,47 +721,40 @@ export default {
|
|
|
292
721
|
if (bindings.md !== undefined) colConfig.md = bindings.md;
|
|
293
722
|
if (bindings.lg !== undefined) colConfig.lg = bindings.lg;
|
|
294
723
|
if (bindings.xl !== undefined) colConfig.xl = bindings.xl;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
724
|
+
|
|
725
|
+
// 如果计算出了 col 配置,合并到 field.cols
|
|
726
|
+
if (Object.keys(colConfig).length) {
|
|
727
|
+
const existing = field.cols && typeof field.cols === 'object' ? field.cols : (field.cols ? { xs: field.cols } : {});
|
|
728
|
+
return {
|
|
729
|
+
...field,
|
|
730
|
+
cols: {
|
|
731
|
+
...existing,
|
|
732
|
+
...colConfig,
|
|
733
|
+
},
|
|
734
|
+
};
|
|
298
735
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
return {
|
|
302
|
-
...field,
|
|
303
|
-
cols: {
|
|
304
|
-
...existing,
|
|
305
|
-
...colConfig,
|
|
306
|
-
},
|
|
307
|
-
};
|
|
736
|
+
|
|
737
|
+
return { ...field };
|
|
308
738
|
});
|
|
309
739
|
},
|
|
310
740
|
|
|
311
|
-
internalLayout() {
|
|
312
|
-
return this.isGridLayout ? 'vertical' : this.layout;
|
|
313
|
-
},
|
|
314
|
-
|
|
315
|
-
slotFields() {
|
|
316
|
-
return this.fields.filter(field => field.type === 'slot');
|
|
317
|
-
},
|
|
318
|
-
|
|
319
741
|
gridRowProps() {
|
|
320
|
-
return this.
|
|
742
|
+
return this.rowProps || {};
|
|
321
743
|
},
|
|
322
|
-
|
|
744
|
+
|
|
745
|
+
// 合并透传属性
|
|
323
746
|
mergedFormProps() {
|
|
324
747
|
// 只排除组件内部明确处理的属性,其他所有属性都透传给 v-form
|
|
325
748
|
const excludedAttrs = [
|
|
326
749
|
'class', 'style',
|
|
327
|
-
// 这些属性在组件内部有特殊处理
|
|
328
750
|
'lazy-validation', 'lazyValidation',
|
|
329
|
-
// JhForm 特有的 props(不在 v-form 中)
|
|
330
751
|
'fields', 'initialData', 'formRef', 'layout', 'showLabels',
|
|
331
752
|
'labelPosition', 'labelWidth', 'labelAlign', 'showRequiredMark',
|
|
332
753
|
'readonly', 'disabled', 'defaultDense', 'defaultFilled', 'defaultOutlined',
|
|
333
754
|
'defaultSingleLine', 'defaultColsMd', 'hideDetails', 'labelClass',
|
|
334
755
|
'inputClass', 'rowClass', 'validationRules', 'submitter', 'onFinish',
|
|
335
|
-
'onFinishFailed', 'dateFormatter', 'omitNil', 'grid', 'colProps', 'rowProps'
|
|
756
|
+
'onFinishFailed', 'dateFormatter', 'omitNil', 'grid', 'colProps', 'rowProps',
|
|
757
|
+
'title', 'description', 'tooltip', 'bordered', 'dense', 'dependencies'
|
|
336
758
|
];
|
|
337
759
|
|
|
338
760
|
const { class: cls, style, ...rest } = this.$attrs || {};
|
|
@@ -340,7 +762,6 @@ export default {
|
|
|
340
762
|
|
|
341
763
|
Object.keys(rest).forEach(key => {
|
|
342
764
|
const keyKebab = key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
343
|
-
// 只排除明确处理的属性,其他都透传
|
|
344
765
|
if (!excludedAttrs.includes(key) && !excludedAttrs.includes(keyKebab)) {
|
|
345
766
|
filteredAttrs[key] = rest[key];
|
|
346
767
|
}
|
|
@@ -348,19 +769,14 @@ export default {
|
|
|
348
769
|
|
|
349
770
|
return filteredAttrs;
|
|
350
771
|
},
|
|
351
|
-
|
|
772
|
+
|
|
773
|
+
// 透传所有事件
|
|
352
774
|
formListeners() {
|
|
353
|
-
|
|
354
|
-
const excludedEvents = [
|
|
355
|
-
// JhForm 特有的事件(不在 v-form 中)
|
|
356
|
-
'field-change'
|
|
357
|
-
];
|
|
358
|
-
|
|
775
|
+
const excludedEvents = ['field-change', 'submit'];
|
|
359
776
|
const listeners = { ...this.$listeners || {} };
|
|
360
777
|
const filteredListeners = {};
|
|
361
778
|
|
|
362
779
|
Object.keys(listeners).forEach(key => {
|
|
363
|
-
// 只排除明确处理的事件,其他都透传
|
|
364
780
|
if (!excludedEvents.includes(key)) {
|
|
365
781
|
filteredListeners[key] = listeners[key];
|
|
366
782
|
}
|
|
@@ -371,24 +787,50 @@ export default {
|
|
|
371
787
|
},
|
|
372
788
|
|
|
373
789
|
watch: {
|
|
374
|
-
|
|
790
|
+
value: {
|
|
375
791
|
handler(val) {
|
|
376
|
-
this.formData
|
|
792
|
+
if (JSON.stringify(val) !== JSON.stringify(this.formData)) {
|
|
793
|
+
this.initFormData();
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
deep: true,
|
|
797
|
+
},
|
|
798
|
+
initialData: {
|
|
799
|
+
handler() {
|
|
800
|
+
this.initFormData();
|
|
377
801
|
},
|
|
378
802
|
immediate: true,
|
|
379
803
|
deep: true,
|
|
380
804
|
},
|
|
805
|
+
fields: {
|
|
806
|
+
handler() {
|
|
807
|
+
this.initFormData();
|
|
808
|
+
},
|
|
809
|
+
deep: true,
|
|
810
|
+
},
|
|
811
|
+
dependencies: {
|
|
812
|
+
handler(newDeps) {
|
|
813
|
+
this.setupDependencyWatchers(newDeps);
|
|
814
|
+
},
|
|
815
|
+
immediate: true,
|
|
816
|
+
},
|
|
381
817
|
},
|
|
382
818
|
|
|
383
819
|
mounted() {
|
|
384
|
-
// 初始化表单数据
|
|
385
820
|
this.initFormData();
|
|
821
|
+
this.setupFieldDependencies();
|
|
822
|
+
},
|
|
823
|
+
|
|
824
|
+
beforeDestroy() {
|
|
825
|
+
this.dependencyWatchers.forEach(unwatch => unwatch());
|
|
826
|
+
this.dependencyWatchers = [];
|
|
386
827
|
},
|
|
387
828
|
|
|
388
829
|
methods: {
|
|
389
830
|
// 初始化表单数据
|
|
390
831
|
initFormData() {
|
|
391
|
-
const
|
|
832
|
+
const sourceData = { ...this.initialData, ...this.value };
|
|
833
|
+
const data = { ...sourceData };
|
|
392
834
|
|
|
393
835
|
// 从 fields 中提取默认值
|
|
394
836
|
this.fields.forEach(field => {
|
|
@@ -402,9 +844,9 @@ export default {
|
|
|
402
844
|
|
|
403
845
|
// Grid 模式下合并列配置
|
|
404
846
|
getColBindings(field) {
|
|
847
|
+
// 只有在 grid 模式下或者显式传递了 colProps 时才生效
|
|
405
848
|
const bindings = { ...(this.colProps || {}), ...(field.colProps || {}) };
|
|
406
849
|
|
|
407
|
-
// 兼容字段 cols 写法
|
|
408
850
|
if (field.cols) {
|
|
409
851
|
if (typeof field.cols === 'object') {
|
|
410
852
|
Object.assign(bindings, field.cols);
|
|
@@ -414,16 +856,17 @@ export default {
|
|
|
414
856
|
}
|
|
415
857
|
|
|
416
858
|
const span = field.colSpan !== undefined ? field.colSpan : bindings.span;
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
delete bindings.span;
|
|
424
|
-
|
|
859
|
+
// 默认在 grid 模式下,如果没有指定 cols,默认使用 6 (两列) 或者 defaultColsMd
|
|
860
|
+
// 原 JhForm 逻辑:mappedCols 默认为 8 (span=8 -> md=4?) Wait, mapGridSpan(8) = (8/24)*12 = 4.
|
|
861
|
+
// 这里我们尽量保持简单:如果没指定,用 defaultColsMd
|
|
862
|
+
|
|
863
|
+
// 如果没有指定任何 col 配置
|
|
425
864
|
if (!('cols' in bindings || 'sm' in bindings || 'md' in bindings || 'lg' in bindings || 'xl' in bindings)) {
|
|
426
|
-
|
|
865
|
+
if (span !== undefined) {
|
|
866
|
+
bindings.md = this.mapGridSpan(span);
|
|
867
|
+
} else if (this.isGridLayout) {
|
|
868
|
+
bindings.md = this.defaultColsMd;
|
|
869
|
+
}
|
|
427
870
|
}
|
|
428
871
|
|
|
429
872
|
return bindings;
|
|
@@ -435,93 +878,300 @@ export default {
|
|
|
435
878
|
return Math.max(1, Math.min(12, mapped || 1));
|
|
436
879
|
},
|
|
437
880
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
881
|
+
// --- 字段渲染辅助方法 ---
|
|
882
|
+
|
|
883
|
+
getFieldValue(key) {
|
|
884
|
+
return this.formData[key];
|
|
885
|
+
},
|
|
886
|
+
|
|
887
|
+
getFieldOptions(field) {
|
|
888
|
+
if (typeof field.options === 'function') {
|
|
889
|
+
return field.options(this.formData);
|
|
890
|
+
}
|
|
891
|
+
return field.options || [];
|
|
892
|
+
},
|
|
893
|
+
|
|
894
|
+
getFieldCols(field) {
|
|
895
|
+
if (this.layout === 'inline') return 'auto';
|
|
896
|
+
if (field.cols) {
|
|
897
|
+
return typeof field.cols === 'object' ? (field.cols.xs || field.cols) : field.cols;
|
|
898
|
+
}
|
|
899
|
+
// 如果没有 cols 属性,但在 grid 模式下,normalizedFields 应该已经注入了 cols。
|
|
900
|
+
// 如果还是没有,且不是 grid 模式,默认 12
|
|
901
|
+
return this.defaultColsMd || 12;
|
|
902
|
+
},
|
|
903
|
+
getFieldSm(field) {
|
|
904
|
+
if (this.layout === 'inline') return 'auto';
|
|
905
|
+
return field.cols && typeof field.cols === 'object' ? field.cols.sm : undefined;
|
|
906
|
+
},
|
|
907
|
+
getFieldMd(field) {
|
|
908
|
+
if (this.layout === 'inline') return 'auto';
|
|
909
|
+
return field.cols && typeof field.cols === 'object' ? field.cols.md : undefined;
|
|
910
|
+
},
|
|
911
|
+
getFieldLg(field) {
|
|
912
|
+
if (this.layout === 'inline') return 'auto';
|
|
913
|
+
return field.cols && typeof field.cols === 'object' ? field.cols.lg : undefined;
|
|
914
|
+
},
|
|
915
|
+
getFieldXl(field) {
|
|
916
|
+
if (this.layout === 'inline') return 'auto';
|
|
917
|
+
return field.cols && typeof field.cols === 'object' ? field.cols.xl : undefined;
|
|
918
|
+
},
|
|
919
|
+
|
|
920
|
+
getFieldColClass(field) {
|
|
921
|
+
return field.colClass || '';
|
|
922
|
+
},
|
|
923
|
+
|
|
924
|
+
getFieldWrapperClass(field) {
|
|
925
|
+
const fieldLayout = field.layout || this.internalLayout;
|
|
926
|
+
const layoutClass = fieldLayout === 'horizontal' ? 'd-flex align-center' : '';
|
|
927
|
+
return `jh-field-wrapper ${layoutClass}`;
|
|
928
|
+
},
|
|
929
|
+
|
|
930
|
+
getHorizontalLabelClass(field) {
|
|
931
|
+
const align = field.labelAlign || this.labelAlign;
|
|
932
|
+
return `jh-field-label jh-input-label jh-field-label--horizontal text-${align} flex-shrink-0`;
|
|
933
|
+
},
|
|
934
|
+
|
|
935
|
+
getHorizontalLabelStyle(field) {
|
|
936
|
+
const width = field.labelWidth || this.labelWidth;
|
|
937
|
+
return {
|
|
938
|
+
width: typeof width === 'number' ? `${width}px` : width,
|
|
939
|
+
minWidth: typeof width === 'number' ? `${width}px` : width,
|
|
940
|
+
};
|
|
941
|
+
},
|
|
942
|
+
|
|
943
|
+
getVerticalLabelClass(field) {
|
|
944
|
+
return `jh-field-label jh-input-label jh-field-label--vertical mb-1`;
|
|
945
|
+
},
|
|
946
|
+
|
|
947
|
+
getFieldInputClass(field) {
|
|
948
|
+
const fieldLayout = field.layout || this.internalLayout;
|
|
949
|
+
return fieldLayout === 'horizontal' ? 'jh-field-input flex-grow-1' : 'jh-field-input';
|
|
950
|
+
},
|
|
951
|
+
|
|
952
|
+
getFieldDisabled(field) {
|
|
953
|
+
if (typeof field.disabled === 'function') {
|
|
954
|
+
return field.disabled(this.formData);
|
|
955
|
+
}
|
|
956
|
+
if (field.disabled !== undefined) {
|
|
957
|
+
return field.disabled;
|
|
958
|
+
}
|
|
959
|
+
return this.disabled;
|
|
960
|
+
},
|
|
961
|
+
|
|
962
|
+
isFieldReadonly(field) {
|
|
963
|
+
if (typeof field.readonly === 'function') {
|
|
964
|
+
return field.readonly(this.formData);
|
|
965
|
+
}
|
|
966
|
+
if (field.readonly !== undefined) {
|
|
967
|
+
return field.readonly;
|
|
968
|
+
}
|
|
969
|
+
return this.readonly;
|
|
970
|
+
},
|
|
971
|
+
|
|
972
|
+
getReadonlyValue(field) {
|
|
973
|
+
const value = this.formData[field.key];
|
|
974
|
+
|
|
975
|
+
if (typeof field.readonlyRender === 'function') {
|
|
976
|
+
return field.readonlyRender(value, this.formData);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if ((field.type === 'select' || field.type === 'radio') && field.options) {
|
|
980
|
+
const options = this.getFieldOptions(field);
|
|
981
|
+
if (field.multiple && Array.isArray(value)) {
|
|
982
|
+
return value.map(v => {
|
|
983
|
+
const option = options.find(opt => opt.value === v);
|
|
984
|
+
return option ? option.text : v;
|
|
985
|
+
}).join(', ');
|
|
986
|
+
} else {
|
|
987
|
+
const option = options.find(opt => opt.value === value);
|
|
988
|
+
return option ? option.text : value;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
if (field.type === 'switch' || field.type === 'checkbox') {
|
|
993
|
+
return value ? '是' : '否';
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (field.type === 'range-slider' && Array.isArray(value)) {
|
|
997
|
+
return value.join(' ~ ');
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return value || '-';
|
|
1001
|
+
},
|
|
1002
|
+
|
|
1003
|
+
getDense(field) {
|
|
1004
|
+
return field.dense !== undefined ? field.dense : this.defaultDense;
|
|
1005
|
+
},
|
|
1006
|
+
getFilled(field) {
|
|
1007
|
+
return field.filled !== undefined ? field.filled : this.defaultFilled;
|
|
1008
|
+
},
|
|
1009
|
+
getOutlined(field) {
|
|
1010
|
+
return field.outlined !== undefined ? field.outlined : this.defaultOutlined;
|
|
1011
|
+
},
|
|
1012
|
+
getSingleLine(field) {
|
|
1013
|
+
return field.singleLine !== undefined ? field.singleLine : this.defaultSingleLine;
|
|
1014
|
+
},
|
|
1015
|
+
|
|
1016
|
+
getRules(field) {
|
|
1017
|
+
const rules = [];
|
|
1018
|
+
if (field.required) {
|
|
1019
|
+
rules.push(v => !!v || `${field.label || '此字段'}为必填项`);
|
|
1020
|
+
}
|
|
1021
|
+
if (field.rules) {
|
|
1022
|
+
if (Array.isArray(field.rules)) {
|
|
1023
|
+
rules.push(...field.rules);
|
|
1024
|
+
} else if (typeof field.rules === 'string') {
|
|
1025
|
+
const ruleNames = field.rules.split('|');
|
|
1026
|
+
ruleNames.forEach(name => {
|
|
1027
|
+
const trimmedName = name.trim();
|
|
1028
|
+
if (this.validationRules[trimmedName]) {
|
|
1029
|
+
rules.push(...this.validationRules[trimmedName]);
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return rules;
|
|
1035
|
+
},
|
|
1036
|
+
|
|
1037
|
+
// --- 事件处理 ---
|
|
1038
|
+
|
|
1039
|
+
handleInput(key, value) {
|
|
1040
|
+
this.$set(this.formData, key, value);
|
|
1041
|
+
this.$emit('input', this.formData);
|
|
1042
|
+
this.$emit('field-input', { key, value, formData: this.formData });
|
|
441
1043
|
},
|
|
442
1044
|
|
|
443
|
-
|
|
444
|
-
this.$
|
|
1045
|
+
handleChange(key, value) {
|
|
1046
|
+
this.$set(this.formData, key, value);
|
|
1047
|
+
this.$emit('input', this.formData);
|
|
1048
|
+
this.$emit('change', this.formData);
|
|
445
1049
|
this.$emit('field-change', { key, value, formData: this.formData });
|
|
446
1050
|
},
|
|
1051
|
+
|
|
1052
|
+
handleColorInput(field, value) {
|
|
1053
|
+
this.handleInput(field.key, value);
|
|
1054
|
+
if (field.closeOnSelect !== false) {
|
|
1055
|
+
this.$set(this.colorMenus, field.key, false);
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
447
1058
|
|
|
448
|
-
|
|
1059
|
+
handleBlur(key, value) {
|
|
449
1060
|
this.$emit('blur', key, value, this.formData);
|
|
1061
|
+
this.$emit('field-blur', { key, value, formData: this.formData });
|
|
450
1062
|
},
|
|
451
1063
|
|
|
452
|
-
// 更新字段值(供插槽使用)
|
|
453
1064
|
updateField(key, value) {
|
|
454
1065
|
this.$set(this.formData, key, value);
|
|
455
|
-
this.
|
|
1066
|
+
this.handleChange(key, value);
|
|
456
1067
|
},
|
|
457
1068
|
|
|
458
|
-
//
|
|
1069
|
+
// --- 依赖处理 ---
|
|
1070
|
+
|
|
1071
|
+
setupFieldDependencies() {
|
|
1072
|
+
this.fields.forEach(field => {
|
|
1073
|
+
if (field.dependencies && Array.isArray(field.dependencies)) {
|
|
1074
|
+
field.dependencies.forEach(depKey => {
|
|
1075
|
+
const unwatch = this.$watch(
|
|
1076
|
+
() => this.formData[depKey],
|
|
1077
|
+
(newVal, oldVal) => {
|
|
1078
|
+
if (newVal !== oldVal) {
|
|
1079
|
+
this.handleDependencyChange(field, depKey, newVal, oldVal);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
);
|
|
1083
|
+
this.dependencyWatchers.push(unwatch);
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
},
|
|
1088
|
+
|
|
1089
|
+
setupDependencyWatchers(deps) {
|
|
1090
|
+
// 这里的 dependencies prop 是全局依赖监听,不同于 field.dependencies
|
|
1091
|
+
// 为了保持兼容性,我们保留这个功能
|
|
1092
|
+
// 实际上之前的 JhFormFields 实现是先清除再添加,这里照做
|
|
1093
|
+
// 但注意不要清除了 field dependencies,所以这里应该分开管理?
|
|
1094
|
+
// 之前的代码中 dependencyWatchers 混用了。
|
|
1095
|
+
// 我们这里简单处理:全部 push 到 dependencyWatchers,销毁时一起销毁。
|
|
1096
|
+
// 但是如果 deps 变化了,我们需要只清除这部分的 watcher。
|
|
1097
|
+
// 鉴于复杂性,且通常 dependencies prop 不会动态变化,我们暂且简化。
|
|
1098
|
+
// 如果必须支持动态,需要更复杂的管理。这里假设 dependencies prop 主要用于监听某些字段变化并发射事件。
|
|
1099
|
+
|
|
1100
|
+
deps.forEach(depKey => {
|
|
1101
|
+
const unwatch = this.$watch(
|
|
1102
|
+
() => this.formData[depKey],
|
|
1103
|
+
(newVal, oldVal) => {
|
|
1104
|
+
if (newVal !== oldVal) {
|
|
1105
|
+
this.$emit('dependency-change', { key: depKey, value: newVal, oldValue: oldVal, formData: this.formData });
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
);
|
|
1109
|
+
this.dependencyWatchers.push(unwatch);
|
|
1110
|
+
});
|
|
1111
|
+
},
|
|
1112
|
+
|
|
1113
|
+
handleDependencyChange(field, depKey, newVal, oldVal) {
|
|
1114
|
+
if (typeof field.onDependencyChange === 'function') {
|
|
1115
|
+
field.onDependencyChange(depKey, newVal, oldVal, this.formData);
|
|
1116
|
+
}
|
|
1117
|
+
this.$emit('field-dependency-change', {
|
|
1118
|
+
field: field.key,
|
|
1119
|
+
dependency: depKey,
|
|
1120
|
+
value: newVal,
|
|
1121
|
+
oldValue: oldVal,
|
|
1122
|
+
formData: this.formData,
|
|
1123
|
+
});
|
|
1124
|
+
},
|
|
1125
|
+
|
|
1126
|
+
// --- 表单操作 API ---
|
|
1127
|
+
|
|
459
1128
|
getForm() {
|
|
460
|
-
return this.$refs
|
|
1129
|
+
return this.$refs.form;
|
|
461
1130
|
},
|
|
462
1131
|
|
|
463
|
-
// 转发 v-form
|
|
1132
|
+
// 转发 v-form 的方法
|
|
464
1133
|
...(() => {
|
|
465
1134
|
const methods = {};
|
|
466
|
-
// 定义需要转发的 v-form 方法
|
|
467
1135
|
const formMethods = ['validate', 'reset', 'resetValidation'];
|
|
468
|
-
|
|
469
1136
|
formMethods.forEach(methodName => {
|
|
470
1137
|
methods[methodName] = function(...args) {
|
|
471
|
-
const form = this.$refs
|
|
1138
|
+
const form = this.$refs.form;
|
|
472
1139
|
if (form && typeof form[methodName] === 'function') {
|
|
473
1140
|
return form[methodName](...args);
|
|
474
1141
|
}
|
|
475
|
-
// 如果 v-form 不存在或方法不存在,返回默认值
|
|
476
1142
|
if (methodName === 'validate') return true;
|
|
477
1143
|
return undefined;
|
|
478
1144
|
};
|
|
479
1145
|
});
|
|
480
|
-
|
|
481
1146
|
return methods;
|
|
482
1147
|
})(),
|
|
483
1148
|
|
|
484
|
-
// 获取表单数据
|
|
485
1149
|
getFormData() {
|
|
486
1150
|
return { ...this.formData };
|
|
487
1151
|
},
|
|
488
1152
|
|
|
489
|
-
// 设置表单数据
|
|
490
1153
|
setFieldsValue(values) {
|
|
491
1154
|
this.formData = { ...this.formData, ...values };
|
|
492
1155
|
},
|
|
493
1156
|
|
|
494
|
-
// 设置单个字段值
|
|
495
1157
|
setFieldValue(key, value) {
|
|
496
1158
|
this.$set(this.formData, key, value);
|
|
497
1159
|
},
|
|
498
1160
|
|
|
499
|
-
//
|
|
1161
|
+
// 覆盖 v-form 的 reset,同时重置数据
|
|
500
1162
|
reset() {
|
|
501
|
-
const form = this.$refs
|
|
502
|
-
if (form)
|
|
503
|
-
form.reset();
|
|
504
|
-
}
|
|
1163
|
+
const form = this.$refs.form;
|
|
1164
|
+
if (form) form.reset();
|
|
505
1165
|
this.initFormData();
|
|
506
1166
|
this.$emit('reset', this.formData);
|
|
507
1167
|
},
|
|
508
1168
|
|
|
509
|
-
// 重置表单(别名,保持向后兼容)
|
|
510
1169
|
resetForm() {
|
|
511
1170
|
this.reset();
|
|
512
1171
|
},
|
|
513
1172
|
|
|
514
|
-
// 重置表单验证 - 与 v-form 的 resetValidation() 方法保持一致
|
|
515
|
-
resetValidation() {
|
|
516
|
-
const form = this.$refs[this.formRef];
|
|
517
|
-
if (form) {
|
|
518
|
-
form.resetValidation();
|
|
519
|
-
}
|
|
520
|
-
},
|
|
521
|
-
|
|
522
|
-
// 验证表单 - 与 v-form 的 validate() 方法保持一致
|
|
523
1173
|
async validate() {
|
|
524
|
-
const form = this.$refs
|
|
1174
|
+
const form = this.$refs.form;
|
|
525
1175
|
if (form) {
|
|
526
1176
|
const isValid = await form.validate();
|
|
527
1177
|
this.$emit('validate', isValid, this.formData);
|
|
@@ -530,19 +1180,7 @@ export default {
|
|
|
530
1180
|
return true;
|
|
531
1181
|
},
|
|
532
1182
|
|
|
533
|
-
//
|
|
534
|
-
async validateField(fieldName) {
|
|
535
|
-
const form = this.$refs[this.formRef];
|
|
536
|
-
if (form && form.$refs && form.$refs[fieldName]) {
|
|
537
|
-
const field = form.$refs[fieldName];
|
|
538
|
-
if (field && typeof field.validate === 'function') {
|
|
539
|
-
return await field.validate();
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
return true;
|
|
543
|
-
},
|
|
544
|
-
|
|
545
|
-
// 提交表单(增强版,支持加载状态和错误处理)
|
|
1183
|
+
// 提交表单
|
|
546
1184
|
async submit(options = {}) {
|
|
547
1185
|
const {
|
|
548
1186
|
validate = true,
|
|
@@ -551,36 +1189,21 @@ export default {
|
|
|
551
1189
|
resetError = true
|
|
552
1190
|
} = options;
|
|
553
1191
|
|
|
554
|
-
|
|
555
|
-
if (
|
|
556
|
-
this.submitError = null;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// 显示加载状态
|
|
560
|
-
if (showLoading) {
|
|
561
|
-
this.submitLoading = true;
|
|
562
|
-
}
|
|
1192
|
+
if (resetError) this.submitError = null;
|
|
1193
|
+
if (showLoading) this.submitLoading = true;
|
|
563
1194
|
|
|
564
1195
|
try {
|
|
565
|
-
// 验证表单
|
|
566
1196
|
if (validate) {
|
|
567
1197
|
const isValid = await this.validate();
|
|
568
1198
|
if (!isValid) {
|
|
569
|
-
|
|
570
|
-
if (this.onFinishFailed) {
|
|
571
|
-
this.onFinishFailed(this.formData);
|
|
572
|
-
}
|
|
1199
|
+
if (this.onFinishFailed) this.onFinishFailed(this.formData);
|
|
573
1200
|
return false;
|
|
574
1201
|
}
|
|
575
1202
|
}
|
|
576
1203
|
|
|
577
|
-
// 转换数据
|
|
578
1204
|
const transformedData = transform ? this.getTransformedData() : { ...this.formData };
|
|
579
|
-
|
|
580
|
-
// 触发 submit 事件
|
|
581
1205
|
this.$emit('submit', transformedData);
|
|
582
1206
|
|
|
583
|
-
// 调用 onFinish 回调
|
|
584
1207
|
if (this.onFinish) {
|
|
585
1208
|
await this.onFinish(transformedData);
|
|
586
1209
|
}
|
|
@@ -592,61 +1215,18 @@ export default {
|
|
|
592
1215
|
this.$emit('submit-error', error);
|
|
593
1216
|
return false;
|
|
594
1217
|
} finally {
|
|
595
|
-
|
|
596
|
-
if (showLoading) {
|
|
597
|
-
this.submitLoading = false;
|
|
598
|
-
}
|
|
1218
|
+
if (showLoading) this.submitLoading = false;
|
|
599
1219
|
}
|
|
600
1220
|
},
|
|
601
|
-
|
|
602
|
-
// 验证单个字段(增强版)
|
|
603
|
-
async validateField(fieldName) {
|
|
604
|
-
// 首先尝试通过 v-form 验证
|
|
605
|
-
const form = this.$refs[this.formRef];
|
|
606
|
-
if (form && form.validate) {
|
|
607
|
-
// v-form 的 validate 方法会验证所有字段
|
|
608
|
-
// 这里我们返回整体验证结果
|
|
609
|
-
return await this.validate();
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// 如果 v-form 不存在,返回 true
|
|
613
|
-
return true;
|
|
614
|
-
},
|
|
615
|
-
|
|
616
|
-
// 获取表单状态
|
|
617
|
-
getFormState() {
|
|
618
|
-
return {
|
|
619
|
-
submitLoading: this.submitLoading,
|
|
620
|
-
submitError: this.submitError,
|
|
621
|
-
formData: { ...this.formData }
|
|
622
|
-
};
|
|
623
|
-
},
|
|
624
|
-
|
|
625
|
-
// 清除提交错误
|
|
626
|
-
clearSubmitError() {
|
|
627
|
-
this.submitError = null;
|
|
628
|
-
},
|
|
629
|
-
|
|
630
|
-
// 批量更新字段值
|
|
631
|
-
batchUpdateFields(values) {
|
|
632
|
-
Object.keys(values).forEach(key => {
|
|
633
|
-
this.$set(this.formData, key, values[key]);
|
|
634
|
-
this.handleFieldChange({ key, value: values[key] });
|
|
635
|
-
});
|
|
636
|
-
},
|
|
637
1221
|
|
|
638
|
-
// 获取转换后的数据
|
|
639
1222
|
getTransformedData() {
|
|
640
1223
|
const data = { ...this.formData };
|
|
641
|
-
|
|
642
|
-
// 应用字段级别的 transform
|
|
643
1224
|
this.fields.forEach(field => {
|
|
644
1225
|
if (field.key && field.transform && typeof field.transform === 'function') {
|
|
645
1226
|
data[field.key] = field.transform(data[field.key], data);
|
|
646
1227
|
}
|
|
647
1228
|
});
|
|
648
1229
|
|
|
649
|
-
// 忽略 null/undefined 值
|
|
650
1230
|
if (this.omitNil) {
|
|
651
1231
|
Object.keys(data).forEach(key => {
|
|
652
1232
|
if (data[key] === null || data[key] === undefined || data[key] === '') {
|
|
@@ -657,56 +1237,59 @@ export default {
|
|
|
657
1237
|
|
|
658
1238
|
return data;
|
|
659
1239
|
},
|
|
660
|
-
|
|
661
|
-
// 获取字段的依赖值
|
|
662
|
-
getDependenciesValues(dependencies) {
|
|
663
|
-
if (!dependencies || !Array.isArray(dependencies)) return [];
|
|
664
|
-
return dependencies.map(key => this.formData[key]);
|
|
665
|
-
},
|
|
666
|
-
|
|
667
|
-
// 检查依赖是否变化
|
|
668
|
-
checkDependenciesChanged(field, oldValues, newValues) {
|
|
669
|
-
if (!field.dependencies) return false;
|
|
670
|
-
|
|
671
|
-
for (let i = 0; i < field.dependencies.length; i++) {
|
|
672
|
-
if (oldValues[i] !== newValues[i]) {
|
|
673
|
-
return true;
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
return false;
|
|
677
|
-
},
|
|
678
1240
|
},
|
|
679
1241
|
};
|
|
680
1242
|
</script>
|
|
681
1243
|
|
|
682
1244
|
<style scoped>
|
|
683
|
-
/* 表单布局样式 */
|
|
684
1245
|
.jh-form {
|
|
685
1246
|
width: 100%;
|
|
686
1247
|
}
|
|
687
1248
|
|
|
688
|
-
|
|
1249
|
+
.jh-form--bordered {
|
|
1250
|
+
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
1251
|
+
border-radius: 4px;
|
|
1252
|
+
padding: 15px 16px 30px;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
.jh-form-header {
|
|
1256
|
+
margin-bottom: 16px;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
.jh-form-title {
|
|
1260
|
+
font-size: 16px;
|
|
1261
|
+
font-weight: 500;
|
|
1262
|
+
color: rgba(0, 0, 0, 0.85);
|
|
1263
|
+
display: flex;
|
|
1264
|
+
align-items: center;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
.jh-form-description {
|
|
1268
|
+
font-size: 14px;
|
|
1269
|
+
color: rgba(0, 0, 0, 0.45);
|
|
1270
|
+
margin-top: 4px;
|
|
1271
|
+
line-height: 1.5;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/* Horizontal Layout */
|
|
689
1275
|
.jh-form--horizontal .jh-field-label--horizontal {
|
|
690
1276
|
padding-top: 0;
|
|
691
1277
|
line-height: 1.5;
|
|
692
1278
|
}
|
|
693
1279
|
|
|
694
|
-
/*
|
|
1280
|
+
/* Inline Layout */
|
|
695
1281
|
.jh-form--inline .v-input {
|
|
696
1282
|
margin-bottom: 0 !important;
|
|
697
1283
|
}
|
|
698
1284
|
|
|
699
|
-
/*
|
|
1285
|
+
/* Readonly Mode */
|
|
700
1286
|
.jh-form--readonly {
|
|
701
|
-
/* 只读模式整体样式 */
|
|
702
1287
|
.jh-field-label {
|
|
703
|
-
/* 只读模式下的标签样式 */
|
|
704
1288
|
color: rgba(0, 0, 0, 0.65);
|
|
705
1289
|
font-weight: 500;
|
|
706
1290
|
}
|
|
707
1291
|
|
|
708
1292
|
.jh-form-readonly-text {
|
|
709
|
-
/* 只读模式下的文本样式 */
|
|
710
1293
|
padding: 6px 12px;
|
|
711
1294
|
color: rgba(0, 0, 0, 0.87);
|
|
712
1295
|
background-color: rgba(0, 0, 0, 0.04);
|
|
@@ -715,33 +1298,12 @@ export default {
|
|
|
715
1298
|
transition: all 0.2s ease;
|
|
716
1299
|
}
|
|
717
1300
|
|
|
718
|
-
|
|
719
|
-
&.jh-form--
|
|
720
|
-
|
|
721
|
-
margin-bottom: 16px;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
/* 垂直布局下的调整 */
|
|
726
|
-
&.jh-form--vertical {
|
|
727
|
-
.jh-field-wrapper {
|
|
728
|
-
margin-bottom: 16px;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
/* 网格布局下的调整 */
|
|
733
|
-
&.jh-form--grid {
|
|
734
|
-
.jh-field-wrapper {
|
|
735
|
-
margin-bottom: 16px;
|
|
736
|
-
}
|
|
1301
|
+
&.jh-form--horizontal .jh-field-wrapper,
|
|
1302
|
+
&.jh-form--vertical .jh-field-wrapper {
|
|
1303
|
+
margin-bottom: 16px;
|
|
737
1304
|
}
|
|
738
1305
|
}
|
|
739
1306
|
|
|
740
|
-
/* 字段标签 */
|
|
741
|
-
.jh-field-label {
|
|
742
|
-
|
|
743
|
-
}
|
|
744
|
-
|
|
745
1307
|
.jh-field-label--horizontal {
|
|
746
1308
|
padding-right: 12px;
|
|
747
1309
|
}
|
|
@@ -750,7 +1312,6 @@ export default {
|
|
|
750
1312
|
display: block;
|
|
751
1313
|
}
|
|
752
1314
|
|
|
753
|
-
/* 表单分组标题 */
|
|
754
1315
|
.jh-form-group-title {
|
|
755
1316
|
margin-top: 8px;
|
|
756
1317
|
margin-bottom: 16px;
|
|
@@ -761,24 +1322,27 @@ export default {
|
|
|
761
1322
|
font-weight: 500;
|
|
762
1323
|
}
|
|
763
1324
|
|
|
764
|
-
/* 字段包装器 */
|
|
765
1325
|
.jh-field-wrapper {
|
|
766
1326
|
width: 100%;
|
|
767
1327
|
}
|
|
768
1328
|
|
|
769
|
-
/* 字段输入区域 */
|
|
770
1329
|
.jh-field-input {
|
|
771
1330
|
width: 100%;
|
|
772
1331
|
}
|
|
773
1332
|
|
|
774
|
-
/* 只读文本 */
|
|
775
1333
|
.jh-form-readonly-text {
|
|
776
1334
|
word-break: break-word;
|
|
777
1335
|
}
|
|
778
1336
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1337
|
+
.jh-color-preview {
|
|
1338
|
+
display: inline-block;
|
|
1339
|
+
width: 20px;
|
|
1340
|
+
height: 20px;
|
|
1341
|
+
border-radius: 4px;
|
|
1342
|
+
border: 1px solid rgba(0, 0, 0, 0.2);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
.jh-form-actions-wrapper {
|
|
1346
|
+
margin-top: 16px;
|
|
783
1347
|
}
|
|
784
|
-
</style>
|
|
1348
|
+
</style>
|