Skip to content

动态个数的输入框

DynamicInput 使用

[]
[]
[]
查看代码
vue
<template>
  <a-form layout="vertical">
    <a-form-item label="单一">
      <DynamicInput :options="state1.option" v-model:data="state1.data" />
      {{ state1.data }}
    </a-form-item>
    <a-form-item label="二列">
      <DynamicInput :options="state2.option" v-model:data="state2.data" />
      {{ state2.data }}
    </a-form-item>
    <a-form-item label="三列">
      <DynamicInput :options="state3.option" v-model:data="state3.data" />
      {{ state3.data }}
    </a-form-item>
  </a-form>
</template>
<script setup lang="ts">
import { Form as AForm, FormItem as AFormItem } from 'ant-design-vue'
import { reactive } from 'vue'
import DynamicInput, { type Options } from './DynamicInput.vue'
type State1 = { value: string }
const state1 = reactive({
  option: [{ key: 'value', placeholder: '请输入值' }] as Options<State1>[],
  data: [] as State1[]
})

type State2 = { label: string; desc: string }
const state2 = reactive({
  option: [
    { key: 'label', placeholder: '请输入标签', span: 8 },
    { key: 'desc', placeholder: '请输入描述', span: 16, required: false }
  ] as Options<State2>[],
  data: [] as State2[]
})

type State3 = { name: string; age: string; address: string }
const state3 = reactive({
  option: [
    { key: 'name', placeholder: '请输入姓名', span: 4 },
    { key: 'age', placeholder: '请输入年龄', span: 4 },
    { key: 'address', placeholder: '请输入地址', span: 16, required: false }
  ] as Options<State3>[],
  data: [] as State3[]
})
</script>
vue
<template>
  <a-row :key="rowIndex" align="middle" style="margin-bottom: 4px" v-for="(row, rowIndex) in state">
    <a-col flex="1">
      <a-row :gutter="[4, 4]">
        <a-col
          :key="colIndex"
          :span="col.span || 24 / options.length"
          v-for="(col, colIndex) in options"
        >
          <a-input
            :placeholder="col.placeholder"
            v-model:value="row[col.key || `value${colIndex}`]"
          />
        </a-col>
      </a-row>
    </a-col>
    <a-col>
      <div class="actions-container">
        <PlusCircleOutlined
          @click="handleAdd"
          class="anticon add"
          v-if="rowIndex === state.length - 1"
        />
        <DeleteOutlined
          @click="handleRemove(rowIndex)"
          class="anticon remove"
          v-if="state.length !== 1"
        />
      </div>
    </a-col>
  </a-row>
</template>

<script lang="ts" setup>
import { DeleteOutlined, PlusCircleOutlined } from '@ant-design/icons-vue'
import { Col as ACol, Input as AInput, Row as ARow, message } from 'ant-design-vue'
import { ref, watch } from 'vue'

export type Options<T extends Record<string, any>> = {
  /** 第几列属性的key */
  key: keyof T & string
  /** 输入框默认占位显示 */
  placeholder?: string
  /** a-col 的栅格占位 */
  span?: number
  /** 默认必填 */
  required?: boolean
}

const props = withDefaults(
  defineProps<{
    /** 列配置 */
    options?: Options<Record<string, any>>[]
  }>(),
  {
    options: () => [{ key: 'value', placeholder: '属性' }] as Options<Record<string, any>>[]
  }
)

const generateEmptyItem = (): Record<string, any> => {
  const keys = props.options?.map(c => c.key) ?? []
  return keys.reduce((acc, key) => {
    acc[key] = ''
    return acc
  }, {} as Record<string, any>)
}

const data = defineModel<Record<string, any>[]>('data')

const state = ref<Record<string, any>[]>([generateEmptyItem()])
watch(
  () => state.value,
  stateData => {
    data.value = stateData
  },
  { deep: true }
)

function validate() {
  const notHasNull = state.value.find(s => {
    const rowHasNull = props?.options?.find((c, i) => {
      const required = c.required ?? true
      if (!required) {
        return false
      }
      return !s[c.key || `value${i}`]
    })
    return rowHasNull
  })
  return !notHasNull
}

function handleAdd() {
  const notHasNull = validate()
  if (!notHasNull) {
    message.warning('请将之前的填写完成后再新增')
    return
  }
  state.value.push(generateEmptyItem())
}

function handleRemove(rowIndex: number) {
  if (rowIndex === 0 && state.value.length === 1) {
    return
  }
  state.value.splice(rowIndex, 1)
}

defineExpose({
  validate,
  initData(data: Record<string, any>[]) {
    state.value = data
  }
})
</script>

<style scoped>
.actions-container {
  width: 60px;
  padding: 0 4px;

  .anticon {
    font-size: 18px;
    padding: 0 4px;
    cursor: pointer;
    transition: transform 0.1s;

    &:hover {
      transform: scale(1.1);
    }

    &.add {
      color: #1677ff;
    }

    &.remove {
      color: #ff4d4f;
    }
  }
}
</style>