gaoluyang
2026-06-24 c0cb161bb52ce0fbdce5c66ec391d107c75e2452
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
<script setup lang="ts">
import type { FormSchema } from '../types';
 
import { computed } from 'vue';
 
import { Plus, X } from '..\..\..\..\base\icons\src';
import {
  VbenButton,
  VbenIconButton,
  VbenRenderContent,
} from '..\..\..\shadcn-ui\src';
import { cn } from '..\..\..\..\base\shared\src\utils';
 
import { useFieldArray } from 'vee-validate';
 
import FormField from '../form-render/form-field.vue';
 
defineOptions({ name: 'VbenFormFieldArray', inheritAttrs: false });
 
const props = withDefaults(
  defineProps<{
    /** 操作列表头文案 */
    actionText?: string;
    /** 「添加」按钮文案 */
    addButtonText?: string;
    /**
     * 新增一行时生成的默认数据;缺省时按 schema 的 fieldName 生成空对象
     */
    createRow?: () => Record<string, any>;
    disabled?: boolean;
    /** 空数据文案 */
    emptyText?: string;
    /** 最多行数 */
    max?: number;
    /** 最少行数 */
    min?: number;
    /**
     * 字段路径,由外层 FormField 通过 componentField 透传(vee-validate 的 name)
     */
    name?: string;
    /**
     * 列定义,每一列就是一个子字段(复用 FormSchema)
     */
    schema?: FormSchema[];
    /** 是否显示序号列 */
    showIndex?: boolean;
  }>(),
  {
    actionText: '操作',
    addButtonText: '添加一行',
    createRow: undefined,
    disabled: false,
    emptyText: '暂无数据',
    max: Number.POSITIVE_INFINITY,
    min: 0,
    name: '',
    schema: () => [],
    showIndex: true,
  },
);
 
const arrayPath = computed(() => props.name);
 
const { fields, push, remove } = useFieldArray<Record<string, any>>(
  () => arrayPath.value,
);
 
const canAdd = computed(() => fields.value.length < props.max);
const canRemove = computed(() => fields.value.length > props.min);
 
function buildDefaultRow(): Record<string, any> {
  if (props.createRow) {
    return props.createRow();
  }
  return Object.fromEntries(props.schema.map((col) => [col.fieldName, null]));
}
 
function addRow() {
  if (props.disabled || !canAdd.value) {
    return;
  }
  push(buildDefaultRow());
}
 
function removeRow(index: number) {
  if (props.disabled || !canRemove.value) {
    return;
  }
  remove(index);
}
 
/**
 * 把列定义转换为子单元格 FormField 所需的 props。
 * - fieldName 替换为嵌套路径 `name[index].fieldName`,让校验与取值落在数组元素上
 * - hideLabel:表头已展示列名,单元格不重复显示
 */
function cellProps(col: FormSchema, index: number) {
  return {
    ...col,
    commonComponentProps: {},
    disabled: props.disabled,
    fieldName: `${arrayPath.value}[${index}].${col.fieldName}`,
    formFieldProps: {},
    hideLabel: true,
  };
}
</script>
 
<template>
  <div :class="cn('w-full', $attrs.class as string)">
    <table class="w-full border-collapse">
      <thead>
        <tr class="border-border border-b">
          <th
            v-if="showIndex"
            class="text-muted-foreground w-12 px-2 py-2 text-left text-sm font-normal"
          >
            #
          </th>
          <th
            v-for="col in schema"
            :key="col.fieldName"
            class="text-muted-foreground px-2 py-2 text-left text-sm font-normal"
          >
            <VbenRenderContent :content="col.label" />
          </th>
          <th
            class="text-muted-foreground w-16 px-2 py-2 text-left text-sm font-normal"
          >
            {{ actionText }}
          </th>
        </tr>
      </thead>
      <tbody>
        <tr
          v-for="(entry, index) in fields"
          :key="entry.key"
          class="border-border/60 border-b align-top"
        >
          <td v-if="showIndex" class="text-muted-foreground px-2 py-3 text-sm">
            {{ index + 1 }}
          </td>
          <td v-for="col in schema" :key="col.fieldName" class="px-2 py-2">
            <FormField v-bind="cellProps(col, index)" />
          </td>
          <td class="px-2 py-3">
            <VbenIconButton
              :disabled="disabled || !canRemove"
              :on-click="() => removeRow(index)"
              class="text-muted-foreground hover:text-destructive"
            >
              <X class="size-4" />
            </VbenIconButton>
          </td>
        </tr>
      </tbody>
    </table>
 
    <div
      v-if="fields.length === 0"
      class="text-muted-foreground border-border/60 border-b py-6 text-center text-sm"
    >
      {{ emptyText }}
    </div>
 
    <VbenButton
      variant="outline"
      size="sm"
      :disabled="disabled || !canAdd"
      class="mt-3 w-full border-dashed"
      @click="addRow"
    >
      <Plus class="mr-1 size-4" />
      {{ addButtonText }}
    </VbenButton>
  </div>
</template>