vue3通过provide/inject二次封装el表单项组件,简化表单校验语法

本组件涵盖了    必填项、特殊字符校验、小数类型校验、整数类型校验、手机号校验、身份证号校验、车牌号校验、银行卡号校验、自定义校验、聚焦反显失焦脱敏等功能

封装hooks函数管理注册的表单组件,在组件中写校验逻辑,不走elementuiplus的校验方法,统一使用hooks函数里的校验方法

创建hooks文件夹,创建useForm.js文件



import { ref, provide, onBeforeUnmount } from 'vue'
 
export default function useForm(options={}){
  /**
   * @param String regionName --> 当前组件区域名,用以对区域内的表单子组件进行校验和清除校验
   */
  const {
    regionName = 'master'
  } = options
 
  //表单项子组件(对象数组)
  let childRefs = ref({})
  //子组件注册事件
  const register = ( key, methods, name='' )=>{
    name = name || regionName
    if(!childRefs.value[name]){
      childRefs.value[name] = new Map()
    }
    childRefs.value[name].set(key, methods)
  }
  //子组件卸载事件
  const unRegister = (key, name='')=>{
    name = name || regionName
    if(childRefs.value[name]){
      childRefs.value[name].delete(key)
    }
  }
  //注册子组件信息 registerRegionChild  通过provide/inject实现表单校验
  provide("registerChild",{
    register,
    unRegister
  })
  //触发子组件校验
  const validate = (name='')=>{
    name = name || regionName
    let isPass = true
    childRefs.value[name]?.forEach(child=>{
      if(child && child.onBlur){
        if(isPass){
          isPass = child.onBlur()
        }else{
          child.onBlur()
        }
      }
    })
    return isPass
  }
  //清除子组件校验
  const clear = (name='')=>{
    name = name || regionName
    childRefs.value[name]?.forEach(child=>{
      if(child && child.onClear){
        child.onClear()
      }
    })
  }
  //手动清除子组件refs,清理Map
  const clearRefs = (name='')=>{
    name = name || regionName
    childRefs.value[name]?.clear()
  }
  //销毁子组件refs,清理Map中的数据
  onBeforeUnmount(()=>{
    Object.keys(childRefs.value).forEach(name=>{
      childRefs.value[name]?.clear()
    })
    childRefs.value = {}
  })
  
  return {
    validate,
    clear,
    clearRefs
  }
}

封装Input组件



<template>
  <!-- input输入框 -->
  <div class="container" ref="container">
    <!-- 文本域 type: textarea -->
    <template v-if="type == 'textarea'">
      <el-input :class="{ 'jcp-input': !isPass }" v-model="inValue" :clearable="clearable" :placeholder="placeholder"
        :maxlength="maxLen" show-word-limit type="textarea" :disabled="disabled" :required="required" :rows="rows"
        @blur="onBlur" @clear="onClear">
        <template v-if="prependText" slot="prepend">{{ prependText }}</template>
        <template v-if="appendText" slot="append">{{ appendText }}</template>
      </el-input>
    </template>
    <!-- other输入框 type: text  float int tel  ID  car -->
    <template v-else>
      <el-input ref="inputRef" :class="{ 'jcp-input': !isPass }" v-model.trim="inValue" :clearable="clearable"
        :placeholder="disabled ? '' : placeholder" :maxlength="maxLen" type="text" :disabled="disabled" :required="required"
        @focus="onFocus" @blur="onBlur" @clear="onClear">
        <template v-if="prependText" slot="prepend">{{ prependText }}</template>
        <template v-if="appendText" slot="append">{{ appendText }}</template>
      </el-input>
    </template>
    <span v-if="!isPass" class="tipClass">{{ tip }}</span>
  </div>
