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.
Files changed (49) hide show
  1. package/dist/jianghu-ui.css +195 -132
  2. package/dist/jianghu-ui.js +1 -1
  3. package/package.json +1 -1
  4. package/src/components/JhDrawer/JhDrawer.stories.js +6 -6
  5. package/src/components/JhDrawer/JhDrawer.vue +7 -1
  6. package/src/components/JhDrawerForm/JhDrawerForm.stories.js +161 -0
  7. package/src/components/JhDrawerForm/JhDrawerForm.vue +1 -1
  8. package/src/components/JhForm/JhForm.stories.js +114 -95
  9. package/src/components/JhForm/JhForm.vue +896 -205
  10. package/src/components/JhFormFields/JhFormFields.vue +42 -16
  11. package/src/components/JhModal/JhModal.stories.js +6 -6
  12. package/src/components/JhModal/JhModal.vue +1 -1
  13. package/src/components/JhModalForm/JhModalForm.vue +1 -1
  14. package/src/components/JhTable/JhTable.stories.js +134 -167
  15. package/src/components/JhTable/JhTable.vue +83 -23
  16. package/src/style/globalCSSVuetifyV4.css +1 -2
  17. package/src/components/JhAddressSelect/JhAddressSelect.md +0 -267
  18. package/src/components/JhCard/JhCard.md +0 -246
  19. package/src/components/JhCheckCard/JhCheckCard.md +0 -245
  20. package/src/components/JhConfirmDialog/JhConfirmDialog.md +0 -70
  21. package/src/components/JhDateRangePicker/JhDateRangePicker.md +0 -56
  22. package/src/components/JhDescriptions/JhDescriptions.md +0 -724
  23. package/src/components/JhDraggable/JhDraggable.md +0 -66
  24. package/src/components/JhDrawer/JhDrawer.md +0 -68
  25. package/src/components/JhDrawerForm/JhDrawerForm.md +0 -69
  26. package/src/components/JhEditableTable/JhEditableTable.md +0 -507
  27. package/src/components/JhFileInput/JhFileInput.md +0 -56
  28. package/src/components/JhForm/JhForm.md +0 -676
  29. package/src/components/JhFormFields/JhFormFields.md +0 -647
  30. package/src/components/JhFormList/JhFormList.md +0 -303
  31. package/src/components/JhJsonEditor/JhJsonEditor.md +0 -54
  32. package/src/components/JhLayout/JhLayout.md +0 -580
  33. package/src/components/JhList/JhList.md +0 -441
  34. package/src/components/JhMarkdownEditor/JhMarkdownEditor.md +0 -56
  35. package/src/components/JhMask/JhMask.md +0 -62
  36. package/src/components/JhMenu/JhMenu.md +0 -85
  37. package/src/components/JhModal/JhModal.md +0 -68
  38. package/src/components/JhModalForm/JhModalForm.md +0 -69
  39. package/src/components/JhPageContainer/JhPageContainer.md +0 -409
  40. package/src/components/JhQueryFilter/JhQueryFilter.md +0 -77
  41. package/src/components/JhScene/JhScene.md +0 -64
  42. package/src/components/JhStatisticCard/JhStatisticCard.md +0 -363
  43. package/src/components/JhStepsForm/JhStepsForm.md +0 -666
  44. package/src/components/JhTable/JhTable.md +0 -730
  45. package/src/components/JhTableAttachment/JhTableAttachment.md +0 -70
  46. package/src/components/JhToast/JhToast.md +0 -67
  47. package/src/components/JhTreeSelect/JhTreeSelect.md +0 -82
  48. package/src/components/JhWaterMark/JhWaterMark.md +0 -190
  49. 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
- <jh-form-fields
10
- v-model="formData"
11
- :fields="normalizedFields"
12
- :layout="internalLayout"
13
- :show-labels="showLabels"
14
- :label-width="labelWidth"
15
- :label-align="labelAlign"
16
- :show-required-mark="showRequiredMark"
17
- :readonly="readonly"
18
- :disabled="disabled"
19
- :default-dense="defaultDense"
20
- :default-filled="defaultFilled"
21
- :default-outlined="defaultOutlined"
22
- :default-single-line="defaultSingleLine"
23
- :default-cols-md="defaultColsMd"
24
- :hide-details="hideDetails"
25
- :label-class="labelClass"
26
- :input-class="inputClass"
27
- :row-class="rowClass"
28
- :validation-rules="validationRules"
29
- :row-props="gridRowProps"
30
- @field-input="handleFieldInput"
31
- @field-change="handleFieldChange"
32
- @field-blur="handleFieldBlur"
33
- >
34
- <template v-for="field in slotFields" v-slot:[`field-${field.key}`]="slotProps">
35
- <slot :name="`field-${field.key}`" v-bind="slotProps"></slot>
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
- <template v-if="isGridLayout">
40
- <v-row class="jh-form-grid-actions-row" v-bind="rowProps">
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
- </v-row>
45
- </template>
46
- <template v-else>
47
- <slot name="actions" :formData="formData" :validate="validate" :resetForm="resetForm"></slot>
48
- </template>
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: 'jhForm',
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
- // 需要转交给 JhFormFields 的字段配置
274
- normalizedFields() {
275
- if (!this.isGridLayout) {
276
- return this.fields;
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
- if (!Object.keys(colConfig).length) {
294
- return { ...field };
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
- const existing = field.cols && typeof field.cols === 'object' ? field.cols : {};
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.isGridLayout ? (this.rowProps || {}) : {};
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
- // 只排除组件内部明确处理的事件,其他所有事件都透传给 v-form
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
- initialData: {
790
+ value: {
372
791
  handler(val) {
373
- this.formData = { ...val };
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 data = { ...this.initialData };
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
- const mappedCols = span !== undefined ? this.mapGridSpan(span) : null;
415
-
416
- if (mappedCols !== null && !('cols' in bindings || 'sm' in bindings || 'md' in bindings || 'lg' in bindings || 'xl' in bindings)) {
417
- bindings.md = mappedCols;
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
- bindings.md = this.mapGridSpan(this.colProps.span || 8);
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
- handleFieldInput({ key, value }) {
436
- this.$emit('input', key, value, this.formData);
437
- this.$emit('field-change', { key, value, formData: this.formData });
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
- handleFieldChange({ key, value }) {
441
- this.$emit('change', key, value, this.formData);
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
- handleFieldBlur({ key, value }) {
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.handleFieldChange({ key, value });
1066
+ this.handleChange(key, value);
453
1067
  },
454
1068
 
455
- // 获取表单引用(供父组件调用) - 与 v-form 保持一致
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[this.formRef];
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
- // 重置表单 - 与 v-form 的 reset() 方法保持一致
1161
+ // 覆盖 v-form 的 reset,同时重置数据
476
1162
  reset() {
477
- const form = this.$refs[this.formRef];
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[this.formRef];
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 isValid = await this.validate();
524
- if (isValid) {
525
- const transformedData = this.getTransformedData();
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
- try {
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 .jh-form-readonly-text {
608
- padding: 8px 0;
609
- min-height: 40px;
610
- color: rgba(0, 0, 0, 0.87);
611
- }
612
-
613
- /* 字段标签 */
614
- .jh-field-label {
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
- .jh-field-extra {
654
- margin-top: -8px;
655
- line-height: 1.5;
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>