|
|
<template>
|
|
|
<view class="uni-forms-item"
|
|
|
:class="['is-direction-' + localLabelPos ,border?'uni-forms-item--border':'' ,border && isFirstBorder?'is-first-border':'']">
|
|
|
<slot name="label">
|
|
|
<view class="uni-forms-item__label" :class="{'no-label':!label && !required}"
|
|
|
:style="{width:localLabelWidth,justifyContent: localLabelAlign}">
|
|
|
<text v-if="required" class="is-required">*</text>
|
|
|
<text>{{label}}</text>
|
|
|
</view>
|
|
|
</slot>
|
|
|
<!-- #ifndef APP-NVUE -->
|
|
|
<view class="uni-forms-item__content">
|
|
|
<slot></slot>
|
|
|
<view class="uni-forms-item__error" :class="{'msg--active':msg}">
|
|
|
<text>{{msg}}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
<!-- #endif -->
|
|
|
<!-- #ifdef APP-NVUE -->
|
|
|
<view class="uni-forms-item__nuve-content">
|
|
|
<view class="uni-forms-item__content">
|
|
|
<slot></slot>
|
|
|
</view>
|
|
|
<view class="uni-forms-item__error" :class="{'msg--active':msg}">
|
|
|
<text class="error-text">{{msg}}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
<!-- #endif -->
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
/**
|
|
|
* uni-fomrs-item 表单子组件
|
|
|
* @description uni-fomrs-item 表单子组件,提供了基础布局已经校验能力
|
|
|
* @tutorial https://ext.dcloud.net.cn/plugin?id=2773
|
|
|
* @property {Boolean} required 是否必填,左边显示红色"*"号
|
|
|
* @property {String } label 输入框左边的文字提示
|
|
|
* @property {Number } labelWidth label的宽度,单位px(默认65)
|
|
|
* @property {String } labelAlign = [left|center|right] label的文字对齐方式(默认left)
|
|
|
* @value left label 左侧显示
|
|
|
* @value center label 居中
|
|
|
* @value right label 右侧对齐
|
|
|
* @property {String } errorMessage 显示的错误提示内容,如果为空字符串或者false,则不显示错误信息
|
|
|
* @property {String } name 表单域的属性名,在使用校验规则时必填
|
|
|
* @property {String } leftIcon 【1.4.0废弃】label左边的图标,限 uni-ui 的图标名称
|
|
|
* @property {String } iconColor 【1.4.0废弃】左边通过icon配置的图标的颜色(默认#606266)
|
|
|
* @property {String} validateTrigger = [bind|submit|blur] 【1.4.0废弃】校验触发器方式 默认 submit
|
|
|
* @value bind 发生变化时触发
|
|
|
* @value submit 提交时触发
|
|
|
* @value blur 失去焦点触发
|
|
|
* @property {String } labelPosition = [top|left] 【1.4.0废弃】label的文字的位置(默认left)
|
|
|
* @value top 顶部显示 label
|
|
|
* @value left 左侧显示 label
|
|
|
*/
|
|
|
|
|
|
export default {
|
|
|
name: 'uniFormsItem',
|
|
|
options: {
|
|
|
virtualHost: true
|
|
|
},
|
|
|
provide() {
|
|
|
return {
|
|
|
uniFormItem: this
|
|
|
}
|
|
|
},
|
|
|
inject: {
|
|
|
form: {
|
|
|
from: 'uniForm',
|
|
|
default: null
|
|
|
},
|
|
|
},
|
|
|
props: {
|
|
|
// 表单校验规则
|
|
|
rules: {
|
|
|
type: Array,
|
|
|
default () {
|
|
|
return null;
|
|
|
}
|
|
|
},
|
|
|
// 表单域的属性名,在使用校验规则时必填
|
|
|
name: {
|
|
|
type: [String, Array],
|
|
|
default: ''
|
|
|
},
|
|
|
required: {
|
|
|
type: Boolean,
|
|
|
default: false
|
|
|
},
|
|
|
label: {
|
|
|
type: String,
|
|
|
default: ''
|
|
|
},
|
|
|
// label的宽度 ,默认 80
|
|
|
labelWidth: {
|
|
|
type: [String, Number],
|
|
|
default: ''
|
|
|
},
|
|
|
// label 居中方式,默认 left 取值 left/center/right
|
|
|
labelAlign: {
|
|
|
type: String,
|
|
|
default: ''
|
|
|
},
|
|
|
// 强制显示错误信息
|
|
|
errorMessage: {
|
|
|
type: [String, Boolean],
|
|
|
default: ''
|
|
|
},
|
|
|
// 1.4.0 弃用,统一使用 form 的校验时机
|
|
|
// validateTrigger: {
|
|
|
// type: String,
|
|
|
// default: ''
|
|
|
// },
|
|
|
// 1.4.0 弃用,统一使用 form 的label 位置
|
|
|
// labelPosition: {
|
|
|
// type: String,
|
|
|
// default: ''
|
|
|
// },
|
|
|
// 1.4.0 以下属性已经废弃,请使用 #label 插槽代替
|
|
|
leftIcon: String,
|
|
|
iconColor: {
|
|
|
type: String,
|
|
|
default: '#606266'
|
|
|
},
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
errMsg: '',
|
|
|
userRules: null,
|
|
|
localLabelAlign: 'left',
|
|
|
localLabelWidth: '65px',
|
|
|
localLabelPos: 'left',
|
|
|
border: false,
|
|
|
isFirstBorder: false,
|
|
|
};
|
|
|
},
|
|
|
computed: {
|
|
|
// 处理错误信息
|
|
|
msg() {
|
|
|
return this.errorMessage || this.errMsg;
|
|
|
}
|
|
|
},
|
|
|
watch: {
|
|
|
// 规则发生变化通知子组件更新
|
|
|
'form.formRules'(val) {
|
|
|
// TODO 处理头条vue3 watch不生效的问题
|
|
|
// #ifndef MP-TOUTIAO
|
|
|
this.init()
|
|
|
// #endif
|
|
|
},
|
|
|
'form.labelWidth'(val) {
|
|
|
// 宽度
|
|
|
this.localLabelWidth = this._labelWidthUnit(val)
|
|
|
|
|
|
},
|
|
|
'form.labelPosition'(val) {
|
|
|
// 标签位置
|
|
|
this.localLabelPos = this._labelPosition()
|
|
|
},
|
|
|
'form.labelAlign'(val) {
|
|
|
|
|
|
}
|
|
|
},
|
|
|
created() {
|
|
|
this.init(true)
|
|
|
if (this.name && this.form) {
|
|
|
// TODO 处理头条vue3 watch不生效的问题
|
|
|
// #ifdef MP-TOUTIAO
|
|
|
this.$watch('form.formRules', () => {
|
|
|
this.init()
|
|
|
})
|
|
|
// #endif
|
|
|
|
|
|
// 监听变化
|
|
|
this.$watch(
|
|
|
() => {
|
|
|
const val = this.form._getDataValue(this.name, this.form.localData)
|
|
|
return val
|
|
|
},
|
|
|
(value, oldVal) => {
|
|
|
const isEqual = this.form._isEqual(value, oldVal)
|
|
|
// 简单判断前后值的变化,只有发生变化才会发生校验
|
|
|
// TODO 如果 oldVal = undefined ,那么大概率是源数据里没有值导致 ,这个情况不哦校验 ,可能不严谨 ,需要在做观察
|
|
|
// fix by mehaotian 暂时取消 && oldVal !== undefined ,如果formData 中不存在,可能会不校验
|
|
|
if (!isEqual) {
|
|
|
const val = this.itemSetValue(value)
|
|
|
this.onFieldChange(val, false)
|
|
|
}
|
|
|
}, {
|
|
|
immediate: false
|
|
|
}
|
|
|
);
|
|
|
}
|
|
|
|
|
|
},
|
|
|
// #ifndef VUE3
|
|
|
destroyed() {
|
|
|
if (this.__isUnmounted) return
|
|
|
this.unInit()
|
|
|
},
|
|
|
// #endif
|
|
|
// #ifdef VUE3
|
|
|
unmounted() {
|
|
|
this.__isUnmounted = true
|
|
|
this.unInit()
|
|
|
},
|
|
|
// #endif
|
|
|
methods: {
|
|
|
/**
|
|
|
* 外部调用方法
|
|
|
* 设置规则 ,主要用于小程序自定义检验规则
|
|
|
* @param {Array} rules 规则源数据
|
|
|
*/
|
|
|
setRules(rules = null) {
|
|
|
this.userRules = rules
|
|
|
this.init(false)
|
|
|
},
|
|
|
// 兼容老版本表单组件
|
|
|
setValue() {
|
|
|
// console.log('setValue 方法已经弃用,请使用最新版本的 uni-forms 表单组件以及其他关联组件。');
|
|
|
},
|
|
|
/**
|
|
|
* 外部调用方法
|
|
|
* 校验数据
|
|
|
* @param {any} value 需要校验的数据
|
|
|
* @param {boolean} 是否立即校验
|
|
|
* @return {Array|null} 校验内容
|
|
|
*/
|
|
|
async onFieldChange(value, formtrigger = true) {
|
|
|
const {
|
|
|
formData,
|
|
|
localData,
|
|
|
errShowType,
|
|
|
validateCheck,
|
|
|
validateTrigger,
|
|
|
_isRequiredField,
|
|
|
_realName
|
|
|
} = this.form
|
|
|
const name = _realName(this.name)
|
|
|
if (!value) {
|
|
|
value = this.form.formData[name]
|
|
|
}
|
|
|
// fixd by mehaotian 不在校验前清空信息,解决闪屏的问题
|
|
|
// this.errMsg = '';
|
|
|
|
|
|
// fix by mehaotian 解决没有检验规则的情况下,抛出错误的问题
|
|
|
const ruleLen = this.itemRules.rules && this.itemRules.rules.length
|
|
|
if (!this.validator || !ruleLen || ruleLen === 0) return;
|
|
|
|
|
|
// 检验时机
|
|
|
// let trigger = this.isTrigger(this.itemRules.validateTrigger, this.validateTrigger, validateTrigger);
|
|
|
const isRequiredField = _isRequiredField(this.itemRules.rules || []);
|
|
|
let result = null;
|
|
|
// 只有等于 bind 时 ,才能开启时实校验
|
|
|
if (validateTrigger === 'bind' || formtrigger) {
|
|
|
// 校验当前表单项
|
|
|
result = await this.validator.validateUpdate({
|
|
|
[name]: value
|
|
|
},
|
|
|
formData
|
|
|
);
|
|
|
|
|
|
// 判断是否必填,非必填,不填不校验,填写才校验 ,暂时只处理 undefined 和空的情况
|
|
|
if (!isRequiredField && (value === undefined || value === '')) {
|
|
|
result = null;
|
|
|
}
|
|
|
|
|
|
// 判断错误信息显示类型
|
|
|
if (result && result.errorMessage) {
|
|
|
if (errShowType === 'undertext') {
|
|
|
// 获取错误信息
|
|
|
this.errMsg = !result ? '' : result.errorMessage;
|
|
|
}
|
|
|
if (errShowType === 'toast') {
|
|
|
uni.showToast({
|
|
|
title: result.errorMessage || '校验错误',
|
|
|
icon: 'none'
|
|
|
});
|
|
|
}
|
|
|
if (errShowType === 'modal') {
|
|
|
uni.showModal({
|
|
|
title: '提示',
|
|
|
content: result.errorMessage || '校验错误'
|
|
|
});
|
|
|
}
|
|
|
} else {
|
|
|
this.errMsg = ''
|
|
|
}
|
|
|
// 通知 form 组件更新事件
|
|
|
validateCheck(result ? result : null)
|
|
|
} else {
|
|
|
this.errMsg = ''
|
|
|
}
|
|
|
return result ? result : null;
|
|
|
},
|
|
|
/**
|
|
|
* 初始组件数据
|
|
|
*/
|
|
|
init(type = false) {
|
|
|
const {
|
|
|
validator,
|
|
|
formRules,
|
|
|
childrens,
|
|
|
formData,
|
|
|
localData,
|
|
|
_realName,
|
|
|
labelWidth,
|
|
|
_getDataValue,
|
|
|
_setDataValue
|
|
|
} = this.form || {}
|
|
|
// 对齐方式
|
|
|
this.localLabelAlign = this._justifyContent()
|
|
|
// 宽度
|
|
|
this.localLabelWidth = this._labelWidthUnit(labelWidth)
|
|
|
// 标签位置
|
|
|
this.localLabelPos = this._labelPosition()
|
|
|
// 将需要校验的子组件加入form 队列
|
|
|
this.form && type && childrens.push(this)
|
|
|
|
|
|
if (!validator || !formRules) return
|
|
|
// 判断第一个 item
|
|
|
if (!this.form.isFirstBorder) {
|
|
|
this.form.isFirstBorder = true;
|
|
|
this.isFirstBorder = true;
|
|
|
}
|
|
|
|
|
|
// 判断 group 里的第一个 item
|
|
|
if (this.group) {
|
|
|
if (!this.group.isFirstBorder) {
|
|
|
this.group.isFirstBorder = true;
|
|
|
this.isFirstBorder = true;
|
|
|
}
|
|
|
}
|
|
|
this.border = this.form.border;
|
|
|
// 获取子域的真实名称
|
|
|
const name = _realName(this.name)
|
|
|
const itemRule = this.userRules || this.rules
|
|
|
if (typeof formRules === 'object' && itemRule) {
|
|
|
// 子规则替换父规则
|
|
|
formRules[name] = {
|
|
|
rules: itemRule
|
|
|
}
|
|
|
validator.updateSchema(formRules);
|
|
|
}
|
|
|
// 注册校验规则
|
|
|
const itemRules = formRules[name] || {}
|
|
|
this.itemRules = itemRules
|
|
|
// 注册校验函数
|
|
|
this.validator = validator
|
|
|
// 默认值赋予
|
|
|
this.itemSetValue(_getDataValue(this.name, localData))
|
|
|
},
|
|
|
unInit() {
|
|
|
if (this.form) {
|
|
|
const {
|
|
|
childrens,
|
|
|
formData,
|
|
|
_realName
|
|
|
} = this.form
|
|
|
childrens.forEach((item, index) => {
|
|
|
if (item === this) {
|
|
|
this.form.childrens.splice(index, 1)
|
|
|
delete formData[_realName(item.name)]
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
},
|
|
|
// 设置item 的值
|
|
|
itemSetValue(value) {
|
|
|
const name = this.form._realName(this.name)
|
|
|
const rules = this.itemRules.rules || []
|
|
|
const val = this.form._getValue(name, value, rules)
|
|
|
this.form._setDataValue(name, this.form.formData, val)
|
|
|
return val
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 移除该表单项的校验结果
|
|
|
*/
|
|
|
clearValidate() {
|
|
|
this.errMsg = '';
|
|
|
},
|
|
|
|
|
|
// 是否显示星号
|
|
|
_isRequired() {
|
|
|
// TODO 不根据规则显示 星号,考虑后续兼容
|
|
|
// if (this.form) {
|
|
|
// if (this.form._isRequiredField(this.itemRules.rules || []) && this.required) {
|
|
|
// return true
|
|
|
// }
|
|
|
// return false
|
|
|
// }
|
|
|
return this.required
|
|
|
},
|
|
|
|
|
|
// 处理对齐方式
|
|
|
_justifyContent() {
|
|
|
if (this.form) {
|
|
|
const {
|
|
|
labelAlign
|
|
|
} = this.form
|
|
|
let labelAli = this.labelAlign ? this.labelAlign : labelAlign;
|
|
|
if (labelAli === 'left') return 'flex-start';
|
|
|
if (labelAli === 'center') return 'center';
|
|
|
if (labelAli === 'right') return 'flex-end';
|
|
|
}
|
|
|
return 'flex-start';
|
|
|
},
|
|
|
// 处理 label宽度单位 ,继承父元素的值
|
|
|
_labelWidthUnit(labelWidth) {
|
|
|
|
|
|
// if (this.form) {
|
|
|
// const {
|
|
|
// labelWidth
|
|
|
// } = this.form
|
|
|
return this.num2px(this.labelWidth ? this.labelWidth : (labelWidth || (this.label ? 65 : 'auto')))
|
|
|
// }
|
|
|
// return '65px'
|
|
|
},
|
|
|
// 处理 label 位置
|
|
|
_labelPosition() {
|
|
|
if (this.form) return this.form.labelPosition || 'left'
|
|
|
return 'left'
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 触发时机
|
|
|
* @param {Object} rule 当前规则内时机
|
|
|
* @param {Object} itemRlue 当前组件时机
|
|
|
* @param {Object} parentRule 父组件时机
|
|
|
*/
|
|
|
isTrigger(rule, itemRlue, parentRule) {
|
|
|
// bind submit
|
|
|
if (rule === 'submit' || !rule) {
|
|
|
if (rule === undefined) {
|
|
|
if (itemRlue !== 'bind') {
|
|
|
if (!itemRlue) {
|
|
|
return parentRule === '' ? 'bind' : 'submit';
|
|
|
}
|
|
|
return 'submit';
|
|
|
}
|
|
|
return 'bind';
|
|
|
}
|
|
|
return 'submit';
|
|
|
}
|
|
|
return 'bind';
|
|
|
},
|
|
|
num2px(num) {
|
|
|
if (typeof num === 'number') {
|
|
|
return `${num}px`
|
|
|
}
|
|
|
return num
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss">
|
|
|
.uni-forms-item {
|
|
|
position: relative;
|
|
|
display: flex;
|
|
|
/* #ifdef APP-NVUE */
|
|
|
// 在 nvue 中,使用 margin-bottom error 信息会被隐藏
|
|
|
padding-bottom: 22px;
|
|
|
/* #endif */
|
|
|
/* #ifndef APP-NVUE */
|
|
|
margin-bottom: 22px;
|
|
|
/* #endif */
|
|
|
flex-direction: row;
|
|
|
|
|
|
&__label {
|
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
|
align-items: center;
|
|
|
text-align: left;
|
|
|
font-size: 14px;
|
|
|
color: #606266;
|
|
|
height: 36px;
|
|
|
padding: 0 12px 0 0;
|
|
|
/* #ifndef APP-NVUE */
|
|
|
vertical-align: middle;
|
|
|
flex-shrink: 0;
|
|
|
/* #endif */
|
|
|
|
|
|
/* #ifndef APP-NVUE */
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
/* #endif */
|
|
|
&.no-label {
|
|
|
padding: 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&__content {
|
|
|
/* #ifndef MP-TOUTIAO */
|
|
|
// display: flex;
|
|
|
// align-items: center;
|
|
|
/* #endif */
|
|
|
position: relative;
|
|
|
font-size: 14px;
|
|
|
flex: 1;
|
|
|
/* #ifndef APP-NVUE */
|
|
|
box-sizing: border-box;
|
|
|
/* #endif */
|
|
|
flex-direction: row;
|
|
|
|
|
|
/* #ifndef APP || H5 || MP-WEIXIN || APP-NVUE */
|
|
|
// TODO 因为小程序平台会多一层标签节点 ,所以需要在多余节点继承当前样式
|
|
|
&>uni-easyinput,
|
|
|
&>uni-data-picker {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
/* #endif */
|
|
|
|
|
|
}
|
|
|
|
|
|
& .uni-forms-item__nuve-content {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
flex: 1;
|
|
|
}
|
|
|
|
|
|
&__error {
|
|
|
color: #f56c6c;
|
|
|
font-size: 12px;
|
|
|
line-height: 1;
|
|
|
padding-top: 4px;
|
|
|
position: absolute;
|
|
|
/* #ifndef APP-NVUE */
|
|
|
top: 100%;
|
|
|
left: 0;
|
|
|
transition: transform 0.3s;
|
|
|
transform: translateY(-100%);
|
|
|
/* #endif */
|
|
|
/* #ifdef APP-NVUE */
|
|
|
bottom: 5px;
|
|
|
/* #endif */
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
.error-text {
|
|
|
// 只有 nvue 下这个样式才生效
|
|
|
color: #f56c6c;
|
|
|
font-size: 12px;
|
|
|
}
|
|
|
|
|
|
&.msg--active {
|
|
|
opacity: 1;
|
|
|
transform: translateY(0%);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 位置修饰样式
|
|
|
&.is-direction-left {
|
|
|
flex-direction: row;
|
|
|
}
|
|
|
|
|
|
&.is-direction-top {
|
|
|
flex-direction: column;
|
|
|
|
|
|
.uni-forms-item__label {
|
|
|
padding: 0 0 8px;
|
|
|
line-height: 1.5715;
|
|
|
text-align: left;
|
|
|
/* #ifndef APP-NVUE */
|
|
|
white-space: initial;
|
|
|
/* #endif */
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.is-required {
|
|
|
// color: $uni-color-error;
|
|
|
color: #dd524d;
|
|
|
font-weight: bold;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
.uni-forms-item--border {
|
|
|
margin-bottom: 0;
|
|
|
padding: 10px 0;
|
|
|
// padding-bottom: 0;
|
|
|
border-top: 1px #eee solid;
|
|
|
|
|
|
/* #ifndef APP-NVUE */
|
|
|
.uni-forms-item__content {
|
|
|
flex-direction: column;
|
|
|
justify-content: flex-start;
|
|
|
align-items: flex-start;
|
|
|
|
|
|
.uni-forms-item__error {
|
|
|
position: relative;
|
|
|
top: 5px;
|
|
|
left: 0;
|
|
|
padding-top: 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* #endif */
|
|
|
|
|
|
/* #ifdef APP-NVUE */
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
|
|
|
.uni-forms-item__error {
|
|
|
position: relative;
|
|
|
top: 0px;
|
|
|
left: 0;
|
|
|
padding-top: 0;
|
|
|
margin-top: 5px;
|
|
|
}
|
|
|
|
|
|
/* #endif */
|
|
|
|
|
|
}
|
|
|
|
|
|
.is-first-border {
|
|
|
/* #ifndef APP-NVUE */
|
|
|
border: none;
|
|
|
/* #endif */
|
|
|
/* #ifdef APP-NVUE */
|
|
|
border-width: 0;
|
|
|
/* #endif */
|
|
|
}
|
|
|
</style>
|