</template>
<script setup>
import { ref, computed, onMounted, inject, onUnmounted, watch } from "vue";
import { desensitization, getTypeOf, isNull } from "@/assets/script/common.js"
const props = defineProps({
  modelValue: {
    type: [String, Number],
    default: ""
  },
  maxLen: {
    //最大输入长度
    type: Number,
    default: 20
  },
  type: {
    //输入框类型用于判断对应的校验规则,取值:text textarea  float(小数) int(整数) tel(手机号)  ID(身份证号)  car(车牌号)
    type: [String, Array],
    default: "text"
  },
  rows: {
    //文本域类型-行数
    type: Number,
    default: 3
  },
  placeholder: {
    type: String,
    default: "请输入内容"
  },
  clearable: {
    type: Boolean,
    default: true
  },
  disabled: {
    type: Boolean,
    default: false
  },
  patt: {
    //特殊字符校验规则,type为text和textarea类型时校验
    type: String,
    default: `[\\[\]$\^&*#¥……?!@-<>【】《》?!\/\|、;:`‘’“”「」{}+=_%;:',,。·~]`
  },
  required: {
    //是否必输项
    type: Boolean,
    default: false
  },
  prependText: {
    //el-input前置内容
    type: String,
    default: ""
  },
  appendText: {
    //el-input后置内容
    type: String,
    default: ""
  },
  requiredText: {
    //必填项提示
    type: String,
    default: "字段为必输项"
  },
  isDisplay: {//数据是否脱敏
    type: Boolean,
    default: false
  },
  region: {//区域名称,inject注册子组件时给子组件分区域用的
    type: String,
    default: 'master'
  },
  noValidate: {//不做任何校验
    type: Boolean,
    default: false
  }
});
const container = ref(null)
let inputRef = ref(null)
let tip = ref("");//校验提示
let isPass = ref(true)//是否通过校验
let isFocused = ref(false)//聚焦反显
const emit = defineEmits(["update:modelValue", "diy-event", "blur"]);
 
const inValue = computed({
  get: () => { 
    if(props.isDisplay && !isFocused.value) {
      return desensitization(props.modelValue?.toString()) 
    }else { 
      return props.modelValue?.toString()
    }
  },
  set: val => emit("update:modelValue", val)
});
//监听值,如果是必填的校验没过,则重新触发一下校验
watch(()=>inValue.value, ()=>{
  if(tip.value===props.requiredText && isPass.value===false){
    onBlur()
  }
})
//通过provide/inject把子组件的ref映射到父组件上,以便父组件可以调用子组件的onBlur方法
const registerChild = inject("registerChild")
const key = Symbol('childInput')
//通过dom操作调用方法,用以vue动态销毁组件
const unMounted = ()=>{
  registerChild.unRegister(key, props.region)
}
onMounted(() => {
  container.value.unMounted = unMounted
  if (registerChild) {
    registerChild.register(key, { onBlur, onClear }, props.region)
  }
});
onUnmounted(()=>{
  registerChild.unRegister(key, props.region)
})
const onFocus = () => {
  isFocused.value = true
}
const onBlur = () => {
  if (props.isDisplay && isFocused.value) {
    isFocused.value = false
  }
  //不做任何校验
  if (props.noValidate || props.disabled) {
    updatePass(true);
    return isPass.value;
  }
  //如果是必填项而且没有值,则校验不通过
  if (props.required && isNull(inValue.value)) {
    updatePass(false, props.requiredText);
    return isPass.value;
  }
 
  if (inValue.value != "") {
    if (getTypeOf(props.type) === 'Array') {
      for (let item of props.type) {
        validateByType(item)
      }
    } else {
      validateByType(props.type)
    }
  } else {
    updatePass(true);
  }
  //校验通过才暴露blur事件
  if (isPass.value) {
    emit('blur')
  }
  return isPass.value;
};
 
