628 lines
16 KiB
Vue
628 lines
16 KiB
Vue
<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(默认70)
|
||
* @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的宽度
|
||
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: '70px',
|
||
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 ? 70 : 'auto')))
|
||
// }
|
||
// return '70px'
|
||
},
|
||
// 处理 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>
|