jianghu-ui 1.0.1

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 (112) hide show
  1. package/README.md +376 -0
  2. package/dist/jianghu-ui.css +2318 -0
  3. package/dist/jianghu-ui.js +2 -0
  4. package/dist/jianghu-ui.js.LICENSE.txt +1 -0
  5. package/package.json +56 -0
  6. package/src/Design.stories.mdx +195 -0
  7. package/src/Introduction.stories.mdx +148 -0
  8. package/src/components/JhAddressSelect/JhAddressSelect.md +250 -0
  9. package/src/components/JhAddressSelect/JhAddressSelect.stories.js +282 -0
  10. package/src/components/JhAddressSelect/JhAddressSelect.vue +261 -0
  11. package/src/components/JhCard/JhCard.md +246 -0
  12. package/src/components/JhCard/JhCard.stories.js +688 -0
  13. package/src/components/JhCard/JhCard.vue +604 -0
  14. package/src/components/JhCheckCard/JhCheckCard.md +245 -0
  15. package/src/components/JhCheckCard/JhCheckCard.stories.js +750 -0
  16. package/src/components/JhCheckCard/JhCheckCard.vue +476 -0
  17. package/src/components/JhConfirmDialog/JhConfirmDialog.md +70 -0
  18. package/src/components/JhConfirmDialog/JhConfirmDialog.stories.js +550 -0
  19. package/src/components/JhConfirmDialog/JhConfirmDialog.vue +181 -0
  20. package/src/components/JhDateRangePicker/JhDateRangePicker.md +56 -0
  21. package/src/components/JhDateRangePicker/JhDateRangePicker.stories.js +320 -0
  22. package/src/components/JhDateRangePicker/JhDateRangePicker.vue +307 -0
  23. package/src/components/JhDescriptions/JhDescriptions.md +724 -0
  24. package/src/components/JhDescriptions/JhDescriptions.stories.js +858 -0
  25. package/src/components/JhDescriptions/JhDescriptions.vue +933 -0
  26. package/src/components/JhDraggable/JhDraggable.md +66 -0
  27. package/src/components/JhDraggable/JhDraggable.stories.js +161 -0
  28. package/src/components/JhDraggable/JhDraggable.vue +254 -0
  29. package/src/components/JhDrawer/JhDrawer.md +68 -0
  30. package/src/components/JhDrawer/JhDrawer.stories.js +478 -0
  31. package/src/components/JhDrawer/JhDrawer.vue +281 -0
  32. package/src/components/JhDrawerForm/JhDrawerForm.md +69 -0
  33. package/src/components/JhDrawerForm/JhDrawerForm.stories.js +492 -0
  34. package/src/components/JhDrawerForm/JhDrawerForm.vue +297 -0
  35. package/src/components/JhEditableTable/JhEditableTable.md +507 -0
  36. package/src/components/JhEditableTable/JhEditableTable.stories.js +615 -0
  37. package/src/components/JhEditableTable/JhEditableTable.vue +685 -0
  38. package/src/components/JhFileInput/JhFileInput.md +56 -0
  39. package/src/components/JhFileInput/JhFileInput.stories.js +103 -0
  40. package/src/components/JhFileInput/JhFileInput.vue +253 -0
  41. package/src/components/JhForm/JhForm.md +676 -0
  42. package/src/components/JhForm/JhForm.stories.js +1375 -0
  43. package/src/components/JhForm/JhForm.vue +657 -0
  44. package/src/components/JhFormField/JhFormField.stories.js +217 -0
  45. package/src/components/JhFormField/JhFormField.vue +439 -0
  46. package/src/components/JhFormFields/JhFormFields.md +647 -0
  47. package/src/components/JhFormFields/JhFormFields.stories.js +922 -0
  48. package/src/components/JhFormFields/JhFormFields.vue +998 -0
  49. package/src/components/JhFormList/JhFormList.md +303 -0
  50. package/src/components/JhFormList/JhFormList.stories.js +661 -0
  51. package/src/components/JhFormList/JhFormList.vue +1127 -0
  52. package/src/components/JhJsonEditor/JhJsonEditor.md +54 -0
  53. package/src/components/JhJsonEditor/JhJsonEditor.stories.js +157 -0
  54. package/src/components/JhJsonEditor/JhJsonEditor.vue +178 -0
  55. package/src/components/JhLayout/JhLayout.md +580 -0
  56. package/src/components/JhLayout/JhLayout.stories.js +414 -0
  57. package/src/components/JhLayout/JhLayout.vue +387 -0
  58. package/src/components/JhList/JhList.md +441 -0
  59. package/src/components/JhList/JhList.stories.js +524 -0
  60. package/src/components/JhList/JhList.vue +571 -0
  61. package/src/components/JhMarkdownEditor/JhMarkdownEditor.md +56 -0
  62. package/src/components/JhMarkdownEditor/JhMarkdownEditor.stories.js +191 -0
  63. package/src/components/JhMarkdownEditor/JhMarkdownEditor.vue +188 -0
  64. package/src/components/JhMask/JhMask.md +62 -0
  65. package/src/components/JhMask/JhMask.stories.js +270 -0
  66. package/src/components/JhMask/JhMask.vue +123 -0
  67. package/src/components/JhMenu/JhMenu.md +85 -0
  68. package/src/components/JhMenu/JhMenu.stories.js +384 -0
  69. package/src/components/JhMenu/JhMenu.vue +545 -0
  70. package/src/components/JhModal/JhModal.md +68 -0
  71. package/src/components/JhModal/JhModal.stories.js +562 -0
  72. package/src/components/JhModal/JhModal.vue +235 -0
  73. package/src/components/JhModalForm/JhModalForm.md +69 -0
  74. package/src/components/JhModalForm/JhModalForm.stories.js +592 -0
  75. package/src/components/JhModalForm/JhModalForm.vue +298 -0
  76. package/src/components/JhPageContainer/JhPageContainer.md +409 -0
  77. package/src/components/JhPageContainer/JhPageContainer.stories.js +209 -0
  78. package/src/components/JhPageContainer/JhPageContainer.vue +72 -0
  79. package/src/components/JhQueryFilter/JhQueryFilter.md +77 -0
  80. package/src/components/JhQueryFilter/JhQueryFilter.stories.js +684 -0
  81. package/src/components/JhQueryFilter/JhQueryFilter.vue +429 -0
  82. package/src/components/JhScene/JhScene.md +64 -0
  83. package/src/components/JhScene/JhScene.stories.js +317 -0
  84. package/src/components/JhScene/JhScene.vue +376 -0
  85. package/src/components/JhStatisticCard/JhStatisticCard.md +363 -0
  86. package/src/components/JhStatisticCard/JhStatisticCard.stories.js +847 -0
  87. package/src/components/JhStatisticCard/JhStatisticCard.vue +459 -0
  88. package/src/components/JhStepsForm/JhStepsForm.md +666 -0
  89. package/src/components/JhStepsForm/JhStepsForm.stories.js +1224 -0
  90. package/src/components/JhStepsForm/JhStepsForm.vue +749 -0
  91. package/src/components/JhTable/JhTable.md +730 -0
  92. package/src/components/JhTable/JhTable.stories.js +1444 -0
  93. package/src/components/JhTable/JhTable.vue +2298 -0
  94. package/src/components/JhTableAttachment/JhTableAttachment.md +70 -0
  95. package/src/components/JhTableAttachment/JhTableAttachment.stories.js +198 -0
  96. package/src/components/JhTableAttachment/JhTableAttachment.vue +264 -0
  97. package/src/components/JhToast/JhToast.md +67 -0
  98. package/src/components/JhToast/JhToast.stories.js +386 -0
  99. package/src/components/JhToast/JhToast.vue +239 -0
  100. package/src/components/JhTreeSelect/JhTreeSelect.md +82 -0
  101. package/src/components/JhTreeSelect/JhTreeSelect.stories.js +391 -0
  102. package/src/components/JhTreeSelect/JhTreeSelect.vue +727 -0
  103. package/src/components/JhWaterMark/JhWaterMark.md +190 -0
  104. package/src/components/JhWaterMark/JhWaterMark.stories.js +675 -0
  105. package/src/components/JhWaterMark/JhWaterMark.vue +351 -0
  106. package/src/components/README.md +52 -0
  107. package/src/index.js +135 -0
  108. package/src/style/globalCSSJHV4.css +348 -0
  109. package/src/style/globalCSSVuetifyV4.css +637 -0
  110. package/src/style/storybook.css +4 -0
  111. package/src/tailwind.css +3 -0
  112. package/src/utils/vuetify.js +31 -0
