373 lines
11 KiB
Vue
373 lines
11 KiB
Vue
<script lang="tsx">
|
||
import { PropType, defineComponent, ref, computed, unref, watch, onMounted } from 'vue'
|
||
import { ElForm, ElFormItem, ElRow, ElCol, FormItemProp } from 'element-plus'
|
||
import { componentMap } from './helper/componentMap'
|
||
import { propTypes } from '@/utils/propTypes'
|
||
import { getSlot } from '@/utils/tsxHelper'
|
||
import {
|
||
setTextPlaceholder,
|
||
setGridProp,
|
||
setComponentProps,
|
||
setItemComponentSlots,
|
||
initModel
|
||
} from './helper'
|
||
import { useRenderSelect } from './components/useRenderSelect'
|
||
import { useRenderRadio } from './components/useRenderRadio'
|
||
import { useRenderCheckbox } from './components/useRenderCheckbox'
|
||
import { useDesign } from '@/hooks/web/useDesign'
|
||
import { findIndex } from '@/utils'
|
||
import { set } from 'lodash-es'
|
||
import { FormProps } from './types'
|
||
import {
|
||
FormSchema,
|
||
FormSetProps,
|
||
ComponentNameEnum,
|
||
SelectComponentProps,
|
||
RadioGroupComponentProps,
|
||
CheckboxGroupComponentProps
|
||
} from './types'
|
||
|
||
const { renderSelectOptions } = useRenderSelect()
|
||
const { renderRadioOptions } = useRenderRadio()
|
||
const { renderCheckboxOptions } = useRenderCheckbox()
|
||
|
||
const { getPrefixCls } = useDesign()
|
||
|
||
const prefixCls = getPrefixCls('form')
|
||
|
||
export default defineComponent({
|
||
name: 'Form',
|
||
props: {
|
||
// 生成Form的布局结构数组
|
||
schema: {
|
||
type: Array as PropType<FormSchema[]>,
|
||
default: () => []
|
||
},
|
||
// 是否需要栅格布局
|
||
isCol: propTypes.bool.def(true),
|
||
// 表单数据对象
|
||
model: {
|
||
type: Object as PropType<Recordable>,
|
||
default: () => ({})
|
||
},
|
||
// 是否自动设置placeholder
|
||
autoSetPlaceholder: propTypes.bool.def(true),
|
||
// 是否自定义内容
|
||
isCustom: propTypes.bool.def(false),
|
||
// 表单label宽度
|
||
labelWidth: propTypes.oneOfType([String, Number]).def('auto')
|
||
},
|
||
emits: ['register', 'validate'],
|
||
setup(props, { slots, expose, emit }) {
|
||
// element form 实例
|
||
const elFormRef = ref<ComponentRef<typeof ElForm>>()
|
||
|
||
// useForm传入的props
|
||
const outsideProps = ref<FormProps>({})
|
||
|
||
const mergeProps = ref<FormProps>({})
|
||
|
||
const getProps = computed(() => {
|
||
const propsObj = { ...props }
|
||
Object.assign(propsObj, unref(mergeProps))
|
||
return propsObj
|
||
})
|
||
|
||
// 存储表单实例
|
||
const formComponents = ref({})
|
||
|
||
// 存储form-item实例
|
||
const formItemComponents = ref({})
|
||
|
||
// 表单数据
|
||
const formModel = ref<Recordable>({})
|
||
|
||
onMounted(() => {
|
||
emit('register', unref(elFormRef)?.$parent, unref(elFormRef))
|
||
})
|
||
|
||
// 对表单赋值
|
||
const setValues = (data: Recordable = {}) => {
|
||
formModel.value = Object.assign(unref(formModel), data)
|
||
}
|
||
|
||
const setProps = (props: FormProps = {}) => {
|
||
mergeProps.value = Object.assign(unref(mergeProps), props)
|
||
outsideProps.value = props
|
||
}
|
||
|
||
const delSchema = (field: string) => {
|
||
const { schema } = unref(getProps)
|
||
|
||
const index = findIndex(schema, (v: FormSchema) => v.field === field)
|
||
if (index > -1) {
|
||
schema.splice(index, 1)
|
||
}
|
||
}
|
||
|
||
const addSchema = (formSchema: FormSchema, index?: number) => {
|
||
const { schema } = unref(getProps)
|
||
if (index !== void 0) {
|
||
schema.splice(index, 0, formSchema)
|
||
return
|
||
}
|
||
schema.push(formSchema)
|
||
}
|
||
|
||
const setSchema = (schemaProps: FormSetProps[]) => {
|
||
const { schema } = unref(getProps)
|
||
for (const v of schema) {
|
||
for (const item of schemaProps) {
|
||
if (v.field === item.field) {
|
||
set(v, item.path, item.value)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const getOptions = async (fn: Function, item: FormSchema) => {
|
||
const options = await fn()
|
||
setSchema([
|
||
{
|
||
field: item.field,
|
||
path:
|
||
item.component === ComponentNameEnum.TREE_SELECT
|
||
? 'componentProps.data'
|
||
: 'componentProps.options',
|
||
value: options
|
||
}
|
||
])
|
||
}
|
||
|
||
/**
|
||
* @description: 获取表单组件实例
|
||
* @param filed 表单字段
|
||
*/
|
||
const getComponentExpose = (filed: string) => {
|
||
return unref(formComponents)[filed]
|
||
}
|
||
|
||
/**
|
||
* @description: 获取formItem实例
|
||
* @param filed 表单字段
|
||
*/
|
||
const getFormItemExpose = (filed: string) => {
|
||
return unref(formItemComponents)[filed]
|
||
}
|
||
|
||
const setComponentRefMap = (ref: any, filed: string) => {
|
||
formComponents.value[filed] = ref
|
||
}
|
||
|
||
const setFormItemRefMap = (ref: any, filed: string) => {
|
||
formItemComponents.value[filed] = ref
|
||
}
|
||
|
||
expose({
|
||
setValues,
|
||
formModel,
|
||
setProps,
|
||
delSchema,
|
||
addSchema,
|
||
setSchema,
|
||
getComponentExpose,
|
||
getFormItemExpose
|
||
})
|
||
|
||
// 监听表单结构化数组,重新生成formModel
|
||
watch(
|
||
() => unref(getProps).schema,
|
||
(schema = []) => {
|
||
formModel.value = initModel(schema, unref(formModel))
|
||
},
|
||
{
|
||
immediate: true,
|
||
deep: true
|
||
}
|
||
)
|
||
|
||
// 渲染包裹标签,是否使用栅格布局
|
||
const renderWrap = () => {
|
||
const { isCol } = unref(getProps)
|
||
const content = isCol ? (
|
||
<ElRow gutter={20}>{renderFormItemWrap()}</ElRow>
|
||
) : (
|
||
renderFormItemWrap()
|
||
)
|
||
return content
|
||
}
|
||
|
||
// 是否要渲染el-col
|
||
const renderFormItemWrap = () => {
|
||
// hidden属性表示隐藏,不做渲染
|
||
const { schema = [], isCol } = unref(getProps)
|
||
|
||
return schema
|
||
.filter((v) => !v.remove)
|
||
.map((item) => {
|
||
// 如果是 Divider 组件,需要自己占用一行
|
||
const isDivider = item.component === 'Divider'
|
||
const Com = componentMap['Divider'] as ReturnType<typeof defineComponent>
|
||
return isDivider ? (
|
||
<Com {...{ contentPosition: 'left', ...item.componentProps }}>{item?.label}</Com>
|
||
) : isCol ? (
|
||
// 如果需要栅格,需要包裹 ElCol
|
||
<ElCol {...setGridProp(item.colProps)}>{renderFormItem(item)}</ElCol>
|
||
) : (
|
||
renderFormItem(item)
|
||
)
|
||
})
|
||
}
|
||
|
||
// 渲染formItem
|
||
const renderFormItem = (item: FormSchema) => {
|
||
// 如果有optionApi,优先使用optionApi
|
||
if (item.optionApi) {
|
||
// 内部自动调用接口,不影响其他渲染
|
||
getOptions(item.optionApi, item)
|
||
}
|
||
const formItemSlots: Recordable = {
|
||
default: () => {
|
||
if (item?.formItemProps?.slots?.default) {
|
||
return item?.formItemProps?.slots?.default(formModel.value)
|
||
} else {
|
||
const Com = componentMap[item.component as string] as ReturnType<typeof defineComponent>
|
||
|
||
const { autoSetPlaceholder } = unref(getProps)
|
||
|
||
const componentSlots = (item?.componentProps as any)?.slots || {}
|
||
const slotsMap: Recordable = {
|
||
...setItemComponentSlots(componentSlots)
|
||
}
|
||
// // 如果是select组件,并且没有自定义模板,自动渲染options
|
||
if (item.component === ComponentNameEnum.SELECT) {
|
||
slotsMap.default = !componentSlots.default
|
||
? () => renderSelectOptions(item)
|
||
: () => {
|
||
return componentSlots.default(
|
||
unref((item?.componentProps as SelectComponentProps)?.options)
|
||
)
|
||
}
|
||
}
|
||
|
||
// 虚拟列表
|
||
if (item.component === ComponentNameEnum.SELECT_V2 && componentSlots.default) {
|
||
slotsMap.default = ({ item }) => {
|
||
return componentSlots.default(item)
|
||
}
|
||
}
|
||
|
||
// 单选框组和按钮样式
|
||
if (
|
||
item.component === ComponentNameEnum.RADIO_GROUP ||
|
||
item.component === ComponentNameEnum.RADIO_BUTTON
|
||
) {
|
||
slotsMap.default = !componentSlots.default
|
||
? () => renderRadioOptions(item)
|
||
: () => {
|
||
return componentSlots.default(
|
||
unref((item?.componentProps as CheckboxGroupComponentProps)?.options)
|
||
)
|
||
}
|
||
}
|
||
|
||
// 多选框组和按钮样式
|
||
if (
|
||
item.component === ComponentNameEnum.CHECKBOX_GROUP ||
|
||
item.component === ComponentNameEnum.CHECKBOX_BUTTON
|
||
) {
|
||
slotsMap.default = !componentSlots.default
|
||
? () => renderCheckboxOptions(item)
|
||
: () => {
|
||
return componentSlots.default(
|
||
unref((item?.componentProps as RadioGroupComponentProps)?.options)
|
||
)
|
||
}
|
||
}
|
||
|
||
const Comp = () => {
|
||
return (
|
||
<Com
|
||
vModel={formModel.value[item.field]}
|
||
ref={(el: any) => setComponentRefMap(el, item.field)}
|
||
{...(autoSetPlaceholder && setTextPlaceholder(item))}
|
||
{...setComponentProps(item)}
|
||
style={item.componentProps?.style || {}}
|
||
>
|
||
{{ ...slotsMap }}
|
||
</Com>
|
||
)
|
||
}
|
||
|
||
return <>{Comp()}</>
|
||
}
|
||
}
|
||
}
|
||
if (item?.formItemProps?.slots?.label) {
|
||
formItemSlots.label = (...args: any[]) => {
|
||
return (item?.formItemProps?.slots as any)?.label(...args)
|
||
}
|
||
}
|
||
if (item?.formItemProps?.slots?.error) {
|
||
formItemSlots.error = (...args: any[]) => {
|
||
return (item?.formItemProps?.slots as any)?.error(...args)
|
||
}
|
||
}
|
||
return (
|
||
<ElFormItem
|
||
v-show={!item.hidden}
|
||
ref={(el: any) => setFormItemRefMap(el, item.field)}
|
||
{...(item.formItemProps || {})}
|
||
prop={item.field}
|
||
label={item.label || ''}
|
||
>
|
||
{formItemSlots}
|
||
</ElFormItem>
|
||
)
|
||
}
|
||
|
||
// 过滤传入Form组件的属性
|
||
const getFormBindValue = () => {
|
||
// 避免在标签上出现多余的属性
|
||
const delKeys = ['schema', 'isCol', 'autoSetPlaceholder', 'isCustom', 'model']
|
||
const props = { ...unref(getProps) }
|
||
for (const key in props) {
|
||
if (delKeys.indexOf(key) !== -1) {
|
||
delete props[key]
|
||
}
|
||
}
|
||
return props
|
||
}
|
||
|
||
const onValidate = (prop: FormItemProp, isValid: boolean, message: string) => {
|
||
emit('validate', prop, isValid, message)
|
||
}
|
||
|
||
return () => (
|
||
<ElForm
|
||
ref={elFormRef}
|
||
{...getFormBindValue()}
|
||
model={unref(getProps).isCustom ? unref(getProps).model : formModel}
|
||
class={prefixCls}
|
||
onValidate={onValidate}
|
||
>
|
||
{{
|
||
// 如果需要自定义,就什么都不渲染,而是提供默认插槽
|
||
default: () => {
|
||
const { isCustom } = unref(getProps)
|
||
return isCustom ? getSlot(slots, 'default') : renderWrap()
|
||
}
|
||
}}
|
||
</ElForm>
|
||
)
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.@{elNamespace}-form.@{namespace}-form .@{elNamespace}-row {
|
||
margin-right: 0 !important;
|
||
margin-left: 0 !important;
|
||
}
|
||
</style>
|