niuma-ui 1.0.2 → 1.1.0

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 (85) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/package.json +1 -1
  3. package/src/__tests__/RsDatePicker.spec.ts +21 -0
  4. package/src/__tests__/RsDialog.spec.ts +61 -0
  5. package/src/__tests__/RsForm.spec.ts +5 -5
  6. package/src/__tests__/RsFormItem.spec.ts +478 -0
  7. package/src/__tests__/RsFormList.spec.ts +109 -0
  8. package/src/__tests__/RsInput.spec.ts +93 -0
  9. package/src/__tests__/RsInputNumber.spec.ts +33 -0
  10. package/src/__tests__/RsSelect.spec.ts +423 -1
  11. package/src/__tests__/RsSwitch.spec.ts +30 -0
  12. package/src/__tests__/RsTimePicker.spec.ts +9 -0
  13. package/src/__tests__/RsTooltip.spec.ts +43 -0
  14. package/src/__tests__/css-token.spec.ts +51 -0
  15. package/src/__tests__/date-picker-utils.spec.ts +49 -0
  16. package/src/__tests__/dialog-utils.spec.ts +43 -0
  17. package/src/__tests__/form-path.spec.ts +46 -0
  18. package/src/__tests__/form-rules.spec.ts +63 -0
  19. package/src/__tests__/theme.spec.ts +2 -0
  20. package/src/components/RsAlert.vue +1 -1
  21. package/src/components/RsAvatar.vue +1 -1
  22. package/src/components/RsBadge.vue +1 -1
  23. package/src/components/RsBreadcrumb.vue +1 -1
  24. package/src/components/RsButton.vue +4 -4
  25. package/src/components/RsCalendarGrid.vue +3 -3
  26. package/src/components/RsCard.vue +4 -4
  27. package/src/components/RsCodeBlock.vue +4 -4
  28. package/src/components/RsCodeEditor.vue +8 -8
  29. package/src/components/RsConfirmDialog.vue +2 -2
  30. package/src/components/RsContextMenu.vue +6 -4
  31. package/src/components/RsDatePicker.vue +75 -31
  32. package/src/components/RsDateTimePicker.vue +9 -1
  33. package/src/components/RsDescriptions.vue +2 -2
  34. package/src/components/RsDialog.vue +26 -16
  35. package/src/components/RsDrawer.vue +1 -1
  36. package/src/components/RsDropdown.vue +1 -1
  37. package/src/components/RsDynamicTags.vue +18 -4
  38. package/src/components/RsEmpty.vue +1 -1
  39. package/src/components/RsForm.vue +68 -1
  40. package/src/components/RsFormItem.vue +423 -0
  41. package/src/components/RsFormItemControl.vue +21 -0
  42. package/src/components/RsFormList.vue +117 -0
  43. package/src/components/RsInput.vue +411 -18
  44. package/src/components/RsInputNumber.vue +13 -4
  45. package/src/components/RsLink.vue +1 -1
  46. package/src/components/RsMarkdown.vue +5 -5
  47. package/src/components/RsMenu.vue +2 -2
  48. package/src/components/RsMonacoEditor.vue +9 -3
  49. package/src/components/RsPagination.vue +1 -1
  50. package/src/components/RsProseEditor.vue +3 -3
  51. package/src/components/RsSelect.vue +332 -611
  52. package/src/components/RsSidebarGroup.vue +1 -1
  53. package/src/components/RsSplitPane.vue +22 -1
  54. package/src/components/RsStatCard.vue +2 -2
  55. package/src/components/RsSteps.vue +2 -2
  56. package/src/components/RsSwitch.vue +20 -7
  57. package/src/components/RsTabs.vue +9 -9
  58. package/src/components/RsTag.vue +1 -1
  59. package/src/components/RsTerminal.vue +58 -19
  60. package/src/components/RsTimePicker.vue +28 -4
  61. package/src/components/RsTimePickerColumns.vue +4 -4
  62. package/src/components/RsToaster.vue +7 -7
  63. package/src/components/RsTooltip.vue +10 -1
  64. package/src/components/RsTooltipProvider.vue +11 -1
  65. package/src/components/RsTree.vue +6 -6
  66. package/src/components/RsUpload.vue +2 -2
  67. package/src/components/date-picker-utils.ts +113 -2
  68. package/src/components/dialog-utils.ts +37 -2
  69. package/src/components/form-item-control.ts +55 -0
  70. package/src/components/form-path.ts +92 -0
  71. package/src/components/form-rules.ts +64 -10
  72. package/src/components/form-utils.ts +94 -2
  73. package/src/components/select-utils.ts +272 -12
  74. package/src/components/table/RsTableHeaderFilter.vue +1 -1
  75. package/src/components/table/rs-table.css +1 -1
  76. package/src/components/use-rs-select.ts +436 -0
  77. package/src/composables/useRsConfig.ts +10 -3
  78. package/src/composables/useRsI18n.ts +16 -5
  79. package/src/index.ts +69 -1
  80. package/src/locale/interpolate.ts +32 -0
  81. package/src/locale/messages.ts +10 -0
  82. package/src/styles.css +68 -12
  83. package/src/theme/brand.example.css +4 -1
  84. package/src/theme/css-token.ts +107 -0
  85. package/src/theme/types.ts +1 -0
