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,1127 @@
1
+ <template>
2
+ <div :class="formListClasses">
3
+ <!-- 列表头部 -->
4
+ <div v-if="title || description || $slots.header" class="jh-form-list-header">
5
+ <slot name="header">
6
+ <h3 v-if="title" class="text-h6 mb-2">{{ title }}</h3>
7
+ <p v-if="description" class="text-caption grey--text mb-0">{{ description }}</p>
8
+ </slot>
9
+ </div>
10
+
11
+ <!-- 空列表占位 -->
12
+ <div v-if="listData.length === 0" class="jh-form-list-empty">
13
+ <slot name="empty">
14
+ <v-icon size="48" color="grey lighten-1">mdi-inbox</v-icon>
15
+ <p class="grey--text mt-2">暂无数据</p>
16
+ </slot>
17
+ </div>
18
+
19
+ <!-- 列表主体 - Card 模式 -->
20
+ <div
21
+ v-else-if="mode === 'card'"
22
+ ref="listBody"
23
+ class="jh-form-list-body jh-form-list-body--card"
24
+ >
25
+ <v-card
26
+ v-for="(item, index) in listData"
27
+ :key="item.__id__"
28
+ :class="getItemClasses(index)"
29
+ v-bind="cardProps"
30
+ class="jh-form-list-item jh-form-list-item--card mb-4"
31
+ >
32
+ <!-- 列表项头部 -->
33
+ <v-card-title class="jh-form-list-item-header py-2 px-4">
34
+ <div class="d-flex align-center">
35
+ <!-- 拖拽把手 -->
36
+ <v-icon
37
+ v-if="sortable && !disabled && !readonly"
38
+ class="jh-form-list-drag-handle mr-2"
39
+ small
40
+ >
41
+ mdi-drag
42
+ </v-icon>
43
+ <!-- 折叠按钮 -->
44
+ <v-btn
45
+ v-if="collapsible"
46
+ icon
47
+ x-small
48
+ @click="toggleCollapse(item)"
49
+ class="mr-2"
50
+ >
51
+ <v-icon small>
52
+ {{ isCollapsed(item) ? 'mdi-chevron-down' : 'mdi-chevron-up' }}
53
+ </v-icon>
54
+ </v-btn>
55
+ <!-- 序号 -->
56
+ <span v-if="showIndex && alwaysShowItemLabel" class="jh-form-list-item-index">
57
+ #{{ index + 1 }}
58
+ </span>
59
+ </div>
60
+ <v-spacer></v-spacer>
61
+ <!-- 操作按钮 -->
62
+ <div v-if="showItemActions && !disabled && !readonly" class="jh-form-list-item-actions">
63
+ <slot name="item-actions" :item="item" :index="index" :removeItem="removeItem" :copyItem="copyItem">
64
+ <template v-if="actionRender">
65
+ <component :is="{ render: () => renderActionButtons(item, index) }" />
66
+ </template>
67
+ <template v-else>
68
+ <v-btn
69
+ v-if="copyable"
70
+ icon
71
+ small
72
+ @click="copyItem(index)"
73
+ title="复制"
74
+ >
75
+ <v-icon small>mdi-content-copy</v-icon>
76
+ </v-btn>
77
+ <v-btn
78
+ icon
79
+ small
80
+ color="error"
81
+ @click="removeItem(index)"
82
+ :disabled="listData.length <= min"
83
+ title="删除"
84
+ >
85
+ <v-icon small>mdi-delete</v-icon>
86
+ </v-btn>
87
+ </template>
88
+ </slot>
89
+ </div>
90
+ </v-card-title>
91
+
92
+ <v-divider v-if="!isCollapsed(item)"></v-divider>
93
+
94
+ <!-- 列表项内容 -->
95
+ <v-expand-transition>
96
+ <v-card-text v-show="!isCollapsed(item)" class="pa-4">
97
+ <slot name="item" :item="item" :index="index" :fields="fields" :updateItem="(key, value) => updateItemField(index, key, value)">
98
+ <jh-form
99
+ :ref="`itemForm_${index}`"
100
+ :fields="fields"
101
+ :initial-data="item"
102
+ :layout="itemLayout"
103
+ :label-width="itemLabelWidth"
104
+ :dense="dense"
105
+ :outlined="outlined"
106
+ :disabled="disabled"
107
+ :readonly="readonly"
108
+ :hide-details="false"
109
+ @field-change="handleItemFieldChange(index, $event)"
110
+ >
111
+ <!-- 传递字段插槽 -->
112
+ <template v-for="field in fields" v-slot:[`field-${field.key}`]="slotProps">
113
+ <slot
114
+ :name="`field-${field.key}`"
115
+ v-bind="{ ...slotProps, index, item }"
116
+ ></slot>
117
+ </template>
118
+ </jh-form>
119
+ </slot>
120
+ </v-card-text>
121
+ </v-expand-transition>
122
+
123
+ <!-- 验证错误提示 -->
124
+ <v-alert
125
+ v-if="validationErrors[index] && !isCollapsed(item)"
126
+ type="error"
127
+ dense
128
+ text
129
+ class="mx-4 mb-3"
130
+ >
131
+ {{ validationErrors[index] }}
132
+ </v-alert>
133
+ </v-card>
134
+ </div>
135
+
136
+ <!-- 列表主体 - Table 模式 -->
137
+ <div
138
+ v-else-if="mode === 'table'"
139
+ class="jh-form-list-body jh-form-list-body--table"
140
+ >
141
+ <v-simple-table :dense="dense">
142
+ <thead>
143
+ <tr>
144
+ <th v-if="showIndex" class="text-center" style="width: 60px">#</th>
145
+ <th v-if="sortable && !disabled && !readonly" style="width: 40px"></th>
146
+ <th v-for="field in fields" :key="field.key">
147
+ <span v-if="field.required" class="error--text mr-1">*</span>
148
+ {{ field.label }}
149
+ </th>
150
+ <th v-if="showItemActions && !disabled && !readonly" class="text-center" :style="{ width: actionColumn.width + 'px' }">
151
+ 操作
152
+ </th>
153
+ </tr>
154
+ </thead>
155
+ <tbody ref="listBody">
156
+ <tr
157
+ v-for="(item, index) in listData"
158
+ :key="item.__id__"
159
+ :class="getItemClasses(index)"
160
+ >
161
+ <!-- 序号 -->
162
+ <td v-if="showIndex" class="text-center">{{ index + 1 }}</td>
163
+ <!-- 拖拽把手 -->
164
+ <td v-if="sortable && !disabled && !readonly" class="text-center">
165
+ <v-icon class="jh-form-list-drag-handle" small>mdi-drag</v-icon>
166
+ </td>
167
+ <!-- 字段列 -->
168
+ <td v-for="field in fields" :key="field.key">
169
+ <slot :name="`field-${field.key}`" :item="item" :index="index" :field="field" :updateItem="(key, value) => updateItemField(index, key, value)">
170
+ <component
171
+ :is="getFieldComponent(field)"
172
+ v-model="item[field.key]"
173
+ :dense="dense"
174
+ :outlined="outlined"
175
+ :disabled="disabled || getFieldDisabled(field, item)"
176
+ :readonly="readonly"
177
+ :placeholder="field.placeholder"
178
+ :hide-details="true"
179
+ v-bind="getFieldProps(field)"
180
+ @input="handleItemFieldChange(index, { key: field.key, value: $event })"
181
+ ></component>
182
+ </slot>
183
+ </td>
184
+ <!-- 操作列 -->
185
+ <td v-if="showItemActions && !disabled && !readonly" class="text-center">
186
+ <slot name="item-actions" :item="item" :index="index" :removeItem="removeItem" :copyItem="copyItem">
187
+ <v-btn
188
+ v-if="copyable"
189
+ icon
190
+ x-small
191
+ @click="copyItem(index)"
192
+ title="复制"
193
+ class="mr-1"
194
+ >
195
+ <v-icon x-small>mdi-content-copy</v-icon>
196
+ </v-btn>
197
+ <v-btn
198
+ icon
199
+ x-small
200
+ color="error"
201
+ @click="removeItem(index)"
202
+ :disabled="listData.length <= min"
203
+ title="删除"
204
+ >
205
+ <v-icon x-small>mdi-delete</v-icon>
206
+ </v-btn>
207
+ </slot>
208
+ </td>
209
+ </tr>
210
+ </tbody>
211
+ </v-simple-table>
212
+ </div>
213
+
214
+ <!-- 列表主体 - Inline 模式 -->
215
+ <div
216
+ v-else-if="mode === 'inline'"
217
+ ref="listBody"
218
+ class="jh-form-list-body jh-form-list-body--inline d-flex flex-wrap align-center"
219
+ >
220
+ <div
221
+ v-for="(item, index) in listData"
222
+ :key="item.__id__"
223
+ :class="getItemClasses(index)"
224
+ class="jh-form-list-item jh-form-list-item--inline mr-2 mb-2"
225
+ >
226
+ <slot name="item" :item="item" :index="index" :fields="fields" :updateItem="(key, value) => updateItemField(index, key, value)">
227
+ <v-chip
228
+ :close="!disabled && !readonly && listData.length > min"
229
+ @click:close="removeItem(index)"
230
+ :draggable="sortable && !disabled && !readonly"
231
+ >
232
+ <v-icon v-if="sortable && !disabled && !readonly" class="jh-form-list-drag-handle mr-1" x-small>
233
+ mdi-drag
234
+ </v-icon>
235
+ {{ getInlineItemDisplay(item) }}
236
+ </v-chip>
237
+ </slot>
238
+ </div>
239
+ </div>
240
+
241
+ <!-- 列表级验证错误 -->
242
+ <v-alert
243
+ v-if="validationErrors._list"
244
+ type="error"
245
+ dense
246
+ text
247
+ class="mt-3"
248
+ >
249
+ {{ validationErrors._list }}
250
+ </v-alert>
251
+
252
+ <!-- 最大数量限制提示 -->
253
+ <v-alert
254
+ v-if="showMaxLimitAlert"
255
+ type="warning"
256
+ dense
257
+ text
258
+ dismissible
259
+ class="mt-3"
260
+ @input="showMaxLimitAlert = false"
261
+ >
262
+ {{ maxLimitText }} (最大: {{ max }})
263
+ </v-alert>
264
+
265
+ <!-- 最小数量限制提示 -->
266
+ <v-alert
267
+ v-if="showMinLimitAlert"
268
+ type="warning"
269
+ dense
270
+ text
271
+ dismissible
272
+ class="mt-3"
273
+ @input="showMinLimitAlert = false"
274
+ >
275
+ {{ minLimitText }} (最小: {{ min }})
276
+ </v-alert>
277
+
278
+ <!-- 列表底部 - 添加按钮 -->
279
+ <div v-if="!disabled && !readonly && computedCreatorButtonProps !== false" class="jh-form-list-footer mt-4">
280
+ <slot name="footer" :addItem="addItem" :canAdd="canAdd">
281
+ <v-btn
282
+ v-bind="computedCreatorButtonProps"
283
+ :disabled="!canAdd"
284
+ @click="addItem()"
285
+ >
286
+ <v-icon left>mdi-plus</v-icon>
287
+ {{ computedCreatorButtonProps.text || addButtonText }}
288
+ </v-btn>
289
+ </slot>
290
+ </div>
291
+ </div>
292
+ </template>
293
+
294
+ <script>
295
+ import JhForm from '../JhForm/JhForm.vue';
296
+
297
+ export default {
298
+ name: 'JhFormList',
299
+
300
+ components: {
301
+ JhForm,
302
+ },
303
+
304
+ props: {
305
+ // 列表数据 (v-model)
306
+ value: {
307
+ type: Array,
308
+ default: () => [],
309
+ },
310
+
311
+ // 字段配置
312
+ fields: {
313
+ type: Array,
314
+ default: () => [],
315
+ },
316
+
317
+ // 渲染模式
318
+ mode: {
319
+ type: String,
320
+ default: 'card',
321
+ validator: (v) => ['card', 'table', 'inline'].includes(v),
322
+ },
323
+
324
+ // 列表标题
325
+ title: {
326
+ type: String,
327
+ default: '',
328
+ },
329
+
330
+ // 列表说明
331
+ description: {
332
+ type: String,
333
+ default: '',
334
+ },
335
+
336
+ // 最小项数
337
+ min: {
338
+ type: Number,
339
+ default: 0,
340
+ },
341
+
342
+ // 最大项数
343
+ max: {
344
+ type: Number,
345
+ default: Infinity,
346
+ },
347
+
348
+ // 是否可排序
349
+ sortable: {
350
+ type: Boolean,
351
+ default: false,
352
+ },
353
+
354
+ // 是否可复制
355
+ copyable: {
356
+ type: Boolean,
357
+ default: true,
358
+ },
359
+
360
+ // 是否显示序号
361
+ showIndex: {
362
+ type: Boolean,
363
+ default: true,
364
+ },
365
+
366
+ // 是否显示项操作按钮
367
+ showItemActions: {
368
+ type: Boolean,
369
+ default: true,
370
+ },
371
+
372
+ // 新增项的默认值
373
+ defaultValue: {
374
+ type: [Object, Function],
375
+ default: () => ({}),
376
+ },
377
+
378
+ // 添加按钮文本
379
+ addButtonText: {
380
+ type: String,
381
+ default: '添加',
382
+ },
383
+
384
+ // 添加按钮 props
385
+ addButtonProps: {
386
+ type: Object,
387
+ default: () => ({}),
388
+ },
389
+
390
+ // 删除确认
391
+ deleteConfirm: {
392
+ type: Boolean,
393
+ default: false,
394
+ },
395
+
396
+ // 删除确认文本
397
+ deleteConfirmText: {
398
+ type: String,
399
+ default: '确定要删除这一项吗?',
400
+ },
401
+
402
+ // 列表项表单布局
403
+ itemLayout: {
404
+ type: String,
405
+ default: 'horizontal',
406
+ },
407
+
408
+ // 列表项标签宽度
409
+ itemLabelWidth: {
410
+ type: [Number, String],
411
+ default: 'auto',
412
+ },
413
+
414
+ // 紧凑模式
415
+ dense: {
416
+ type: Boolean,
417
+ default: true,
418
+ },
419
+
420
+ // 边框样式
421
+ outlined: {
422
+ type: Boolean,
423
+ default: true,
424
+ },
425
+
426
+ // 禁用
427
+ disabled: {
428
+ type: Boolean,
429
+ default: false,
430
+ },
431
+
432
+ // 只读
433
+ readonly: {
434
+ type: Boolean,
435
+ default: false,
436
+ },
437
+
438
+ // 列表级验证规则
439
+ rules: {
440
+ type: Array,
441
+ default: () => [],
442
+ },
443
+
444
+ // 操作列配置 (table 模式)
445
+ actionColumn: {
446
+ type: Object,
447
+ default: () => ({ width: 100 }),
448
+ },
449
+
450
+ // 卡片 props (card 模式)
451
+ cardProps: {
452
+ type: Object,
453
+ default: () => ({}),
454
+ },
455
+
456
+ // 列表项自定义样式类
457
+ itemClass: {
458
+ type: String,
459
+ default: '',
460
+ },
461
+
462
+ // Inline 模式显示字段
463
+ inlineDisplayKey: {
464
+ type: String,
465
+ default: '',
466
+ },
467
+
468
+ // 始终显示项标签 (card 模式)
469
+ alwaysShowItemLabel: {
470
+ type: Boolean,
471
+ default: true,
472
+ },
473
+
474
+ // 自定义项渲染函数
475
+ itemRender: {
476
+ type: Function,
477
+ default: null,
478
+ },
479
+
480
+ // 创建按钮配置 (ProFormList 风格)
481
+ creatorButtonProps: {
482
+ type: [Object, Boolean],
483
+ default: () => ({}),
484
+ },
485
+
486
+ // 创建记录的初始值 (ProFormList 风格)
487
+ creatorRecord: {
488
+ type: [Object, Function],
489
+ default: null,
490
+ },
491
+
492
+ // 操作守卫 (删除前确认)
493
+ actionGuard: {
494
+ type: Object,
495
+ default: () => ({
496
+ beforeAddRow: null,
497
+ beforeRemoveRow: null,
498
+ }),
499
+ },
500
+
501
+ // 删除后回调
502
+ onAfterRemove: {
503
+ type: Function,
504
+ default: null,
505
+ },
506
+
507
+ // 卡片是否可折叠
508
+ collapsible: {
509
+ type: Boolean,
510
+ default: false,
511
+ },
512
+
513
+ // 默认折叠状态
514
+ defaultCollapsed: {
515
+ type: Boolean,
516
+ default: false,
517
+ },
518
+
519
+ // 操作按钮渲染配置
520
+ actionRender: {
521
+ type: Function,
522
+ default: null,
523
+ },
524
+
525
+ // 最大数量提示文本
526
+ maxLimitText: {
527
+ type: String,
528
+ default: '已达到最大数量限制',
529
+ },
530
+
531
+ // 最小数量提示文本
532
+ minLimitText: {
533
+ type: String,
534
+ default: '已达到最小数量限制',
535
+ },
536
+ },
537
+
538
+ data() {
539
+ return {
540
+ listData: [],
541
+ validationErrors: {},
542
+ sortableInstance: null,
543
+ collapsedItems: new Set(), // 折叠状态管理
544
+ showMaxLimitAlert: false, // 显示最大数量提示
545
+ showMinLimitAlert: false, // 显示最小数量提示
546
+ };
547
+ },
548
+
549
+ computed: {
550
+ formListClasses() {
551
+ return {
552
+ 'jh-form-list': true,
553
+ [`jh-form-list--${this.mode}`]: true,
554
+ 'jh-form-list--disabled': this.disabled,
555
+ 'jh-form-list--readonly': this.readonly,
556
+ };
557
+ },
558
+
559
+ canAdd() {
560
+ return this.listData.length < this.max;
561
+ },
562
+
563
+ // 计算创建按钮配置
564
+ computedCreatorButtonProps() {
565
+ if (this.creatorButtonProps === false) return false;
566
+ return {
567
+ color: 'primary',
568
+ outlined: true,
569
+ ...this.creatorButtonProps,
570
+ };
571
+ },
572
+
573
+ // 计算创建记录初始值
574
+ computedCreatorRecord() {
575
+ if (this.creatorRecord) {
576
+ return typeof this.creatorRecord === 'function'
577
+ ? this.creatorRecord
578
+ : () => this.creatorRecord;
579
+ }
580
+ return typeof this.defaultValue === 'function'
581
+ ? this.defaultValue
582
+ : () => this.defaultValue;
583
+ },
584
+ },
585
+
586
+ watch: {
587
+ value: {
588
+ handler(val) {
589
+ this.initListData();
590
+ },
591
+ immediate: true,
592
+ deep: true,
593
+ },
594
+
595
+ sortable(val) {
596
+ if (val) {
597
+ this.$nextTick(() => {
598
+ this.initSortable();
599
+ });
600
+ } else {
601
+ this.destroySortable();
602
+ }
603
+ },
604
+ },
605
+
606
+ mounted() {
607
+ if (this.sortable) {
608
+ this.initSortable();
609
+ }
610
+ },
611
+
612
+ beforeDestroy() {
613
+ this.destroySortable();
614
+ },
615
+
616
+ methods: {
617
+ // 生成唯一 ID
618
+ generateItemId() {
619
+ return `item_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
620
+ },
621
+
622
+ // 初始化列表数据
623
+ initListData() {
624
+ this.listData = (this.value || []).map(item => ({
625
+ ...item,
626
+ __id__: item.__id__ || this.generateItemId(),
627
+ }));
628
+ },
629
+
630
+ // 获取清理后的数据(移除 __id__)
631
+ getCleanData() {
632
+ return this.listData.map(item => {
633
+ const { __id__, ...cleanItem } = item;
634
+ return cleanItem;
635
+ });
636
+ },
637
+
638
+ // 添加项
639
+ async addItem(item = null, index = null) {
640
+ // 检查最大数量限制
641
+ if (this.listData.length >= this.max) {
642
+ this.showMaxLimitAlert = true;
643
+ setTimeout(() => {
644
+ this.showMaxLimitAlert = false;
645
+ }, 3000);
646
+ this.$emit('max-limit', this.max);
647
+ return;
648
+ }
649
+
650
+ // 执行添加前守卫
651
+ if (this.actionGuard?.beforeAddRow) {
652
+ const canAdd = await this.actionGuard.beforeAddRow(this.listData.length);
653
+ if (canAdd === false) return;
654
+ }
655
+
656
+ // 获取默认值
657
+ const defaultValue = this.computedCreatorRecord(this.listData.length);
658
+
659
+ // 创建新项
660
+ const newItem = {
661
+ ...defaultValue,
662
+ ...item,
663
+ __id__: this.generateItemId(),
664
+ };
665
+
666
+ // 插入列表
667
+ if (index !== null && index >= 0 && index <= this.listData.length) {
668
+ this.listData.splice(index, 0, newItem);
669
+ } else {
670
+ this.listData.push(newItem);
671
+ }
672
+
673
+ // 触发事件
674
+ this.emitInput();
675
+ this.$emit('add', this.getCleanItem(newItem), index ?? this.listData.length - 1);
676
+ },
677
+
678
+ // 删除项
679
+ async removeItem(index) {
680
+ // 检查最小数量限制
681
+ if (this.listData.length <= this.min) {
682
+ this.showMinLimitAlert = true;
683
+ setTimeout(() => {
684
+ this.showMinLimitAlert = false;
685
+ }, 3000);
686
+ this.$emit('min-limit', this.min);
687
+ return;
688
+ }
689
+
690
+ // 执行删除前守卫
691
+ if (this.actionGuard?.beforeRemoveRow) {
692
+ const canRemove = await this.actionGuard.beforeRemoveRow(index, this.listData[index]);
693
+ if (canRemove === false) return;
694
+ }
695
+
696
+ // 确认弹窗
697
+ if (this.deleteConfirm) {
698
+ const confirmed = await this.confirm(this.deleteConfirmText);
699
+ if (!confirmed) return;
700
+ }
701
+
702
+ const removedItem = this.listData[index];
703
+ this.listData.splice(index, 1);
704
+
705
+ // 清理验证错误
706
+ delete this.validationErrors[index];
707
+ // 清理折叠状态
708
+ this.collapsedItems.delete(removedItem.__id__);
709
+
710
+ this.emitInput();
711
+ this.$emit('remove', this.getCleanItem(removedItem), index);
712
+
713
+ // 执行删除后回调
714
+ if (this.onAfterRemove) {
715
+ this.onAfterRemove(index, this.getCleanItem(removedItem));
716
+ }
717
+ },
718
+
719
+ // 复制项
720
+ copyItem(index) {
721
+ if (!this.canAdd) {
722
+ this.$emit('max-limit', this.max);
723
+ return;
724
+ }
725
+
726
+ const sourceItem = this.listData[index];
727
+ const { __id__, ...cleanSource } = sourceItem;
728
+ const newItem = {
729
+ ...cleanSource,
730
+ __id__: this.generateItemId(),
731
+ };
732
+
733
+ // 在源项后面插入
734
+ this.listData.splice(index + 1, 0, newItem);
735
+
736
+ this.emitInput();
737
+ this.$emit('copy', this.getCleanItem(sourceItem), this.getCleanItem(newItem));
738
+ },
739
+
740
+ // 移动项
741
+ moveItem(oldIndex, newIndex) {
742
+ const item = this.listData.splice(oldIndex, 1)[0];
743
+ this.listData.splice(newIndex, 0, item);
744
+
745
+ this.emitInput();
746
+ this.$emit('sort', oldIndex, newIndex);
747
+ },
748
+
749
+ // 更新项字段
750
+ updateItemField(index, key, value) {
751
+ if (this.listData[index]) {
752
+ this.$set(this.listData[index], key, value);
753
+ this.emitInput();
754
+ this.$emit('item-change', index, this.getCleanItem(this.listData[index]));
755
+ }
756
+ },
757
+
758
+ // 处理项字段变化
759
+ handleItemFieldChange(index, { key, value }) {
760
+ this.updateItemField(index, key, value);
761
+ },
762
+
763
+ // 获取清理后的单个项
764
+ getCleanItem(item) {
765
+ const { __id__, ...cleanItem } = item;
766
+ return cleanItem;
767
+ },
768
+
769
+ // 触发 input 事件
770
+ emitInput() {
771
+ this.$emit('input', this.getCleanData());
772
+ this.$emit('change', this.getCleanData());
773
+ },
774
+
775
+ // 获取项样式类
776
+ getItemClasses(index) {
777
+ const classes = [];
778
+ if (this.itemClass) {
779
+ classes.push(this.itemClass);
780
+ }
781
+ if (this.validationErrors[index]) {
782
+ classes.push('jh-form-list-item--error');
783
+ }
784
+ return classes.join(' ');
785
+ },
786
+
787
+ // 切换折叠状态
788
+ toggleCollapse(item) {
789
+ const id = item.__id__;
790
+ if (this.collapsedItems.has(id)) {
791
+ this.collapsedItems.delete(id);
792
+ } else {
793
+ this.collapsedItems.add(id);
794
+ }
795
+ this.$forceUpdate();
796
+ },
797
+
798
+ // 判断是否折叠
799
+ isCollapsed(item) {
800
+ return this.collapsedItems.has(item.__id__);
801
+ },
802
+
803
+ // 渲染操作按钮
804
+ renderActionButtons(item, index) {
805
+ if (this.actionRender) {
806
+ return this.actionRender({
807
+ item: this.getCleanItem(item),
808
+ index,
809
+ removeItem: () => this.removeItem(index),
810
+ copyItem: () => this.copyItem(index),
811
+ });
812
+ }
813
+ return null;
814
+ },
815
+
816
+ // 获取字段组件
817
+ getFieldComponent(field) {
818
+ const componentMap = {
819
+ text: 'v-text-field',
820
+ textarea: 'v-textarea',
821
+ number: 'v-text-field',
822
+ select: 'v-select',
823
+ autocomplete: 'v-autocomplete',
824
+ switch: 'v-switch',
825
+ checkbox: 'v-checkbox',
826
+ };
827
+ return componentMap[field.type] || 'v-text-field';
828
+ },
829
+
830
+ // 获取字段 props
831
+ getFieldProps(field) {
832
+ const props = { ...field.props };
833
+
834
+ if (field.type === 'select' || field.type === 'autocomplete') {
835
+ props.items = field.options || [];
836
+ props.itemText = field.itemText || 'text';
837
+ props.itemValue = field.itemValue || 'value';
838
+ }
839
+
840
+ if (field.type === 'number') {
841
+ props.type = 'number';
842
+ }
843
+
844
+ return props;
845
+ },
846
+
847
+ // 获取字段禁用状态
848
+ getFieldDisabled(field, item) {
849
+ if (typeof field.disabled === 'function') {
850
+ return field.disabled(item);
851
+ }
852
+ return field.disabled || false;
853
+ },
854
+
855
+ // Inline 模式显示内容
856
+ getInlineItemDisplay(item) {
857
+ if (this.inlineDisplayKey) {
858
+ return item[this.inlineDisplayKey] || '-';
859
+ }
860
+ // 默认显示第一个字段的值
861
+ const firstField = this.fields[0];
862
+ return firstField ? (item[firstField.key] || '-') : '-';
863
+ },
864
+
865
+ // 初始化拖拽排序
866
+ initSortable() {
867
+ if (!this.sortable || this.sortableInstance) return;
868
+
869
+ this.$nextTick(() => {
870
+ const container = this.$refs.listBody;
871
+ if (!container) return;
872
+
873
+ // 动态导入 sortablejs
874
+ if (typeof window !== 'undefined' && window.Sortable) {
875
+ this.sortableInstance = window.Sortable.create(container, {
876
+ animation: 150,
877
+ handle: '.jh-form-list-drag-handle',
878
+ ghostClass: 'jh-form-list-ghost',
879
+ dragClass: 'jh-form-list-dragging',
880
+ onEnd: (evt) => {
881
+ const { oldIndex, newIndex } = evt;
882
+ if (oldIndex !== newIndex) {
883
+ this.moveItem(oldIndex, newIndex);
884
+ }
885
+ },
886
+ });
887
+ } else {
888
+ console.warn('JhFormList: sortable 功能需要引入 SortableJS 库');
889
+ }
890
+ });
891
+ },
892
+
893
+ // 销毁拖拽实例
894
+ destroySortable() {
895
+ if (this.sortableInstance) {
896
+ this.sortableInstance.destroy();
897
+ this.sortableInstance = null;
898
+ }
899
+ },
900
+
901
+ // 验证整个列表
902
+ async validate() {
903
+ const errors = {};
904
+ let isValid = true;
905
+
906
+ // 1. 验证列表规则
907
+ if (this.rules && this.rules.length) {
908
+ for (const rule of this.rules) {
909
+ const result = rule(this.getCleanData());
910
+ if (typeof result === 'string') {
911
+ errors._list = result;
912
+ isValid = false;
913
+ break;
914
+ }
915
+ }
916
+ }
917
+
918
+ // 2. 验证每个项
919
+ for (let i = 0; i < this.listData.length; i++) {
920
+ const itemValid = await this.validateItem(i);
921
+ if (!itemValid) {
922
+ isValid = false;
923
+ errors[i] = '该项存在验证错误';
924
+ }
925
+ }
926
+
927
+ this.validationErrors = errors;
928
+ this.$emit('validate', isValid, errors);
929
+ return isValid;
930
+ },
931
+
932
+ // 验证指定项
933
+ async validateItem(index) {
934
+ const formRef = this.$refs[`itemForm_${index}`];
935
+ if (formRef && formRef[0]) {
936
+ return await formRef[0].validate();
937
+ }
938
+ return true;
939
+ },
940
+
941
+ // 重置验证
942
+ resetValidation() {
943
+ this.validationErrors = {};
944
+ // 重置每个项的表单验证
945
+ this.listData.forEach((item, index) => {
946
+ const formRef = this.$refs[`itemForm_${index}`];
947
+ if (formRef && formRef[0]) {
948
+ formRef[0].resetValidation();
949
+ }
950
+ });
951
+ },
952
+
953
+ // 重置列表
954
+ reset() {
955
+ this.listData = [];
956
+ this.validationErrors = {};
957
+ this.emitInput();
958
+ },
959
+
960
+ // 获取列表数据
961
+ getItems() {
962
+ return this.getCleanData();
963
+ },
964
+
965
+ // 设置列表数据
966
+ setItems(items) {
967
+ this.listData = items.map(item => ({
968
+ ...item,
969
+ __id__: item.__id__ || this.generateItemId(),
970
+ }));
971
+ this.emitInput();
972
+ },
973
+
974
+ // 获取指定项数据
975
+ getItemValue(index) {
976
+ return this.listData[index] ? this.getCleanItem(this.listData[index]) : null;
977
+ },
978
+
979
+ // 设置指定项数据
980
+ setItemValue(index, value) {
981
+ if (this.listData[index]) {
982
+ this.$set(this.listData, index, {
983
+ ...value,
984
+ __id__: this.listData[index].__id__,
985
+ });
986
+ this.emitInput();
987
+ }
988
+ },
989
+
990
+ // 确认对话框
991
+ confirm(message) {
992
+ // 简单的确认实现,实际项目中可以使用 JhConfirmDialog
993
+ return new Promise((resolve) => {
994
+ const result = window.confirm(message);
995
+ resolve(result);
996
+ });
997
+ },
998
+ },
999
+ };
1000
+ </script>
1001
+
1002
+ <style scoped>
1003
+ /* 表单列表容器 */
1004
+ .jh-form-list {
1005
+ width: 100%;
1006
+ }
1007
+
1008
+ /* 列表头部 */
1009
+ .jh-form-list-header {
1010
+ margin-bottom: 16px;
1011
+ }
1012
+
1013
+ /* 列表主体 */
1014
+ .jh-form-list-body {
1015
+ position: relative;
1016
+ }
1017
+
1018
+ .jh-form-list-body--inline {
1019
+ gap: 8px;
1020
+ }
1021
+
1022
+ /* 列表项 */
1023
+ .jh-form-list-item {
1024
+ position: relative;
1025
+ }
1026
+
1027
+ .jh-form-list-item--card {
1028
+ transition: box-shadow 0.2s ease;
1029
+ }
1030
+
1031
+ .jh-form-list-item--card:hover {
1032
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1033
+ }
1034
+
1035
+ .jh-form-list-item--error {
1036
+ border-color: #f44336 !important;
1037
+ }
1038
+
1039
+ /* 列表项头部 */
1040
+ .jh-form-list-item-header {
1041
+ min-height: 48px;
1042
+ }
1043
+
1044
+ .jh-form-list-item-index {
1045
+ font-weight: 500;
1046
+ font-size: 14px;
1047
+ color: rgba(0, 0, 0, 0.6);
1048
+ }
1049
+
1050
+ /* 列表项操作 */
1051
+ .jh-form-list-item-actions {
1052
+ display: flex;
1053
+ gap: 4px;
1054
+ }
1055
+
1056
+ /* 拖拽把手 */
1057
+ .jh-form-list-drag-handle {
1058
+ cursor: move;
1059
+ color: rgba(0, 0, 0, 0.4);
1060
+ }
1061
+
1062
+ .jh-form-list-drag-handle:hover {
1063
+ color: rgba(0, 0, 0, 0.6);
1064
+ }
1065
+
1066
+ /* 拖拽状态 */
1067
+ .jh-form-list-ghost {
1068
+ opacity: 0.5;
1069
+ background: rgba(0, 0, 0, 0.05);
1070
+ }
1071
+
1072
+ .jh-form-list-dragging {
1073
+ cursor: move;
1074
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1075
+ }
1076
+
1077
+ /* 列表底部 */
1078
+ .jh-form-list-footer {
1079
+ display: flex;
1080
+ justify-content: center;
1081
+ }
1082
+
1083
+ /* 空列表占位 */
1084
+ .jh-form-list-empty {
1085
+ padding: 60px 20px;
1086
+ text-align: center;
1087
+ color: rgba(0, 0, 0, 0.4);
1088
+ }
1089
+
1090
+ .jh-form-list-empty p {
1091
+ margin: 0;
1092
+ font-size: 14px;
1093
+ }
1094
+
1095
+ /* 禁用状态 */
1096
+ .jh-form-list--disabled {
1097
+ opacity: 0.6;
1098
+ pointer-events: none;
1099
+ }
1100
+
1101
+ /* 只读状态 */
1102
+ .jh-form-list--readonly .jh-form-list-item-actions,
1103
+ .jh-form-list--readonly .jh-form-list-footer {
1104
+ display: none;
1105
+ }
1106
+
1107
+ /* Table 模式样式优化 */
1108
+ .jh-form-list-body--table >>> .v-data-table tbody tr:hover {
1109
+ background-color: rgba(0, 0, 0, 0.02);
1110
+ }
1111
+
1112
+ .jh-form-list-body--table >>> .v-input {
1113
+ margin-top: 0;
1114
+ padding-top: 0;
1115
+ }
1116
+
1117
+ /* 响应式 */
1118
+ @media (max-width: 600px) {
1119
+ .jh-form-list-item-header {
1120
+ flex-wrap: wrap;
1121
+ }
1122
+
1123
+ .jh-form-list-item-actions {
1124
+ margin-top: 8px;
1125
+ }
1126
+ }
1127
+ </style>