jobsys-mpower
Version:
Enhanced component based on Taro & NutUI
421 lines (362 loc) • 11.5 kB
JSX
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>
)
},
})