const validateByType = (type) => {
  if (type == "text") {
    const patt = new RegExp(props.patt);
    if (patt.test(props.modelValue)) {
      updatePass(false, "请输入正确的字符");
    } else {
      updatePass(true);
    }
  } else if (type == "textarea") {
    const patt = new RegExp(props.patt);
    if (patt.test(props.modelValue)) {
      updatePass(false, "请输入正确的字符");
    } else {
      updatePass(true);
    }
  } else if (type == "float") {
    //浮点数类型校验
    let pattern = `(^(\d{1,${props.maxLen}})\.(\d{1,3})$)|(^\d{0,${props.maxLen}}$)`;
    const patt = new RegExp(pattern);
    if (patt.test(props.modelValue)) {
      updatePass(true);
    } else {
      updatePass(false, "请正确输入数字字符,并且最多保留三位小数");
    }
  } else if (type == "int") {
    //整数类型校验
    let pattern = `^\d{0,${props.maxLen}}$`;
    const patt = new RegExp(pattern);
    if (patt.test(props.modelValue)) {
      updatePass(true);
    } else {
      updatePass(false, "请正确输入整数数字字符");
    }
  } else if (type == "tel") {
    //手机号校验
    const patt = new RegExp("^1[3-9]\d{9}$");
    if (patt.test(props.modelValue)) {
      updatePass(true);
    } else {
      updatePass(false, "手机号码格式不正确");
    }
  } else if (type == "ID") {
    //身份证号校验
    const patt = new RegExp(
      "^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$|^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|[Xx])$"
    );
    if (patt.test(props.modelValue)) {
      updatePass(true);
    } else {
      updatePass(false, "身份证号格式不正确");
    }
  } else if (type == "car") {
    //车牌号校验
    const patt = new RegExp(
      "^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-Z0-9]{4,5}[A-Z0-9学挂警港澳]$"
    );
    if (patt.test(props.modelValue)) {
      updatePass(true);
    } else {
      updatePass(false, "车牌号格式不正确");
    }
  } else if (type == 'bank') {//银行卡号
    const patt = new RegExp("^([1-9]{1})(\d{15}|\d{16}|\d{18})$")
    if (patt.test(props.modelValue)) {
      updatePass(true)
    } else {
      updatePass(false, "银行卡号格式不正确")
    }
  } else if (type == "diy") {
    if (props.type.indexOf(type) !== 0 && !isPass.value) {
      return
    }
    //自定义校验规则
    emit('diy-event', { inValue, updatePass })
  }
}
 
//清空校验和数据
const onClear = () => {
  updatePass(true);
};
//更新校验状态
const updatePass = (bool, info = "") => {
  isPass.value = bool
  tip.value = info
}
</script>
<style lang="scss" scoped>
.container {
  position: relative;
  width: 100%;
 
  .tipClass {
    color: #f56c6c;
    font-size: 12px;
    line-height: 1;
    padding-top: 4px;
    position: absolute;
    top: 100%;
    left: 0;
  }
 
  .jcp-input {
    :deep(.el-input__wrapper) {
      box-shadow: 0 0 0 1px #f56c6c inset !important;
    }
    :deep(.el-textarea__inner) {
      box-shadow: 0 0 0 1px #f56c6c inset !important;
    }
  }
 
  .zc-input-group__append {
    color: #c0c4cc;
  }
}
</style>

使用组件



