365 lines
11 KiB
Plaintext
365 lines
11 KiB
Plaintext
<script lang="ts" setup>
|
|
import { getCurrentInstance } from "vue"
|
|
import { getUid } from "../../core/util/xCoreUtil.uts"
|
|
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
|
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
|
import { xConfig } from "../../config/xConfig.uts"
|
|
import { FORM_ITEM } from "../x-form/interface.uts"
|
|
import { FORM_RULE, FORM_SUBMIT_OBJECT, FORM_SUBMIT_RESULT } from "../../interface.uts"
|
|
type FORMITEM_R = {
|
|
key : string,
|
|
value : any | null,
|
|
}
|
|
type FORMITEM_J = {
|
|
value : any | null,
|
|
item : FORM_ITEM
|
|
}
|
|
type labelDirType = 'vertical' | 'horizontal'
|
|
type errorAlignType = 'left' | 'center'| 'right'
|
|
type flatFunCall = (obj: UTSJSONObject, prefix: string) => UTSJSONObject
|
|
/**
|
|
* @name 表单 xForm
|
|
* @description 从1.1.2开始允许xform可以嵌套view进行form-item布局了,但建议不要嵌套太深,影响性能.
|
|
* @page /pages/index/form
|
|
* @category 表单组件
|
|
* @constant 平台兼容
|
|
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
|
*/
|
|
defineOptions({name:"xForm"})
|
|
const proxy = getCurrentInstance()?.proxy??null;
|
|
const emits = defineEmits([
|
|
/**
|
|
* 按钮提交表单时触发。
|
|
* @param {FORM_SUBMIT_RESULT} result 表单验证结果
|
|
*/
|
|
'submit',
|
|
/**
|
|
* 校验事件,这个不是submit是用户在输入内容或者在校验阶段向外发出的事件
|
|
* 里面包含了实时的校验信息,用于绑定外部的按钮提供实时的可提交状态。
|
|
* 它对外输出,不能够双向绑定对内更改校验状态。
|
|
*/
|
|
'update:modelValid'
|
|
])
|
|
export type xFormPropsType = {
|
|
/**
|
|
* 标签宽(横向表单时有效)
|
|
* 不支持动态修改(uniappx 3.99不支持动态传递 inject),可在子组件上动态修改
|
|
*/
|
|
labelWidth:string,
|
|
/**
|
|
* 不支持动态修改(uniappx 3.99不支持动态传递 inject),可在子组件上动态修改
|
|
* vertical,horizontal
|
|
*/
|
|
labelDirection:labelDirType,
|
|
/**
|
|
* 标签的文本颜色,不支持动态修改(uniappx 3.99不支持动态传递 inject),可在子组件上动态修改
|
|
*/
|
|
labelFontColor:string,
|
|
/**
|
|
* 出错时,是否滚动到表单的位置
|
|
* 不支持动态修改(uniappx 3.99不支持动态传递 inject),可在子组件上动态修改
|
|
*/
|
|
errorAutoPage:boolean,
|
|
/**
|
|
* 等同v-model
|
|
*/
|
|
modelValue:any,
|
|
/**
|
|
* 标签文本大小
|
|
* 不支持动态修改(uniappx 3.99不支持动态传递 inject),可在子组件上动态修改
|
|
*/
|
|
labelFontSize:string,
|
|
/**
|
|
* 是否显示标题,不支持动态修改(uniappx 3.99不支持动态传递 inject),可在子组件上动态修改
|
|
*/
|
|
showLabel:boolean,
|
|
/**
|
|
* 错误标签的对齐方式,left,center,right
|
|
*/
|
|
errorAlign:errorAlignType,
|
|
/**
|
|
* rules这里提供和与formItem上提供的不冲突都可以校验
|
|
* 如果两个名称相同会被校验两次。如果两有一边提供也会校验一次。
|
|
*/
|
|
rules:Map<string,FORM_RULE[]>,
|
|
/**
|
|
* 是否开启实时全部字段校验获取当前实时的校验状态,
|
|
* 通过vmodel:valid对外输出当前实时的校验值,当字段值改变时会一直监测并监听所有字段并返回结果到对外
|
|
* 如果你没有这个需求场景,请保持关闭状态,如果你确实外部需要实时改变并观察提交按钮状态可以打开,请在字段少的场景使用。
|
|
*/
|
|
watchValidStatus:boolean,
|
|
/**
|
|
* 当前的校验状态,请不要外部改变此值,此值只对外输出当前校验状态
|
|
* 需要设置watchValidStatus为true才会实时监听并检测状态。请通过v-model:valid来得到状态值
|
|
*/
|
|
modelValid:boolean,
|
|
}
|
|
const list = ref<FORM_ITEM[]>([])
|
|
const top = ref(0)
|
|
const isOkTop = ref(false)
|
|
const isdestry = ref(false)
|
|
const oldFormData = ref<any>({})
|
|
const id = ("xForm-" + getUid()) as string
|
|
|
|
const props = withDefaults(defineProps<xFormPropsType>(), {
|
|
labelWidth:"100",
|
|
labelDirection:"horizontal",
|
|
labelFontColor:"#333333",
|
|
errorAutoPage:true,
|
|
modelValue:{} as UTSJSONObject,
|
|
labelFontSize:"16",
|
|
showLabel:true,
|
|
errorAlign:'left',
|
|
rules: ():Map<string,FORM_RULE[]> => new Map<string,FORM_RULE[]>(),
|
|
watchValidStatus:false,
|
|
modelValid:false
|
|
})
|
|
|
|
const _modelValue = computed(():any =>props.modelValue)
|
|
const _rules = computed(():Map<string,FORM_RULE[]> =>props.rules)
|
|
let validTimeid = 12
|
|
provide('XFORMITEM_TOP',computed(():number=>top.value))
|
|
provide('XFORMITEM_LABEL_WIDTH',computed(():string=>props.labelWidth))
|
|
provide('XFORMITEM_LABEL_Direction',computed(():string=>props.labelDirection))
|
|
provide('labelFontColor',computed(():string=>props.labelFontColor))
|
|
provide('XFORMITEM_LABEL_SCROLL',computed(():boolean=>props.errorAutoPage))
|
|
provide('XFORMITEM_LABEL_FontSize',computed(():string=>props.labelFontSize))
|
|
provide('XFORMITEM_SHOWLABEL',computed(():boolean=>props.showLabel))
|
|
provide('XFORMITEM_ERROR_ALIGN',computed(():string=>props.errorAlign))
|
|
|
|
/**
|
|
* 将嵌套对象拍平为点分隔的键值对
|
|
* @param obj 要拍平的对象
|
|
* @param prefix 键前缀
|
|
* @returns 拍平后的对象
|
|
*/
|
|
let flattenObject:flatFunCall|null = null;
|
|
|
|
flattenObject = (obj: UTSJSONObject, prefix: string): UTSJSONObject => {
|
|
let flattened: UTSJSONObject = {}
|
|
const flatMap = obj.toMap()
|
|
for (const aitem of flatMap) {
|
|
let item = aitem as Array<any|null>
|
|
let key = item[0]! as string;
|
|
let value = item[1];
|
|
if (flatMap.has(key)) {
|
|
const newKey = prefix!='' ? `${prefix}/${key}` : key
|
|
if (value !== null && value instanceof UTSJSONObject ) {
|
|
// 递归处理嵌套对象
|
|
let fun = flattenObject!
|
|
flattened = {...flattened,...fun(value, newKey)}
|
|
} else {
|
|
flattened.set(newKey,value)
|
|
}
|
|
}
|
|
}
|
|
return flattened
|
|
}
|
|
|
|
function getNodeInfo() {
|
|
uni.createSelectorQuery().in(proxy)
|
|
.select(".xForm")
|
|
.boundingClientRect().exec((ret) => {
|
|
let nodeinfo = ret[0] as NodeInfo
|
|
top.value = nodeinfo.top!;
|
|
isOkTop.value = true;
|
|
})
|
|
}
|
|
function actionsRequired(templist : FORMITEM_R[]) {
|
|
list.value.forEach((el : FORM_ITEM) => {
|
|
let index = templist.findIndex((ele : FORMITEM_R) : boolean => ele.key == el.name)
|
|
if (index > -1) {
|
|
el.ele.vaildCompele(templist[index].value)
|
|
}
|
|
})
|
|
|
|
}
|
|
//比较哪个字段有变化
|
|
function diffFiled(newvals : any, olds : any) {
|
|
if (isdestry.value) return;
|
|
if (typeof newvals != 'object' || typeof olds != 'object') return;
|
|
let newval = JSON.parseObject(JSON.stringify(newvals)) as UTSJSONObject;
|
|
let old = JSON.parseObject(JSON.stringify(olds)) as UTSJSONObject;
|
|
newval = flattenObject!(newval,'');
|
|
old = flattenObject!(old,'');
|
|
let fileds = [] as FORMITEM_R[]
|
|
for (let key in old) {
|
|
let val : any | null = newval.get(key)
|
|
let oldval : any | null = old.get(key)
|
|
if (Array.isArray(val) && Array.isArray(oldval)) {
|
|
let temval = val.length
|
|
let temoldval = oldval.length
|
|
if (temval != temoldval) {
|
|
fileds.push({
|
|
key,
|
|
value: val
|
|
} as FORMITEM_R)
|
|
}
|
|
} else if (val != oldval) {
|
|
fileds.push({
|
|
key,
|
|
value: val
|
|
} as FORMITEM_R)
|
|
}
|
|
|
|
}
|
|
actionsRequired(fileds)
|
|
oldFormData.value = JSON.parse<any>(JSON.stringify(_modelValue.value))!;
|
|
}
|
|
|
|
function _valid(keys : string[],isShowwStatusInTag:boolean = true) : FORM_SUBMIT_RESULT {
|
|
let toptemp = 0
|
|
let result = {
|
|
valid: true,
|
|
errorMessage: "",
|
|
key: "",
|
|
formData: [] as FORM_SUBMIT_OBJECT[]
|
|
} as FORM_SUBMIT_RESULT
|
|
let modevalue = JSON.parseObject(JSON.stringify(_modelValue.value)) as UTSJSONObject;
|
|
modevalue = flattenObject!(modevalue,'');
|
|
|
|
for (let key in modevalue) {
|
|
if (keys.length == 0 || keys.includes(key)) {
|
|
|
|
let valdata : any | null = modevalue.get(key)
|
|
let index = list.value.findIndex((ele : FORM_ITEM) : boolean => ele.name == key)
|
|
|
|
if (index > -1) {
|
|
let resultJg = null as FORM_SUBMIT_OBJECT|null
|
|
if(isShowwStatusInTag){
|
|
resultJg = list.value[index].ele.vaildCompele(valdata, false) as FORM_SUBMIT_OBJECT
|
|
}else{
|
|
resultJg = list.value[index].ele.getVaildStatus(valdata) as FORM_SUBMIT_OBJECT
|
|
}
|
|
if (!resultJg!.valid && result.valid) {
|
|
result.valid = false;
|
|
result.key = key;
|
|
result.errorMessage = resultJg!.errorMessage;
|
|
toptemp = list.value[index].top
|
|
}
|
|
result.formData.push(resultJg!)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if(isShowwStatusInTag){
|
|
/**
|
|
* 按钮提交表单时触发。
|
|
* @param {FORM_SUBMIT_RESULT} result 表单验证结果。
|
|
*/
|
|
emits('submit', result)
|
|
if (props.errorAutoPage && !result.valid && toptemp > 0) {
|
|
uni.pageScrollTo({
|
|
scrollTop: toptemp,
|
|
fail: (_) => {
|
|
console.error("当前页面没有放置可滚动的列表,但使用了表单出错滚动功能,请关闭表单属性:error-auto-page")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// 全局检测校验整体状态。
|
|
function getStatus(){
|
|
if(!props.watchValidStatus) return;
|
|
clearTimeout(validTimeid)
|
|
validTimeid = setTimeout(function() {
|
|
const result = _valid([] as string[],false)
|
|
console.log(result.valid)
|
|
emits('update:modelValid',result.valid)
|
|
}, 120);
|
|
|
|
}
|
|
|
|
onMounted(()=>{
|
|
oldFormData.value = JSON.parse<any>(JSON.stringify(_modelValue.value))!;
|
|
getNodeInfo();
|
|
})
|
|
onBeforeUnmount(()=>{
|
|
clearTimeout(validTimeid)
|
|
isdestry.value = true;
|
|
list.value = [] as FORM_ITEM[]
|
|
oldFormData.value = {} as UTSJSONObject
|
|
})
|
|
|
|
|
|
|
|
watch(():any=>props.modelValue,(newValue : any)=>{
|
|
diffFiled(newValue, oldFormData.value)
|
|
if(props.watchValidStatus){
|
|
getStatus()
|
|
}
|
|
},{deep:true})
|
|
|
|
|
|
|
|
defineExpose({
|
|
pushAdd(item : FORM_ITEM) {
|
|
|
|
if (isdestry.value) return;
|
|
let index = list.value.findIndex((el : FORM_ITEM) : boolean => el.id == item.id);
|
|
if (index > -1) {
|
|
list.value.splice(index, 1, item)
|
|
} else {
|
|
list.value.push(item)
|
|
}
|
|
|
|
},
|
|
delItem(id : string) {
|
|
if (isdestry.value) return;
|
|
if (list.value.length == 0) return;
|
|
let index : number = list.value.findIndex((el : FORM_ITEM) : boolean => el.id == id);
|
|
if (index > -1) {
|
|
list.value.splice(index, 1)
|
|
}
|
|
},
|
|
getRules(key:string):FORM_RULE[]{
|
|
const _localRules = _rules.value.get(key)
|
|
if(_localRules == null) return [] as FORM_RULE[]
|
|
return _localRules!
|
|
},
|
|
/**
|
|
* 用来首次同步检测:modelValid,一定要在onready生命期中执行,并且你赋值完表数据后再来个,nextTick中执行本方法,可以确保不会有遗漏。
|
|
*/
|
|
checkAsyncVaildStatus(){
|
|
getStatus();
|
|
},
|
|
/**
|
|
* 手动执行触发校验函数,如果提供空数组,表示校验所有,如果提供了指定值,则表示只校验提供的字段。
|
|
* @public
|
|
* @param {string[]} keys - 待校验的字段
|
|
* @returns {FORM_SUBMIT_RESULT} 校验结果
|
|
*/
|
|
valid(keys : string[]) : FORM_SUBMIT_RESULT {
|
|
return _valid(keys);
|
|
},
|
|
/**
|
|
* 清除校验状态并回到初始状态。
|
|
*/
|
|
clearValid(){
|
|
list.value.forEach((el : FORM_ITEM) => {
|
|
el.ele.clearValid()
|
|
})
|
|
},
|
|
submit() : FORM_SUBMIT_RESULT {
|
|
|
|
return _valid([] as string[]);
|
|
}
|
|
})
|
|
|
|
</script>
|
|
<template>
|
|
<view class="xForm">
|
|
<!--
|
|
@slot 插槽内只能放置x-form-item子节点组件
|
|
-->
|
|
<slot></slot>
|
|
</view>
|
|
</template>
|
|
<style scoped>
|
|
</style> |