在uniapp开发过程中,表单组件的使用场景非常广泛。为了提高开发效率,我们可以将常用的表单组件进行封装。本文将带你了解如何在uniapp中封装一个表单组件,让你只需要通过属性配置轻松实现各种表单,效果图如下:
一、准备工作
在开始封装表单组件之前,请确保你已经掌握了以下知识:
1、uniapp基础知识
2、Vue.js基础知识
3、组件通信与传值
二、分析需求
在封装表单组件之前,我们需要明确以下需求:
1、支持多种表单元素(如:输入框、可下拉选择输入框、时间选择器、数字加减器、图片上传、下拉框等)
2、支持自定义确认和取消按钮
3、支持表单验证
4、支持添加插槽
5、支持表单数据提交
三、封装步骤
1、在uniapp项目的components目录下,创建一个名为customForm的文件夹,并在该文件夹下创建index.vue文件,文件代码如下:
javascript
<template>
<view>
<form @submit="formSubmit" @reset="formReset">
<view v-for="(item, idx) in formConfig" :key="item.name"
class="flex w-p-100 align-center row fz-17 relative">
<view v-if="item.label" class="w-p-30">{{item.label}}</view>
<view v-if="item.require" class="inline-block absolute t-15 l-9 fz-18 text-red">*</view>
<!-- 输入框 -->
<input class="flex1 form-input" v-if="item.type=='input'" :placeholder="item.placeholder" :name="idx"
v-model="data[idx]" :disabled="item.disabled" :type="item.inputType || 'text'">
<!-- 下拉选择框 -->
<picker v-else-if="item.type=='picker'" @change="bindPickerChange" :data-index="idx"
:value="item.selectedIndex" :range="item.arrayData" :range-key="item.rangeKey" :name="idx">
<input :value="item.rangeKey ? item.arrayData[item.selectedIndex]&&item.arrayData[item.selectedIndex][item.rangeKey] : item.arrayData[item.selectedIndex]" disabled :placeholder="item.placeholder" class="uni-input" />
</picker>
<!-- 时间选择 -->
<datePicker v-else-if="item.type=='datePicker'" :timeFormat="item.timeFormat"
@datetimeChange="e => datetimeChange(e,idx)"></datePicker>
<!-- 图片上传 -->
<view v-else-if="item.type=='upLoad'" class="file-picker">
<uni-file-picker :limit="item.limit" @select="e => handleSelect(e, item, idx)" @delete="e => deletePictrue(e, idx)"
:autoUpload="false" :value="data[idx]?[{url:data[idx]}]:''"></uni-file-picker>
</view>
<!-- 可下拉选择输入框 -->
<input-select class="flex1 fz-18" v-else-if="item.type == 'inputSelect'" :placeholder="item.placeholder"
:options="item.options" :value="data[idx]" @change="e => inputSelectChange(e, idx)"></input-select>
<!-- 数字加减器 -->
<view class="flex1 h-p-100 flex align-center" v-else-if="item.type == 'numberBox'">
<uni-number-box @change="e => bindNumberChange(e, idx)" class="uni-number-box" :min="1"
v-model="data[idx]" />
</view>
<!-- 插槽 -->
<slot v-else-if="item.type == 'slot'" :name="item.slotName"></slot>
</view>
<view class="p-15">
<button form-type="submit" type="primary">{{submitTxet}}</button>
<button v-if="reset" form-type="reset">{{resetText}}</button>
</view>
</form>
</view>
</template>
<script setup>
import datePicker from '../datePicker/datePicker.vue';
import {
reactive,
ref,
watch,
toRefs
} from 'vue'
import * as utils from '@/utils/index.js'
const props = defineProps({
reset: {
type: Boolean,
default: false
},
resetTxet: {
type: String,
default: '重置'
},
submitTxet: {
type: String,
default: '提交'
},
formConfig: {
type: Object,
required: true,
default: () => {
return {}
}
},
resultData: {
type: Object,
default: () => {
return {}
}
}
})
let data = reactive(props.resultData)
// const pickerValue = ref('')
const bindPickerChange = (e) => {
let index = e.detail.value,
idx = e.target.dataset.index,
item = props.formConfig[idx]
item.selectedIndex = index
data[idx] = item.rangeKey ? item.arrayData[index][item.key || 'id'] : item.arrayData[index]
}
const handleSelect = (e, item, idx) => {
if (item.success) {
item.success(e, idx)
} else {
uploadSuccess(e, idx)
}
}
const uploadSuccess = (e, idx) => {
data[idx] = e.tempFilePaths[0]
}
const deletePictrue = (e, idx) => {
data[idx] = ''
}
const datetimeChange = (e, idx) => {
data[idx] = e.detail.valueStr
}
const inputSelectChange = (e, idx) => {
data[idx] = e.detail.value
}
const bindNumberChange = (e, idx) => {
data[idx] = e
}
const emit = defineEmits(['formSubmit'])
const formSubmit = (e) => {
let bool = utils.formVerify(data, props.formConfig)
bool && emit('formSubmit', data)
}
</script>
<style>
.row {
min-height: 90rpx;
padding: 0 0 0 40rpx;
box-sizing: border-box;
border-bottom: 1px solid #ccc;
}
.file-picker {
width: 264rpx;
}
.form-input {
height: 90rpx;
}
</style>
2、在父组件中使用:
父组件.vue文件中:
javascript
<template>
<view>
<customForm :formConfig="fromConfigRef" :resultData="resultData" submitTxet="确定" @formSubmit="formSubmit"></customForm>
</view>
</template>
<script setup>
import {
ref,
reactive
} from 'vue'
import {
fromConfig
} from './fromConfig';
import {
onLoad
} from '@dcloudio/uni-app'
import API from '@/api/index.js'
const fromConfigRef = reactive(fromConfig)
const resultData = reactive({})
let statusIndex = null
onLoad(async(options) => {
if(options.resultData) {
let data = JSON.parse(decodeURIComponent(options.resultData))
// 更新响应式对象resultData的属性
for (const key in data) {
resultData[key] = data[key];
}
}
statusIndex = options.statusIndex
let res = await API.SiteOrder.pullDownInstrumentName()
fromConfigRef['factoryName'].options = res
// console.log(fromConfigRef)
})
const formSubmit = (data) => {
if(!data.number) {
data.number = 1
}
const backData = {
statusIndex,
data
}
uni.navigateBack({
delta: 1,
success: () => {
uni.$emit('pushData', backData)
}
})
}
</script>
<style>
</style>
fromConfigRef.js配置文件:
javascript
export const fromConfig = {
'factoryName':{
label:'器具名称',
type: 'inputSelect',
placeholder: '请输入器具名称',
options:[],
require: true
},
'factoryFormat':{
label:'器具规格',
type: 'input',
placeholder: '请输入器具规格',
require: true
},
'factoryNo':{
label:'器具编号',
type: 'input',
placeholder: '请输入器具编号'
},
'number':{
label:'数量',
type: 'numberBox'
},
'person':{
label:'联系人',
type: 'input',
placeholder: '请输入联系人',
require: true
},
'marks':{
label:'备注',
type: 'input',
placeholder: '请输入备注',
},
}
3、关于customForm组件的index.vue文件,有以下几点需要注意:
1、class样式
我采用了原子化css样式,所以在这个文件style中并没有太多的 样式 ,而是直接用了原子化css里面的class名,比如:class="flex",表示display:flex。原子化css文件已给出。
2、自定义组件
组件中datePicker、input-select为另外封装的自定义组件,主要实现了日期时间选择和可输入可选择下拉框。
datePicker.vue组件文件如下,如需要可自取:
javascript
<template>
<view style="height: 100%">
<picker mode="multiSelector" :value="dateTime" @change="changeDateTime" @columnchange="changeDateTimeColumn" :range="dateTimeArray">
<view class="weui-input">
<block v-if="timeFormat == 'YYYY-MM-DD HH:mm'">
{{ dateTimeArray && dateTimeArray[0][dateTime[0]] }}-{{ dateTimeArray && dateTimeArray[1][dateTime[1]] }}-{{ dateTimeArray && dateTimeArray[2][dateTime[2]] }} {{ dateTimeArray && dateTimeArray[3][dateTime[3]] }}:{{
dateTimeArray && dateTimeArray[4][dateTime[4]]
}}
</block>
<block v-else>
{{ dateTimeArray && dateTimeArray[0][dateTime[0]] }}-{{ dateTimeArray && dateTimeArray[1][dateTime[1]] }}-{{ dateTimeArray && dateTimeArray[2][dateTime[2]] }} {{ dateTimeArray && dateTimeArray[3][dateTime[3]] }}:{{
dateTimeArray && dateTimeArray[4][dateTime[4]]
}}:{{ dateTimeArray && dateTimeArray[5][dateTime[5]] }}
</block>
</view>
</picker>
</view>
</template>
<script>
import * as utils from '@/utils/index.js'
export default {
name:'datePicker',
data() {
return {
dateTimeArray: null,
//时间年月日时分秒数组
dateTime: null,
//选中的年月日时分秒每个数组的下标
startYear: 1900,
//起始年份
endYear: 2200 //结束年份
};
},
/**
* 组件的属性列表
*/
props: {
value: {
type: String,
default: ''
},
//默认值,不传为当前时间
timeFormat: {
type: String,
default: 'YYYY-MM-DD HH:mm:ss'
} //时间格式 YYYY-MM-DD HH:mm:ss 和 YYYY-MM-DD HH:mm两种
},
/**
* 组件的方法列表
*/
methods: {
attached() {
//初始化
var obj = utils.dateTimePicker(this.startYear, this.endYear, this.value);
if (this.timeFormat == 'YYYY-MM-DD HH:mm') {
//如果是精准到分,则去掉分的数据
obj.dateTimeArray.pop();
}
this.dateTime = obj.dateTime;
this.dateTimeArray = obj.dateTimeArray
//将初始化后的时间值返回给绑定的value
let dateTime = '';
let dateTimeStr = '';
if (this.timeFormat == 'YYYY-MM-DD HH:mm') {
dateTime =
this.dateTimeArray[0][this.dateTime[0]] +
'-' +
this.dateTimeArray[1][this.dateTime[1]] +
'-' +
this.dateTimeArray[2][this.dateTime[2]] +
' ' +
this.dateTimeArray[3][this.dateTime[3]] +
':' +
this.dateTimeArray[4][this.dateTime[4]];
dateTimeStr =
this.dateTimeArray[0][this.dateTime[0]] +
'-' +
this.dateTimeArray[1][this.dateTime[1]] +
'-' +
this.dateTimeArray[2][this.dateTime[2]] +
'T' +
this.dateTimeArray[3][this.dateTime[3]] +
':' +
this.dateTimeArray[4][this.dateTime[4]] +
':00.000Z';
} else {
dateTime =
this.dateTimeArray[0][this.dateTime[0]] +
'-' +
this.dateTimeArray[1][this.dateTime[1]] +
'-' +
this.dateTimeArray[2][this.dateTime[2]] +
' ' +
this.dateTimeArray[3][this.dateTime[3]] +
':' +
this.dateTimeArray[4][this.dateTime[4]] +
':' +
this.dateTimeArray[5][this.dateTime[5]];
dateTimeStr =
this.dateTimeArray[0][this.dateTime[0]] +
'-' +
this.dateTimeArray[1][this.dateTime[1]] +
'-' +
this.dateTimeArray[2][this.dateTime[2]] +
'T' +
this.dateTimeArray[3][this.dateTime[3]] +
':' +
this.dateTimeArray[4][this.dateTime[4]] +
':' +
this.dateTimeArray[5][this.dateTime[5]] +
'.000Z';
}
this.$emit('datetimeChange', {
detail: {
value: dateTime,
valueStr: dateTimeStr
}
});
},
changeDateTime(e) {
this.dateTime = e.detail.value
let dateTime = '';
let dateTimeStr = '';
if (this.timeFormat == 'YYYY-MM-DD HH:mm') {
dateTime =
this.dateTimeArray[0][this.dateTime[0]] +
'-' +
this.dateTimeArray[1][this.dateTime[1]] +
'-' +
this.dateTimeArray[2][this.dateTime[2]] +
' ' +
this.dateTimeArray[3][this.dateTime[3]] +
':' +
this.dateTimeArray[4][this.dateTime[4]];
dateTimeStr =
this.dateTimeArray[0][this.dateTime[0]] +
'-' +
this.dateTimeArray[1][this.dateTime[1]] +
'-' +
this.dateTimeArray[2][this.dateTime[2]] +
'T' +
this.dateTimeArray[3][this.dateTime[3]] +
':' +
this.dateTimeArray[4][this.dateTime[4]] +
':00.000Z';
} else {
dateTime =
this.dateTimeArray[0][this.dateTime[0]] +
'-' +
this.dateTimeArray[1][this.dateTime[1]] +
'-' +
this.dateTimeArray[2][this.dateTime[2]] +
' ' +
this.dateTimeArray[3][this.dateTime[3]] +
':' +
this.dateTimeArray[4][this.dateTime[4]] +
':' +
this.dateTimeArray[5][this.dateTime[5]];
dateTimeStr =
this.dateTimeArray[0][this.dateTime[0]] +
'-' +
this.dateTimeArray[1][this.dateTime[1]] +
'-' +
this.dateTimeArray[2][this.dateTime[2]] +
'T' +
this.dateTimeArray[3][this.dateTime[3]] +
':' +
this.dateTimeArray[4][this.dateTime[4]] +
':' +
this.dateTimeArray[5][this.dateTime[5]] +
'.000Z';
}
this.$emit('datetimeChange', {
detail: {
value: dateTime,
valueStr: dateTimeStr
}
});
},
changeDateTimeColumn(e) {
var arr = this.dateTime;
var dateArr = this.dateTimeArray;
arr[e.detail.column] = e.detail.value;
dateArr[2] = utils.getMonthDay(dateArr[0][arr[0]], dateArr[1][arr[1]]);
this.dateTimeArray = dateArr
this.dateTime = arr
}
},
mounted() {
// 处理小程序 attached 生命周期
this.attached();
},
created: function () {}
};
</script>
<style>
.icon-box-img {
position: absolute;
left: 5px;
top: 8px;
height: 10px;
color: #ddd;
}
.weui-input {
width: 200px;
height: 2.5em;
min-height: 2.5em;
line-height: 2.5em;
position: relative;
border-radius: 3px;
}
</style>
input-select.vue组件代码如下,如需要可自取:
javascript
<template>
<view class="select-box">
<view :class="isShow ? 'select-current-open' : 'select-current'" @tap.stop.prevent="openClose">
<input @input="bindinput" @blur="inputBlur" class="current-name" :placeholder="placeholder" v-model="inputValue" />
</view>
<view class="option-list" v-if="isShow" @tap.stop.prevent="optionTap" style="overflow-y: auto; overflow-x: hidden; max-height: 200px">
<text :data-index="index" :class="'option ' + (item.selection ? 'selection' : '')" v-for="(item, index) in result" :key="item.id">{{ item[label] }}</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
result: [],
//转换后的候选项数据
selection: 'selection',
//选中样式
inputValue: '',
//输入框的值
isShow: false,
index: null,
// 选中的下标
inputFocus: false //输入框是否有焦点
};
},
props: {
options: {
type: Array,
default: () => []
},
label: {
type: String,
default: 'name'
},
value: {
type: String,
default: ''
},
placeholder: {
type: String,
default: '请选择'
}
},
watch: {
//监听数据变化
inputValue: function (value) {},
options: function (value) {
this.result = value
},
value: {
handler(newValue, oldVal) {
this.inputValue = newValue
},
immediate: true
}
},
methods: {
attached() {
// 属性名称转换, 如果不是 { id: '', name:'' } 格式,则转为 { id: '', name:'' } 格式
let result = [];
if (this.key !== 'id' || this.text !== 'name' || this.text !== 'yes') {
for (let item of this.options) {
let { [this.key]: id, [this.text]: name, [this.selection]: selection } = item;
result.push({
id,
name,
selection
});
}
}
this.result = result
},
optionTap(e) {
let that = this;
let resuleObj = {
flag: true
}; //传递父组件的值.flag 表示是否是新增的 . true是新增,false不是新增
this.index = e.target.dataset.index;
this.inputValue = that.result[that.index][that.label]
//选中的id
var id = this.result[this.index].id;
for (var i = 0; i < this.options.length; i++) {
if (this.options[i].id == id) {
this.options[i].selection = true;
resuleObj.id = this.options[i].id;
resuleObj.flag = false;
} else {
this.options[i].selection = false;
}
}
this.isShow = false
this.result = this.options
resuleObj.value = that.inputValue;
//调用父组件方法,并传参
this.$emit('change', {
detail: resuleObj
});
},
openClose() {
//如果是获取到焦点的状况下,就不关闭下拉选项
if (this.inputFocusFun && this.isShow) {
return;
}
var that = this;
this.isShow = !that.isShow
if (!this.isShow) {
this.closeSetInputValue();
}
//只要操作当前项,就是获取到当前项的焦点
this.$emit('focus', {
detail: {
value: true
}
});
},
// 此方法供父组件调用
close() {
this.isShow = false
this.closeSetInputValue();
},
closeSetInputValue() {
//通过close和openClose方法隐藏选项时,设置inputValue的值
let that = this;
let inputValue = this.inputValue;
//如果为空,直接返回
if (!inputValue) {
return;
}
//返回的数据结构
let resuleObj = {
flag: true
};
for (let i = 0; i < this.options.length; i++) {
if (this.options[i][this.label] == inputValue) {
this.options[i].selection = true;
resuleObj.id = this.options[i].id;
resuleObj.flag = false;
} else {
this.options[i].selection = false;
}
}
resuleObj.value = that.inputValue;
//调用父组件方法,并传参
this.$emit('change', {
detail: resuleObj
});
},
inputFocusFun() {
this.inputFocus = true
},
inputBlur() {
this.inputFocus = false
},
bindinput(e) {
var keyWord = e.detail.value;
this.inputValue = e.detail.value;
var tempresult = [];
if (keyWord) {
var obj = {
id: -1
};
obj[this.label] = keyWord;
tempresult.push(obj);
}
for (var i = 0; i < this.options.length; i++) {
if (this.options[i][this.label] == keyWord) {
this.options[i].selection = true;
tempresult.push(this.options[i]);
tempresult.splice(0, 1);
continue;
}
if (this.options[i][this.label].indexOf(keyWord) != -1) {
this.options[i].selection = false;
tempresult.push(this.options[i]);
}
}
this.result = tempresult
}
},
mounted() {
// 处理小程序 attached 生命周期
this.attached();
},
created: function () {}
};
</script>
<style>
.select-box {
position: relative;
width: 100%;
font-size: 17px;
}
.select-current {
position: relative;
width: 100%;
padding: 0 20px 0 6px;
border: 1rpx solid #ddd;
border-radius: 1px;
box-sizing: border-box;
line-height: 32px;
}
.select-current::after {
position: absolute;
display: block;
right: 10px;
top: 15px;
content: '';
width: 0;
height: 0;
border: 4px solid transparent;
border-top: 5px solid #999;
}
.select-current-open {
position: relative;
width: 100%;
padding: 0 20px 0 6px;
border: 1rpx solid #ddd;
border-radius: 1px;
box-sizing: border-box;
line-height: 32px;
}
.select-current-open::after {
position: absolute;
display: block;
right: 10px;
top: 10px;
content: '';
width: 0;
height: 0;
border: 4px solid transparent;
border-bottom: 5px solid #999;
}
.selection {
color: #00bbff;
}
.current-name {
display: block;
width: 85%;
height: 32px;
word-wrap: normal;
overflow: hidden;
}
.option-list {
position: absolute;
font-size: 14px;
left: 0;
width: 100%;
border-radius: 6rpx;
box-sizing: border-box;
z-index: 99;
border: 1px solid #ddd;
border-top: none;
background-color: #fff;
}
.option {
display: block;
width: 100%;
line-height: 32px;
height: 32px;
border-bottom: 1px solid #eee;
padding: 0 6px;
}
.option:last-child {
border-bottom: none;
padding-bottom: 0;
}
</style>
总结:
在实际项目中,你可以根据需求进一步完善组件功能,如添加自定义子组件、自定义样式等。掌握组件封装技巧,将有助于提高你的uniapp开发效率。