UNPKG

jobsys-explore

Version:

Enhanced component based on vant

550 lines (479 loc) 14.6 kB
import { computed, defineComponent, inject, nextTick, onMounted, reactive, ref, watch } from "vue" import { EX_UPLOADER, EX_FORM } from "../provider/ExProvider.jsx" import { cloneDeep, every, isEqual, isFunction, isObject, isString, pick } from "lodash-es" import { initItemDefaultValue } from "./utils" import { useCache, useFetch, useFormFail, useFormFormat, useProcessStatusSuccess } from "../../hooks" import { CellGroup, Form, showConfirmDialog, showSuccessToast, Skeleton } from "vant" import createFormItem from "./FormItem.jsx" import ExButton from "../button/ExButton.jsx" /** * ExForm 表单 * * @version 1.0.0 */ export default defineComponent({ name: "ExForm", props: { /** * 表单标题 */ title: { type: String, default: "" }, /** * 表单项 label 宽度,默认单位为px */ labelWidth: { type: [String, Number], default: "6.2em" }, /** * 是否在 label 后面添加冒号 */ colon: { type: Boolean, default: false }, /** * 是否使用卡片模式 */ inset: { type: Boolean, default: false }, /** * 表单数据,用于初始化表单,并会进行 Watch */ data: { type: [Object, String], default: "" }, /** * 是否自动加载 * true: 表示自动加载数据 * Array,String: 表示会对 `extraData` 数据中的相关字段进行非空验证,不为空再加载数据 */ autoLoad: { //自动加载数据,在fetchData里找 type: [Boolean, Array, String], default: true, }, /** * 获取表单数据的URL */ fetchUrl: { type: String, default: "" }, /** * 额外的数据,在提交时会合并到表单数据中并一起提交 */ extraData: { type: Object, default: () => ({}) }, /** * 是否禁用表单 */ disabled: { type: Boolean, default: false }, /** * 是否只读 */ readonly: { type: Boolean, default: false }, /** * submit button 是否固定在底部 */ fixed: { type: Boolean, default: false }, /** * 提交数据URL */ submitUrl: { type: String, default: "" }, /** * 提交按钮文字 */ submitButtonText: { type: String, default: "保存" }, /** * 提交确认提示内容 */ submitConfirmText: { type: String, default: "" }, /** * 是否禁用提交按钮 */ submitDisabled: { type: Boolean, default: false }, /** * 是否显示关闭按钮 */ closable: { type: Boolean, default: false }, /** * 取消按钮文字 */ cancelButtonText: { type: String, default: "取消" }, /** * 点击关闭时的动作 */ close: { type: Function, default: null }, /** * 当有分割线时,分割线的配置 */ dividerProps: { type: Object, default: () => ({}) }, /** * 是否缓存表单数据 * 开启后,该数据会缓存在 localStorage */ cacheable: { type: String, default: "" }, /** * * 表单配置 * * @typedef {Object} FormItemConfig * @property {string} key 数据库关联名称 * @property {string} title 显示的名字 * @property {string} [type] 类型,默认是input * @property {array|Function} [options] 组件选项 * @property {array|Function} [rows] 矩阵组件行标题 * @property {string} [placeholder] 组件里的提示 * @property {string|Function} [help] ExField 里的提示 * @property {string|Function} [append] ExField 外的提示 * @property {array} [rules] 验证规则 * @property {boolean} [isLink] 是否展示右侧箭头并开启点击反馈 * @property {boolean} [readonly] 是否只读 * @property {boolean|Function} [required] 是否必填,默认是false * @property {boolean|Function} [disabled] 组件不可编辑状态,默认是false * @property {boolean|Function} [hidden] 组件是否隐藏 * @property {Function} [match] 支持根据条件返回不同的配置进行动态渲染 * @property {Function} [init] 初始化函数,用于初始化表单项的值 * @property {Function} [beforeSubmit] 在提交前修改表单项的值,该函数会在 ExForm 的 beforeSubmit 之前调用 * @property {boolean|string} [break] 新起一行,默认为false,如果为 String 则以 Divider 分割 * @property {Object} [fieldProps] ExField 的原生配置 * @property {Object} [exProps] Ex组件配置 * @property {Object} [defaultProps] 原生 Vant 组件的配置 * @property {Object} [defaultSlots] 混合 Field 和 input slot 组件的 slots 组合 * @property {*} [defaultValue] 默认值,默认是空字符串 * @property {*} [_temp] 临时数据,内部使用 */ /** * 表单配置,[见下表](#form-表单配置) */ form: { type: Array, default() { return [] }, }, /** * fetch 返回数据处理函数 * @return {Object} 返回处理后的数据,将用于初始化表单 */ afterFetched: { type: Function, default: null }, /** * * @typedef {Object} ExposedFormData * @property {Object} formatForm Format后的表单数据 * @property {Object} originalForm 原生的表单数据 * * * 提交数据处理函数 * @param {ExposedFormData} data * @return {Boolean|Object} return false会阻止提交操作,return Object会替换提交的数据 * */ beforeSubmit: { type: Function, default: null }, /** * 提交成功后的回调 */ afterSubmit: { type: Function, default: null }, /** * [原生配置](https://vant-contrib.gitee.io/vant/#/zh-CN/form) */ formProps: { type: Object, default: () => ({}) }, }, emits: ["success"], setup(props, { expose, emit, slots }) { const formRef = ref(null) //表单容器 const state = reactive({ temporary: {}, // 用于存放一些临时数据 submitFetcher: { loading: false, }, isInitializing: true, //是否正在初始化 rules: {}, submitForm: {}, //提交表单,初始化数据后会生成 submitFormBackup: {}, //初始化后的表单数据备份,用于重置表单以及脏数据判断 }) const formItems = computed(() => props.form) const uploaderProvider = inject(EX_UPLOADER, () => ({})) const formProvider = inject(EX_FORM, () => ({})) // 初始化表单数据 const dataForInit = ref(props.data) watch( () => props.data, (newV) => { dataForInit.value = newV initFormData(newV || false) }, ) watch( () => state.submitForm, () => { if (props.cacheable) { useCache(props.cacheable, localStorage).set(state.submitForm) } }, { deep: true }, ) watch( () => formItems.value, () => { initFormData(dataForInit.value || false) }, ) const init = (data) => { state.isInitializing = false initFormData(data || dataForInit.value || false) } onMounted(() => { if (props.autoLoad && props.fetchUrl) { let auto = true if (props.autoLoad && isObject(props.autoLoad)) { auto = every(Object.values(pick(props.extraData, Object.keys(props.autoLoad)))) } else if (props.autoLoad && isString(props.autoLoad)) { auto = !!props.extraData[props.autoLoad] } if (auto) { fetchItem() } else { init() } } else { if (props.cacheable) { const cachedData = useCache(props.cacheable, localStorage).get() if (cachedData) { showConfirmDialog({ message: "存在未提交的数据,是否恢复?", lockScroll: false }) .then(() => { init(cachedData) }) .catch(() => { useCache(props.cacheable, localStorage).remove() init() }) } else { init() } } else { init() } } }) /** * 初始化表单数据 * * @param {Object} formData 表单数据 */ const initFormData = (formData) => { // 如果有 FormData, 则从中提取 FormItem 中有定义的数据,并进行初始化后存放于 extractFormData // 无 FromData 则直接使用 FormItem 的 defaultValue let extractFormData = {} let existingData = formData ? cloneDeep(formData) : false formItems.value.forEach((item) => { extractFormData[item.key] = initItemDefaultValue(item, existingData, state.submitForm, { uploaderProvider }) }) if (existingData) { // 将初始化后的数据覆盖原有数据,并保留不在 FormItems 中的数据 extractFormData = { ...existingData, ...extractFormData } } state.submitForm = extractFormData state.submitFormBackup = cloneDeep(extractFormData) } //远程拿数据模式 const fetchItem = () => { if (props.fetchUrl) { state.isInitializing = true useFetch() .get(props.fetchUrl, { params: props.extraData }) .then((res) => { state.isInitializing = false useProcessStatusSuccess(res, () => { if (props.afterFetched && isFunction(props.afterFetched)) { res = props.afterFetched(res) } else if (formProvider.afterFetched && isFunction(formProvider.afterFetched)) { res = formProvider.afterFetched(res) } dataForInit.value = res //有可能出现 isInitializing 未生效 skeleton 还未关闭的情况 nextTick(() => { initFormData(res) }) }) }) .finally(() => { state.isInitializing = false }) } } const onSubmit = async () => { await formRef.value.validate() if (props.submitConfirmText) { try { await showConfirmDialog({ message: props.submitConfirmText }) } catch (e) { return } } // 为了在 item 中也能定制 beforeSubmit 这里和下面的 useFormFormat 会重复 copy 一次 submitForm let form = cloneDeep(state.submitForm) const itemsWithBeforeSubmit = formItems.value .map((item) => { if (item.match) { // match 的属性需要在这里处理 return { ...item, ...item.match(form) } } return item }) .filter((item) => item.beforeSubmit && isFunction(item.beforeSubmit)) for (const item of itemsWithBeforeSubmit) { form[item.key] = await item.beforeSubmit({ value: form[item.key], submitForm: form, //改成将 form 传出去,这样可以在 form 中添加参数 }) } form = useFormFormat(form, formProvider.format || {}) if (props.beforeSubmit && isFunction(props.beforeSubmit)) { form = await props.beforeSubmit({ formatForm: form, originalForm: state.submitForm }) if (form === false) { return } } try { let res = await useFetch(state.submitFetcher).post(props.submitUrl, form) //提交后再次备份表单数据,isDirty 检测即为 false state.submitFormBackup = cloneDeep(state.submitForm) if (props.cacheable) { useCache(props.cacheable, localStorage).remove() } if (props.afterSubmit) { props.afterSubmit(res) } else { useProcessStatusSuccess(res, () => { showSuccessToast(`${props.submitButtonText}成功`) emit("success", res) }) } } catch (e) { useFormFail(e) } } /********** exposes **********/ /** * 获取复制的表单数据 * @return {*} */ const getFormStandalone = () => cloneDeep(state.submitForm) /** * 获取表单实时数据,慎用,会改变内部的值 * @return {*} */ const getFormRealtime = () => state.submitForm /** * 获取表单的字段值 * @param {String} key * @return {*} */ const getField = (key) => cloneDeep(state.submitForm[key]) /** * * 设置表单数据 * @param {Object} fields */ const setForm = (fields) => { Object.keys(fields).forEach((key) => { state.submitForm[key] = fields[key] }) } /** * 判断表单是否被修改 * @return {boolean} */ const isDirty = () => { return !isEqual(state.submitForm, state.submitFormBackup) } /** * 重置表单 * @param {Object} formData 表单数据 */ const reset = (formData) => { initFormData(formData) nextTick(() => { formRef.value.resetValidation() }) } expose({ getForm: getFormStandalone, getFormStandalone, getFormRealtime, getField, setForm, isDirty, fetchItem, reset, }) /********** render **********/ const formItemElems = () => formItems.value.map((formItem) => createFormItem(formItem, state.submitForm, { props, slots, }), ) const skeletonElem = () => ( <Skeleton row={10} title loading={state.isInitializing}> {{ default: () => formItemElems(), }} </Skeleton> ) const footerElem = () => { if (state.isInitializing) { return null } if (slots.footer) { return <div class={"ex-form__footer"}>{slots.footer()}</div> } return null } const buttonsElem = () => { if (state.isInitializing) { return null } if (props.readonly) { return null } const cancelBtn = ( <ExButton class={"ex-form__cancel-btn"} type={"default"} plain onClick={() => { if (props.close && isFunction(props.close)) { props.close() } }} > {() => props.cancelButtonText} </ExButton> ) const submitBtn = ( <ExButton disabled={props.submitDisabled} type={"primary"} fetcher={state.submitFetcher} buttonProps={{ nativeType: "submit" }}> {() => props.submitButtonText} </ExButton> ) if (props.fixed) { return <div class={"ex-form__btn-wrapper-fixed van-hairline--top"}>{[props.closable ? cancelBtn : null, submitBtn]}</div> } return <div class={"ex-form__btn-wrapper"}>{[props.closable ? cancelBtn : null, submitBtn]}</div> } return () => ( <Form ref={formRef} labelWidth={props.labelWidth} colon={props.colon} class={`ex-form ${props.fixed ? "ex-form__fixed" : ""}`} disabled={props.disabled} readonly={props.readonly} scrollToError={true} validateFirst={true} onSubmit={onSubmit} {...props.formProps} > {{ default: () => [ <CellGroup inset={props.inset} title={props.title}> {{ default: () => skeletonElem() }} </CellGroup>, footerElem(), buttonsElem(), ], }} </Form> ) }, })