jianghu-ui 1.0.6 → 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 +195 -132
- package/dist/jianghu-ui.js +1 -1
- package/package.json +1 -1
- package/src/components/JhDrawer/JhDrawer.stories.js +6 -6
- package/src/components/JhDrawer/JhDrawer.vue +7 -1
- package/src/components/JhDrawerForm/JhDrawerForm.stories.js +161 -0
- package/src/components/JhDrawerForm/JhDrawerForm.vue +1 -1
- package/src/components/JhForm/JhForm.stories.js +114 -95
- package/src/components/JhForm/JhForm.vue +896 -205
- package/src/components/JhFormFields/JhFormFields.vue +42 -16
- package/src/components/JhModal/JhModal.stories.js +6 -6
- package/src/components/JhModal/JhModal.vue +1 -1
- package/src/components/JhModalForm/JhModalForm.vue +1 -1
- package/src/components/JhTable/JhTable.stories.js +134 -167
- package/src/components/JhTable/JhTable.vue +83 -23
- package/src/style/globalCSSVuetifyV4.css +1 -2
- package/src/components/JhAddressSelect/JhAddressSelect.md +0 -267
- package/src/components/JhCard/JhCard.md +0 -246
- package/src/components/JhCheckCard/JhCheckCard.md +0 -245
- package/src/components/JhConfirmDialog/JhConfirmDialog.md +0 -70
- package/src/components/JhDateRangePicker/JhDateRangePicker.md +0 -56
- package/src/components/JhDescriptions/JhDescriptions.md +0 -724
- package/src/components/JhDraggable/JhDraggable.md +0 -66
- package/src/components/JhDrawer/JhDrawer.md +0 -68
- package/src/components/JhDrawerForm/JhDrawerForm.md +0 -69
- package/src/components/JhEditableTable/JhEditableTable.md +0 -507
- package/src/components/JhFileInput/JhFileInput.md +0 -56
- package/src/components/JhForm/JhForm.md +0 -676
- package/src/components/JhFormFields/JhFormFields.md +0 -647
- package/src/components/JhFormList/JhFormList.md +0 -303
- package/src/components/JhJsonEditor/JhJsonEditor.md +0 -54
- package/src/components/JhLayout/JhLayout.md +0 -580
- package/src/components/JhList/JhList.md +0 -441
- package/src/components/JhMarkdownEditor/JhMarkdownEditor.md +0 -56
- package/src/components/JhMask/JhMask.md +0 -62
- package/src/components/JhMenu/JhMenu.md +0 -85
- package/src/components/JhModal/JhModal.md +0 -68
- package/src/components/JhModalForm/JhModalForm.md +0 -69
- package/src/components/JhPageContainer/JhPageContainer.md +0 -409
- package/src/components/JhQueryFilter/JhQueryFilter.md +0 -77
- package/src/components/JhScene/JhScene.md +0 -64
- package/src/components/JhStatisticCard/JhStatisticCard.md +0 -363
- package/src/components/JhStepsForm/JhStepsForm.md +0 -666
- package/src/components/JhTable/JhTable.md +0 -730
- package/src/components/JhTableAttachment/JhTableAttachment.md +0 -70
- package/src/components/JhToast/JhToast.md +0 -67
- package/src/components/JhTreeSelect/JhTreeSelect.md +0 -82
- package/src/components/JhWaterMark/JhWaterMark.md +0 -190
- package/src/components/README.md +0 -52
|
@@ -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],
|
|
@@ -246,11 +615,51 @@ 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() {
|
|
252
652
|
return {
|
|
253
653
|
formData: {},
|
|
654
|
+
// 表单提交状态
|
|
655
|
+
submitLoading: false,
|
|
656
|
+
submitError: null,
|
|
657
|
+
|
|
658
|
+
// 字段状态
|
|
659
|
+
dateMenus: {},
|
|
660
|
+
timeMenus: {},
|
|
661
|
+
colorMenus: {},
|
|
662
|
+
dependencyWatchers: [],
|
|
254
663
|
};
|
|
255
664
|
},
|
|
256
665
|
|
|
@@ -259,6 +668,7 @@ export default {
|
|
|
259
668
|
formClasses() {
|
|
260
669
|
return {
|
|
261
670
|
'jh-form': true,
|
|
671
|
+
'jh-form--bordered': this.bordered,
|
|
262
672
|
[`jh-form--${this.layout}`]: true,
|
|
263
673
|
'jh-form--readonly': this.readonly,
|
|
264
674
|
'jh-form--disabled': this.disabled,
|
|
@@ -269,18 +679,40 @@ export default {
|
|
|
269
679
|
isGridLayout() {
|
|
270
680
|
return this.grid || this.layout === 'grid';
|
|
271
681
|
},
|
|
682
|
+
|
|
683
|
+
// 内部布局模式
|
|
684
|
+
internalLayout() {
|
|
685
|
+
return this.isGridLayout ? 'vertical' : this.layout;
|
|
686
|
+
},
|
|
272
687
|
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
},
|
|
278
703
|
|
|
704
|
+
// 标准化字段配置 (处理 Grid 列配置)
|
|
705
|
+
normalizedFields() {
|
|
279
706
|
return this.fields.map(field => {
|
|
280
707
|
if (field.type === 'group') {
|
|
281
708
|
return { ...field };
|
|
282
709
|
}
|
|
283
710
|
|
|
711
|
+
// 如果不是 grid 布局,且没有显式指定 cols,则保持原样 (render 时会默认占满或 auto)
|
|
712
|
+
if (!this.isGridLayout && !field.cols) {
|
|
713
|
+
return { ...field };
|
|
714
|
+
}
|
|
715
|
+
|
|
284
716
|
const bindings = this.getColBindings(field);
|
|
285
717
|
const colConfig = {};
|
|
286
718
|
|
|
@@ -289,47 +721,40 @@ export default {
|
|
|
289
721
|
if (bindings.md !== undefined) colConfig.md = bindings.md;
|
|
290
722
|
if (bindings.lg !== undefined) colConfig.lg = bindings.lg;
|
|
291
723
|
if (bindings.xl !== undefined) colConfig.xl = bindings.xl;
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
+
};
|
|
295
735
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
return {
|
|
299
|
-
...field,
|
|
300
|
-
cols: {
|
|
301
|
-
...existing,
|
|
302
|
-
...colConfig,
|
|
303
|
-
},
|
|
304
|
-
};
|
|
736
|
+
|
|
737
|
+
return { ...field };
|
|
305
738
|
});
|
|
306
739
|
},
|
|
307
740
|
|
|
308
|
-
internalLayout() {
|
|
309
|
-
return this.isGridLayout ? 'vertical' : this.layout;
|
|
310
|
-
},
|
|
311
|
-
|
|
312
|
-
slotFields() {
|
|
313
|
-
return this.fields.filter(field => field.type === 'slot');
|
|
314
|
-
},
|
|
315
|
-
|
|
316
741
|
gridRowProps() {
|
|
317
|
-
return this.
|
|
742
|
+
return this.rowProps || {};
|
|
318
743
|
},
|
|
319
|
-
|
|
744
|
+
|
|
745
|
+
// 合并透传属性
|
|
320
746
|
mergedFormProps() {
|
|
321
747
|
// 只排除组件内部明确处理的属性,其他所有属性都透传给 v-form
|
|
322
748
|
const excludedAttrs = [
|
|
323
749
|
'class', 'style',
|
|
324
|
-
// 这些属性在组件内部有特殊处理
|
|
325
750
|
'lazy-validation', 'lazyValidation',
|
|
326
|
-
// JhForm 特有的 props(不在 v-form 中)
|
|
327
751
|
'fields', 'initialData', 'formRef', 'layout', 'showLabels',
|
|
328
752
|
'labelPosition', 'labelWidth', 'labelAlign', 'showRequiredMark',
|
|
329
753
|
'readonly', 'disabled', 'defaultDense', 'defaultFilled', 'defaultOutlined',
|
|
330
754
|
'defaultSingleLine', 'defaultColsMd', 'hideDetails', 'labelClass',
|
|
331
755
|
'inputClass', 'rowClass', 'validationRules', 'submitter', 'onFinish',
|
|
332
|
-
'onFinishFailed', 'dateFormatter', 'omitNil', 'grid', 'colProps', 'rowProps'
|
|
756
|
+
'onFinishFailed', 'dateFormatter', 'omitNil', 'grid', 'colProps', 'rowProps',
|
|
757
|
+
'title', 'description', 'tooltip', 'bordered', 'dense', 'dependencies'
|
|
333
758
|
];
|
|
334
759
|
|
|
335
760
|
const { class: cls, style, ...rest } = this.$attrs || {};
|
|
@@ -337,7 +762,6 @@ export default {
|
|
|
337
762
|
|
|
338
763
|
Object.keys(rest).forEach(key => {
|
|
339
764
|
const keyKebab = key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
340
|
-
// 只排除明确处理的属性,其他都透传
|
|
341
765
|
if (!excludedAttrs.includes(key) && !excludedAttrs.includes(keyKebab)) {
|
|
342
766
|
filteredAttrs[key] = rest[key];
|
|
343
767
|
}
|
|
@@ -345,19 +769,14 @@ export default {
|
|
|
345
769
|
|
|
346
770
|
return filteredAttrs;
|
|
347
771
|
},
|
|
348
|
-
|
|
772
|
+
|
|
773
|
+
// 透传所有事件
|
|
349
774
|
formListeners() {
|
|
350
|
-
|
|
351
|
-
const excludedEvents = [
|
|
352
|
-
// JhForm 特有的事件(不在 v-form 中)
|
|
353
|
-
'submit', 'reset', 'validate', 'input', 'change', 'blur', 'field-change'
|
|
354
|
-
];
|
|
355
|
-
|
|
775
|
+
const excludedEvents = ['field-change', 'submit'];
|
|
356
776
|
const listeners = { ...this.$listeners || {} };
|
|
357
777
|
const filteredListeners = {};
|
|
358
778
|
|
|
359
779
|
Object.keys(listeners).forEach(key => {
|
|
360
|
-
// 只排除明确处理的事件,其他都透传
|
|
361
780
|
if (!excludedEvents.includes(key)) {
|
|
362
781
|
filteredListeners[key] = listeners[key];
|
|
363
782
|
}
|
|
@@ -368,24 +787,50 @@ export default {
|
|
|
368
787
|
},
|
|
369
788
|
|
|
370
789
|
watch: {
|
|
371
|
-
|
|
790
|
+
value: {
|
|
372
791
|
handler(val) {
|
|
373
|
-
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();
|
|
374
801
|
},
|
|
375
802
|
immediate: true,
|
|
376
803
|
deep: true,
|
|
377
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
|
+
},
|
|
378
817
|
},
|
|
379
818
|
|
|
380
819
|
mounted() {
|
|
381
|
-
// 初始化表单数据
|
|
382
820
|
this.initFormData();
|
|
821
|
+
this.setupFieldDependencies();
|
|
822
|
+
},
|
|
823
|
+
|
|
824
|
+
beforeDestroy() {
|
|
825
|
+
this.dependencyWatchers.forEach(unwatch => unwatch());
|
|
826
|
+
this.dependencyWatchers = [];
|
|
383
827
|
},
|
|
384
828
|
|
|
385
829
|
methods: {
|
|
386
830
|
// 初始化表单数据
|
|
387
831
|
initFormData() {
|
|
388
|
-
const
|
|
832
|
+
const sourceData = { ...this.initialData, ...this.value };
|
|
833
|
+
const data = { ...sourceData };
|
|
389
834
|
|
|
390
835
|
// 从 fields 中提取默认值
|
|
391
836
|
this.fields.forEach(field => {
|
|
@@ -399,9 +844,9 @@ export default {
|
|
|
399
844
|
|
|
400
845
|
// Grid 模式下合并列配置
|
|
401
846
|
getColBindings(field) {
|
|
847
|
+
// 只有在 grid 模式下或者显式传递了 colProps 时才生效
|
|
402
848
|
const bindings = { ...(this.colProps || {}), ...(field.colProps || {}) };
|
|
403
849
|
|
|
404
|
-
// 兼容字段 cols 写法
|
|
405
850
|
if (field.cols) {
|
|
406
851
|
if (typeof field.cols === 'object') {
|
|
407
852
|
Object.assign(bindings, field.cols);
|
|
@@ -411,16 +856,17 @@ export default {
|
|
|
411
856
|
}
|
|
412
857
|
|
|
413
858
|
const span = field.colSpan !== undefined ? field.colSpan : bindings.span;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
delete bindings.span;
|
|
421
|
-
|
|
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 配置
|
|
422
864
|
if (!('cols' in bindings || 'sm' in bindings || 'md' in bindings || 'lg' in bindings || 'xl' in bindings)) {
|
|
423
|
-
|
|
865
|
+
if (span !== undefined) {
|
|
866
|
+
bindings.md = this.mapGridSpan(span);
|
|
867
|
+
} else if (this.isGridLayout) {
|
|
868
|
+
bindings.md = this.defaultColsMd;
|
|
869
|
+
}
|
|
424
870
|
}
|
|
425
871
|
|
|
426
872
|
return bindings;
|
|
@@ -432,72 +878,300 @@ export default {
|
|
|
432
878
|
return Math.max(1, Math.min(12, mapped || 1));
|
|
433
879
|
},
|
|
434
880
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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;
|
|
438
918
|
},
|
|
439
919
|
|
|
440
|
-
|
|
441
|
-
|
|
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 });
|
|
1043
|
+
},
|
|
1044
|
+
|
|
1045
|
+
handleChange(key, value) {
|
|
1046
|
+
this.$set(this.formData, key, value);
|
|
1047
|
+
this.$emit('input', this.formData);
|
|
1048
|
+
this.$emit('change', this.formData);
|
|
442
1049
|
this.$emit('field-change', { key, value, formData: this.formData });
|
|
443
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
|
+
},
|
|
444
1058
|
|
|
445
|
-
|
|
1059
|
+
handleBlur(key, value) {
|
|
446
1060
|
this.$emit('blur', key, value, this.formData);
|
|
1061
|
+
this.$emit('field-blur', { key, value, formData: this.formData });
|
|
447
1062
|
},
|
|
448
1063
|
|
|
449
|
-
// 更新字段值(供插槽使用)
|
|
450
1064
|
updateField(key, value) {
|
|
451
1065
|
this.$set(this.formData, key, value);
|
|
452
|
-
this.
|
|
1066
|
+
this.handleChange(key, value);
|
|
453
1067
|
},
|
|
454
1068
|
|
|
455
|
-
//
|
|
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
|
+
|
|
456
1128
|
getForm() {
|
|
457
|
-
return this.$refs
|
|
1129
|
+
return this.$refs.form;
|
|
458
1130
|
},
|
|
1131
|
+
|
|
1132
|
+
// 转发 v-form 的方法
|
|
1133
|
+
...(() => {
|
|
1134
|
+
const methods = {};
|
|
1135
|
+
const formMethods = ['validate', 'reset', 'resetValidation'];
|
|
1136
|
+
formMethods.forEach(methodName => {
|
|
1137
|
+
methods[methodName] = function(...args) {
|
|
1138
|
+
const form = this.$refs.form;
|
|
1139
|
+
if (form && typeof form[methodName] === 'function') {
|
|
1140
|
+
return form[methodName](...args);
|
|
1141
|
+
}
|
|
1142
|
+
if (methodName === 'validate') return true;
|
|
1143
|
+
return undefined;
|
|
1144
|
+
};
|
|
1145
|
+
});
|
|
1146
|
+
return methods;
|
|
1147
|
+
})(),
|
|
459
1148
|
|
|
460
|
-
// 获取表单数据
|
|
461
1149
|
getFormData() {
|
|
462
1150
|
return { ...this.formData };
|
|
463
1151
|
},
|
|
464
1152
|
|
|
465
|
-
// 设置表单数据
|
|
466
1153
|
setFieldsValue(values) {
|
|
467
1154
|
this.formData = { ...this.formData, ...values };
|
|
468
1155
|
},
|
|
469
1156
|
|
|
470
|
-
// 设置单个字段值
|
|
471
1157
|
setFieldValue(key, value) {
|
|
472
1158
|
this.$set(this.formData, key, value);
|
|
473
1159
|
},
|
|
474
1160
|
|
|
475
|
-
//
|
|
1161
|
+
// 覆盖 v-form 的 reset,同时重置数据
|
|
476
1162
|
reset() {
|
|
477
|
-
const form = this.$refs
|
|
478
|
-
if (form)
|
|
479
|
-
form.reset();
|
|
480
|
-
}
|
|
1163
|
+
const form = this.$refs.form;
|
|
1164
|
+
if (form) form.reset();
|
|
481
1165
|
this.initFormData();
|
|
482
1166
|
this.$emit('reset', this.formData);
|
|
483
1167
|
},
|
|
484
1168
|
|
|
485
|
-
// 重置表单(别名,保持向后兼容)
|
|
486
1169
|
resetForm() {
|
|
487
1170
|
this.reset();
|
|
488
1171
|
},
|
|
489
1172
|
|
|
490
|
-
// 重置表单验证 - 与 v-form 的 resetValidation() 方法保持一致
|
|
491
|
-
resetValidation() {
|
|
492
|
-
const form = this.$refs[this.formRef];
|
|
493
|
-
if (form) {
|
|
494
|
-
form.resetValidation();
|
|
495
|
-
}
|
|
496
|
-
},
|
|
497
|
-
|
|
498
|
-
// 验证表单 - 与 v-form 的 validate() 方法保持一致
|
|
499
1173
|
async validate() {
|
|
500
|
-
const form = this.$refs
|
|
1174
|
+
const form = this.$refs.form;
|
|
501
1175
|
if (form) {
|
|
502
1176
|
const isValid = await form.validate();
|
|
503
1177
|
this.$emit('validate', isValid, this.formData);
|
|
@@ -506,54 +1180,53 @@ export default {
|
|
|
506
1180
|
return true;
|
|
507
1181
|
},
|
|
508
1182
|
|
|
509
|
-
// 验证单个字段 - 与 v-form 的 validate() 方法保持一致
|
|
510
|
-
async validateField(fieldName) {
|
|
511
|
-
const form = this.$refs[this.formRef];
|
|
512
|
-
if (form && form.$refs && form.$refs[fieldName]) {
|
|
513
|
-
const field = form.$refs[fieldName];
|
|
514
|
-
if (field && typeof field.validate === 'function') {
|
|
515
|
-
return await field.validate();
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
return true;
|
|
519
|
-
},
|
|
520
|
-
|
|
521
1183
|
// 提交表单
|
|
522
|
-
async submit() {
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
1184
|
+
async submit(options = {}) {
|
|
1185
|
+
const {
|
|
1186
|
+
validate = true,
|
|
1187
|
+
transform = true,
|
|
1188
|
+
showLoading = false,
|
|
1189
|
+
resetError = true
|
|
1190
|
+
} = options;
|
|
1191
|
+
|
|
1192
|
+
if (resetError) this.submitError = null;
|
|
1193
|
+
if (showLoading) this.submitLoading = true;
|
|
1194
|
+
|
|
1195
|
+
try {
|
|
1196
|
+
if (validate) {
|
|
1197
|
+
const isValid = await this.validate();
|
|
1198
|
+
if (!isValid) {
|
|
1199
|
+
if (this.onFinishFailed) this.onFinishFailed(this.formData);
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const transformedData = transform ? this.getTransformedData() : { ...this.formData };
|
|
526
1205
|
this.$emit('submit', transformedData);
|
|
527
1206
|
|
|
528
|
-
// 调用 onFinish 回调
|
|
529
1207
|
if (this.onFinish) {
|
|
530
|
-
|
|
531
|
-
await this.onFinish(transformedData);
|
|
532
|
-
} catch (error) {
|
|
533
|
-
console.error('Form submit error:', error);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
} else {
|
|
537
|
-
// 调用 onFinishFailed 回调
|
|
538
|
-
if (this.onFinishFailed) {
|
|
539
|
-
this.onFinishFailed(this.formData);
|
|
1208
|
+
await this.onFinish(transformedData);
|
|
540
1209
|
}
|
|
1210
|
+
|
|
1211
|
+
return true;
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
console.error('Form submit error:', error);
|
|
1214
|
+
this.submitError = error.message || '提交失败';
|
|
1215
|
+
this.$emit('submit-error', error);
|
|
1216
|
+
return false;
|
|
1217
|
+
} finally {
|
|
1218
|
+
if (showLoading) this.submitLoading = false;
|
|
541
1219
|
}
|
|
542
|
-
return isValid;
|
|
543
1220
|
},
|
|
544
1221
|
|
|
545
|
-
// 获取转换后的数据
|
|
546
1222
|
getTransformedData() {
|
|
547
1223
|
const data = { ...this.formData };
|
|
548
|
-
|
|
549
|
-
// 应用字段级别的 transform
|
|
550
1224
|
this.fields.forEach(field => {
|
|
551
1225
|
if (field.key && field.transform && typeof field.transform === 'function') {
|
|
552
1226
|
data[field.key] = field.transform(data[field.key], data);
|
|
553
1227
|
}
|
|
554
1228
|
});
|
|
555
1229
|
|
|
556
|
-
// 忽略 null/undefined 值
|
|
557
1230
|
if (this.omitNil) {
|
|
558
1231
|
Object.keys(data).forEach(key => {
|
|
559
1232
|
if (data[key] === null || data[key] === undefined || data[key] === '') {
|
|
@@ -564,55 +1237,71 @@ export default {
|
|
|
564
1237
|
|
|
565
1238
|
return data;
|
|
566
1239
|
},
|
|
567
|
-
|
|
568
|
-
// 获取字段的依赖值
|
|
569
|
-
getDependenciesValues(dependencies) {
|
|
570
|
-
if (!dependencies || !Array.isArray(dependencies)) return [];
|
|
571
|
-
return dependencies.map(key => this.formData[key]);
|
|
572
|
-
},
|
|
573
|
-
|
|
574
|
-
// 检查依赖是否变化
|
|
575
|
-
checkDependenciesChanged(field, oldValues, newValues) {
|
|
576
|
-
if (!field.dependencies) return false;
|
|
577
|
-
|
|
578
|
-
for (let i = 0; i < field.dependencies.length; i++) {
|
|
579
|
-
if (oldValues[i] !== newValues[i]) {
|
|
580
|
-
return true;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
return false;
|
|
584
|
-
},
|
|
585
1240
|
},
|
|
586
1241
|
};
|
|
587
1242
|
</script>
|
|
588
1243
|
|
|
589
1244
|
<style scoped>
|
|
590
|
-
/* 表单布局样式 */
|
|
591
1245
|
.jh-form {
|
|
592
1246
|
width: 100%;
|
|
593
1247
|
}
|
|
594
1248
|
|
|
595
|
-
|
|
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 */
|
|
596
1275
|
.jh-form--horizontal .jh-field-label--horizontal {
|
|
597
1276
|
padding-top: 0;
|
|
598
1277
|
line-height: 1.5;
|
|
599
1278
|
}
|
|
600
1279
|
|
|
601
|
-
/*
|
|
1280
|
+
/* Inline Layout */
|
|
602
1281
|
.jh-form--inline .v-input {
|
|
603
1282
|
margin-bottom: 0 !important;
|
|
604
1283
|
}
|
|
605
1284
|
|
|
606
|
-
/*
|
|
607
|
-
.jh-form--readonly
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
1285
|
+
/* Readonly Mode */
|
|
1286
|
+
.jh-form--readonly {
|
|
1287
|
+
.jh-field-label {
|
|
1288
|
+
color: rgba(0, 0, 0, 0.65);
|
|
1289
|
+
font-weight: 500;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
.jh-form-readonly-text {
|
|
1293
|
+
padding: 6px 12px;
|
|
1294
|
+
color: rgba(0, 0, 0, 0.87);
|
|
1295
|
+
background-color: rgba(0, 0, 0, 0.04);
|
|
1296
|
+
border-radius: 4px;
|
|
1297
|
+
line-height: 1.5;
|
|
1298
|
+
transition: all 0.2s ease;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
&.jh-form--horizontal .jh-field-wrapper,
|
|
1302
|
+
&.jh-form--vertical .jh-field-wrapper {
|
|
1303
|
+
margin-bottom: 16px;
|
|
1304
|
+
}
|
|
616
1305
|
}
|
|
617
1306
|
|
|
618
1307
|
.jh-field-label--horizontal {
|
|
@@ -623,7 +1312,6 @@ export default {
|
|
|
623
1312
|
display: block;
|
|
624
1313
|
}
|
|
625
1314
|
|
|
626
|
-
/* 表单分组标题 */
|
|
627
1315
|
.jh-form-group-title {
|
|
628
1316
|
margin-top: 8px;
|
|
629
1317
|
margin-bottom: 16px;
|
|
@@ -634,24 +1322,27 @@ export default {
|
|
|
634
1322
|
font-weight: 500;
|
|
635
1323
|
}
|
|
636
1324
|
|
|
637
|
-
/* 字段包装器 */
|
|
638
1325
|
.jh-field-wrapper {
|
|
639
1326
|
width: 100%;
|
|
640
1327
|
}
|
|
641
1328
|
|
|
642
|
-
/* 字段输入区域 */
|
|
643
1329
|
.jh-field-input {
|
|
644
1330
|
width: 100%;
|
|
645
1331
|
}
|
|
646
1332
|
|
|
647
|
-
/* 只读文本 */
|
|
648
1333
|
.jh-form-readonly-text {
|
|
649
1334
|
word-break: break-word;
|
|
650
1335
|
}
|
|
651
1336
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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;
|
|
656
1347
|
}
|
|
657
|
-
</style>
|
|
1348
|
+
</style>
|