初始化项目,自 v1.7.1 版本开始

This commit is contained in:
YunaiV
2023-02-11 00:44:00 +08:00
parent 11161afc1a
commit 56f3017baa
548 changed files with 52096 additions and 61 deletions

View File

@@ -0,0 +1,15 @@
import Form from './src/Form.vue'
import { ElForm } from 'element-plus'
import { FormSchema, FormSetPropsType } from '@/types/form'
export interface FormExpose {
setValues: (data: Recordable) => void
setProps: (props: Recordable) => void
delSchema: (field: string) => void
addSchema: (formSchema: FormSchema, index?: number) => void
setSchema: (schemaProps: FormSetPropsType[]) => void
formModel: Recordable
getElFormRef: () => ComponentRef<typeof ElForm>
}
export { Form }

View File

@@ -0,0 +1,302 @@
<script lang="tsx">
import { PropType, defineComponent, ref, computed, unref, watch, onMounted } from 'vue'
import { ElForm, ElFormItem, ElRow, ElCol, ElTooltip } from 'element-plus'
import { componentMap } from './componentMap'
import { propTypes } from '@/utils/propTypes'
import { getSlot } from '@/utils/tsxHelper'
import {
setTextPlaceholder,
setGridProp,
setComponentProps,
setItemComponentSlots,
initModel,
setFormItemSlots
} 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 { Icon } from '@/components/Icon'
import { FormSchema, FormSetPropsType } from '@/types/form'
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'],
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 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: FormSetPropsType[]) => {
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 getElFormRef = (): ComponentRef<typeof ElForm> => {
return unref(elFormRef) as ComponentRef<typeof ElForm>
}
expose({
setValues,
formModel,
setProps,
delSchema,
addSchema,
setSchema,
getElFormRef
})
// 监听表单结构化数组重新生成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.hidden)
.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) => {
// 单独给只有options属性的组件做判断
const notRenderOptions = ['SelectV2', 'Cascader', 'Transfer']
const slotsMap: Recordable = {
...setItemComponentSlots(slots, item?.componentProps?.slots, item.field)
}
if (
item?.component !== 'SelectV2' &&
item?.component !== 'Cascader' &&
item?.componentProps?.options
) {
slotsMap.default = () => renderOptions(item)
}
const formItemSlots: Recordable = setFormItemSlots(slots, item.field)
// 如果有 labelMessage自动使用插槽渲染
if (item?.labelMessage) {
formItemSlots.label = () => {
return (
<>
<span>{item.label}</span>
<ElTooltip placement="right" raw-content>
{{
content: () => <span v-html={item.labelMessage}></span>,
default: () => (
<Icon
icon="ep:warning"
size={16}
color="var(--el-color-primary)"
class="ml-2px relative top-1px"
></Icon>
)
}}
</ElTooltip>
</>
)
}
}
return (
<ElFormItem {...(item.formItemProps || {})} prop={item.field} label={item.label || ''}>
{{
...formItemSlots,
default: () => {
const Com = componentMap[item.component as string] as ReturnType<
typeof defineComponent
>
const { autoSetPlaceholder } = unref(getProps)
return slots[item.field] ? (
getSlot(slots, item.field, formModel.value)
) : (
<Com
vModel={formModel.value[item.field]}
{...(autoSetPlaceholder && setTextPlaceholder(item))}
{...setComponentProps(item)}
style={item.componentProps?.style}
{...(notRenderOptions.includes(item?.component as string) &&
item?.componentProps?.options
? { options: item?.componentProps?.options || [] }
: {})}
>
{{ ...slotsMap }}
</Com>
)
}
}}
</ElFormItem>
)
}
// 渲染options
const renderOptions = (item: FormSchema) => {
switch (item.component) {
case 'Select':
case 'SelectV2':
const { renderSelectOptions } = useRenderSelect(slots)
return renderSelectOptions(item)
case 'Radio':
case 'RadioButton':
const { renderRadioOptions } = useRenderRadio()
return renderRadioOptions(item)
case 'Checkbox':
case 'CheckboxButton':
const { renderCheckboxOptions } = useRenderCheckbox()
return renderCheckboxOptions(item)
default:
break
}
}
// 过滤传入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
}
return () => (
<ElForm
ref={elFormRef}
{...getFormBindValue()}
model={props.isCustom ? props.model : formModel}
class={prefixCls}
>
{{
// 如果需要自定义,就什么都不渲染,而是提供默认插槽
default: () => {
const { isCustom } = unref(getProps)
return isCustom ? getSlot(slots, 'default') : renderWrap()
}
}}
</ElForm>
)
}
})
</script>
<style lang="scss" scoped>
.#{$elNamespace}-form.#{$namespace}-form .#{$elNamespace}-row {
margin-right: 0 !important;
margin-left: 0 !important;
}
</style>

