Vue带新增行按钮动态表单实现方案及接口设计咨询
Vue长表单改造相关问题解答
问题背景
我正在尝试将一份长表单改造为Vue实现,以提升表单交互性,目前已经完成基础字段的双向绑定,基础功能运行良好。后续需要支持用户点击「新增行」按钮添加多条可重复记录(例如教育经历信息),原始Blade模板版本是直接循环$user->educationDetails渲染已有教育经历数据,现有两个疑问:
- 在Vue中,是否推荐将这类关联对象数据加载到data属性中,再通过
v-for指令循环渲染?
- 在Vue中,是否推荐将这类关联对象数据加载到data属性中,再通过
- 若采用上述方案,接口需要同时返回用户基础信息及其他关联模型数据,这类数据应该拆分到不同接口,还是可以在同一个接口中统一返回?
现有实现示例代码
<template> <form v-on:submit.prevent action="" method="post"> <div class="row no-gutter application-section-container"> <div class="col-12 col-lg-4 application-section-category-sidebar"> <h3 id="profile" class="application-section-header">Profile</h3> <small class="d-block application-section-info">Please fill in your profile details.</small> <small class="d-block application-section-requirements"><strong>(All Fields Required)</strong></small> <small class="d-block application-section-status"><strong>Incomplete</strong></small> </div> <div class="col-12 col-lg-8"> <div class="application-section" id="application-user-profile"> <div class="form-row"> <div class="col"> <label for="first_name">First Name<span class="required">*</span></label> <input type="text" class="form-control" name="first_name" id="first_name" placeholder="First Name" autocomplete="given-name" value="" readonly v-model="first_name"> </div> <div class="col"> <label for="last_name">Last Name<span class="required">*</span></label> <input type="text" class="form-control" name="last_name" id="last_name" placeholder="Last Name" autocomplete="family-name" value="" readonly v-model="last_name"> </div> </div> <div class="form-row"> <div class="col"> <label for="email">Email Address<span class="required">*</span></label> <input type="email" class="form-control" name="email" id="email" placeholder="john.doe@example.com" autocomplete="email" value="" readonly v-model="email"> </div> <div class="col"> <label for="contact_number">Contact Number<span class="required">*</span></label> <input required type="tel" class="form-control" name="contact_number" id="contact_number" minlength="11" maxlength="14" autocomplete="tel" value="" v-model="contact_number"> </div> </div> <div class="form-row"> <div class="col-12 col-sm-6 col-md-4"> <label for="gender">Gender<span class="required">*</span></label> <select required name="gender" id="gender" class="form-control" v-model="gender"> <option v-for="gender in genders" v-bind:key="gender" v-bind:value="gender">{{ gender }}</option> </select> </div> </div> <div class="form-row"> <div class="col-12 mb-3"> <label for="profile_address_line_1">Street Address<span class="required">*</span></label> <input type="text" class="form-control mb-2" name="profile_address_line_1" id="profile_address_line_1" placeholder="Address, Line 1" value="" v-model="profile_address_line_1"> <input type="text" class="form-control mb-2" name="profile_address_line_2" id="profile_address_line_2" placeholder="Address, Line 2" value="" v-model="profile_address_line_2"> <input type="text" class="form-control mb-2" name="profile_address_line_3" id="profile_address_line_3" placeholder="Address, Line 3" value="" v-model="profile_address_line_3"> </div> <div class="col-6 col-sm-4 col-md-4 mb-3"> <label for="profile_town">Town/City<span class="required">*</span></label> <input type="text" class="form-control" name="profile_town" id="profile_town" value="" v-model="profile_town"> </div> <div class="col-6 col-sm-4 col-md-4 mb-3"> <label for="profile_county">Region/County<span class="required">*</span></label> <input type="text" class="form-control" name="profile_county" id="profile_county" value="" v-model="profile_county"> </div> <div class="col-12 col-sm-4 col-md-4 mb-3"> <label for="profile_postcode">Postcode<span class="required">*</span></label> <input type="text" class="form-control" name="profile_postcode" id="profile_postcode" value="" v-model="profile_postcode"> </div> <div class="col-12"> <label for="profile_country">Country<span class="required">*</span></label> <select class="form-control" name="profile_country" id="profile_country" v-model="profile_country"> </select> </div> </div> <div class="form-row"> <div class="col-12"> <div class="custom-control custom-checkbox"> <input class="custom-control-input profile_has_disability" type="checkbox" name="profile_has_disability" value="" id="profile_has_disability" v-model="profile_has_disability"> <label class="custom-control-label" for="profile_has_disability"> Do you have any disabilities? </label> </div> </div> <div class="col-12 profile-disabilities-container" v-if="profile_has_disability"> <label for="profile_disabilities">Please state your disabilities:</label> <textarea class="form-control" name="profile_disabilities" id="profile_disabilities" rows="4" v-model="profile_disabilities"></textarea> <small>*This will enable us to make any reasonable adjustments for candidates at interview stage</small> </div> </div> </div> </div> </div> </form> </template> <script> export default { data(){ return{ 'genders': [ 'Male', 'Female', 'Other', 'Prefer not to say' ], 'first_name': null, 'last_name': null, 'email': null, 'contact_number': null, 'gender': null, 'profile_address_line_1': null, 'profile_address_line_2': null, 'profile_address_line_3': null, 'profile_town': null, 'profile_county': null, 'profile_postcode': null, 'profile_country': null, 'profile_has_disability': null, 'profile_disabilities': null, } }, mounted() { this.getProfile(); }, methods:{ getProfile(){ axios.get('/account/api/profile') .then((response) => { console.log(response.data); this.first_name = response.data.user.first_name; this.last_name = response.data.user.last_name; this.email = response.data.user.email; this.contact_number = response.data.user.contact_number; this.gender = response.data.user.gender; this.profile_address_line_1 = response.data.user.profile_address_line_1; this.profile_address_line_2 = response.data.user.profile_address_line_2; this.profile_address_line_3 = response.data.user.profile_address_line_3; this.profile_town = response.data.user.town; this.profile_county = response.data.user.county; this.profile_postcode = response.data.user.postcode; this.profile_country = response.data.user.country; this.profile_has_disability = response.data.user.has_disability; this.profile_disabilities = response.data.user.disabilities; }).catch((err) => { console.log(err); }); } } } </script>
问题解答
关于关联数据是否存在data中用v-for渲染
非常推荐这么做,这就是Vue处理这类动态列表场景的标准方案。
你要做可新增、删除、编辑的重复记录模块(比如教育经历、工作经历),直接在data里定义一个数组类型的字段(比如educationList),接口拉到已有数据就存进这个数组,点「新增行」就给数组push一个带空字段的新对象,点删除就splice对应索引的项,模板里直接v-for="(item, index) in educationList"循环渲染每一行的表单项,表单项直接用v-model="item.xxx"绑定对应字段就行,实现起来非常顺,响应式也完全不用手动操作DOM,比之前jQuery/Blade混写省至少一半代码。
唯一要注意的点是给v-for绑定唯一key,有后端返回的记录id就用id当key,新增的临时项可以用自增数或者时间戳当临时key,别直接用index当key,不然列表排序、删项的时候容易出表单值错位的问题。
关于接口是合并还是拆分
没有绝对标准答案,按表单实际场景选即可:
- 如果长表单是单页一次性加载完所有模块(用户进页面就要看到个人信息+教育经历+工作经历所有内容),直接一个接口把所有关联数据一次性返回就行,省得发多次请求拖慢加载速度,后端做个接口关联查询把用户基础信息、各关联表数据一起吐出来,前端拿到直接赋值给对应data字段就好,逻辑更简单。
- 如果表单是分步骤/分模块懒加载的(比如用户点到「教育经历」标签才加载对应内容),那就拆成独立接口,用户进入对应模块的时候再发请求拉数据,能减少首屏加载的数据包大小,首屏打开更快。
- 如果关联数据量特别大(比如用户有几十上百条工作经历),哪怕是单页表单也建议拆接口做分页加载,不然一次性拉所有数据接口响应太慢反而影响体验。
你现在做的申请表单场景,一般用户的教育经历、工作经历也就三五条,直接合并到profile接口里一起返回完全没问题,不用过度拆分。
内容的提问来源于stack exchange,提问作者Jesse Luke Orange
相关产品推荐
相关产品推荐