<template>
  <div>
    <PageStructure>
      <template #searchForm>
        <el-form 
          :model="searchForm" 
          label-width="180px" 
          label-suffix=":"
          class="formGrid3" 
          v-stretch>
          <el-form-item label="测试text类型">
            <Input v-model="searchForm.num1" />
          </el-form-item>
          <el-form-item label="测试必输项" required>
            <Input v-model="searchForm.num2" required/>
          </el-form-item>
          <el-form-item label="测试float类型">
            <Input v-model="searchForm.num3" type="float" :maxLen="6"/>
          </el-form-item>
          <el-form-item label="测试int类型">
            <Input v-model="searchForm.num4" type="int" :maxLen="10"/>
          </el-form-item>
          <el-form-item label="测试手机号类型">
            <Input v-model="searchForm.num5" type="tel"/>
          </el-form-item>
          <el-form-item label="测试身份证号类型">
            <Input v-model="searchForm.num6" type="ID"/>
          </el-form-item>
          <el-form-item label="测试车牌号类型">
            <Input v-model="searchForm.num7" type="car"/>
          </el-form-item>
          <el-form-item label="测试自定义校验规则">
            <Input v-model="searchForm.num8" type="diy" @diy-event="(childData) => diyFunction(childData)"/>
          </el-form-item>
          <!-- <el-form-item label="测试textarea类型" required>
            <Input v-model="searchForm.area" required type="textarea"/>
          </el-form-item> -->
          <el-form-item label="测试自定义正则">
            <Input v-model="searchForm.num9" patt="[[]$^&*#¥……?]"/>
          </el-form-item>
          <el-form-item label="测试银行卡号类型">
            <Input v-model="searchForm.num10" type="bank"/>
          </el-form-item>
          <el-form-item label="测试下拉框和必输项" required>
            <Select
              v-model="searchForm.status"
              required
              :selectList="selectList"
            ></Select>
          </el-form-item>
          <el-form-item label="测试下拉框拼接label" >
            <Select
              v-model="searchForm.status2"
              :selectList="selectList"
              :sLabel="['value', 'label']"
              sValue="value"
            ></Select>
          </el-form-item>
          <el-form-item label="测试下拉框多选" >
            <Select
              v-model="searchForm.status3"
              :selectList="multipleSelectList"
              multiple
              :sLabel="['value', 'label']"
              sValue="value"
            ></Select>
          </el-form-item>
          <el-form-item label="测试单选和必输项" required>
            <Radio
              v-model="searchForm.radioval"
              required
              :radioList="sexList"
            />
          </el-form-item>
          <el-form-item label="测试单选边框样式">
            <Radio
              v-model="searchForm.radioval"
              :radioList="sexList"
              border
            />
          </el-form-item>
          <el-form-item label="测试单选按钮样式" >
            <Radio
              v-model="searchForm.radioval"
              :radioList="sexList"
              isButton
            />
          </el-form-item>
          <el-form-item label="测试日期范围和必输项" required>
            <Date
              v-model="searchForm.daterange"
              required
              type="daterange"
            ></Date>
          </el-form-item>
          <el-form-item label="测试日期" required>
            <Date
              v-model="searchForm.daterange2"
              required
              :disabledDate="disabledDate"
            ></Date>
          </el-form-item>
          <el-form-item label="测试日期时间范围" required>
            <DateTime
              v-model="searchForm.dateTimerange"
              type="datetimerange"
              required
            />
          </el-form-item>
          <el-form-item label="测试日期时间" required>
            <DateTime
              v-model="searchForm.dateTime"
              required
            />
          </el-form-item>
        </el-form>
      </template>
      <template #operationButton>
        <ButtonRef
          @query="queryClick"
          @reset="resetClick"
        />
      </template>
      <template #dataTable>
        <Table :dataLabel="dataLabel" :dataList="dataList" isOrder isSelect v-model:selected="selected" radioLabel="id" v-model:radioVal="radioVal" isScrollUp isFilterColumn>
          <template #topLeft>
            <el-button type="primary" @click="addClick">新增</el-button>
            <el-button type="primary" @click="delClick">删除</el-button>
            <el-button type="primary" @click="validateSlave">测试分区域表单校验</el-button>
            <el-button type="primary" @click="clearSlave">测试分区域表单清除校验</el-button>
          </template>
          <template #sex="{row}">
            {{ codeToName(row.sex,sexList,'value','label') }}
          </template>
          <template #volume="{row}">
            <Input region="slave" v-model="row.volume" disabled type="diy" @diy-event="(childData) => diyVolume(row, childData)"/>
          </template>
          <template #length="{row}">
            <Input region="slave" v-model="row.length" required type="float" :maxLen="6"/>
          </template>
          <template #height="{row}">
            <Input region="slave" v-model="row.height" type="float" :maxLen="6"/>
          </template>
          <template #width="{row}">
            <Input region="slave" v-model="row.width" type="float" :maxLen="6"/>
          </template>
          <template #operate>
            <el-table-column label="操作" align="center" width="160" fixed="right">
              <template #default="{row}">
                <el-button link type="primary" @click="edit(row)">编辑</el-button>
              </template>
            </el-table-column>
          </template>
        </Table>
      </template>
      <template #pagination>
        <Pageination
          :total="total"
          v-model:current="pageNo"
          v-model:pageSize="pageSize"
          @pageChange="pageChange"
        />
      </template>
    </PageStructure>
    <EditDialog ref="editDialog"/>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import useForm from '@/hooks/useForm.js'
import dayjs from 'dayjs'
import { codeToName } from '@/assets/script/common.js'
import EditDialog from './components/editDialog.vue'
const editDialog = ref(null)
const { validate, clear } = useForm()
const selectList = [
  { value: "1", label: "未开始" },
  { value: "2", label: "进行中", disabled: true },
  { value: "3", label: "已完成" },
]
const multipleSelectList = [
  { value: "1", label: "篮球" },
  { value: "2", label: "羽毛球", disabled: true },
  { value: "3", label: "足球" },
  { value: "4", label: "乒乓球" },
  { value: "5", label: "橄榄球" },
]
const sexList = [
  { value: "1", label: "男"},
  { value: "2", label: "女", disabled: true},
]
const disabledDate = (time)=>{
  return time.getTime() < dayjs().subtract(1,'day')
}
 