View File

@@ -0,0 +1,55 @@
import type { Component } from 'vue'
import {
ElCascader,
ElCheckboxGroup,
ElColorPicker,
ElDatePicker,
ElInput,
ElInputNumber,
ElRadioGroup,
ElRate,
ElSelect,
ElSelectV2,
ElTreeSelect,
ElSlider,
ElSwitch,
ElTimePicker,
ElTimeSelect,
ElTransfer,
ElAutocomplete,
ElDivider
} from 'element-plus'
import { InputPassword } from '@/components/InputPassword'
import { Editor } from '@/components/Editor'
import { UploadImg, UploadImgs, UploadFile } from '@/components/UploadFile'
import { ComponentName } from '@/types/components'
const componentMap: Recordable<Component, ComponentName> = {
Radio: ElRadioGroup,
Checkbox: ElCheckboxGroup,
CheckboxButton: ElCheckboxGroup,
Input: ElInput,
Autocomplete: ElAutocomplete,
InputNumber: ElInputNumber,
Select: ElSelect,
Cascader: ElCascader,
Switch: ElSwitch,
Slider: ElSlider,
TimePicker: ElTimePicker,
DatePicker: ElDatePicker,
Rate: ElRate,
ColorPicker: ElColorPicker,
Transfer: ElTransfer,
Divider: ElDivider,
TimeSelect: ElTimeSelect,
SelectV2: ElSelectV2,
TreeSelect: ElTreeSelect,
RadioButton: ElRadioGroup,
InputPassword: InputPassword,
Editor: Editor,
UploadImg: UploadImg,
UploadImgs: UploadImgs,
UploadFile: UploadFile
}
export { componentMap }

View File

@@ -0,0 +1,26 @@
import { FormSchema } from '@/types/form'
import { ElCheckbox, ElCheckboxButton } from 'element-plus'
import { defineComponent } from 'vue'
export const useRenderCheckbox = () => {
const renderCheckboxOptions = (item: FormSchema) => {
// 如果有别名,就取别名
const labelAlias = item?.componentProps?.optionsAlias?.labelField
const valueAlias = item?.componentProps?.optionsAlias?.valueField
const Com = (item.component === 'Checkbox' ? ElCheckbox : ElCheckboxButton) as ReturnType<
typeof defineComponent
>
return item?.componentProps?.options?.map((option) => {
const { ...other } = option
return (
<Com {...other} label={option[valueAlias || 'value']}>
{option[labelAlias || 'label']}
</Com>
)
})
}
return {
renderCheckboxOptions
}
}

View File

@@ -0,0 +1,26 @@
import { FormSchema } from '@/types/form'
import { ElRadio, ElRadioButton } from 'element-plus'
import { defineComponent } from 'vue'
export const useRenderRadio = () => {
const renderRadioOptions = (item: FormSchema) => {
// 如果有别名,就取别名
const labelAlias = item?.componentProps?.optionsAlias?.labelField
const valueAlias = item?.componentProps?.optionsAlias?.valueField
const Com = (item.component === 'Radio' ? ElRadio : ElRadioButton) as ReturnType<
typeof defineComponent
>
return item?.componentProps?.options?.map((option) => {
const { ...other } = option
return (
<Com {...other} label={option[valueAlias || 'value']}>
{option[labelAlias || 'label']}
</Com>
)
})
}
return {
renderRadioOptions
}
}

View File

@@ -0,0 +1,57 @@
import { FormSchema } from '@/types/form'
import { ComponentOptions } from '@/types/components'
import { ElOption, ElOptionGroup } from 'element-plus'
import { getSlot } from '@/utils/tsxHelper'
import { Slots } from 'vue'
export const useRenderSelect = (slots: Slots) => {
// 渲染 select options
const renderSelectOptions = (item: FormSchema) => {
// 如果有别名,就取别名
const labelAlias = item?.componentProps?.optionsAlias?.labelField
return item?.componentProps?.options?.map((option) => {
if (option?.options?.length) {
return (
<ElOptionGroup label={option[labelAlias || 'label']}>
{() => {
return option?.options?.map((v) => {
return renderSelectOptionItem(item, v)
})
}}
</ElOptionGroup>
)
} else {
return renderSelectOptionItem(item, option)
}
})
}
// 渲染 select option item
const renderSelectOptionItem = (item: FormSchema, option: ComponentOptions) => {
// 如果有别名,就取别名
const labelAlias = item?.componentProps?.optionsAlias?.labelField
const valueAlias = item?.componentProps?.optionsAlias?.valueField
const { label, value, ...other } = option
return (
<ElOption
{...other}
label={labelAlias ? option[labelAlias] : label}
value={valueAlias ? option[valueAlias] : value}
>
{{
default: () =>
// option 插槽名规则,{field}-option
item?.componentProps?.optionsSlot
? getSlot(slots, `${item.field}-option`, { item: option })
: undefined
}}
</ElOption>
)
}
return {
renderSelectOptions
}
}

