最近有小伙伴在问动态表单如何再次封装,比如配合模态框或者抽屉封装多一层,这样可以大大提高开发效率,结合之前的写的
vue2 + antd 封装动态表单组件(一)
vue2 + antd 封装动态表单组件(二)
做了优化,效果如下:
废话不多说,直接上代码
1.封装输入框组件,显示可输入的长度,其他自定义组件可采用类似的方法;
my-input.vue
<template>
<div class="my-input">
<a-input ref="myInput" v-on="$listeners" v-bind="$attrs" :maxLength="max" v-model="val">
<template v-for="(value, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData || {}"></slot>
</template>
</a-input>
<div class="my-input-count">
<!-- {{ $attrs.value && $attrs.value.length ? $attrs.value.length : 0 }}/{{max}} -->
{{ value.length }} / {{ max }}
</div>
</div>
</template>
<script>
export default {
model: {
prop: "value",
event: "change",
},
props: {
value: {
type: String,
default: ""
},
max: {
type: Number,
default: 20,
},
},
watch: {
value(newVal) {
this.val = newVal
}
},
data() {
return {
val: this.$attrs['data-__meta'].initialValue
}
},
};
</script>
<style lang="less" scoped>
.my-input {
position: relative;
display: flex;
/deep/.ant-input {
padding-right: 45px;
}
}
.my-input-count {
position: absolute;
right: 8px;
top: -4px;
color: #909399;
font-size: 12px;
transition: color 0.3s;
}
</style>
2.封装动态表单组件,已解决了自定义表单项,表单联动,异步获取数据,属性透传等问题,其他比如表单验证,事件透传,样式等问题小伙伴们也可以思考思考,顺便优化优化;
dynamic-form.vue
<template>
<a-spin :spinning="loading">
<a-form
:form="form"
ref="form"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<div v-for="(field, fieldIndex) in fieldItemOptions" :key="field.key">
<a-form-item
:label="field.label"
:required="field.required"
v-if="!field.hidden"
>
<!-- text文本框 -->
<template v-if="field.type === 'text'">
<MyInput
v-decorator="[
field.key,
{
rules: [
{
required: field.required,
message: `${field.label}不能为空`,
},
],
initialValue: field.value,
},
]"
:placeholder="`请输入${field.label}`"
:disabled="field.disabled"
:max="field.max"
v-bind="field.props"
></MyInput>
</template>
<!-- textarea 文本域 -->
<template v-else-if="field.type === 'textarea'">
<a-textarea
v-decorator="[
field.key,
{
rules: [
{
required: field.required,
message: `${field.label}不能为空`,
},
],
initialValue: field.value,
},
]"
:placeholder="`请输入${field.label}`"
:disabled="field.disabled"
v-bind="{ ...field.props }"
/>
</template>
<!-- select下拉框 -->
<template v-else-if="field.type === 'select'">
<a-select
:placeholder="`请选择${field.label}`"
v-decorator="[
field.key,
{
rules: [
{
required: field.required,
message: `请选择${field.label}`,
},
],
initialValue: field.value,
},
]"
v-bind="{ ...field.props }"
@change="handleChange($event, field, fieldIndex)"
>
<a-select-option
v-for="option in field.options"
:key="option.value"
:value="option.value"
>{{ option.label }}</a-select-option
>
</a-select>
</template>
<!-- checkbox多选框 -->
<template v-else-if="field.type === 'checkbox'">
<a-checkbox-group
v-decorator="[
field.key,
{
rules: [
{
required: field.required,
message: `请选择${field.label}`,
},
],
initialValue: field.value,
},
]"
v-bind="{ ...field.props }"
@change="handleChange($event, field, fieldIndex)"
>
<a-checkbox
v-for="option in field.options"
:key="option.value"
:value="option.value"
:style="{ width: field.width }"
>{{ option.label }}</a-checkbox
>
</a-checkbox-group>
</template>
<!-- radio单选框 -->
<template v-else-if="field.type === 'radio'">
<a-radio-group
v-decorator="[
field.key,
{
rules: [
{
required: field.required,
message: `请选择${field.label}`,
},
],
initialValue: field.value,
},
]"
v-bind="{ ...field.props }"
@change="handleChange($event, field, fieldIndex)"
>
<a-radio
v-for="option in field.options"
:key="option.value"
:value="option.value"
>{{ option.label }}</a-radio
>
</a-radio-group>
</template>
<template v-else-if="field.type === 'datePicker'">
<a-date-picker
:placeholder="`请选择${field.label}`"
v-decorator="[
field.key,
{
rules: [
{
required: field.required,
message: `请选择${field.label}`,
},
],
initialValue: field.value,
},
]"
v-bind="{ ...field.props }"
>
</a-date-picker>
</template>
</a-form-item>
</div>
</a-form>
</a-spin>
</template>
<script>
import { deepClone } from "@/common/utils";
import MyInput from "./my-input";
export default {
components: { MyInput },
props: {
// 表单域配置
fieldOptions: {
type: Array,
default: () => [],
},
// 编辑时表单回显的默认数据
model: {
type: Object,
default: () => ({}),
},
// 标签宽度
labelCol: {
type: Object,
default: () => {
return {
xs: { span: 24 },
sm: { span: 6 },
};
},
},
// 控件宽度
wrapperCol: {
type: Object,
default: () => {
return {
xs: { span: 24 },
sm: { span: 16 },
};
},
},
},
computed: {},
data() {
return {
loading: false,
fieldItemOptions: [],
fieldItemRelativeOptions: [],
form: this.$form.createForm(this, { name: "dynamic-form" }),
};
},
methods: {
// 初始化表单,只初始化一次,相比之前watch监听的写法,这里优化了性能
async initForm() {
this.loading = true;
const fieldOptions = deepClone(this.fieldOptions);
for (let i = 0; i < fieldOptions.length; i++) {
const c = fieldOptions[i];
if (!c.props) c.props = {};
c.value = this.model[c.key];
for (const key in c) {
if (c[key] && c[key] instanceof Function) {
c[key] = await c[key](this.model);
}
}
}
this.fieldItemRelativeOptions = fieldOptions.filter(
(c) => c?.relativeList?.length
);
this.fieldItemOptions = deepClone(fieldOptions);
this.loading = false;
},
// 提交表单
handleSubmit() {
return new Promise((resolve, reject) => {
this.form.validateFields((err, formData) => {
if (err) {
reject(err);
return;
}
const formatFormData = this.formatFormData();
for (const key in formatFormData) {
formatFormData[key](formData);
}
// 提交表单逻辑
console.log("表单数据:", formData);
resolve(formData);
});
});
},
// 表单数据格式化
formatFormData() {
return {
// datePicker类型
datePicker: (formData) => {
console.log('datePicker', formData);
const type = this.fieldItemOptions.filter(
(c) => c.type === "datePicker"
);
if (type.length) {
type.forEach((c) => {
formData[c.key] = formData[c.key].format('YYYY-MM-DD');
});
}
},
// 其他类型
};
},
// 处理关联表单项,只处理关联项,相比之前写onValuesChange优化了性能
handleChange(e, field, fieldIndex) {
if (this.fieldItemRelativeOptions.length) {
this.fieldItemRelativeOptions.forEach((c) => {
if (c.key === field.key) {
c.relativeList.forEach((d) => {
const target = this.fieldOptions.find((k) => k.key === d.key);
const targetIndex = this.fieldOptions.findIndex(
(k) => k.key === d.key
);
d.props.forEach(async (x) => {
this.fieldItemOptions[targetIndex][x] = await target[x](
this.form.getFieldsValue()
);
});
});
}
});
}
},
},
mounted() {
this.initForm();
},
};
</script>
3.弹窗组件a-modal
嵌套动态表单组件,抽屉组件a-drawer
也可采用类似的方法进行封装;
dynamic-form-modal.vue
<template>
<a-modal
:title="isEdit ? '编辑' : '新增'"
:visible="visible"
destroyOnClose
@cancel="handleClose()"
:bodyStyle="{ maxHeight: `calc(100vh - ` + 300 + `px)`, overflowY: 'auto' }"
v-bind="$attrs"
>
<template slot="footer">
<a-button @click="handleClose()">取消</a-button>
<a-button type="primary" @click="handleSubmitDebounce()">
<a-icon v-if="confirmLoading" type="loading"></a-icon>
{{ confirmLoading ? "正在提交..." : $attrs.okText || "确定" }}
</a-button>
</template>
<a-spin :spinning="confirmLoading">
<dynamic-form
ref="dynamicForm"
:fieldOptions="fieldOptions"
:labelCol="labelCol"
:wrapperCol="wrapperCol"
:model="model"
:modelKey="modelKey"
></dynamic-form>
</a-spin>
</a-modal>
</template>
<script>
import { debounce } from "@/common/utils";
import DynamicForm from "./dynamic-form";
export default {
components: { DynamicForm },
props: {
// 表单域配置
fieldOptions: {
type: Array,
default: () => [],
},
// 弹窗显示or隐藏
visible: {
type: Boolean,
default: false,
},
// 编辑时表单回显的默认数据
model: {
type: Object,
default: () => ({}),
},
// 判断编辑or新增的key
modelKey: {
type: String,
default: "id",
},
// 标签宽度
labelCol: {
type: Object,
default: () => {
return {
xs: { span: 24 },
sm: { span: 6 },
};
},
},
// 控件宽度
wrapperCol: {
type: Object,
default: () => {
return {
xs: { span: 24 },
sm: { span: 16 },
};
},
},
// 新增api
addApi: {
type: Function,
default: () => function () {},
},
// 编辑api
updateApi: {
type: Function,
default: () => function () {},
},
// 补充的参数
params: {
type: Object,
default: () => ({}),
},
},
computed: {
// 判断编辑or新增
isEdit() {
return !!this.model[this.modelKey];
},
// 操作文本
actionText() {
return this.isEdit ? "编辑" : "新增";
},
},
data() {
return {
// 提交按钮加载状态
confirmLoading: false,
// 提交按钮防抖
handleSubmitDebounce: debounce(this.handleOk, 500),
};
},
methods: {
// 模态框提交表单
handleOk() {
if (this.confirmLoading) {
this.$message.info("正在提交表单,请稍后再操作");
return;
}
this.$refs.dynamicForm
.handleSubmit()
.then(async (formData) => {
// 新增参数和编辑参数
this.confirmLoading = true;
let resultParams = { ...formData, ...this.params };
if (this.isEdit) {
resultParams[this.modelKey] = this.model[this.modelKey];
}
// 新增api或编辑api
console.log("弹窗参数", resultParams);
let api = this.isEdit ? this.updateApi : this.addApi;
api(resultParams)
.then(() => {
this.handleClose();
this.$message.success(`${this.actionText}成功`);
// 提交表单后的回调
this.$emit("afterSubmit", resultParams);
})
.catch(() => {
this.$message.error(`${this.actionText}失败`);
})
.finally(() => {
this.confirmLoading = false;
});
})
.catch(() => {});
},
// 关闭
handleClose() {
// 关闭弹窗的回调
this.$emit("update:visible", false);
this.$emit("close");
let form = this.$refs.dynamicForm.form;
// 延迟清空表单
let timer = setTimeout(() => {
form.resetFields();
clearTimeout(timer);
timer = null;
form = null;
}, 100);
},
},
};
</script>
<style lang='less' scoped>
</style>
4.使用该弹窗组件,注意使用时传封装好的新增api
/编辑api
demo.vue
<template>
<div style="display: flex; height: 100vh; width: 100vw">
<div style="padding: 32px; border: 1px solid #ccc; margin: auto">
<a-button style="margin-right: 32px" @click="openModal()">新增</a-button>
<a-button type="primary" @click="openModal(record)">编辑</a-button>
</div>
<DynamicFormModal
:fieldOptions="fieldOptions"
:visible.sync="showModal"
:model="model"
width="50%"
okText="提交"
@afterSubmit="() => '提交表单后的操作,比如刷新table数据'"
></DynamicFormModal>
</div>
</template>
<script>
import DynamicFormModal from "./dynamic-form-modal.vue";
export default {
components: {
DynamicFormModal,
},
data() {
return {
showModal: false,
fieldOptions: [
{
label: "姓名",
key: "name",
value: "",
type: "text",
required: true,
disabled: (formData) => formData.country === 2,
max: 20,
},
{
label: "性别",
key: "sex",
value: 1,
type: "radio",
required: true,
options: [
{
value: 1,
label: "男",
},
{
value: 2,
label: "女",
},
],
},
{
label: "生日",
key: "birthday",
value: null,
type: "datePicker",
required: true,
},
{
label: "兴趣爱好",
key: "hobby",
value: [],
type: "checkbox",
required: true,
options: [
{
value: 1,
label: "足球",
},
{
value: 2,
label: "篮球",
},
{
value: 3,
label: "排球",
},
],
hidden: (formData) => formData.country === 2,
},
{
label: "国家",
key: "country",
value: undefined,
type: "select",
required: true,
options: async () => await this.getCountryList(),
relativeList: [
{
key: "name",
props: ["disabled"],
},
{
key: "hobby",
props: ["hidden"],
},
{
key: "desc",
props: ["required"],
},
],
},
{
label: "个人简介",
key: "desc",
value: "",
type: "textarea",
required: (formData) => formData.country === 2,
},
],
model: {},
record: {
id: 1,
message: "我是数据源",
name: "动态表单",
sex: 2,
hobby: [1, 2],
country: 1,
desc: "这是一个简单的例子",
},
};
},
methods: {
openModal(record = {}) {
this.model = { ...record };
this.showModal = true;
},
// 模拟获取后台数据
getCountryList() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = [
{
value: 1,
label: "中国",
},
{
value: 2,
label: "美国",
},
{
value: 3,
label: "俄罗斯",
},
];
resolve(data);
}, 1200);
});
},
},
};
</script>