Files
hospital-rescue-app-v2/uni_modules/tmx-ui/components/x-form/x-form.uvue
T
2026-09-24 16:25:22 +08:00

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>