import { h, ref, toRefs, nextTick } from 'vue' import { ElForm } from 'element-plus' import { useFormProvide } from '../hooks/use-form' import CipFormItem from '../cip-form-item' import CipFormDirectory from './form-directory' import CipFormLayout from '../cip-form-layout' import { DRender } from '../helper/d-render' import { toUpperFirstCase, getFieldValue } from '@cip/utils/util' import CipMessage from '../cip-message' import ElementClass from './element' import scrollIntoView from 'scroll-into-view-if-needed' import './index.less' const dRender = new DRender() export default { name: 'CipForm', props: { model: Object, fieldList: Array, showOnly: Boolean, modelKey: { type: [String, Function] }, grid: { type: [Number, Boolean] }, // 是否开启grid布局 useDirectory: Boolean, labelPosition: String, equipment: { type: String, default: 'pc', validate: (val) => ['pc', 'mobile'].includes(val) }, isScrollError: { type: Boolean, default: true }, border: Boolean, // showOnly + border 将出现边框 enterHandler: Function, // 回车触发回调 options: Object }, emits: ['update:model', 'submit', 'cancel'], setup (props, context) { // 下发属性 const uploadQueue = ref({}) useFormProvide(props, uploadQueue) const directoryConfig = ref([]) const { model, fieldList } = toRefs(props) const cipFormRef = ref() // 修改model的值 const updateModel = (val) => { context.emit('update:model', val) } const generateComponentKey = (key) => { if (props.modelKey) { const appendKey = toUpperFirstCase(key) if (typeof props.modelKey === 'function') { return `${props.modelKey(props.model)}${appendKey}` } else { const value = getFieldValue(props.model, props.modelKey) return `${value || ''}${appendKey}` } } else { return key } } // 获取layout及item组件需要的props const getComponentProps = (key, config) => { const componentKey = generateComponentKey(key) const componentProps = { key: componentKey, componentKey: componentKey, model, fieldKey: key, config, readonly: props.showOnly, grid: props.grid, formLabelPosition: props.labelPosition, 'onUpdate:model': (val) => { if (componentKey === generateComponentKey(key)) { updateModel(val) } } } if (props.enterHandler) { componentProps.onKeyup = (e) => { const { keyCode } = e if (keyCode === 13) { props.enterHandler() } } } return componentProps } // 布局字段渲染方式 const getFormLayout = (componentProps) => { return h(CipFormLayout, { ...componentProps, onValidate: (cb) => { validate(cb) }, onSubmit: () => { context.emit('submit') }, onCancel: () => { context.emit('cancel') } }, { item: ({ children = [], isShow } = {}) => { return children.map((v) => getFormDefaultSlot(v, isShow)) } }) } // 输入字段渲染方式 const getFormItem = (componentProps) => { return h(CipFormItem, componentProps) } // 渲染单个字段 const getFormDefaultSlot = ({ key, config } = {}, isShow) => { // 若存在字段key值的插槽覆盖则配置整个ElFormItem config._isGrid = props.grid config._isShow = isShow if (context.slots[key]) { return context.slots[key]({ key, config }) } const componentProps = getComponentProps(key, config) // 若存在字段key值+Input的插槽覆盖则配置ElFormItem内的Input if (context.slots[`${key}Input`]) { return h(CipFormItem, { ...componentProps, customSlots: context.slots[`${key}Input`] }) } // 判断是否为布局类型的字段 if (dRender.isLayoutType(config.type)) { // layout类型字段 return getFormLayout(componentProps) } else { // input类型字段 // 如果需要表单目录导航则添加 if (config.directory) { directoryConfig.value[key] = { label: config.staticInfo || config.label, level: config.directory } } return getFormItem(componentProps) } } // 渲染表单 const getFormDefaultSlots = () => { if (props.useDirectory) { return fieldList.value.map((v) => getFormDefaultSlot(v)).concat( [h(CipFormDirectory, { directory: directoryConfig.value })] ) } else { return fieldList.value.map((v) => getFormDefaultSlot(v)) } } /** start父组件通过ref调用方法 **/ const validateUpload = () => { return new Promise((resolve, reject) => { const keys = Object.keys(uploadQueue.value) for (let i = 0; i < keys.length; i++) { const key = keys[i] if (uploadQueue.value[key]) { CipMessage.error('请等待文件上传', '提示') resolve(false) break } } resolve(true) }) } const validateField = (props, cb) => { return cipFormRef.value.validateField(props, cb) } const validate = async (cb = () => {}) => { const isUpload = await validateUpload() if (!isUpload) { // eslint-disable-next-line standard/no-callback-literal cb(false) throw new Error('请等待文件上传') } else { // const res = await cipFormRef.value.validate() // 此方式返回的res为 true or false return new Promise((resolve, reject) => { cipFormRef.value.validate(async (isValid, invalidFields) => { // 自动定位到error项 props.isScrollError && await scrollToField(null, props.options) if (typeof cb === 'function') cb(isValid, invalidFields) if (isValid) { resolve(isValid) } else { reject(isValid) } }) }) // console.log(res) // return await cipFormRef.value.validate(async (isValid, invalidFields) => { // // 自动定位到error项 // props.isScrollError && await scrollToField(null, props.options) // if (typeof cb === 'function') { // cb(isValid, invalidFields) // } // }) } } const scrollToField = async (name, options = {}) => { await nextTick(() => { const elements = new ElementClass(cipFormRef.value.$el) // 加了name是手动定位,不检查是否验证通过 // 自动定位到第一个未校验同通过项 const target = name ? elements.getItemByName(name) : elements.getFirstErrorItem() // 原生scrollIntoView会导致整个页面滚动 target && scrollIntoView(target, { scrollMode: 'if-needed', block: 'nearest', behavior: 'smooth', inline: 'nearest', ...options }) }) } const clearValidate = () => { return cipFormRef.value?.clearValidate() } context.expose({ validateUpload, validateField, validate, clearValidate, scrollToField }) /** end父组件通过ref调用方法 **/ return () => h(ElForm, { ...context.attrs, ref: cipFormRef, hideRequiredAsterisk: true, model: model, // 待进行测试 使用model.value后数据是否正常 class: ['cip-form', `cip-form--${props.equipment}`, { 'cip-form--grid': props.grid, 'cip-form--border': props.border && props.showOnly }], style: { gridTemplateColumns: `repeat(${typeof props.grid === 'number' ? props.grid : 3},1fr)` }, size: 'small', labelPosition: props.labelPosition, onSubmit: ev => { ev.preventDefault() } }, { default: () => [getFormDefaultSlots(), context.slots.default?.()] }) } }