效果图:
重点:
1、父级rc-form-item 上不要加 prop,要分别加在内部的各个表单元素上
2、自定组件内部表单元素上的prop写法要用这种拼接的方式
:prop="props.attValName + '.selectVal'"
:prop="props.attValName + '.' + index + '.value'"
而不是下面这种,下面的写法无法触发校验
:prop="props.attValName[ index ].value'"
3、自定义组件写prop的那一层要写rules
4、要达到父级form能控制自定义组件内部的各个表单元素的校验和数据回显、重置等能力,
自定义组件的state定义的数据格式尤为重要,以地址级联组件AttrAddress为例:
emit('update:modelValue', state.attVals)
即props.modelValue 所包含的数据,要被state.attVals全部包含,这样就可以由父级的form完全托管校验重置等能力。
源码:
index.vue
<template>
<!-- 自定义组件 使用样例 -->
<div>
<rc-form ref="formRef" :model="form" :rules="rules" label-width="146px">
<rc-form-item label-width="0">
<!-- 注意这里的 rc-form-item 不要加prop 做规则校验,通过required控制是否必填,内部校验-->
<AttrSelectAlias
v-model="form.attrSelectAlias"
:att-id="238290"
att-name="属性值可别名"
att-val-name="attrSelectAlias"
:required="true"
extra=""
/>
</rc-form-item>
<rc-form-item label-width="0">
<!-- 注意这里的 rc-form-item 不要加prop 做规则校验,通过required控制是否必填,内部校验-->
<AttrSelectPercent
v-model="form.attrSelectPercent"
:att-id="217746"
att-name="复选百分比"
att-val-name="attrSelectPercent"
:required="true"
extra=""
label-width="146px"
/>
</rc-form-item>
<rc-form-item label-width="0">
<!-- 注意这里的 rc-form-item 不要加prop 做规则校验,通过required控制是否必填,内部校验-->
<AttrCombinedInputs
v-model="form.combinedInputs2"
extra=""
:att-id="238296"
att-name="组合输入框有前缀"
att-val-name="combinedInputs2"
:required="true"
:prefix-list="prefixList2"
:unit-list="unitList1"
/>
</rc-form-item>
<rc-form-item label-width="0">
<!-- 注意这里的 rc-form-item 不要加prop 做规则校验,通过required控制是否必填,内部校验-->
<AttrCascader
v-model="form.cascader"
:att-id="238291"
att-name="级联选择"
att-val-name="cascader"
:required="true"
label-width="146px"
/>
</rc-form-item>
<rc-form-item label-width="0">
<!-- 注意这里的 rc-form-item 不要加prop 做规则校验,通过required控制是否必填,内部校验-->
<AttrAddress
v-model="form.attrAddress2"
:att-id="238841"
att-name="地址级联选择4级"
att-val-name="attrAddress2"
:level="4"
:required="true"
label-width="146px"
/>
</rc-form-item>
<rc-form-item>
<rc-button type="primary" size="large" @click="submitForm(formRef)"> 提交 </rc-button>
<rc-button plain size="large" @click="resetForm(formRef)"> 重置 </rc-button>
</rc-form-item>
</rc-form>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'ant-design-vue'
import AttrSelectAlias from './components/AttrSelectAlias/index.vue'
import AttrSelectPercent from './components/AttrSelectPercent/index.vue'
import AttrCombinedInputs, {
prefixList1,
prefixList2,
unitList1,
} from './components/AttrCombinedInputs/index.vue'
import AttrCascader from './components/AttrCascader/index.vue'
import AttrAddress from './components/AttrAddress/index.vue'
const formRef = ref<FormInstance>()
const form = reactive({
attrSelectAlias: { selectVal: '1093079', alias: '我是别名' },
// attrSelectAlias: undefined,
attrSelectPercent: [], // 复选百分比
// combinedInputs2:undefined,
combinedInputs2: [
{ prefix: '长', value: '100', valueUnit: 'cm' },
{ prefix: '宽', value: '50', valueUnit: 'ml' },
{ prefix: '高', value: '10', valueUnit: 'cm' },
],
// cascader: undefined, // 级联选择
cascader: [1093079, 1093081], // 级联选择
attrAddress2: { address: ['1', '2901', '55549'], isOverSea: false },
})
const rules = reactive<FormRules>({})
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
console.log('submit!===', form)
} else {
console.log('error submit!-fields', fields)
}
})
}
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
}
</script>
<style lang="scss" scoped></style>
./components/AttrSelectAlias/index.vue
<template>
<!-- 属性值 可别名:下拉选择框 + 输入框 -->
<rc-form-item
:label="props.attName"
:prop="props.attValName + '.selectVal'"
:rules="{
required: props.required,
message: '请选择',
trigger: 'change',
}"
>
<rc-select
v-model="state.attVal.selectVal"
placeholder="请选择"
:multiple="props.multiple"
clearable
style="width: 320px"
@change="onSelectChange"
>
<rc-option
v-for="item in state.options"
:key="item.id"
:label="item.name"
:value="item.id.toString()"
/>
</rc-select>
</rc-form-item>
<rc-form-item
style="margin-left: 8px"
:prop="props.attValName + '.alias'"
:rules="{
required: props.required,
message: '请输入别名',
trigger: 'blur',
}"
>
<rc-input
v-model="state.attVal.alias"
placeholder="请输入别名"
style="width: 320px"
@input="onInputChange"
/>
</rc-form-item>
<div v-if="extra" class="extra"> 备注&示例:{{ extra }} </div>
</template>
<script lang="ts">
interface AttValue {
selectVal: string
alias: string
}
interface AttValueItem {
id: number
name: string
}
</script>
<script setup lang="ts">
import { onMounted, reactive } from 'vue'
import mockData from '../../mock.json'
const props = defineProps({
modelValue: {
type: Object,
default: undefined,
},
attId: {
type: Number,
default: undefined,
},
attName: {
type: String,
default: undefined,
},
attValName: {
type: String,
default: undefined,
},
required: {
type: Boolean,
default: false,
},
multiple: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
extra: {
type: String,
default: '',
},
labelWidth: {
type: String,
default: '',
},
})
const emit = defineEmits(['update:modelValue', 'change', 'blur'])
const state = reactive<{
attVal: AttValue
options: AttValueItem[]
}>({
attVal: { selectVal: '', alias: '' },
options: [],
})
onMounted(() => {
getData()
})
const getData = () => {
if (!props.attId) return
// 接口获取选择框下拉选项
setTimeout(() => {
state.options = mockData
initAttVal()
}, 300)
}
const initAttVal = () => {
// console.log('props.modelValue===', props.modelValue)
if (!props.modelValue || props.modelValue?.length == 0) {
// 初始没有值
state.attVal = { selectVal: '', alias: '' }
} else {
// 回显
state.attVal = props.modelValue as AttValue
}
emit('update:modelValue', state.attVal)
emit('change')
}
const onInputChange = () => {
// console.log('onInputChange===', state.alias)
}
const onSelectChange = () => {
// console.log('onSelectChange===', state.selectVal)
}
</script>
<style lang="scss" scoped>
.extra {
margin-left: 8px;
color: var(--rcd-color-text-200);
font-size: 12px;
}
</style>
./components/AttrSelectPercent/index.vue
<template>
<!-- 复选百分比:选择框+输入框+% -->
<rc-form-item
v-for="(attVal, index) in state.attVals"
:key="index"
:style="{ marginBottom: index == state.attVals.length - 1 ? '0' : '20px' }"
>
<rc-form-item
:label="index == 0 ? props.attName : ''"
:prop="props.attValName + '.' + index + '.value'"
:rules="{
required: props.required,
message: '请选择',
trigger: 'change',
}"
:label-width="props.labelWidth"
>
<rc-select
v-model="state.attVals[index].value"
placeholder="请选择"
:style="{
width: '220px',
}"
@change="onSelectChange"
>
<rc-option
v-for="item in state.options"
:key="item.id"
:label="item.name"
:value="item.id.toString()"
/>
</rc-select>
</rc-form-item>
<rc-form-item
:prop="props.attValName + '.' + index + '.percentage'"
:rules="{
required: props.required,
message: '请输入',
trigger: 'blur',
}"
>
<rc-input
v-model="state.attVals[index].percentage"
placeholder="请输入"
class="input"
:maxlength="3"
@change="onInputChange"
>
<template #append> % </template>
</rc-input>
<rc-button style="margin-left: 8px" @click.prevent="removeDomain(attVal)"> 删除 </rc-button>
</rc-form-item>
</rc-form-item>
<rc-button style="margin-left: 8px" @click="addDomain"> 新增 </rc-button>
<div v-if="extra" class="extra"> 备注&示例:{{ extra }} </div>
</template>
<script lang="ts">
interface AttValItem {
value: string
percentage: string
valUnit: string
}
interface AttValueItem {
id: number
name: string
}
</script>
<script setup lang="ts">
import { reactive, onMounted } from 'vue'
import mockData from '../../mock.json'
const props = defineProps({
modelValue: {
type: Array,
default: undefined,
},
attId: {
type: Number,
default: undefined,
},
attName: {
type: String,
default: undefined,
},
attValName: {
type: String,
default: undefined,
},
required: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
extra: {
type: String,
default: '',
},
labelWidth: {
type: String,
default: '',
}
})
const emit = defineEmits(['update:modelValue', 'change'])
const state = reactive<{
attVals: AttValItem[]
options: AttValueItem[]
}>({
attVals: [],
options: [],
})
onMounted(() => {
getData()
})
const removeDomain = (item: AttValItem) => {
const index = state.attVals.indexOf(item)
if (index !== -1) {
state.attVals.splice(index, 1)
}
}
const addDomain = () => {
state.attVals.push({ value: '', percentage: '', valUnit: '%' })
}
const getData = () => {
if (!props.attId) return
// 接口获取选择框下拉选项
setTimeout(()=>{
state.options = mockData
initAttVal()
emit('update:modelValue', state.attVals)
emit('change')
},300)
}
const initAttVal = () => {
// console.log('props.modelValue===', props.modelValue)
if (!props.modelValue || props.modelValue?.length == 0) {
// 初始没有值
addDomain()
} else {
// 回显
state.attVals = props.modelValue as AttValItem[]
}
}
const onInputChange = () => {
// console.log('onInputChange===', state.attVal)
}
const onSelectChange = () => {
// console.log('onSelectChange===', state.attVal)
}
</script>
<style lang="scss" scoped>
.extra {
margin-left: 8px;
color: var(--rcd-color-text-200);
font-size: 12px;
}
.input {
width: 104px;
margin-left: -4px;
:deep(.rcd-input__wrapper) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
</style>
./components/AttrCombinedInputs/index.vue
<template>
<!-- 组合类型:前缀(+/-/''/长/宽/高) + 输入框 + 单位下拉选择框 -->
<rc-form-item v-for="(prefix, index) in state.prefixList" :key="index">
<rc-form-item
:label="index == 0 ? props.attName : ''"
:prop="props.attValName + '.' + index + '.value'"
:rules="{
required: props.required,
message: '请输入',
trigger: 'blur',
}"
>
<rc-input
v-model="state.attVal[index].value"
placeholder="请输入"
:style="{ width: state.prefixList.length == 1 ? '320px' : '200px' }"
@change="onInputChange"
>
<template v-if="prefix" #prepend>
<div style="width: 14px">
{{ prefix }}
</div>
</template>
<template #append>
<rc-select style="width: 71px" />
</template>
</rc-input>
</rc-form-item>
<rc-form-item :prop="props.attValName + '.' + index + '.valueUnit'">
<rc-select
v-model="state.attVal[index].valueUnit"
class="select"
:style="{ marginRight: index == state.prefixList.length - 1 ? 0 : '24px' }"
@change="onUnitChange"
>
<rc-option v-for="(unit, idx) in state.unitList" :key="idx" :label="unit" :value="unit" />
</rc-select>
</rc-form-item>
</rc-form-item>
<div v-if="extra" class="extra"> 备注&示例:{{ extra }} </div>
</template>
<script lang="ts">
interface AttValItem {
prefix: string
value: string
valueUnit: string
}
// 自测数据
export const prefixList2 = ['长', '宽', '高'] // 有前缀
export const prefixList1 = [''] // 没有前缀
export const unitList1 = ['mm', 'cm', 'm']
</script>
<script setup lang="ts">
import { reactive, onMounted } from 'vue'
const props = defineProps({
modelValue: {
type: Array,
default: undefined,
},
attId: {
type: Number,
default: undefined,
},
attName: {
type: String,
default: undefined,
},
attValName: {
type: String,
default: undefined,
},
required: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
prefixList: {
type: Array,
default() {
return []
},
},
unitList: {
type: Array,
default() {
return []
},
},
extra: {
type: String,
default: '',
},
})
const emit = defineEmits(['update:modelValue', 'change'])
const state = reactive({
attVal: [] as AttValItem[],
prefixList: [] as string[],
unitList: [] as string[],
})
onMounted(() => {
getData()
})
const getData = () => {
setTimeout(() => {
state.prefixList = props.prefixList as string[]
state.unitList = props.unitList as string[]
initAttVal()
emit('update:modelValue', state.attVal)
emit('change')
}, 1000)
}
const initAttVal = () => {
// console.log('props.modelValue===', props.modelValue)
if (!props.modelValue || props.modelValue?.length == 0) {
// 初始没有值
state.attVal = state.prefixList.map((prefix) => {
return { prefix, value: '', valueUnit: state.unitList[0] }
})
} else {
// 回显
state.attVal = state.prefixList.map((prefix) => {
const temp = props.modelValue?.find((item: any) => item.prefix == prefix) as AttValItem
return { prefix, value: temp?.value, valueUnit: temp?.valueUnit }
})
}
}
const onInputChange = () => {
// console.log('onInputChange===', state.attVal)
}
const onUnitChange = () => {
// console.log('onUnitChange===', state.attVal)
}
</script>
<style lang="scss" scoped>
.extra {
margin-left: 8px;
color: var(--rcd-color-text-200);
font-size: 12px;
}
.select {
width: 72px;
margin-left: -72px;
z-index: 2;
:deep(.rcd-input__wrapper) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
</style>
./components/AttrCascader/index.vue
<template>
<!-- 针对动态获取数据,且级联层级不确定的情况,有的选项层级多,有的选项层级少 -->
<rc-form-item
:label="props.attName"
:prop="props.attValName + '.' + 0"
:rules="{
required: props.required,
message: '请选择',
trigger: 'change',
}"
:label-width="props.labelWidth"
>
<rc-select
v-model="state.attVals[0]"
placeholder="请选择"
:disabled="props.disabled"
style="width: 320px; margin-right: 8px"
@change="onChange1"
>
<rc-option
v-for="item in option.options1"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</rc-select>
</rc-form-item>
<rc-form-item
v-if="option.options2.length > 0"
:prop="props.attValName + '.' + 1"
:rules="{
required: props.required,
message: '请选择',
trigger: 'change',
}"
>
<rc-select
v-model="state.attVals[1]"
placeholder="请选择"
:disabled="props.disabled"
style="width: 320px"
@change="onChange2"
>
<rc-option
v-for="item in option.options2"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</rc-select>
</rc-form-item>
<div v-if="extra" class="extra"> 备注&示例:{{ extra }} </div>
</template>
<script lang="ts">
interface AttValueByLevelItem {
id: number
name: string
}
</script>
<script setup lang="ts">
import { onMounted, reactive } from 'vue'
import mockData from '../../mock.json'
const props = defineProps({
modelValue: {
type: Array,
default: undefined,
},
attId: {
type: Number,
default: undefined,
},
attName: {
type: String,
default: undefined,
},
attValName: {
type: String,
default: undefined,
},
required: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
extra: {
type: String,
default: '',
},
labelWidth: {
type: String,
default: '',
},
})
const emit = defineEmits(['update:modelValue', 'change'])
const state = reactive({
attVals: [] as number[],
})
const option = reactive<{ [key: string]: AttValueByLevelItem[] }>({
options1: [],
options2: [],
})
const getData = (level: number, parentId?: number, cb?: (hasNext: boolean) => void) => {
// const params = {
// attId: props.attId,
// attLevel: level,
// parentId,
// }
// getAttValueByLevel(params).then((res) => {
// option['options' + level] = res || []
// cb && cb(res ? true : false)
// })
setTimeout(()=>{
const res = mockData
option['options' + level] = res || []
cb && cb(res ? true : false)
},300)
}
const onChange1 = (val: number) => {
state.attVals.splice(1, 1) // 删掉第二个
option.options2 = []
getData(2, val)
}
const onChange2 = (val: number) => {
//
}
const initAttVal = () => {
// console.log('props.modelValue===', props.modelValue)
if (!props.modelValue || props.modelValue?.length == 0) {
// 初始没有值
} else {
// 回显
state.attVals = props.modelValue as number[]
if (props.modelValue.length > 1) {
getData(2, state.attVals[0])
}
}
}
onMounted(() => {
getData(1, undefined, () => {
initAttVal()
emit('update:modelValue', state.attVals)
emit('change')
})
})
</script>
<style lang="scss" scoped>
.extra {
margin-left: 8px;
color: var(--rcd-color-text-200);
font-size: 12px;
}
</style>
./components/AttrAddress/index.vue
<template>
<!-- 地址级联选择,动态获取下一级,且层级数不定,可能3级可能4级 -->
<rc-form-item
style="width: 100%"
:label="props.attName"
:label-width="props.labelWidth"
:prop="props.attValName + '.isOverSea'"
:rules="{
required: props.required,
message: '请选择',
trigger: 'change',
}"
>
<rc-radio-group v-model="state.attVals.isOverSea" @change="onIsOverSeaChange">
<rc-radio :label="false"> 国内 </rc-radio>
<rc-radio :label="true"> 海外 </rc-radio>
</rc-radio-group>
</rc-form-item>
<rc-form-item
:label-width="props.labelWidth"
:prop="props.attValName + '.address.' + 0"
:rules="{
required: props.required,
message: '请选择',
trigger: 'change',
}"
>
<rc-select
v-model="state.attVals.address[0]"
placeholder="请选择"
style="width: 200px; margin-right: 8px"
@change="(v: any) => onChange(v, 0 + 1)"
>
<rc-option
v-for="item in option['options' + (0 + 1)]"
:key="item.districtId"
:label="item.districtName"
:value="item.districtId.toString()"
/>
</rc-select>
</rc-form-item>
<rc-form-item
v-if="option.options2.length > 0"
:prop="props.attValName + '.address.' + 1"
:rules="{
required: props.required,
message: '请选择',
trigger: 'change',
}"
>
<rc-select
v-model="state.attVals.address[1]"
placeholder="请选择"
style="width: 200px; margin-right: 8px"
@change="(v: any) => onChange(v, 1 + 1)"
>
<rc-option
v-for="item in option['options' + (1 + 1)]"
:key="item.districtId"
:label="item.districtName"
:value="item.districtId.toString()"
/>
</rc-select>
</rc-form-item>
<rc-form-item
v-if="option.options3.length > 0"
:prop="props.attValName + '.address.' + 2"
:rules="{
required: props.required,
message: '请选择',
trigger: 'change',
}"
>
<rc-select
v-model="state.attVals.address[2]"
placeholder="请选择"
style="width: 200px; margin-right: 8px"
@change="(v: any) => onChange(v, 2 + 1)"
>
<rc-option
v-for="item in option['options' + (2 + 1)]"
:key="item.districtId"
:label="item.districtName"
:value="item.districtId.toString()"
/>
</rc-select>
</rc-form-item>
<rc-form-item
v-if="option.options4.length > 0"
:prop="props.attValName + '.address.' + 3"
:rules="{
required: props.required,
message: '请选择',
trigger: 'change',
}"
>
<rc-select
v-model="state.attVals.address[3]"
placeholder="请选择"
style="width: 200px"
@change="(v: any) => onChange(v, 3 + 1)"
>
<rc-option
v-for="item in option['options' + (3 + 1)]"
:key="item.districtId"
:label="item.districtName"
:value="item.districtId.toString()"
/>
</rc-select>
</rc-form-item>
<div v-if="extra" class="extra"> 备注&示例:{{ extra }} </div>
</template>
<script setup lang="ts">
import { onMounted, reactive } from 'vue'
import { getAddressByParentId } from '@/services/productIntroduction'
const props = defineProps({
modelValue: {
type: Object,
default: undefined,
},
attId: {
type: Number,
default: undefined,
},
// 可选层级
level: {
type: Number,
default: 1,
},
attName: {
type: String,
default: undefined,
},
attValName: {
type: String,
default: undefined,
},
required: {
type: Boolean,
default: false,
},
multiple: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
extra: {
type: String,
default: '',
},
labelWidth: {
type: String,
default: '',
},
})
const emit = defineEmits(['update:modelValue', 'change'])
interface AttValue {
address: string[]
isOverSea: boolean
}
const state = reactive<{ attVals: AttValue }>({
attVals: {
address: [],
isOverSea: false,
},
})
interface AddressItem {
districtId: number
districtName: string
districtFatherId: number
}
const option = reactive<{ [key: string]: AddressItem[] }>({
options1: [],
options2: [],
options3: [],
options4: [],
})
const getData = (parentId: string, level: number, cb?: (hasNext: boolean) => void) => {
getAddressByParentId({ parentId }).then((res) => {
option['options' + level] = res.output?.areas || []
cb && cb(res.output?.areas ? true : false)
})
}
const onChange = (val: string, level: number) => {
switch (level) {
case 1:
onChange1(val)
break
case 2:
onChange2(val)
break
case 3:
onChange3(val)
break
case 4:
onChange4(val)
break
}
}
const onChange1 = (val: string) => {
if (props.level <= 1) {
//
} else {
// 截取数组,指定长度
const len = state.attVals.address.length
state.attVals.address.splice(1, len - 1)
option.options2 = []
option.options3 = []
option.options4 = []
getData(val, 2)
}
}
const onChange2 = (val: string) => {
if (props.level <= 2) {
//
} else {
const len = state.attVals.address.length
state.attVals.address.splice(2, len - 2)
option.options3 = []
option.options4 = []
getData(val, 3)
}
}
const onChange3 = (val: string) => {
if (props.level <= 3) {
//
} else {
const len = state.attVals.address.length
state.attVals.address.splice(3, len - 3)
option.options4 = []
getData(val, 4)
}
}
const onChange4 = (val: string) => {
//
}
const onIsOverSeaChange = (isOverSea: number) => {
// 重置地址选择框
const len = state.attVals.address.length
state.attVals.address.splice(0, len)
option.options1 = []
option.options2 = []
option.options3 = []
option.options4 = []
const parentId = isOverSea ? '53283' : '4744'
getData(parentId, 1) // 获取第一级选项
}
const initAttVal = () => {
// console.log('props.modelValue===', props.modelValue)
if (!props.modelValue) {
// 初始没有值
} else {
// 回显
state.attVals = props.modelValue as AttValue
if (state.attVals.address.length > 1) {
getData(state.attVals.address[0], 2)
}
if (state.attVals.address.length > 2) {
getData(state.attVals.address[1], 3)
}
if (state.attVals.address.length > 3) {
getData(state.attVals.address[2], 4)
}
}
}
onMounted(() => {
const parentId = props.modelValue?.isOverSea ? '53283' : '4744'
getData(parentId, 1, () => {
initAttVal()
emit('update:modelValue', state.attVals)
emit('change')
})
})
</script>
<style lang="scss" scoped>
.extra {
margin-left: 8px;
color: var(--rcd-color-text-200);
font-size: 12px;
}
</style>
mock.json
[
{
"id": 1093079,
"name": "测试1"
},
{
"id": 1093080,
"name": "测试2"
},
{
"id": 1093081,
"name": "测试3"
}
]