一、前言
1、mpVue微信小程序不支持动态组件(<component> )
2、mpVue微信小程序不支持动态属性及事件穿透(
$attrs
和$listeners
)3、mpVue微信小程序不支持
render
函数
二、最终效果
三、配置参数(Attributes)
1. 简介:基于 vant-weapp 组件的二次封装,着重于数据层面,HTML 一行代码
TForm 表单组件
代码示例:
<t-form
ref="t-form"
:formOpts="formOpts"
:listDefaultInfo="formOpts.listDefaultInfo"
:listTypeInfo="formOpts.listTypeInfo"
/>
2. 配置参数
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
className | 自定义类名 | String | - |
listTypeInfo | 下拉选择数据源(type:'date/datetime/radio/checkbox’有效) | Object | {} |
listDefaultInfo | 下拉选择默认值及弹窗字段(type:'date/datetime/radio/checkbox’有效) | Object | {} |
formOpts | 表单配置项 | Object | {} |
—fieldList | form 表单每项 list (输入框继承van-field) | Array | [] |
------slotName | 自定义表单某一项输入框 | String/slot | - |
------event | type非date/datetime/radio/checkbox有效 | String | - |
------type | date/datetime/radio/checkbox/text/password/textarea | String | - |
------label | form 表单每一项 title | String | - |
------slotLabelName | 自定义某一项 title | String/slot | - |
------slotRightIcon | van-field右侧icon | Object | - |
----------name | icon 名称 | String | - |
----------fun | 点击icon事件 | funciton | - |
----------color | icon颜色 | String | - |
------slotButton | van-field右侧按钮 | Object | - |
----------name | 插槽名 | String | - |
----------fun | 点击按钮事件 | funciton | - |
----------type | 继承van-button type | String | - |
----------size | 继承van-button size | String | - |
------value | form 表单每一项传给后台的参数(对应formData每一项) | String | - |
------required | 是否显示红点 | Boolean | false |
------list | 下拉选择数据源(仅仅对 type:'date/datetime/radio/checkbox’有效) | String | - |
------defaultValue | 表单下拉选择回显界面值(type:'date/datetime/radio/checkbox’有效) | String | - |
------defaultPopup | 表单下拉选择弹窗字段值(type:'date/datetime/radio/checkbox’有效) | String | - |
—formData | 表单提交数据(对应 fieldList 每一项的 value 值) | Object | - |
—rules | 三种规则校验(required(是否必填)/min:max(最小最大)/pattern(正则校验) | Object | - |
3. events
事件名 | 说明 | 返回值 |
---|---|---|
handleEvent | 输入框触发事件 | fieldList event 值及/输入框–输入的值 |
4. Methods
事件名 | 说明 | 参数 |
---|---|---|
validateField | 单个字段校验 | 传入rules值 |
validate | 整个表单校验成功返回true | - |
四、源码
<template>
<div :class="['t_form',className]">
<div v-for="(item, index) in formOpts.fieldList" :key="index">
<!-- 日期年月日 -->
<van-cell-group v-if="item.type === 'date'">
<van-cell
:title="item.label"
:required="item.required"
input-align="right"
is-link
:value-class="listDefaultInfo[item.defaultValue]?'van-cell_date_select':'van-cell_date'"
:arrow-direction="listDefaultInfo[item.defaultPopup]?'down':''"
:value="listDefaultInfo[item.defaultValue] || item.placeholder"
@click="listDefaultInfo[item.defaultPopup] = true"
/>
<van-popup :show="listDefaultInfo[item.defaultPopup]" position="bottom" round>
<div class="popup_wrap">
<van-datetime-picker
:type="item.type||'date'"
:value="formOpts.formData[item.value]"
@cancel="listDefaultInfo[item.defaultPopup] = false"
@confirm="(e) => {listDefaultInfo[item.defaultValue] = dayjs(e.mp.detail).format('YYYY-MM-DD'),formOpts.formData[item.value] = dayjs(e.mp.detail).format('YYYY-MM-DD'),listDefaultInfo[item.defaultPopup] = false}"
/>
</div>
</van-popup>
</van-cell-group>
<!-- 日期年月日时分秒 -->
<van-cell-group v-else-if="item.type === 'datetime'">
<van-cell
:title="item.label"
input-align="right"
:required="item.required"
is-link
:value="listDefaultInfo[item.defaultValue] || item.placeholder"
:arrow-direction="listDefaultInfo[item.defaultPopup]?'down':''"
:value-class="listDefaultInfo[item.defaultValue]?'van-cell_date_select':'van-cell_date'"
@click="listDefaultInfo[item.defaultPopup] = true"
/>
<van-popup :show="listDefaultInfo[item.defaultPopup]" position="bottom" round>
<div class="popup_wrap">
<van-datetime-picker
:type="item.type||'datetime'"
:value="formOpts.formData[item.value]"
@cancel="listDefaultInfo[item.defaultPopup] = false"
@confirm="(e) => {listDefaultInfo[item.defaultValue] = dayjs(e.mp.detail).format('YYYY-MM-DD HH:mm:ss'),formOpts.formData[item.value] = dayjs(e.mp.detail).format('YYYY-MM-DD HH:mm:ss'),listDefaultInfo[item.defaultPopup] = false}"
/>
</div>
</van-popup>
</van-cell-group>
<!-- 下拉单选框 -->
<van-cell-group v-else-if="item.type === 'radio'">
<van-cell
:title="item.label"
input-align="right"
:required="item.required"
is-link
:value="listDefaultInfo[item.defaultValue] || item.placeholder"
:arrow-direction="listDefaultInfo[item.defaultPopup]?'down':''"
:value-class="listDefaultInfo[item.defaultValue]?'van-cell_date_select':'van-cell_date'"
@click="listDefaultInfo[item.defaultPopup] = true"
/>
<van-popup :show="listDefaultInfo[item.defaultPopup]" position="bottom" round>
<van-picker
show-toolbar
:title="item.label"
:columns="listTypeInfo[item.list]"
@cancel="listDefaultInfo[item.defaultPopup] = false"
@confirm="(e) => {listDefaultInfo[item.defaultValue] = e.mp.detail.value,formOpts.formData[item.value] = e.mp.detail.value,listDefaultInfo[item.defaultPopup] = false}"
/>
</van-popup>
</van-cell-group>
<!-- 下拉复选框 -->
<van-cell-group v-else-if="item.type === 'checkbox'">
<van-cell
:title="item.label"
input-align="right"
:required="item.required"
is-link
:value="listDefaultInfo[item.defaultValue].length>0?listDefaultInfo[item.defaultValue]:listDefaultInfo[item.defaultValue].length || item.placeholder"
:arrow-direction="listDefaultInfo[item.defaultPopup]?'down':''"
:value-class="listDefaultInfo[item.defaultValue].length?'van-cell_date_select':'van-cell_date'"
@click="listDefaultInfo[item.defaultPopup] = true"
/>
<van-popup
:show="listDefaultInfo[item.defaultPopup]"
custom-class="t_checkbox_picker"
position="bottom"
round
>
<div class="t_checkbox_picker__toolbar">
<div
class="t_checkbox_picker__cancel"
@click="listDefaultInfo[item.defaultPopup] = false"
>取消</div>
<div class="t_checkbox_picker__title t-oneline-overflow-hidden">{{item.label}}</div>
<div
class="t_checkbox_picker__confirm"
@click="() => {formOpts.formData[item.value] = listDefaultInfo[item.defaultValue],listDefaultInfo[item.defaultPopup] = false}"
>确认</div>
</div>
<van-checkbox-group
:value="listDefaultInfo[item.defaultValue]"
@change="(event)=>listDefaultInfo[item.defaultValue] = event.mp.detail"
>
<div
v-for="(value,key) in listTypeInfo[item.list]"
:key="key"
class="t_checkbox_content"
>
<van-checkbox
custom-class="t_checkbox"
:name="value[arrLabel || 'label']"
:disabled="value.disabled||false"
shape="square"
>{{value[arrLabel || 'label']}}</van-checkbox>
</div>
</van-checkbox-group>
</van-popup>
</van-cell-group>
<!-- 自定义插槽 -->
<template v-else-if="item.slotName">
<slot :name="item.slotName"></slot>
</template>
<!-- 输入框 -->
<van-cell-group v-else>
<van-field
:name="item.value"
v-model="formOpts.formData[item.value]"
:placeholder="item.placeholder||''"
:label="item.label"
:type="item.type"
:disabled="item.disabled"
:readonly="item.readonly"
:required="item.required"
:title-width="item.titleWidth||'6.2em'"
:maxlength="item.maxlength||-1"
:cursor="item.cursor||0"
:show-word-limit="item.showWordLimit"
:is-link="item.isLink"
:input-align="item.inputAlign||'right'"
:autosize="item.autosize||item.type==='textarea'"
:left-icon="item.leftIcon||''"
:right-icon="item.rightIcon||''"
clearable
@change="e => formOpts.formData[item.value] = e.mp.detail"
@click="() =>$emit('handleEvent',item.event,formOpts.formData[item.value])"
>
<!-- @change="e => formOpts.formData[item.value] = e.mp.detail" -->
<div
v-if="!item.label"
:class="item.labelNameClass"
:style="item.labelNameStyle"
slot="label"
>
<slot :name="item.slotLabelName" />
</div>
<van-icon
v-if="item.slotLeftIcon"
:class="item.slotLeftIcon.class"
:color="item.slotLeftIcon.color||'inherit'"
@click="item.slotLeftIcon.fun"
:name="item.slotLeftIcon.name"
slot="left-icon"
/>
<van-icon
v-if="item.slotRightIcon"
:class="item.slotRightIcon.class"
:color="item.slotRightIcon.color||'inherit'"
:name="item.slotRightIcon.name"
@click="item.slotRightIcon.fun"
slot="right-icon"
/>
<van-button
v-if="item.slotButton&&item.slotButton.name"
:class="item.slotButton.class"
:style="item.slotButton.style"
:size="item.slotButton.size"
:type="item.slotButton.type"
@click="item.slotButton.fun"
slot="button"
>
<slot :name="item.slotButton.name" />
</van-button>
</van-field>
</van-cell-group>
</div>
</div>
</template>
<script>
import dayjs from 'dayjs'
export default {
name: 'TForm',
components: {},
props: {
/** 表单配置项说明
* formData object 表单提交数据
* rules object 验证规则
* fieldList Array 表单渲染数据
*/
formOpts: {
type: Object,
default: () => ({})
},
// 下拉选项数据
listTypeInfo: {
type: Object,
default: () => ({})
},
// 下拉选择默认值
listDefaultInfo: {
type: Object,
default: () => ({})
},
className: String
},
computed: {
dayjs() {
return dayjs
}
},
watch: {
'formOpts.fieldList': {
handler(val) {
// console.log('fieldList---watch', val)
val.map(item => {
// 插槽提示
if (item.slotLabelName) {
!Object.keys(this.$slots).includes(item.slotLabelName) && this.$toast(`请在表单组件添加${item.slotLabelName}插槽!`)
}
setTimeout(() => {
if (item.slotButton && item.slotButton.name) {
!Object.keys(this.$slots).includes(item.slotButton.name) && this.$toast(`请在表单组件添加${item.slotButton.name}插槽!`)
}
}, 2000)
setTimeout(() => {
if (item.slotName) {
!Object.keys(this.$slots).includes(item.slotName) && this.$toast(`请在表单组件添加${item.slotName}插槽!`)
}
}, 3000)
})
},
immediate: true,
deep: true // 深度监听
}
},
data() {
return {
...this.listDefaultInfo
}
},
methods: {
// 单个字段校验
validateField(key) {
let value = this.formOpts.formData[key]
if (this.formOpts.rules[key] && this.formOpts.rules[key].length) {
for (let i = 0; i < this.formOpts.rules[key].length; i++) {
let rule = this.formOpts.rules[key][i]
if (rule.required) {
// 必填校验
if (!value) {
return {
valid: false,
msg: rule.message
}
}
} else if (rule.max || rule.min) {
// 最大最小长度校验
if (value && (value.length > rule.max || value.length < rule.min)) {
return {
valid: false,
msg: rule.message
}
}
} else if (rule.pattern && value) {
// 正则校验
let reg = new RegExp(rule.pattern)
if (!reg.test(value)) {
return {
valid: false,
msg: rule.message
}
}
}
}
}
return {
valid: true
}
},
// 全部校验
validate() {
let res = []
Object.keys(this.formOpts.rules).forEach(key => {
res.push(this.validateField(key))
})
for (let i = 0; i < res.length; i++) {
if (!res[i].valid) {
wx.showToast({
title: res[i].msg,
icon: 'none'
})
return false
}
}
return true
}
}
}
</script>
<style lang="scss">
.t_form {
.van-cell_date {
color: var(--cell-value-color, #c8c9cc);
}
.van-cell_date_select {
color: var(--cell-value-color, #000);
}
.t_checkbox_picker {
min-height: 260px;
padding: 10px 15px;
.t_checkbox_picker__toolbar {
display: flex;
height: var(--picker-toolbar-height, 44px);
justify-content: space-between;
line-height: var(--picker-toolbar-height, 44px);
.t_checkbox_picker__cancel,
.t_checkbox_picker__confirm {
font-size: var(--picker-action-font-size, 14px);
padding: var(--picker-action-padding, 0 16px);
}
.t_checkbox_picker__cancel {
color: var(--picker-cancel-action-color, #969799);
}
.t_checkbox_picker__title {
font-size: var(--picker-option-font-size, 16px);
font-weight: var(--font-weight-bold, 500);
max-width: 50%;
text-align: center;
}
.t_checkbox_picker__confirm {
color: var(--picker-confirm-action-color, #576b95);
}
}
._van-checkbox-group {
.t_checkbox_content {
margin: 10px;
}
}
}
}
</style>
五、页面使用
<template>
<div class="demo_form">
<t-header title="表单组件示例" />
<t-form
ref="t-form"
:formOpts="formOpts"
:listDefaultInfo="formOpts.listDefaultInfo"
:listTypeInfo="formOpts.listTypeInfo"
>
<div slot="custom">自定义label</div>
<div slot="button">发送验证码</div>
</t-form>
<van-button block type="primary" @click="formCom">表单组件</van-button>
</div>
</template>
<script>
import TForm from '@/components/TForm/index.vue'
export default {
name: 'DemoForm',
components: {
TForm
},
data() {
return {
formOpts: {
formData: {
account: '', // *用户账号
password: '', // *用户密码
name: '', // *用户昵称
email: '', // 邮箱
desc: '', // 描述
sex: '', // *性别: 0:男 1:女
status: '', // *状态: 停用,启用',
hobby: '', // *爱好:多选
creatDate: '', // 出生日期
date: '', // 出生日期---时分秒
phone: '' // 手机号码
},
// 下拉选择默认值
listDefaultInfo: {
statusDefault: '',
statusPopup: false,
sexDefault: '',
sexPopup: false,
showDatePopup: false, // 日期弹窗
showDatetimePopup: false, // 日期弹窗(含时分秒)
dateDefault: '', // 默认日期-- 年月日
datetimeDefault: '', // 默认日期--含时分秒
hobbyPopup: false, // 爱好弹窗
hobbyDefault: [] // 爱好
},
fieldList: [
{ label: '账号', value: 'account', type: 'text', placeholder: '请输入账号', required: true },
{ label: '', value: 'password', type: 'password', placeholder: '请输入密码', required: true, slotLabelName: 'custom', labelNameStyle: 'color:red', slotRightIcon: { name: 'location-o', fun: this.clickIcon } },
{ label: '昵称', value: 'name', type: 'text', placeholder: '请输入昵称', required: true, slotButton: { name: 'button', size: 'small', type: 'primary', fun: this.test } },
{ label: '邮箱', value: 'email', type: 'text', placeholder: '请输入邮箱', required: true },
{ label: '出生日期', value: 'creatDate', type: 'date', placeholder: '请选择出生日期', defaultValue: 'dateDefault', defaultPopup: 'showDatePopup' },
{ label: '日期时分秒', value: 'date', type: 'datetime', placeholder: '请选择日期时分秒', defaultValue: 'datetimeDefault', defaultPopup: 'showDatetimePopup' },
{ label: '性别', value: 'sex', type: 'radio', list: 'sexList', placeholder: '请选择性别', defaultValue: 'sexDefault', defaultPopup: 'sexPopup' },
{ label: '爱好', value: 'hobby', type: 'checkbox', list: 'hobbyList', placeholder: '请选择爱好(多选)', required: true, defaultValue: 'hobbyDefault', defaultPopup: 'hobbyPopup' },
{ label: '状态', value: 'status', type: 'radio', list: 'statusList', placeholder: '请选择状态', defaultValue: 'statusDefault', defaultPopup: 'statusPopup' },
{ label: '手机号码', value: 'phone', type: 'text', maxlength: 11, placeholder: '请输入手机号码', required: true },
{ label: '描述', value: 'desc', type: 'textarea', placeholder: '请输入描述', autosize: { maxHeight: 80, minHeight: 50 } }
],
rules: {
account: [
{ required: true, message: '请输入账号' },
{ min: 2, max: 4, message: '账号只能大于2个字符小于4个字符' }
],
password: [{ required: true, message: '请输入密码' }],
name: [{ required: true, message: '请输入昵称' }],
email: [
{ required: true, message: '请输入邮箱' },
{ pattern: /\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/, message: '请输入正确的邮箱' }
],
hobby: [{ required: true, message: '请至少选择一个爱好' }],
phone: [
{ required: true, message: '请输入手机号码' },
{ pattern: /^1[3456789]\d{9}$/, message: '请输入正确的手机号码' }
]
},
// 相关列表
listTypeInfo: {
hobbyList: [
{ label: '吉他', value: '0' },
{ label: '看书', value: '1' },
{ label: '美剧', value: '2' },
{ label: '旅游', value: '3' },
{ label: '音乐', value: '4' }
],
sexList: ['女', '男'],
statusList: ['启用', '停用']
}
}
}
},
methods: {
// TForm表单提交
formCom() {
console.log('form组件点击提交--数据', this.formOpts.formData)
let res = this.$refs['t-form'].validate()
console.log('提交数据规则', res)
if (!res) return
console.log('全部校验通过--最终数据', this.formOpts.formData)
},
test(val) {
console.log('测试点击右侧按钮---', val)
},
clickIcon(val) {
console.log('点击图标', val)
}
}
}
</script>
<style lang="scss">
.demo_form {
width: 100%;
}
</style>
六、仓库地址(待开放)
七、相关文章
Vue3 + Vite + Ts开源后台管理系统模板
基于ElementUi或AntdUI再次封装基础组件文档
基于Element-plus再次封装基础组件文档(vue3+ts)