let searchForm = ref({})
 
let dataLabel = ref([
  { prop: "id", label: "id", width: "80" },
  { prop: "name", label: "姓名",dsn:true },
  { prop: "cardNo", label: "身份证号",dsn:true },
  { prop: "sex",label:"性别" },
  { prop: "volume",label:"体积" },
  { prop: "length",label:"长" },
  { prop: "width",label:"宽" },
  { prop: "height",label:"高" },
  { prop: "createtime", label: "创建时间", noDrag: true, fixed:'right' },
  { prop: "updatetime", label: "更新时间", noDrag: true, fixed:'right' }
])
//这里是demo演示的table假数据
let dataList = ref([
  {id:11,name:'李',cardNo:'220620199011112222',sex:1,volume:'4',createtime:'2025-03-26',updatetime:"2025-03-26"},
  {id:22,name:'杜甫',cardNo:'220620199011112222',sex:1,createtime:'2025-03-26',updatetime:"2025-03-26"},
  {id:33,name:'liability',cardNo:'220620199011112222',sex:1,createtime:'2025-03-26',updatetime:"2025-03-26"},
  {id:41,name:'liability',cardNo:'220620199011112222',sex:1,createtime:'2025-03-26',updatetime:"2025-03-26"},
  {id:52,name:'liability',cardNo:'220620199011112222',sex:1,createtime:'2025-03-26',updatetime:"2025-03-26"},
  {id:63,name:'liability',cardNo:'220620199011112222',sex:1,createtime:'2025-03-26',updatetime:"2025-03-26"},
  {id:111,name:'liability',cardNo:'220620199011112222',sex:1,createtime:'2025-03-26',updatetime:"2025-03-26"},
  {id:221,name:'liability',cardNo:'220620199011112222',sex:1,createtime:'2025-03-26',updatetime:"2025-03-26"},
  {id:331,name:'liability',cardNo:'220620199011112222',sex:1,createtime:'2025-03-26',updatetime:"2025-03-26"},
  {id:411,name:'liability',cardNo:'220620199011112222',sex:2,createtime:'2025-03-26',updatetime:"2025-03-26"},
  {id:521,name:'liability',cardNo:'220620199011112222',sex:2,createtime:'2025-03-26',updatetime:"2025-03-26"},
  {id:631,name:'liability',cardNo:'220620199011112222',sex:2,createtime:'2025-03-26',updatetime:"2025-03-26"},
])
//单选的数据
let radioVal = ref({})
//多选的数据
let selected = ref([])
 
onMounted(()=>{
  // queryClick()
})
 
//点击查询
const queryClick = (page=1)=>{
  console.log('表单数据', searchForm.value)
  if(!validate()) return
  pageNo.value = page
  console.log("你的逻辑")
}
//点击重置
const resetClick = ()=>{
  searchForm.value = {}
  clear()
  console.log("你的逻辑")
  // queryClick()
}
//分页参数
let total = ref(0)
let pageNo = ref(1)
let pageSize = ref(10)
//点击分页器
const pageChange = ()=>{
  queryClick(pageNo.value)
}
 
const addClick = ()=>{
  editDialog.value.open()
 
}
const edit = (row)=>{
  editDialog.value.open('edit',row)
}
const delClick = ()=>{
 
}
//这里是demo演示如果有需要分区域校验的表单需要怎么做
const validateSlave = ()=>{
  validate('slave')
}
//这里是demo演示如果有需要分区域校验的表单需要怎么做
const clearSlave = ()=>{
  clear('slave')
}
 
//diy自定义校验规则
const diyFunction = (childData)=>{
  if(childData.inValue.value.length<3 || childData.inValue.value.length>10){
    childData.updatePass(false, "输入字符长度不能大于10位小于3位")
  }else{
    childData.updatePass(true)
  }
}
const diyVolume = (row,childData)=>{
  if(Number(row.volume)!==Number(row?.length)*Number(row?.width)*Number(row?.height)){
    childData.updatePass(false, "体积与长宽高不匹配")
  }else{
    childData.updatePass(true)
  }
}
</script>

© 版权声明

相关文章

暂无评论

none
暂无评论...