@@ -0,0 +1,998 @@
1
+ <template>
2
+ <div :class="formFieldsClasses">
3
+ <!-- 标题区域 -->
4
+ <div v-if="title || $slots.title" class="jh-form-fields-header">
5
+ <slot name="title">
6
+ <div class="jh-form-fields-title">
7
+ {{ title }}
8
+ <v-tooltip v-if="tooltip" bottom>
9
+ <template v-slot:activator="{ on, attrs }">
10
+ <v-icon small class="ml-1" v-bind="attrs" v-on="on">mdi-help-circle-outline</v-icon>
11
+ </template>
12
+ <span>{{ tooltip }}</span>
13
+ </v-tooltip>
14
+ </div>
15
+ </slot>
16
+ <div v-if="description" class="jh-form-fields-description">{{ description }}</div>
17
+ </div>
18
+
19
+ <!-- 字段内容区域 -->
20
+ <v-row :class="rowClass" :dense="dense" v-bind="rowProps">
21
+ <template v-for="(field, index) in visibleFields">
22
+ <!-- 分组标题 -->
23
+ <v-col v-if="field.type === 'group'" :key="`group-${index}`" cols="12">
24
+ <div class="jh-form-group-title" :class="field.titleClass">
25
+ <v-divider v-if="field.divider && index > 0" class="mb-4"></v-divider>
26
+ <h3 v-if="field.title" class="text-h6 mb-2">{{ field.title }}</h3>
27
+ <p v-if="field.description" class="text-caption grey--text mb-3">{{ field.description }}</p>
28
+ </div>
29
+ </v-col>
30
+
31
+ <!-- 普通字段 -->
32
+ <v-col
33
+ v-else
34
+ :key="field.key"
35
+ :cols="getFieldCols(field)"
36
+ :sm="getFieldSm(field)"
37
+ :md="getFieldMd(field)"
38
+ :lg="getFieldLg(field)"
39
+ :xl="getFieldXl(field)"
40
+ :class="getFieldColClass(field)"
41
+ >
42
+ <!-- 字段包装器 -->
43
+ <div :class="getFieldWrapperClass(field)">
44
+ <!-- 字段标签 (水平布局) -->
45
+ <div
46
+ v-if="field.label && showLabels && (layout === 'horizontal' || field.layout === 'horizontal')"
47
+ :class="getHorizontalLabelClass(field)"
48
+ :style="getHorizontalLabelStyle(field)"
49
+ >
50
+ <span v-if="field.required && showRequiredMark" class="error--text mr-1">*</span>
51
+ {{ field.label }}
52
+ <v-tooltip v-if="field.tooltip" bottom>
53
+ <template v-slot:activator="{ on, attrs }">
54
+ <v-icon small class="ml-1" v-bind="attrs" v-on="on">mdi-help-circle-outline</v-icon>
55
+ </template>
56
+ <span>{{ field.tooltip }}</span>
57
+ </v-tooltip>
58
+ </div>
59
+
60
+ <!-- 字段输入区域 -->
61
+ <div :class="getFieldInputClass(field)">
62
+ <!-- 字段标签 (垂直/行内布局) -->
63
+ <div
64
+ v-if="field.label && showLabels && layout !== 'horizontal' && field.layout !== 'horizontal'"
65
+ :class="getVerticalLabelClass(field)"
66
+ >
67
+ <span v-if="field.required && showRequiredMark" class="error--text">*</span>
68
+ {{ field.label }}
69
+ <v-tooltip v-if="field.tooltip" bottom>
70
+ <template v-slot:activator="{ on, attrs }">
71
+ <v-icon small class="ml-1" v-bind="attrs" v-on="on">mdi-help-circle-outline</v-icon>
72
+ </template>
73
+ <span>{{ field.tooltip }}</span>
74
+ </v-tooltip>
75
+ </div>
76
+
77
+ <!-- 额外提示信息 -->
78
+ <div v-if="field.extra" class="jh-field-extra text-caption grey--text mb-2">
79
+ {{ field.extra }}
80
+ </div>
81
+
82
+ <!-- 只读模式展示 -->
83
+ <div v-if="isFieldReadonly(field)" class="jh-form-readonly-text">
84
+ {{ getReadonlyValue(field) }}
85
+ </div>
86
+
87
+ <!-- 表单字段 -->
88
+ <template v-else>
89
+ <!-- 文本输入框 -->
90
+ <v-text-field
91
+ v-if="field.type === 'text' || !field.type"
92
+ :class="inputClass"
93
+ :dense="getDense(field)"
94
+ :single-line="getSingleLine(field)"
95
+ :filled="getFilled(field)"
96
+ :outlined="getOutlined(field)"
97
+ :value="getFieldValue(field.key)"
98
+ @input="handleInput(field.key, $event)"
99
+ :rules="getRules(field)"
100
+ :disabled="getFieldDisabled(field)"
101
+ :readonly="field.readonly"
102
+ :placeholder="field.placeholder"
103
+ :prefix="field.prefix"
104
+ :suffix="field.suffix"
105
+ :hide-details="field.hideDetails || hideDetails"
106
+ v-bind="field.props"
107
+ @change="handleChange(field.key, $event)"
108
+ @blur="handleBlur(field.key, $event)"
109
+ ></v-text-field>
110
+
111
+ <!-- 文本域 -->
112
+ <v-textarea
113
+ v-else-if="field.type === 'textarea'"
114
+ :class="inputClass"
115
+ :dense="getDense(field)"
116
+ :filled="getFilled(field)"
117
+ :outlined="getOutlined(field)"
118
+ :value="getFieldValue(field.key)"
119
+ @input="handleInput(field.key, $event)"
120
+ :rules="getRules(field)"
121
+ :disabled="getFieldDisabled(field)"
122
+ :readonly="field.readonly"
123
+ :placeholder="field.placeholder"
124
+ :rows="field.rows || 3"
125
+ :hide-details="field.hideDetails || hideDetails"
126
+ v-bind="field.props"
127
+ @change="handleChange(field.key, $event)"
128
+ @blur="handleBlur(field.key, $event)"
129
+ ></v-textarea>
130
+
131
+ <!-- 数字输入框 -->
132
+ <v-text-field
133
+ v-else-if="field.type === 'number'"
134
+ :class="inputClass"
135
+ type="number"
136
+ :dense="getDense(field)"
137
+ :single-line="getSingleLine(field)"
138
+ :filled="getFilled(field)"
139
+ :outlined="getOutlined(field)"
140
+ :value="getFieldValue(field.key)"
141
+ @input="handleInput(field.key, $event)"
142
+ :rules="getRules(field)"
143
+ :disabled="getFieldDisabled(field)"
144
+ :readonly="field.readonly"
145
+ :placeholder="field.placeholder"
146
+ :hide-details="field.hideDetails || hideDetails"
147
+ v-bind="field.props"
148
+ @change="handleChange(field.key, $event)"
149
+ @blur="handleBlur(field.key, $event)"
150
+ ></v-text-field>
151
+
152
+ <!-- 下拉选择框 -->
153
+ <v-select
154
+ v-else-if="field.type === 'select'"
155
+ :class="inputClass"
156
+ :dense="getDense(field)"
157
+ :filled="getFilled(field)"
158
+ :outlined="getOutlined(field)"
159
+ :value="getFieldValue(field.key)"
160
+ @input="handleInput(field.key, $event)"
161
+ :items="getFieldOptions(field)"
162
+ :rules="getRules(field)"
163
+ :disabled="getFieldDisabled(field)"
164
+ :readonly="field.readonly"
165
+ :placeholder="field.placeholder"
166
+ :item-text="field.itemText || 'text'"
167
+ :item-value="field.itemValue || 'value'"
168
+ :multiple="field.multiple"
169
+ :chips="field.chips"
170
+ :hide-details="field.hideDetails || hideDetails"
171
+ v-bind="field.props"
172
+ @change="handleChange(field.key, $event)"
173
+ @blur="handleBlur(field.key, $event)"
174
+ ></v-select>
175
+
176
+ <!-- 自动完成 -->
177
+ <v-autocomplete
178
+ v-else-if="field.type === 'autocomplete'"
179
+ :class="inputClass"
180
+ :dense="getDense(field)"
181
+ :filled="getFilled(field)"
182
+ :outlined="getOutlined(field)"
183
+ :value="getFieldValue(field.key)"
184
+ @input="handleInput(field.key, $event)"
185
+ :items="getFieldOptions(field)"
186
+ :rules="getRules(field)"
187
+ :disabled="getFieldDisabled(field)"
188
+ :readonly="field.readonly"
189
+ :placeholder="field.placeholder"
190
+ :item-text="field.itemText || 'text'"
191
+ :item-value="field.itemValue || 'value'"
192
+ :hide-details="field.hideDetails || hideDetails"
193
+ v-bind="field.props"
194
+ @change="handleChange(field.key, $event)"
195
+ @blur="handleBlur(field.key, $event)"
196
+ ></v-autocomplete>
197
+
198
+ <!-- 日期选择器 -->
199
+ <v-menu
200
+ v-else-if="field.type === 'date'"
201
+ v-model="dateMenus[field.key]"
202
+ :close-on-content-click="false"
203
+ transition="scale-transition"
204
+ offset-y
205
+ min-width="290px"
206
+ >
207
+ <template v-slot:activator="{ on, attrs }">
208
+ <v-text-field
209
+ :class="inputClass"
210
+ :dense="getDense(field)"
211
+ :filled="getFilled(field)"
212
+ :outlined="getOutlined(field)"
213
+ :value="getFieldValue(field.key)"
214
+ :rules="getRules(field)"
215
+ :disabled="getFieldDisabled(field)"
216
+ :placeholder="field.placeholder"
217
+ :hide-details="field.hideDetails || hideDetails"
218
+ readonly
219
+ v-bind="attrs"
220
+ v-on="on"
221
+ ></v-text-field>
222
+ </template>
223
+ <v-date-picker
224
+ :value="getFieldValue(field.key)"
225
+ @input="dateMenus[field.key] = false; handleInput(field.key, $event)"
226
+ :locale="field.locale || 'zh-cn'"
227
+ v-bind="field.pickerProps"
228
+ ></v-date-picker>
229
+ </v-menu>
230
+
231
+ <!-- 时间选择器 -->
232
+ <v-menu
233
+ v-else-if="field.type === 'time'"
234
+ v-model="timeMenus[field.key]"
235
+ :close-on-content-click="false"
236
+ transition="scale-transition"
237
+ offset-y
238
+ min-width="290px"
239
+ >
240
+ <template v-slot:activator="{ on, attrs }">
241
+ <v-text-field
242
+ :class="inputClass"
243
+ :dense="getDense(field)"
244
+ :filled="getFilled(field)"
245
+ :outlined="getOutlined(field)"
246
+ :value="getFieldValue(field.key)"
247
+ :rules="getRules(field)"
248
+ :disabled="getFieldDisabled(field)"
249
+ :placeholder="field.placeholder"
250
+ :hide-details="field.hideDetails || hideDetails"
251
+ readonly
252
+ v-bind="attrs"
253
+ v-on="on"
254
+ ></v-text-field>
255
+ </template>
256
+ <v-time-picker
257
+ :value="getFieldValue(field.key)"
258
+ @input="timeMenus[field.key] = false; handleInput(field.key, $event)"
259
+ format="24hr"
260
+ v-bind="field.pickerProps"
261
+ ></v-time-picker>
262
+ </v-menu>
263
+
264
+ <!-- 颜色选择器 -->
265
+ <v-menu
266
+ v-else-if="field.type === 'color'"
267
+ v-model="colorMenus[field.key]"
268
+ :close-on-content-click="false"
269
+ transition="scale-transition"
270
+ offset-y
271
+ min-width="320px"
272
+ >
273
+ <template v-slot:activator="{ on, attrs }">
274
+ <v-text-field
275
+ :class="inputClass"
276
+ :dense="getDense(field)"
277
+ :filled="getFilled(field)"
278
+ :outlined="getOutlined(field)"
279
+ :value="getFieldValue(field.key)"
280
+ :rules="getRules(field)"
281
+ :disabled="getFieldDisabled(field)"
282
+ :placeholder="field.placeholder"
283
+ :hide-details="field.hideDetails || hideDetails"
284
+ readonly
285
+ v-on="on"
286
+ v-bind="field.props"
287
+ >
288
+ <template v-slot:append>
289
+ <span
290
+ class="jh-color-preview"
291
+ :style="{ backgroundColor: getFieldValue(field.key) || field.defaultValue || '#ffffff' }"
292
+ ></span>
293
+ </template>
294
+ </v-text-field>
295
+ </template>
296
+ <v-color-picker
297
+ :value="getFieldValue(field.key) || field.defaultValue || '#000000'"
298
+ flat
299
+ :hide-mode-switch="field.hideModeSwitch !== false"
300
+ @input="handleColorInput(field, $event)"
301
+ v-bind="field.pickerProps"
302
+ ></v-color-picker>
303
+ </v-menu>
304
+
305
+ <!-- 滑块 -->
306
+ <v-slider
307
+ v-else-if="field.type === 'slider'"
308
+ :class="inputClass"
309
+ :value="getFieldValue(field.key)"
310
+ @input="handleInput(field.key, $event)"
311
+ :rules="getRules(field)"
312
+ :disabled="getFieldDisabled(field)"
313
+ :readonly="field.readonly"
314
+ :min="field.min !== undefined ? field.min : 0"
315
+ :max="field.max !== undefined ? field.max : 100"
316
+ :step="field.step !== undefined ? field.step : 1"
317
+ :thumb-label="field.thumbLabel"
318
+ :ticks="field.ticks"
319
+ :tick-size="field.tickSize"
320
+ :color="field.color || 'primary'"
321
+ :hide-details="field.hideDetails || hideDetails"
322
+ v-bind="field.props"
323
+ ></v-slider>
324
+
325
+ <!-- 区间滑块 -->
326
+ <v-range-slider
327
+ v-else-if="field.type === 'range-slider'"
328
+ :class="inputClass"
329
+ :value="getFieldValue(field.key)"
330
+ @input="handleInput(field.key, $event)"
331
+ :rules="getRules(field)"
332
+ :disabled="getFieldDisabled(field)"
333
+ :readonly="field.readonly"
334
+ :min="field.min !== undefined ? field.min : 0"
335
+ :max="field.max !== undefined ? field.max : 100"
336
+ :step="field.step !== undefined ? field.step : 1"
337
+ :thumb-label="field.thumbLabel"
338
+ :ticks="field.ticks"
339
+ :tick-size="field.tickSize"
340
+ :color="field.color || 'primary'"
341
+ :hide-details="field.hideDetails || hideDetails"
342
+ v-bind="field.props"
343
+ ></v-range-slider>
344
+
345
+ <!-- 开关 -->
346
+ <v-switch
347
+ v-else-if="field.type === 'switch'"
348
+ :input-value="getFieldValue(field.key)"
349
+ @change="handleChange(field.key, $event)"
350
+ :label="field.switchLabel"
351
+ :disabled="getFieldDisabled(field)"
352
+ :readonly="field.readonly"
353
+ :color="field.color || 'success'"
354
+ :hide-details="field.hideDetails || hideDetails"
355
+ v-bind="field.props"
356
+ ></v-switch>
357
+
358
+ <!-- 复选框 -->
359
+ <v-checkbox
360
+ v-else-if="field.type === 'checkbox'"
361
+ :input-value="getFieldValue(field.key)"
362
+ @change="handleChange(field.key, $event)"
363
+ :label="field.checkboxLabel"
364
+ :disabled="getFieldDisabled(field)"
365
+ :readonly="field.readonly"
366
+ :color="field.color || 'success'"
367
+ :hide-details="field.hideDetails || hideDetails"
368
+ v-bind="field.props"
369
+ ></v-checkbox>
370
+
371
+ <!-- 单选按钮组 -->
372
+ <v-radio-group
373
+ v-else-if="field.type === 'radio'"
374
+ :value="getFieldValue(field.key)"
375
+ @change="handleChange(field.key, $event)"
376
+ :rules="getRules(field)"
377
+ :disabled="getFieldDisabled(field)"
378
+ :readonly="field.readonly"
379
+ :row="field.row !== false"
380
+ :hide-details="field.hideDetails || hideDetails"
381
+ v-bind="field.props"
382
+ >
383
+ <v-radio
384
+ v-for="option in getFieldOptions(field)"
385
+ :key="option.value"
386
+ :label="option.text"
387
+ :value="option.value"
388
+ :color="field.color || 'success'"
389
+ ></v-radio>
390
+ </v-radio-group>
391
+
392
+ <!-- 自定义字段插槽 -->
393
+ <slot
394
+ v-else-if="field.type === 'slot'"
395
+ :name="`field-${field.key}`"
396
+ :field="field"
397
+ :value="getFieldValue(field.key)"
398
+ :values="values"
399
+ :updateField="updateField"
400
+ ></slot>
401
+ </template>
402
+ </div>
403
+ </div>
404
+ </v-col>
405
+ </template>
406
+ </v-row>
407
+
408
+ <!-- 底部插槽 -->
409
+ <slot name="footer" :values="values"></slot>
410
+ </div>
411
+ </template>
412
+
413
+ <script>
414
+ export default {
415
+ name: 'JhFormFields',
416
+
417
+ props: {
418
+ // 字段配置列表
419
+ fields: {
420
+ type: Array,
421
+ default: () => []
422
+ },
423
+ // 表单的初始值
424
+ value: {
425
+ type: Object,
426
+ default: () => ({})
427
+ },
428
+ // 分组标题
429
+ title: {
430
+ type: String,
431
+ default: ''
432
+ },
433
+ // 分组描述
434
+ description: {
435
+ type: String,
436
+ default: ''
437
+ },
438
+ // 标题旁的 tooltip
439
+ tooltip: {
440
+ type: String,
441
+ default: ''
442
+ },
443
+ // 布局方式 horizontal | vertical | inline
444
+ layout: {
445
+ type: String,
446
+ default: 'vertical',
447
+ validator: (v) => ['horizontal', 'vertical', 'inline'].includes(v)
448
+ },
449
+ // 是否展示 label
450
+ showLabels: {
451
+ type: Boolean,
452
+ default: true
453
+ },
454
+ // label 固定宽度
455
+ labelWidth: {
456
+ type: [Number, String],
457
+ default: 'auto'
458
+ },
459
+ // label 对齐方式
460
+ labelAlign: {
461
+ type: String,
462
+ default: 'right',
463
+ validator: (v) => ['left', 'right', 'center'].includes(v)
464
+ },
465
+ // 是否展示必填星号
466
+ showRequiredMark: {
467
+ type: Boolean,
468
+ default: true
469
+ },
470
+ // 只读模式
471
+ readonly: {
472
+ type: Boolean,
473
+ default: false
474
+ },
475
+ // 禁用全部字段
476
+ disabled: {
477
+ type: Boolean,
478
+ default: false
479
+ },
480
+ // 默认 dense 样式
481
+ defaultDense: {
482
+ type: Boolean,
483
+ default: true
484
+ },
485
+ // 默认 filled 样式
486
+ defaultFilled: {
487
+ type: Boolean,
488
+ default: true
489
+ },
490
+ // 默认 outlined 样式
491
+ defaultOutlined: {
492
+ type: Boolean,
493
+ default: false
494
+ },
495
+ // 默认单行 label
496
+ defaultSingleLine: {
497
+ type: Boolean,
498
+ default: true
499
+ },
500
+ // md 断点下的默认列宽
501
+ defaultColsMd: {
502
+ type: Number,
503
+ default: 6
504
+ },
505
+ // 是否隐藏 Vuetify 的 details
506
+ hideDetails: {
507
+ type: [Boolean, String],
508
+ default: false
509
+ },
510
+ // label 的额外类名
511
+ labelClass: {
512
+ type: String,
513
+ default: 'jh-input-label'
514
+ },
515
+ // 输入框额外类名
516
+ inputClass: {
517
+ type: String,
518
+ default: 'jh-v-input'
519
+ },
520
+ // 行容器类名
521
+ rowClass: {
522
+ type: String,
523
+ default: ''
524
+ },
525
+ // 行参数配置
526
+ rowProps: {
527
+ type: Object,
528
+ default: () => ({})
529
+ },
530
+ // 通用校验规则
531
+ validationRules: {
532
+ type: Object,
533
+ default: () => ({
534
+ require: [v => !!v || '必填'],
535
+ email: [v => !v || /.+@.+\..+/.test(v) || '邮箱格式不正确'],
536
+ phone: [v => !v || /^1[3-9]\d{9}$/.test(v) || '手机号格式不正确'],
537
+ number: [v => !v || !isNaN(v) || '请输入数字'],
538
+ integer: [v => !v || Number.isInteger(Number(v)) || '请输入整数'],
539
+ }),
540
+ },
541
+ // 压缩外边距/间距
542
+ dense: {
543
+ type: Boolean,
544
+ default: false
545
+ },
546
+ // 是否显示边框
547
+ bordered: {
548
+ type: Boolean,
549
+ default: false
550
+ },
551
+ // 字段依赖配置
552
+ dependencies: {
553
+ type: Array,
554
+ default: () => []
555
+ },
556
+ },
557
+
558
+ data() {
559
+ return {
560
+ values: {},
561
+ dateMenus: {},
562
+ timeMenus: {},
563
+ colorMenus: {},
564
+ dependencyWatchers: [],
565
+ };
566
+ },
567
+
568
+ computed: {
569
+ formFieldsClasses() {
570
+ return {
571
+ 'jh-form-fields': true,
572
+ 'jh-form-fields--bordered': this.bordered,
573
+ 'jh-form-fields--readonly': this.readonly,
574
+ 'jh-form-fields--disabled': this.disabled,
575
+ [`jh-form-fields--${this.layout}`]: true,
576
+ };
577
+ },
578
+
579
+ visibleFields() {
580
+ return this.fields.filter(field => {
581
+ if (typeof field.visible === 'function') {
582
+ return field.visible(this.values);
583
+ }
584
+ if (field.visible !== undefined) {
585
+ return field.visible;
586
+ }
587
+ return true;
588
+ });
589
+ },
590
+ },
591
+
592
+ watch: {
593
+ value: {
594
+ handler(val) {
595
+ this.values = { ...val };
596
+ },
597
+ immediate: true,
598
+ deep: true,
599
+ },
600
+
601
+ dependencies: {
602
+ handler(newDeps) {
603
+ this.setupDependencyWatchers(newDeps);
604
+ },
605
+ immediate: true,
606
+ },
607
+ },
608
+
609
+ mounted() {
610
+ this.initValues();
611
+ this.setupFieldDependencies();
612
+ },
613
+
614
+ beforeDestroy() {
615
+ this.dependencyWatchers.forEach(unwatch => unwatch());
616
+ this.dependencyWatchers = [];
617
+ },
618
+
619
+ methods: {
620
+ initValues() {
621
+ const values = { ...this.value };
622
+ this.fields.forEach(field => {
623
+ if (field.defaultValue !== undefined && values[field.key] === undefined) {
624
+ values[field.key] = field.defaultValue;
625
+ }
626
+ });
627
+ this.values = values;
628
+ },
629
+
630
+ setupFieldDependencies() {
631
+ this.fields.forEach(field => {
632
+ if (field.dependencies && Array.isArray(field.dependencies)) {
633
+ field.dependencies.forEach(depKey => {
634
+ const unwatch = this.$watch(
635
+ () => this.values[depKey],
636
+ (newVal, oldVal) => {
637
+ if (newVal !== oldVal) {
638
+ this.handleDependencyChange(field, depKey, newVal, oldVal);
639
+ }
640
+ }
641
+ );
642
+ this.dependencyWatchers.push(unwatch);
643
+ });
644
+ }
645
+ });
646
+ },
647
+
648
+ setupDependencyWatchers(deps) {
649
+ this.dependencyWatchers.forEach(unwatch => unwatch());
650
+ this.dependencyWatchers = [];
651
+
652
+ deps.forEach(depKey => {
653
+ const unwatch = this.$watch(
654
+ () => this.values[depKey],
655
+ (newVal, oldVal) => {
656
+ if (newVal !== oldVal) {
657
+ this.$emit('dependency-change', { key: depKey, value: newVal, oldValue: oldVal, values: this.values });
658
+ }
659
+ }
660
+ );
661
+ this.dependencyWatchers.push(unwatch);
662
+ });
663
+ },
664
+
665
+ handleDependencyChange(field, depKey, newVal, oldVal) {
666
+ if (typeof field.onDependencyChange === 'function') {
667
+ field.onDependencyChange(depKey, newVal, oldVal, this.values);
668
+ }
669
+ this.$emit('field-dependency-change', {
670
+ field: field.key,
671
+ dependency: depKey,
672
+ value: newVal,
673
+ oldValue: oldVal,
674
+ values: this.values,
675
+ });
676
+ },
677
+
678
+ getFieldValue(key) {
679
+ return this.values[key];
680
+ },
681
+
682
+ getFieldOptions(field) {
683
+ if (typeof field.options === 'function') {
684
+ return field.options(this.values);
685
+ }
686
+ return field.options || [];
687
+ },
688
+
689
+ getFieldCols(field) {
690
+ if (this.layout === 'inline') return 'auto';
691
+ if (field.cols) {
692
+ return typeof field.cols === 'object' ? (field.cols.xs || field.cols) : field.cols;
693
+ }
694
+ return 12;
695
+ },
696
+
697
+ getFieldSm(field) {
698
+ if (this.layout === 'inline') return 'auto';
699
+ return field.cols && typeof field.cols === 'object' ? field.cols.sm : 12;
700
+ },
701
+
702
+ getFieldMd(field) {
703
+ if (this.layout === 'inline') return 'auto';
704
+ if (field.cols && typeof field.cols === 'object') {
705
+ return field.cols.md || this.defaultColsMd;
706
+ }
707
+ return field.cols || this.defaultColsMd;
708
+ },
709
+
710
+ getFieldLg(field) {
711
+ if (this.layout === 'inline') return 'auto';
712
+ if (field.cols && typeof field.cols === 'object') {
713
+ return field.cols.lg || field.cols.md || this.defaultColsMd;
714
+ }
715
+ return field.cols || this.defaultColsMd;
716
+ },
717
+
718
+ getFieldXl(field) {
719
+ if (this.layout === 'inline') return 'auto';
720
+ if (field.cols && typeof field.cols === 'object') {
721
+ return field.cols.xl || field.cols.md || this.defaultColsMd;
722
+ }
723
+ return field.cols || this.defaultColsMd;
724
+ },
725
+
726
+ getFieldColClass(field) {
727
+ return field.colClass || '';
728
+ },
729
+
730
+ getFieldWrapperClass(field) {
731
+ const fieldLayout = field.layout || this.layout;
732
+ const layoutClass = fieldLayout === 'horizontal' ? 'd-flex align-center' : '';
733
+ return `jh-field-wrapper ${layoutClass}`;
734
+ },
735
+
736
+ getHorizontalLabelClass(field) {
737
+ const align = field.labelAlign || this.labelAlign;
738
+ return `jh-field-label jh-input-label jh-field-label--horizontal text-${align} flex-shrink-0`;
739
+ },
740
+
741
+ getHorizontalLabelStyle(field) {
742
+ const width = field.labelWidth || this.labelWidth;
743
+ return {
744
+ width: typeof width === 'number' ? `${width}px` : width,
745
+ minWidth: typeof width === 'number' ? `${width}px` : width,
746
+ };
747
+ },
748
+
749
+ getVerticalLabelClass(field) {
750
+ return `jh-field-label jh-input-label jh-field-label--vertical mb-1`;
751
+ },
752
+
753
+ getFieldInputClass(field) {
754
+ const fieldLayout = field.layout || this.layout;
755
+ return fieldLayout === 'horizontal' ? 'jh-field-input flex-grow-1' : 'jh-field-input';
756
+ },
757
+
758
+ getFieldDisabled(field) {
759
+ if (typeof field.disabled === 'function') {
760
+ return field.disabled(this.values);
761
+ }
762
+ if (field.disabled !== undefined) {
763
+ return field.disabled;
764
+ }
765
+ return this.disabled;
766
+ },
767
+
768
+ isFieldReadonly(field) {
769
+ if (typeof field.readonly === 'function') {
770
+ return field.readonly(this.values);
771
+ }
772
+ if (field.readonly !== undefined) {
773
+ return field.readonly;
774
+ }
775
+ return this.readonly;
776
+ },
777
+
778
+ getReadonlyValue(field) {
779
+ const value = this.values[field.key];
780
+
781
+ if (typeof field.readonlyRender === 'function') {
782
+ return field.readonlyRender(value, this.values);
783
+ }
784
+
785
+ if ((field.type === 'select' || field.type === 'radio') && field.options) {
786
+ const options = this.getFieldOptions(field);
787
+ if (field.multiple && Array.isArray(value)) {
788
+ return value.map(v => {
789
+ const option = options.find(opt => opt.value === v);
790
+ return option ? option.text : v;
791
+ }).join(', ');
792
+ } else {
793
+ const option = options.find(opt => opt.value === value);
794
+ return option ? option.text : value;
795
+ }
796
+ }
797
+
798
+ if (field.type === 'switch' || field.type === 'checkbox') {
799
+ return value ? '是' : '否';
800
+ }
801
+
802
+ if (field.type === 'range-slider' && Array.isArray(value)) {
803
+ return value.join(' ~ ');
804
+ }
805
+
806
+ return value || '-';
807
+ },
808
+
809
+ getDense(field) {
810
+ return field.dense !== undefined ? field.dense : this.defaultDense;
811
+ },
812
+
813
+ getFilled(field) {
814
+ return field.filled !== undefined ? field.filled : this.defaultFilled;
815
+ },
816
+
817
+ getOutlined(field) {
818
+ return field.outlined !== undefined ? field.outlined : this.defaultOutlined;
819
+ },
820
+
821
+ getSingleLine(field) {
822
+ return field.singleLine !== undefined ? field.singleLine : this.defaultSingleLine;
823
+ },
824
+
825
+ getRules(field) {
826
+ const rules = [];
827
+
828
+ if (field.required) {
829
+ rules.push(v => !!v || `${field.label || '此字段'}为必填项`);
830
+ }
831
+
832
+ if (field.rules) {
833
+ if (Array.isArray(field.rules)) {
834
+ rules.push(...field.rules);
835
+ } else if (typeof field.rules === 'string') {
836
+ const ruleNames = field.rules.split('|');
837
+ ruleNames.forEach(name => {
838
+ const trimmedName = name.trim();
839
+ if (this.validationRules[trimmedName]) {
840
+ rules.push(...this.validationRules[trimmedName]);
841
+ }
842
+ });
843
+ }
844
+ }
845
+
846
+ return rules;
847
+ },
848
+
849
+ handleInput(key, value) {
850
+ this.$set(this.values, key, value);
851
+ this.$emit('input', this.values);
852
+ this.$emit('field-input', { key, value, values: this.values });
853
+ },
854
+
855
+ handleChange(key, value) {
856
+ this.$set(this.values, key, value);
857
+ this.$emit('input', this.values);
858
+ this.$emit('field-change', { key, value, values: this.values });
859
+ },
860
+
861
+ handleColorInput(field, value) {
862
+ this.handleInput(field.key, value);
863
+ if (field.closeOnSelect !== false) {
864
+ this.$set(this.colorMenus, field.key, false);
865
+ }
866
+ },
867
+
868
+ handleBlur(key, value) {
869
+ this.$emit('field-blur', { key, value, values: this.values });
870
+ },
871
+
872
+ updateField(key, value) {
873
+ this.$set(this.values, key, value);
874
+ this.$emit('input', this.values);
875
+ this.$emit('field-change', { key, value, values: this.values });
876
+ },
877
+
878
+ getValues() {
879
+ return { ...this.values };
880
+ },
881
+
882
+ setFieldsValue(values) {
883
+ this.values = { ...this.values, ...values };
884
+ this.$emit('input', this.values);
885
+ },
886
+
887
+ setFieldValue(key, value) {
888
+ this.$set(this.values, key, value);
889
+ this.$emit('input', this.values);
890
+ },
891
+
892
+ resetFields() {
893
+ const values = {};
894
+ this.fields.forEach(field => {
895
+ if (field.defaultValue !== undefined) {
896
+ values[field.key] = field.defaultValue;
897
+ }
898
+ });
899
+ this.values = values;
900
+ this.$emit('input', this.values);
901
+ this.$emit('reset', this.values);
902
+ },
903
+ },
904
+ };
905
+ </script>
906
+
907
+ <style scoped>
908
+ .jh-form-fields {
909
+ width: 100%;
910
+ }
911
+
912
+ .jh-form-fields--bordered {
913
+ border: 1px solid rgba(0, 0, 0, 0.12);
914
+ border-radius: 4px;
915
+ padding: 16px;
916
+ }
917
+
918
+ .jh-form-fields-header {
919
+ margin-bottom: 16px;
920
+ }
921
+
922
+ .jh-form-fields-title {
923
+ font-size: 16px;
924
+ font-weight: 500;
925
+ color: rgba(0, 0, 0, 0.85);
926
+ display: flex;
927
+ align-items: center;
928
+ }
929
+
930
+ .jh-form-fields-description {
931
+ font-size: 14px;
932
+ color: rgba(0, 0, 0, 0.45);
933
+ margin-top: 4px;
934
+ line-height: 1.5;
935
+ }
936
+
937
+ .jh-form-fields--horizontal .jh-field-label--horizontal {
938
+ padding-top: 0;
939
+ line-height: 1.5;
940
+ }
941
+
942
+ .jh-form-fields--inline .v-input {
943
+ margin-bottom: 0 !important;
944
+ }
945
+
946
+ .jh-form-fields--readonly .jh-form-readonly-text {
947
+ padding: 8px 0;
948
+ min-height: 40px;
949
+ color: rgba(0, 0, 0, 0.87);
950
+ }
951
+
952
+ .jh-field-label {
953
+
954
+ }
955
+
956
+ .jh-field-label--horizontal {
957
+ padding-right: 12px;
958
+ }
959
+
960
+ .jh-field-label--vertical {
961
+ display: block;
962
+ }
963
+
964
+ .jh-form-group-title {
965
+ margin-top: 8px;
966
+ margin-bottom: 16px;
967
+ }
968
+
969
+ .jh-form-group-title h3 {
970
+ color: rgba(0, 0, 0, 0.85);
971
+ font-weight: 500;
972
+ }
973
+
974
+ .jh-field-wrapper {
975
+ width: 100%;
976
+ }
977
+
978
+ .jh-field-input {
979
+ width: 100%;
980
+ }
981
+
982
+ .jh-form-readonly-text {
983
+ word-break: break-word;
984
+ }
985
+
986
+ .jh-field-extra {
987
+ margin-top: -8px;
988
+ line-height: 1.5;
989
+ }
990
+
991
+ .jh-color-preview {
992
+ display: inline-block;
993
+ width: 20px;
994
+ height: 20px;
995
+ border-radius: 4px;
996
+ border: 1px solid rgba(0, 0, 0, 0.2);
997
+ }
998
+ </style>