uniapp实战教程:如何封装一个可复用的表单组件

在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开发效率。

相关推荐
2301_789169541 分钟前
react crash course 2024(9) proxying
前端·javascript·react.js
计算机学姐10 分钟前
基于nodejs+vue的超市管理系统
前端·javascript·vue.js·vscode·前端框架·node.js·ecmascript
Z_B_L12 分钟前
three.js----快速上手,如何用vue在web页面中导入 gltf/glb , fbx , obj 模型
开发语言·前端·javascript
诗雅颂14 分钟前
【js逆向学习】qqmusic(qq音乐)webpack智能导出
javascript·学习·webpack
重生之我在20年代敲代码35 分钟前
HTML讲解(三)通用部分
前端·笔记·html
家有狸花38 分钟前
PYCHARM 使用笔记(一):常见功能和快捷键
ide·笔记·pycharm
计算机学姐1 小时前
基于nodejs+vue的宠物医院管理系统
前端·javascript·vue.js·mysql·npm·node.js·sass
余生H1 小时前
前端大模型入门:使用Transformers.js手搓纯网页版RAG(二)- qwen1.5-0.5B - 纯前端不调接口
前端·javascript·人工智能·大语言模型·rag·端侧大模型·webml
科研online2 小时前
ArcGIS Pro高级地图可视化—双变量符号地图
开发语言·javascript·arcgis
你会发光哎u2 小时前
深入理解包管理工具
开发语言·前端·javascript·node.js