jobsys-newbie
Version:
Enhanced component based on ant-design-vue
572 lines (503 loc) • 15.1 kB
JSX
import { computed, defineComponent, inject, onMounted, reactive, ref, watch } from "vue"
import { cloneDeep, every, isEqual, isFunction, isObject, isString, pick } from "lodash-es"
import { useFetch, useFormFail, useFormFormat, useI18nJoin, useProcessStatusSuccess } from "../../hooks"
import { Button, Card, Form, message, Modal, Skeleton, Space } from "ant-design-vue"
import createLayout from "./components/Layout.jsx"
import { NEWBIE_UPLOADER, NEWBIE_FORM } from "../provider/NewbieProvider.jsx"
import "./index.less"
import { initItemDefaultValue } from "./utils"
import { useI18n } from "vue-i18n"
/**
* 表单组件
*
* 通过 JSON 配置生成表单,并集成表单验证,获取数据,提交数据等功能。
*
*
* @version 1.0.0
*/
export default defineComponent({
name: "NewbieForm",
props: {
/**
* 表单标题,Card 模式下生效
*/
title: { type: String, default: "" },
/**
* 表单数据,用于初始化表单,并会进行 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: () => ({}) },
/**
* 提交数据URL
*/
submitUrl: { type: String, default: "" },
/**
* 提交按钮文字
*/
submitButtonText: { type: String, default: "" },
/**
* 提交确认提示内容
*/
submitConfirmText: { type: String, default: "" },
/**
* 是否显示关闭按钮
*/
closable: { type: Boolean, default: true },
/**
* 关闭按钮文字
*/
closeButtonText: { type: String, default: "" },
/**
* 是否将`关闭`按钮放在`提交`按钮前面
*/
closeButtonFirst: { type: Boolean, default: false },
/**
* 是否隐藏按钮
*/
hideButtons: { type: Boolean, default: false },
/**
* 是否禁用表单
*/
disabled: { type: Boolean, default: false },
/**
* 是否只读
*/
readonly: { type: Boolean, default: false },
/**
* 是否禁用提交按钮
*/
submitDisabled: { type: Boolean, default: false },
/**
* 分隔类型,默认为 `divider` 分割线
* @values divider, collapse
*/
breakMode: { type: String, default: "divider" },
/**
* 当 breakMode 为 `divider` 时,divider 的配置[原生分割线配置](https://www.antdv.com/components/divider-cn)
*/
dividerProps: { type: Object, default: () => ({}) },
/**
* 表单布局,默认为单列,当为多列时,按 24 栅格布局
* 如:columns: [12, 12]
*/
columns: { type: Array, default: () => [24] },
/**
* 如有 fixed 列,默认 fixed 列的宽度为 6,可自定义
*/
fixedColumns: { type: Number, default: 6 },
/**
* 是否用 `Card` 包裹
*/
cardWrapper: { type: Boolean, default: true },
/**
* Card 的配置
*/
cardProps: {
type: Object,
default: () => {},
},
/**
* Card 的Slots
*/
cardSlots: {
type: Object,
default: () => {},
},
/**
*
* 表单配置
*
* @typedef {Object} NewbieFormItemConfig
* @property {string} key 数据库关联名称
* @property {string} title 显示的名字
* @property {string} [type] 类型,默认是input
* @property {array|Function} [rows] 矩阵组件行标题
* @property {array|Function} [options] 组件选项
* @property {string} [placeholder] 组件里的提示
* @property {string|Function} [help] form item里的提示
* @property {array} [rules] 验证规则
* @property {string|number} [width] 组件宽度
* @property {string} [style] 样式
* @property {string} [class] 类名
* @property {boolean|Function} [required] 是否必填,默认是false
* @property {string} [requiredMessage] 必填项提示消息
* @property {boolean} [readonly] 是否只读,默认是false
* @property {boolean|Function} [disabled] 组件不可编辑状态,默认为 false
* @property {boolean|Function} [hidden] 组件是否隐藏,默认为 false
* @property {boolean|Array} [optional] 表单项是否开启后才可以输入
* @property {Function} [init] 初始化函数,用于初始化表单项的值
* @property {Function} [beforeSubmit] 在提交前修改表单项的值,该函数会在 NewbieForm 的 beforeSubmit 之前调用
* @property {Function} [match] 支持根据条件返回不同的配置进行动态渲染
* @property {number|Array|string} [columnIndex] 渲染在哪一列,默认为0,第一列
* @property {boolean|string} [break] 新起一行,默认为false,如果为 String 则以 Divider 分割
* @property {Object} [defaultProps] 组件的配置
* @property {Object} [defaultSlots] 组件的默认插槽
* @property {Object} [formItemProps] FormItem 的原生配置
* @property {Object} [formItemSlots] FormItem 的原生Slot配置
* @property {*} [defaultValue] 默认值,默认是空字符串
* @property {Array.<NewbieFormItemConfig>|Function} [children] 子表单配置
* @property {Array} [childrenOperations] 子表单操作
* @property {Object} [cellProps] 单元格的配置
*/
/**
* 表单配置,[见表单项配置](#newbieformitemconfig-表单项配置)
*/
form: {
type: [Array, Function],
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 },
/**
* 点击关闭时的动作
*/
close: { type: Function, default: null },
/**
* 提交成功后的回调
*/
afterSubmit: { type: Function, default: null },
/**
* 原生的 [From](https://www.antdv.com/components/form-cn#api) 配置
*/
formProps: { type: Object, default: () => ({}) },
},
emits: ["success"],
setup(props, { expose, slots, emit }) {
const { t, locale } = useI18n()
const editorRef = ref(null) //总的容器
const state = reactive({
temporary: {}, // 用于存放一些临时数据
submitFetcher: {
loading: false,
},
isInitializing: true, //是否正在初始化
submitForm: {}, //提交表单,初始化数据后会生成
submitFormBackup: {}, //初始化后的表单数据备份,用于重置表单以及脏数据判断
})
const uploaderProvider = inject(NEWBIE_UPLOADER, () => ({}))
const formProvider = inject(NEWBIE_FORM, () => ({}))
const formState = {} // 用于记录各个表单项的状态
const formItems = computed(() => {
const items = isFunction(props.form) ? props.form() : props.form
return items.map((item) => {
if (!item.type) {
item.type = "input"
}
if (formProvider?.columns?.[item.type]) {
item = { ...formProvider.columns[item.type], ...item }
}
if (!formState[item.key]) {
formState[item.key] = reactive({})
}
return item
})
})
watch(
() => props.data,
(data) => {
initFormData(data || "")
},
)
const init = () => {
state.isInitializing = false
initFormData(props.data || "")
}
onMounted(() => {
let auto = false
if (props.autoLoad && props.fetchUrl) {
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()
}
})
/**
* 初始化表单数据
*
* @param {Object} formData 表单数据
*/
const initFormData = (formData) => {
// 如果有 FormData, 则从中提取 FormItem 中有定义的数据,并进行初始化后存放于 extractFormData
// 无 FromData 则直接使用 FormItem 的 defaultValue
let extractFormData = {}
let existingData = formData ? cloneDeep(formData) : false
formItems.value.forEach((item) => {
//处理组合组件,只允许一层嵌套
if (item.type === "combiner") {
item.children.forEach((child) => {
extractFormData[child.key] = initItemDefaultValue(child, existingData, state.submitForm, { uploaderProvider })
})
} else {
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
})
}
}
const onSubmit = () =>
new Promise((resolve, reject) => {
editorRef.value
.validate()
.then(async () => {
// TODO need improve
// 为了在 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 })
// 如果返回 false 则阻止提交
if (form === false) {
return resolve()
}
}
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, () => {
message.success(useI18nJoin(props.submitButtonText || t("common.save"), t("common.success"), { locale }))
emit("success", res)
})
}
return resolve()
})
.catch((info) => {
useFormFail(info)
return reject(info)
})
})
/********** exposes **********/
/**
* 是否在加载中
* @param {Boolean} value
*/
const isInitializing = (value) => {
state.isInitializing = value
}
const reset = () => {
editorRef.value.resetFields()
}
/**
* 获取复制的表单数据
* @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)
}
expose({
submit: onSubmit,
getForm: getFormStandalone,
getFormStandalone,
getFormRealtime,
getField,
setForm,
reset,
isDirty,
isInitializing,
})
/********** render **********/
const submitBtnElem = () => {
const submitBtn = () => (
<Button
loading={state.submitFetcher.loading}
type={"primary"}
htmlType={"submit"}
disabled={props.submitDisabled}
onClick={() => {
if (props.submitConfirmText) {
Modal.confirm({
content: props.submitConfirmText,
onOk() {
onSubmit()
},
})
} else {
onSubmit()
}
}}
>
{{
default: () => `${props.submitButtonText || t("common.save")}${state.submitFetcher.loading ? "..." : ""}`,
}}
</Button>
)
const closeBtn = () => (
<Button
type={"default"}
onClick={() => {
if (props.close && isFunction(props.close)) {
props.close()
}
}}
>
{{ default: () => props.closeButtonText || t("common.close") }}
</Button>
)
return (
<Space size={"large"}>
{{
default: () =>
props.closeButtonFirst
? [props.closable ? closeBtn() : null, submitBtn()]
: [submitBtn(), props.closable ? closeBtn() : null],
}}
</Space>
)
}
const layoutElem = () =>
createLayout(formItems.value, state.submitForm, submitBtnElem(), {
props,
slots,
formState,
})
const formElem = () => (
<Form
model={state.submitForm}
ref={editorRef}
class={`newbie-form`}
labelCol={{ span: 6 }}
scrollToFirstError={true}
{...props.formProps}
>
{{
default: () => [layoutElem()],
}}
</Form>
)
const skeletonElem = () => (
<Skeleton active paragraph={{ rows: 10 }} loading={state.isInitializing}>
{{
default: () => [slots.prepend?.(), formElem(), slots.append?.()],
}}
</Skeleton>
)
return () => (
<div class={"newbie-form-wrapper"}>
{props.cardWrapper ? (
<Card {...props.cardProps} title={props.title}>
{{
default: () => skeletonElem(),
...props.cardSlots,
}}
</Card>
) : (
skeletonElem()
)}
</div>
)
},
})