View File

@@ -0,0 +1,148 @@
import type { Slots } from 'vue'
import { getSlot } from '@/utils/tsxHelper'
import { PlaceholderMoel } from './types'
import { FormSchema } from '@/types/form'
import { ColProps } from '@/types/components'
/**
*
* @param schema 对应组件数据
* @returns 返回提示信息对象
* @description 用于自动设置placeholder
*/
export const setTextPlaceholder = (schema: FormSchema): PlaceholderMoel => {
const { t } = useI18n()
const textMap = ['Input', 'Autocomplete', 'InputNumber', 'InputPassword']
const selectMap = ['Select', 'SelectV2', 'TimePicker', 'DatePicker', 'TimeSelect', 'TimeSelect']
if (textMap.includes(schema?.component as string)) {
return {
placeholder: t('common.inputText')
}
}
if (selectMap.includes(schema?.component as string)) {
// 一些范围选择器
const twoTextMap = ['datetimerange', 'daterange', 'monthrange', 'datetimerange', 'daterange']
if (
twoTextMap.includes(
(schema?.componentProps?.type || schema?.componentProps?.isRange) as string
)
) {
return {
startPlaceholder: t('common.startTimeText'),
endPlaceholder: t('common.endTimeText'),
rangeSeparator: '-'
}
} else {
return {
placeholder: t('common.selectText')
}
}
}
return {}
}
/**
*
* @param col 内置栅格
* @returns 返回栅格属性
* @description 合并传入进来的栅格属性
*/
export const setGridProp = (col: ColProps = {}): ColProps => {
const colProps: ColProps = {
// 如果有span代表用户优先级更高所以不需要默认栅格
...(col.span
? {}
: {
xs: 24,
sm: 12,
md: 12,
lg: 12,
xl: 12
}),
...col
}
return colProps
}
/**
*
* @param item 传入的组件属性
* @returns 默认添加 clearable 属性
*/
export const setComponentProps = (item: FormSchema): Recordable => {
const notNeedClearable = ['ColorPicker']
const componentProps: Recordable = notNeedClearable.includes(item.component as string)
? { ...item.componentProps }
: {
clearable: true,
...item.componentProps
}
// 需要删除额外的属性
delete componentProps?.slots
return componentProps
}
/**
*
* @param slots 插槽
* @param slotsProps 插槽属性
* @param field 字段名
*/
export const setItemComponentSlots = (
slots: Slots,
slotsProps: Recordable = {},
field: string
): Recordable => {
const slotObj: Recordable = {}
for (const key in slotsProps) {
if (slotsProps[key]) {
// 由于组件有可能重复,需要有一个唯一的前缀
slotObj[key] = (data: Recordable) => {
return getSlot(slots, `${field}-${key}`, data)
}
}
}
return slotObj
}
/**
*
* @param schema Form表单结构化数组
* @param formModel FormMoel
* @returns FormMoel
* @description 生成对应的formModel
*/
export const initModel = (schema: FormSchema[], formModel: Recordable) => {
const model: Recordable = { ...formModel }
schema.map((v) => {
// 如果是hidden就删除对应的值
if (v.hidden) {
delete model[v.field]
} else if (v.component && v.component !== 'Divider') {
const hasField = Reflect.has(model, v.field)
// 如果先前已经有值存在,则不进行重新赋值,而是采用现有的值
model[v.field] = hasField ? model[v.field] : v.value !== void 0 ? v.value : ''
}
})
return model
}
/**
* @param slots 插槽
* @param field 字段名
* @returns 返回FormIiem插槽
*/
export const setFormItemSlots = (slots: Slots, field: string): Recordable => {
const slotObj: Recordable = {}
if (slots[`${field}-error`]) {
slotObj['error'] = (data: Recordable) => {
return getSlot(slots, `${field}-error`, data)
}
}
if (slots[`${field}-label`]) {
slotObj['label'] = (data: Recordable) => {
return getSlot(slots, `${field}-label`, data)
}
}
return slotObj
}

View File

@@ -0,0 +1,17 @@
import { FormSchema } from '@/types/form'
export interface PlaceholderMoel {
placeholder?: string
startPlaceholder?: string
endPlaceholder?: string
rangeSeparator?: string
}
export type FormProps = {
schema?: FormSchema[]
isCol?: boolean
model?: Recordable
autoSetPlaceholder?: boolean
isCustom?: boolean
labelWidth?: string | number
} & Recordable