UNPKG

jobsys-mpower

Version:

Enhanced component based on Taro & NutUI

421 lines (362 loc) 11.5 kB
import { computed, defineComponent, inject, onMounted, reactive, ref, watch } from "vue" import { MP_UPLOADER, MP_FORM } from "../provider/MpProvider.jsx" import { cloneDeep, every, isEqual, isFunction, isObject, isString, pick } from "lodash-es" import { initItemDefaultValue } from "./utils" import { useFetch, useFormFail, useFormFormat, useProcessStatusSuccess } from "../../hooks" import createFormItem from "./FormItem.jsx" import MpButton from "../button/MpButton.jsx" import MpSkeleton from "../skeleton/MpSkeleton.jsx" import Taro from "@tarojs/taro" /** * MpForm 表单 * * @version 1.0.0 */ export default defineComponent({ name: "MpForm", props: { /** * 表单标题 */ title: { type: String, default: "" }, /** * 表单项 label 宽度,默认单位为px */ labelWidth: { type: [String, Number], default: "6.2em" }, /** * 是否在 label 后面添加冒号 */ colon: { 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 }, /** * 当有分割线时,分割线的配置 */ dividerProps: { type: Object, default: () => ({}) }, /** * * 表单配置 * * @typedef {Object} FormItemConfig * @property {string} key 数据库关联名称 * @property {string} title 显示的名字 * @property {string} [type] 类型,默认是input * @property {array|Function} [options] 组件选项 * @property {string} [placeholder] 组件里的提示 * @property {string|Function} [help] MpField 里的提示 * @property {array} [rules] 验证规则 * @property {boolean} [is-link] 是否展示右侧箭头并开启点击反馈 * @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] 在提交前修改表单项的值,该函数会在 MpForm 的 beforeSubmit 之前调用 * @property {boolean|string} [break] 新起一行,默认为false,如果为 String 则以 Divider 分割 * @property {Object} [fieldProps] MpField 的原生配置 * @property {Object} [defaultProps] 组件的配置 * @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} MpposedFormData * @property {Object} formatForm Format后的表单数据 * @property {Object} originalForm 原生的表单数据 * * * 提交数据处理函数 * @param {MpposedFormData} data * @return {Boolean|Object} return false会阻止提交操作,return Object会替换提交的数据 * */ beforeSubmit: { type: Function, default: null }, /** * 提交成功后的回调 */ afterSubmit: { type: Function, default: null }, /** * [原生配置](https://nutui.jd.com/taro/vue/4x/#/zh-CN/component/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(MP_UPLOADER, () => ({})) const formProvider = inject(MP_FORM, () => ({})) watch( () => props.data, (newV) => { initFormData(newV || "") }, ) const init = () => { state.isInitializing = false initFormData(props.data || "") } onMounted(() => { 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) } initFormData(res) }) }) .finally(() => { state.isInitializing = false }) } } if (props.autoLoad) { 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() } } const showConfirmDialog = ({ content }) => { return new Promise((resolve, reject) => { Taro.showModal({ content, success(res) { if (res.confirm) { resolve() } else { reject() } }, }) }) } const onSubmit = async () => { await formRef.value.validate() if (props.submitConfirmText) { try { await showConfirmDialog({ content: props.submitConfirmText }) } catch (e) { return } } // 为了在 item 中也能定制 beforeSubmit 这里和下面的 useFormFormat 会重复 copy 一次 submitForm let form = cloneDeep(state.submitForm) formItems.value .filter((item) => item.beforeSubmit && isFunction(item.beforeSubmit)) .forEach((item) => { form[item.key] = item.beforeSubmit({ value: form[item.key], submitForm: state.submitForm, }) }) 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.afterSubmit) { props.afterSubmit(res) } else { useProcessStatusSuccess(res, () => { Taro.showToast({ icon: "success", title: `${props.submitButtonText}成功`, }) emit("success", res) }) } } catch (e) { useFormFail(e) } } /********** exposes **********/ /** * 获取复制的表单数据 * @return {*} */ const getFormStandalone = () => cloneDeep(state.submitForm) /** * 获取表单实时数据 * @return {*} */ const getForm = () => state.submitForm /** * * 设置表单数据 * @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) } expose({ getForm, getFormStandalone, setForm, isDirty }) /********** render **********/ const formItemElems = () => formItems.value.map((formItem) => createFormItem(formItem, state.submitForm, { props, slots, }), ) const footerElem = () => { if (slots.footer) { return <div class={"mp-form__footer"}>{slots.footer()}</div> } return null } const submitBtnElem = () => { if (props.readonly) { return null } const submitBtn = ( <MpButton disabled={props.submitDisabled} type={"primary"} fetcher={state.submitFetcher} buttonProps={{ nativeType: "submit" }}> {() => props.submitButtonText || "提交"} </MpButton> ) if (props.fixed) { return <div class={"mp-form__submit-btn-fixed van-hairline--top"}>{submitBtn}</div> } return <div class={"mp-form__submit-btn"}>{submitBtn}</div> } return () => ( <Form ref={formRef} colon={props.colon} class={`mp-form ${props.fixed ? "mp-form__fixed" : ""}`} disabled={props.disabled} readonly={props.readonly} onSubmit={onSubmit} > {() => [ <nut-cell-group title={props.title}>{state.isInitializing ? <MpSkeleton /> : formItemElems()}</nut-cell-group>, footerElem(), submitBtnElem(), ]} </Form> ) }, })