Vue3非组合式API下自定义array-field组件v-model类型异常问题
Vue 2迁移Vue 3后自定义array-field组件v-model类型异常(数组变字符串)
问题概述
从Vue 2迁移到Vue 3(使用Options API),基于Vuetify v-text-field封装的array-field组件,原本支持生成多个输入框并返回字符串数组给后端,但现在输入内容后,绑定的v-model会从数组变为字符串,导致后端请求失败,Vue 2环境下功能正常。
组件代码(原异常版本)
<template> <div ref="array_field" class="array_field__wrapper" > <v-text-field v-for="(element, index) in elements" v-bind="$attrs" :key="index" v-model="elements[index]" :label="isFirstValue(index) ? labelText : ''" class="array_field" :rules="rules.name" dense outlined :class="{ 'indent_right': !isLastValue(index) }" :append-inner-icon="isEmptyArray ? null : $icons['mdi-close']" :append-icon="isLastValue(index) ? $icons['mdi-plus'] : null" @click:append="addValue(index)" @click:append-inner="deleteValue(index)" @update:modelValue="onInput" @blur="onBlur" > <template #message="{ message }"> {{ translateError(errorMessage, message) }} </template> </v-text-field> </div> </template> <script> import { isEmptyOrNil } from '@/utils' export default { props: { value: Array, labelText: String, translateError: Function, errorMessage: String }, data () { return { elements: !isEmptyOrNil(this.value) ? this.value : [''], isEmptyArray: isEmptyOrNil(this.value), rules: {} } }, watch: { elements (value) { this.$emit('input', this.isEmptyArray ? [] : value) } }, mounted () { if (!isEmptyOrNil(this.value) && isEmptyOrNil(this.value[0])) { this.setLabelPositionOnTop() } }, methods: { isFirstValue (index) { return index === 0 }, isLastValue (index) { return index === this.elements.length - 1 }, addValue (index) { this.elements[index + 1] = '' this.isEmptyArray = false this.setLabelPositionOnTop() }, deleteValue (index) { if (this.elements.length > 1) { this.elements.splice(index, 1) } else { this.elements = [''] this.isEmptyArray = true this.setLabelPositionByDefault() } }, setLabelPositionOnTop () { const commonWrapper = this.$refs.array_field.firstChild.firstChild.firstChild const legendWrapper = commonWrapper.firstChild const labelWrapper = legendWrapper.nextSibling const label = labelWrapper.firstChild label.classList.add('v-label--active') const legend = legendWrapper.querySelector('legend').querySelector('span') legend.style.color = 'white' legend.style.fontSize = '12px' legend.style.textWrap = 'nowrap' legend.style.padding = '2px' const labelText = label.innerHTML legend.innerHTML = labelText }, setLabelPositionByDefault () { const commonWrapper = this.$refs.array_field.firstChild.firstChild.firstChild const legendWrapper = commonWrapper.firstChild const labelWrapper = legendWrapper.nextSibling const label = labelWrapper.firstChild label.classList.remove('v-label--active') const legend = legendWrapper.querySelector('legend').querySelector('span') legend.innerHTML = '\u200b' legend.style.padding = 0 }, onInput (value) { this.isEmptyArray = false console.log(value) }, onBlur () { if (!this.isEmptyArray) { this.setLabelPositionOnTop() } } } } </script>
组件使用示例
<isf-array-field v-model="authSource.add.ldap_phone" />
弹窗Data定义
data () { const authSourceTemplate = { add: { name: null, url: null, base_dn: null, login: null, password: null, type: 'ldap', ldap_filter: null, ldap_phone: [], ldap_email: [], ldap_uuid: [], ldap_lastname: [], ldap_firstname: [], ldap_middlename: [], ldap_internal_phone: [], with_internal_phone: null } } return { authSourceTemplate: authSourceTemplate, authSource: this.clone(authSourceTemplate), actionPending: false, authSourcesDialogsProp: this.authsourcesDialogs, rules: {} } }
问题原因
- Vue3 v-model协议不兼容:Vue2使用
valueprop +input事件实现v-model,而Vue3默认使用modelValueprop +update:modelValue事件。当前组件仍沿用Vue2的协议,但外层使用Vue3的v-model时,会自动传递modelValue和监听update:modelValue,组件未处理这些,导致绑定逻辑混乱。 - $attrs传递冲突:组件使用
v-bind="$attrs"将所有未声明的prop传递给内部v-text-field,其中包含Vue3 v-model的modelValue和onUpdate:modelValue,导致内部单个输入框的字符串更新直接触发外层v-model,将数组覆盖为字符串。 - 不必要的事件绑定:内部v-text-field绑定了
@update:modelValue="onInput",该事件返回单个输入字符串,进一步干扰了外层v-model的类型。
修复方案
修改后的组件代码
<template> <div ref="array_field" class="array_field__wrapper" > <v-text-field v-for="(element, index) in elements" v-bind="filteredAttrs" :key="index" v-model="elements[index]" :label="isFirstValue(index) ? labelText : ''" class="array_field" :rules="rules.name" dense outlined :class="{ 'indent_right': !isLastValue(index) }" :append-inner-icon="isEmptyArray ? null : $icons['mdi-close']" :append-icon="isLastValue(index) ? $icons['mdi-plus'] : null" @click:append="addValue(index)" @click:append-inner="deleteValue(index)" @blur="onBlur" > <template #message="{ message }"> {{ translateError(errorMessage, message) }} </template> </v-text-field> </div> </template> <script> import { isEmptyOrNil } from '@/utils' export default { inheritAttrs: false, // 阻止未声明prop自动传递到根元素 props: { modelValue: Array, // 适配Vue3 v-model的prop名 labelText: String, translateError: Function, errorMessage: String }, data () { return { elements: !isEmptyOrNil(this.modelValue) ? [...this.modelValue] : [''], // 浅拷贝避免修改原prop isEmptyArray: isEmptyOrNil(this.modelValue), rules: {} } }, computed: { filteredAttrs () { // 过滤v-model相关属性,避免传递给内部输入框 const { modelValue, 'onUpdate:modelValue': _onUpdateModelValue, ...rest } = this.$attrs return rest } }, watch: { elements (value) { // 过滤空值后,触发Vue3的v-model更新事件 const filteredValues = value.filter(item => !isEmptyOrNil(item)) this.$emit('update:modelValue', filteredValues.length > 0 ? filteredValues : []) }, // 同步外部modelValue的变化到内部elements modelValue (newVal) { if (!isEmptyOrNil(newVal)) { this.elements = [...newVal] this.isEmptyArray = false } else { this.elements = [''] this.isEmptyArray = true } } }, mounted () { if (!isEmptyOrNil(this.modelValue) && isEmptyOrNil(this.modelValue[0])) { this.setLabelPositionOnTop() } }, methods: { isFirstValue (index) { return index === 0 }, isLastValue (index) { return index === this.elements.length - 1 }, addValue (index) { this.elements.splice(index + 1, 0, '') // 用splice保证数组响应式更新 this.isEmptyArray = false this.setLabelPositionOnTop() }, deleteValue (index) { if (this.elements.length > 1) { this.elements.splice(index, 1) } else { this.elements = [''] this.isEmptyArray = true this.setLabelPositionByDefault() } }, setLabelPositionOnTop () { const commonWrapper = this.$refs.array_field.firstChild.firstChild.firstChild const legendWrapper = commonWrapper.firstChild const labelWrapper = legendWrapper.nextSibling const label = labelWrapper.firstChild label.classList.add('v-label--active') const legend = legendWrapper.querySelector('legend').querySelector('span') legend.style.color = 'white' legend.style.fontSize = '12px' legend.style.textWrap = 'nowrap' legend.style.padding = '2px' const labelText = label.innerHTML legend.innerHTML = labelText }, setLabelPositionByDefault () { const commonWrapper = this.$refs.array_field.firstChild.firstChild.firstChild const legendWrapper = commonWrapper.firstChild const labelWrapper = legendWrapper.nextSibling const label = labelWrapper.firstChild label.classList.remove('v-label--active') const legend = legendWrapper.querySelector('legend').querySelector('span') legend.innerHTML = '\u200b' legend.style.padding = 0 }, onBlur () { if (!this.isEmptyArray) { this.setLabelPositionOnTop() } } } } </script>
关键修改点
- 适配Vue3 v-model协议:将
valueprop改为modelValue,触发update:modelValue事件替代原input事件。 - 阻止attrs冲突:添加
inheritAttrs: false,并通过计算属性filteredAttrs过滤掉v-model相关属性,避免传递给内部输入框。 - 保证响应式与类型安全:用浅拷贝初始化
elements避免修改原prop,监听modelValue同步外部变化,过滤空值后再触发更新,确保返回始终是数组类型。 - 移除干扰逻辑:删除不必要的
onInput方法和对应的事件绑定,避免单个字符串更新干扰外层v-model。
内容的提问来源于stack exchange,提问作者Carl
相关产品推荐
相关产品推荐