@@ -0,0 +1,423 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, useId, useSlots, watch } from 'vue'
3
+ import { useRsI18n } from '../composables/useRsI18n'
4
+ import RsVNodeHost from './RsVNodeHost.vue'
5
+ import RsFormItemControl from './RsFormItemControl.vue'
6
+ import {
7
+ provideRsFormItemContext,
8
+ useRsFormContext,
9
+ useRsFormField,
10
+ useRsFormListContext,
11
+ type RsFormLabelAlign,
12
+ type RsFormLabelPosition,
13
+ type RsFormValidateStatus,
14
+ } from './form-utils'
15
+ import {
16
+ buildLocalInputRules,
17
+ normalizeFormRules,
18
+ runFormFieldRules,
19
+ type RsFormRuleItem,
20
+ type RsFormRuleTrigger,
21
+ } from './form-rules'
22
+ import {
23
+ concatNamePath,
24
+ isIndexSegment,
25
+ namePathKey,
26
+ type RsFormNamePath,
27
+ } from './form-path'
28
+
29
+ /**
30
+ * 表单项容器:包住任意自定义控件,负责 name / rules / 错误文案。
31
+ * 对标 Ant Design Form.Item、Element Plus ElFormItem。
32
+ * 内置 RsInput / RsSelect 已自行注册;放在已声明 name 的 Item 内时不再重复注册。
33
+ */
34
+ defineOptions({ name: 'RsFormItem' })
35
+
36
+ const model = defineModel<unknown>()
37
+
38
+ const props = withDefaults(
39
+ defineProps<{
40
+ name?: RsFormNamePath
41
+ label?: string
42
+ required?: boolean
43
+ rules?: RsFormRuleItem | RsFormRuleItem[]
44
+ invalid?: boolean
45
+ errorMessage?: string
46
+ showMessage?: boolean
47
+ hint?: string
48
+ /** 始终展示的辅助说明(错误出现时仍保留) */
49
+ extra?: string
50
+ /** 无错误时的说明;出错时被错误文案替换 */
51
+ help?: string
52
+ validateStatus?: RsFormValidateStatus
53
+ /** 不渲染标签与外壳,只保留控件与错误(schema 表单外置 label) */
54
+ noStyle?: boolean
55
+ /** 依赖字段变化时重新校验本字段(对齐 Ant Design dependencies) */
56
+ dependencies?: RsFormNamePath[]
57
+ labelPosition?: RsFormLabelPosition
58
+ }>(),
59
+ {
60
+ showMessage: true,
61
+ noStyle: false,
62
+ },
63
+ )
64
+
65
+ const emit = defineEmits<{
66
+ validate: [result: { valid: boolean; message?: string }]
67
+ }>()
68
+
69
+ const { t } = useRsI18n()
70
+ const slots = useSlots()
71
+ const formContext = useRsFormContext()
72
+ const listContext = useRsFormListContext()
73
+ const fieldId = useId()
74
+ const errorId = computed(() => `${fieldId}-error`)
75
+ const extraId = computed(() => `${fieldId}-extra`)
76
+
77
+ const touched = ref(false)
78
+ const autoInvalid = ref(false)
79
+ const autoMessage = ref('')
80
+
81
+ const resolvedNamePath = computed(() => {
82
+ if (props.name == null) return undefined
83
+ const own = concatNamePath(props.name)
84
+ if (!listContext) return own
85
+ return concatNamePath(listContext.prefix.value, own)
86
+ })
87
+ const nameKey = computed(() =>
88
+ resolvedNamePath.value?.length ? namePathKey(resolvedNamePath.value) : undefined,
89
+ )
90
+ const ownsField = computed(() => Boolean(nameKey.value))
91
+
92
+ const resolvedLabelPosition = computed(
93
+ () => props.labelPosition ?? formContext?.labelPosition.value ?? 'top',
94
+ )
95
+ const resolvedLabelAlign = computed<RsFormLabelAlign>(
96
+ () => formContext?.labelAlign.value ?? 'start',
97
+ )
98
+ const resolvedSize = computed(() => formContext?.size.value ?? 'md')
99
+
100
+ function readValue(): unknown {
101
+ const path = resolvedNamePath.value
102
+ if (formContext && path?.length && formContext.model.value) {
103
+ return formContext.getFieldValue(path)
104
+ }
105
+ return model.value
106
+ }
107
+
108
+ function writeValue(value: unknown): void {
109
+ model.value = value
110
+ const path = resolvedNamePath.value
111
+ if (formContext?.model.value && path?.length) {
112
+ formContext.setFieldValue(path, value)
113
+ }
114
+ }
115
+
116
+ /**
117
+ * 解析 dependencies / getFieldValue 路径。
118
+ * List 内以数字下标开头的视为相对路径([0, 'password'] → users.0.password);
119
+ * 其它视为表单根路径,便于 List 内依赖根字段。
120
+ */
121
+ function resolveWatchName(name: RsFormNamePath): Array<string | number> {
122
+ const segs = concatNamePath(name)
123
+ const prefix = listContext?.prefix.value
124
+ if (!prefix?.length || !segs.length) return segs
125
+ const first = segs[0]
126
+ if (first !== undefined && isIndexSegment(first)) {
127
+ return concatNamePath(prefix, segs)
128
+ }
129
+ return segs
130
+ }
131
+
132
+ const showRequiredMark = computed(() => {
133
+ if (props.required) return true
134
+ const formRules = formContext?.getFieldRules(nameKey.value) ?? []
135
+ const localRules = [
136
+ ...buildLocalInputRules({ required: props.required }),
137
+ ...normalizeFormRules(props.rules),
138
+ ]
139
+ return [...formRules, ...localRules].some((rule) => rule.required)
140
+ })
141
+
142
+ const displayMessage = computed(() => {
143
+ if (props.errorMessage) return props.errorMessage
144
+ if (props.showMessage && autoInvalid.value) return autoMessage.value
145
+ return ''
146
+ })
147
+
148
+ const isInvalid = computed(() => {
149
+ if (props.validateStatus === 'error') return true
150
+ if (props.validateStatus) return false
151
+ return props.invalid || autoInvalid.value
152
+ })
153
+
154
+ const mergedStatus = computed<RsFormValidateStatus>(() => {
155
+ if (props.validateStatus) return props.validateStatus
156
+ if (isInvalid.value) return 'error'
157
+ return ''
158
+ })
159
+
160
+ const describedBy = computed(() => {
161
+ const ids: string[] = []
162
+ if (displayMessage.value || props.help) ids.push(errorId.value)
163
+ if (props.extra) ids.push(extraId.value)
164
+ return ids.length ? ids.join(' ') : undefined
165
+ })
166
+
167
+ const errorSlotProps = computed(() => ({
168
+ name: nameKey.value,
169
+ message: displayMessage.value,
170
+ value: readValue(),
171
+ }))
172
+
173
+ const formErrorContent = computed(() => {
174
+ if (!displayMessage.value || !formContext?.renderError) return null
175
+ return formContext.renderError(errorSlotProps.value)
176
+ })
177
+
178
+ const fieldStyle = computed(() => {
179
+ if (props.noStyle || resolvedLabelPosition.value !== 'left') return undefined
180
+ const labelWidth = formContext?.labelWidth.value
181
+ if (!labelWidth) return undefined
182
+ return { '--rs-field-label-width': labelWidth } as Record<string, string>
183
+ })
184
+
185
+ const showLabel = computed(() => Boolean(props.label || slots.label) && !props.noStyle)
186
+
187
+ const controlInject = computed(() => ({
188
+ id: fieldId,
189
+ invalid: isInvalid.value,
190
+ showValidateMessage: false,
191
+ 'aria-invalid': isInvalid.value || undefined,
192
+ 'aria-describedby': describedBy.value,
193
+ modelValue: readValue(),
194
+ 'onUpdate:modelValue': writeValue,
195
+ }))
196
+
197
+ async function runValidate(trigger: RsFormRuleTrigger = 'submit'): Promise<boolean> {
198
+ touched.value = true
199
+ const formRules = formContext?.getFieldRules(nameKey.value) ?? []
200
+ const localRules = [
201
+ ...buildLocalInputRules({ required: props.required }),
202
+ ...normalizeFormRules(props.rules),
203
+ ]
204
+ const rules = [...formRules, ...localRules]
205
+
206
+ if (!rules.length) {
207
+ autoInvalid.value = false
208
+ autoMessage.value = ''
209
+ emit('validate', { valid: true })
210
+ return true
211
+ }
212
+
213
+ const result = await runFormFieldRules(readValue(), rules, {
214
+ trigger,
215
+ t,
216
+ name: nameKey.value,
217
+ label: typeof props.label === 'string' ? props.label : undefined,
218
+ validateMessages: formContext?.validateMessages.value,
219
+ getFieldValue: (name) => formContext?.getFieldValue(resolveWatchName(name)),
220
+ getFieldsValue: () => formContext?.getFieldsValue() ?? {},
221
+ })
222
+ autoInvalid.value = !result.valid
223
+ autoMessage.value = result.message ?? ''
224
+ emit('validate', result)
225
+ return result.valid
226
+ }
227
+
228
+ function setError(message: string): void {
229
+ autoInvalid.value = true
230
+ autoMessage.value = message
231
+ }
232
+
233
+ function setValue(value: unknown): void {
234
+ writeValue(value)
235
+ }
236
+
237
+ function clearValidation(): void {
238
+ touched.value = false
239
+ autoInvalid.value = false
240
+ autoMessage.value = ''
241
+ }
242
+
243
+ useRsFormField(() => {
244
+ if (!nameKey.value) return undefined
245
+ return {
246
+ name: nameKey.value,
247
+ getValue: () => readValue(),
248
+ setValue,
249
+ validate: async (trigger) => {
250
+ const valid = await runValidate(trigger ?? 'submit')
251
+ return { valid, message: autoMessage.value || undefined, name: nameKey.value }
252
+ },
253
+ clearValidation,
254
+ setError,
255
+ }
256
+ })
257
+
258
+ provideRsFormItemContext({
259
+ name: nameKey,
260
+ ownsField,
261
+ invalid: isInvalid,
262
+ message: displayMessage,
263
+ status: mergedStatus,
264
+ fieldId: computed(() => fieldId),
265
+ errorId,
266
+ describedBy,
267
+ })
268
+
269
+ watch(
270
+ () => readValue(),
271
+ () => {
272
+ if (touched.value) void runValidate('change')
273
+ },
274
+ )
275
+
276
+ watch(
277
+ () =>
278
+ JSON.stringify(
279
+ (props.dependencies ?? []).map((name) =>
280
+ formContext?.getFieldValue(resolveWatchName(name)),
281
+ ),
282
+ ),
283
+ (_next, prev) => {
284
+ if (prev === undefined) return
285
+ const current = readValue()
286
+ const hasValue =
287
+ current != null && !(typeof current === 'string' && current.trim() === '')
288
+ if (touched.value || autoInvalid.value || hasValue) {
289
+ void runValidate('change')
290
+ }
291
+ },
292
+ )
293
+
294
+ defineExpose({
295
+ validate: runValidate,
296
+ clearValidation,
297
+ setValue,
298
+ setError,
299
+ })
300
+ </script>
301
+
302
+ <template>
303
+ <div
304
+ class="rs-form-item"
305
+ :class="[
306
+ !noStyle ? 'rs-field' : undefined,
307
+ !noStyle ? `rs-field--label-${resolvedLabelPosition}` : undefined,
308
+ !noStyle && resolvedLabelPosition === 'left' && resolvedLabelAlign === 'end'
309
+ ? 'rs-field--label-align-end'
310
+ : undefined,
311
+ mergedStatus ? `rs-form-item--${mergedStatus}` : undefined,
312
+ `rs-form-item--${resolvedSize}`,
313
+ ]"
314
+ :style="fieldStyle"
315
+ :data-rs-form-item="nameKey"
316
+ >
317
+ <span v-if="showLabel" class="rs-field__label">
318
+ <slot name="label">
319
+ <label :for="fieldId">{{ label }}</label>
320
+ </slot>
321
+ <span v-if="showRequiredMark" class="rs-field__required" aria-hidden="true">*</span>
322
+ </span>
323
+
324
+ <div class="rs-form-item__control">
325
+ <RsFormItemControl :inject="controlInject">
326
+ <slot
327
+ :id="fieldId"
328
+ :invalid="isInvalid"
329
+ :message="displayMessage"
330
+ :field="nameKey"
331
+ :status="mergedStatus"
332
+ />
333
+ </RsFormItemControl>
334
+ </div>
335
+
336
+ <p
337
+ v-if="displayMessage"
338
+ :id="errorId"
339
+ class="rs-form-item__error"
340
+ role="alert"
341
+ >
342
+ <slot name="error" v-bind="errorSlotProps">
343
+ <RsVNodeHost
344
+ v-if="formErrorContent !== null && formErrorContent !== undefined"
345
+ :content="formErrorContent"
346
+ />
347
+ <template v-else>{{ displayMessage }}</template>
348
+ </slot>
349
+ </p>
350
+ <p
351
+ v-else-if="help"
352
+ :id="errorId"
353
+ class="rs-form-item__help"
354
+ >
355
+ {{ help }}
356
+ </p>
357
+
358
+ <span v-if="hint && !noStyle" class="rs-field__hint">{{ hint }}</span>
359
+ <span v-if="extra" :id="extraId" class="rs-form-item__extra">{{ extra }}</span>
360
+ </div>
361
+ </template>
362
+
363
+ <style scoped>
364
+ .rs-form-item__control {
365
+ width: 100%;
366
+ min-height: var(--rs-control-height-md);
367
+ min-width: 0;
368
+ }
369
+
370
+ /*
371
+ * 左侧标签与控件行等高、文字垂直居中。
372
+ * 网格仍是 align-items:start,错误文案在第 2 列,不会把 label 顶歪。
373
+ */
374
+ .rs-field--label-left > .rs-field__label {
375
+ display: flex;
376
+ align-items: center;
377
+ box-sizing: border-box;
378
+ min-height: var(--rs-control-height-md);
379
+ }
380
+
381
+ .rs-form-item--ssm.rs-field--label-left > .rs-field__label,
382
+ .rs-form-item--ssm .rs-form-item__control {
383
+ min-height: var(--rs-control-height-ssm);
384
+ }
385
+
386
+ .rs-form-item--sm.rs-field--label-left > .rs-field__label,
387
+ .rs-form-item--sm .rs-form-item__control {
388
+ min-height: var(--rs-control-height-sm);
389
+ }
390
+
391
+ .rs-form-item--lg.rs-field--label-left > .rs-field__label,
392
+ .rs-form-item--lg .rs-form-item__control {
393
+ min-height: var(--rs-control-height-lg);
394
+ }
395
+
396
+ .rs-field--label-left .rs-form-item__control,
397
+ .rs-field--label-left .rs-form-item__error,
398
+ .rs-field--label-left .rs-form-item__help,
399
+ .rs-field--label-left .rs-form-item__extra {
400
+ grid-column: 2;
401
+ }
402
+
403
+ .rs-form-item__error,
404
+ .rs-form-item__help,
405
+ .rs-form-item__extra {
406
+ margin: 0.25rem 0 0;
407
+ font-size: var(--rs-font-size-xs);
408
+ line-height: var(--rs-line-height-tight);
409
+ }
410
+
411
+ .rs-form-item__error {
412
+ color: var(--rs-danger);
413
+ }
414
+
415
+ .rs-form-item__help,
416
+ .rs-form-item__extra {
417
+ color: var(--rs-muted);
418
+ }
419
+
420
+ .rs-form-item--warning .rs-form-item__error {
421
+ color: var(--rs-warning, #d48806);
422
+ }
423
+ </style>
@@ -0,0 +1,21 @@
1
+ <script lang="ts">
2
+ import { defineComponent, type PropType, type VNode } from 'vue'
3
+ import { bindFormItemControl } from './form-item-control'
4
+
5
+ /**
6
+ * 将 inject 合并到默认插槽第一个子节点,供 Form.Item 注入控件 props。
7
+ */
8
+ export default defineComponent({
9
+ name: 'RsFormItemControl',
10
+ inheritAttrs: false,
11
+ props: {
12
+ inject: {
13
+ type: Object as PropType<Record<string, unknown>>,
14
+ default: () => ({}),
15
+ },
16
+ },
17
+ setup(props, { slots }) {
18
+ return () => bindFormItemControl(slots.default?.() as VNode[] | undefined, props.inject)
19
+ },
20
+ })
21
+ </script>
@@ -0,0 +1,117 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue'
3
+ import {
4
+ provideRsFormListContext,
5
+ useRsFormContext,
6
+ useRsFormListContext,
7
+ type RsFormListField,
8
+ type RsFormListOperations,
9
+ } from './form-utils'
10
+ import { concatNamePath, type RsFormNamePath } from './form-path'
11
+
12
+ /**
13
+ * 动态数组字段(对齐 Ant Design Form.List)。
14
+ * 需要 Form.model;子 Item 的 name 为相对路径,会拼上 List 前缀。
15
+ */
16
+ defineOptions({ name: 'RsFormList' })
17
+
18
+ const props = defineProps<{
19
+ name: RsFormNamePath
20
+ }>()
21
+
22
+ const form = useRsFormContext()
23
+ const parentList = useRsFormListContext()
24
+
25
+ const prefix = computed(() => concatNamePath(parentList?.prefix.value, props.name))
26
+
27
+ provideRsFormListContext({ prefix })
28
+
29
+ const rowKeys = ref<number[]>([])
30
+ let keySeed = 0
31
+
32
+ function readList(): unknown[] {
33
+ const raw = form?.getFieldValue(prefix.value)
34
+ return Array.isArray(raw) ? raw : []
35
+ }
36
+
37
+ function writeList(next: unknown[]): void {
38
+ form?.setFieldValue(prefix.value, next)
39
+ }
40
+
41
+ function syncKeys(length: number): void {
42
+ const keys = rowKeys.value
43
+ if (keys.length === length) return
44
+ if (keys.length < length) {
45
+ const next = keys.slice()
46
+ while (next.length < length) {
47
+ keySeed += 1
48
+ next.push(keySeed)
49
+ }
50
+ rowKeys.value = next
51
+ return
52
+ }
53
+ rowKeys.value = keys.slice(0, length)
54
+ }
55
+
56
+ watch(
57
+ () => readList().length,
58
+ (length) => {
59
+ syncKeys(length)
60
+ },
61
+ { immediate: true },
62
+ )
63
+
64
+ const fields = computed<RsFormListField[]>(() =>
65
+ readList().map((_, index) => ({
66
+ key: rowKeys.value[index] ?? index,
67
+ name: index,
68
+ index,
69
+ })),
70
+ )
71
+
72
+ const add: RsFormListOperations['add'] = (defaultValue = {}) => {
73
+ const current = readList()
74
+ keySeed += 1
75
+ rowKeys.value = [...rowKeys.value, keySeed]
76
+ writeList([...current, defaultValue])
77
+ }
78
+
79
+ const remove: RsFormListOperations['remove'] = (index) => {
80
+ const current = readList()
81
+ if (index < 0 || index >= current.length) return
82
+ rowKeys.value = rowKeys.value.filter((_, i) => i !== index)
83
+ writeList(current.filter((_, i) => i !== index))
84
+ }
85
+
86
+ const move: RsFormListOperations['move'] = (from, to) => {
87
+ const current = readList()
88
+ if (
89
+ from === to ||
90
+ from < 0 ||
91
+ to < 0 ||
92
+ from >= current.length ||
93
+ to >= current.length
94
+ ) {
95
+ return
96
+ }
97
+ const next = current.slice()
98
+ const [row] = next.splice(from, 1)
99
+ next.splice(to, 0, row)
100
+ const keys = rowKeys.value.slice()
101
+ const [key] = keys.splice(from, 1)
102
+ keys.splice(to, 0, key!)
103
+ rowKeys.value = keys
104
+ writeList(next)
105
+ }
106
+ </script>
107
+
108
+ <template>
109
+ <div class="rs-form-list">
110
+ <slot
111
+ :fields="fields"
112
+ :add="add"
113
+ :remove="remove"
114
+ :move="move"
115
+ />
116
+ </div>
117
+ </template>