vue2+antd 实现一个可自由拖拽配置的动态表单模板
Vue.Draggable 是一个基于 Vue.js 的拖放组件,它允许用户在 Vue.js 应用中实现拖放功能。该项目是基于 Sortable.js 构建的,提供了丰富的功能和灵活的配置选项,适用于各种拖放场景。说一下我的需求吧,我这边是要实现一个类似钉钉那种行政管理模块,需要开发一个可以设置多套表单模板的动态表单;整体代码就在这里了,如果有需要的可以试一下,把接口的数据自己换一下,我这个为了传
说一下我的需求吧,我这边是要实现一个类似钉钉那种行政管理模块,需要开发一个可以设置多套表单模板的动态表单;基本情况就是这样,好了,我们直接开始吧

表单设计区域拖拽排序,表单设置补全内容,这三项代码配置即可,我只简单的配置了几项
首先第一步 安装Vue.Draggable
我这里使用的是draggable实现拖拽
draggable简介
Vue.Draggable 是一个基于 Vue.js 的拖放组件,它允许用户在 Vue.js 应用中实现拖放功能。该项目是基于 Sortable.js 构建的,提供了丰富的功能和灵活的配置选项,适用于各种拖放场景。
draggable的使用文档可以参考vue.draggable中文文档 - itxst.com
npm install vuedraggable
# 或者使用 yarn
yarn add vuedraggable
安装完成之后就可以开始使用了,在需要使用的页面引入draggable
vue2需要用components注册组件
import draggable from 'vuedraggable';
components: { draggable, },
然后就是开始使用了,在需要使用到拖拽的内容进行包裹
<draggable v-model="formComponents" @end="onDragEnd" handle=".handle">
<transition-group>
<div v-for="(component, index) in formComponents" :key="index" class="form-component"
:class="{ 'selected': selectedComponent === component, 'hover-effect': true }"
@click="selectComponent(component)">
<div class="handle">
<a-row type="flex" justify="space-between" align="middle">
<a-col :span="6">
<span>{{ component.props.label || '未命名' }}</span>
</a-col>
<a-col :span="14">
<component :is="component.type" v-bind="component.props" />
</a-col>
<a-col :span="4">
<a-popconfirm title="确定删除当前控件吗?" ok-text="确定" cancel-text="取消"
@confirm="removeComponent(index)">
<a-button type="link" v-if="formComponents.length > 1">删除</a-button>
</a-popconfirm>
</a-col>
</a-row>
</div>
</div>
</transition-group>
</draggable>
组件库我使用的是动态组件生产这样的话好进行配置
html代码
<a-card title="组件库">
<a-button class="atd-btn" v-for="item in assemblyData" :key="item.key" :icon="item.icon"
type="dashed" @click="addComponent(item.key)">{{
item.value }}</a-button>
</a-card>
addComponent(type) {
const component = {
type: '',
props: {},
fieldTitle: '',
dataInfType: undefined,//select数据类型
};
switch (type) {
case 'TEXT':
component.type = 'a-input';
component.fieldTitle = 'TEXT';
component.props = { placeholder: '请输入', label: '文本框', style: { width: '100%' }, required: true };
break;
case 'LIST':
component.type = 'a-select';
component.fieldTitle = 'LIST';
component.props = { placeholder: '请选择', label: '选择框', style: { width: '100%' }, required: true };
break;
case 'NUMBER':
component.type = 'a-input-number';
component.fieldTitle = 'NUMBER';
component.props = { placeholder: '请输入', label: '数字输入', style: { width: '100%' }, required: true };
break;
case 'MONEY':
component.type = 'a-input-number';
component.fieldTitle = 'MONEY';
component.props = { placeholder: '请输入', label: '金额输入', style: { width: '100%' }, step: 0.01, required: true };
break;
case 'DATE':
component.type = 'a-date-picker';
component.fieldTitle = 'DATE';
component.props = { placeholder: '请选择', label: '日期', style: { width: '100%' }, required: true };
break;
default:
break;
}
this.formComponents.push(component);
this.selectComponent(component);
},
整体实现
<template>
<a-modal title="获取审批表单详情" width="80%" :visible="visible" :confirm-loading="confirmLoading" @ok="submitClick"
@cancel="closeCancel">
<a-row>
<a-col :span="8">
<a-form-model-item ref="name" label="审批表单名称" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-input v-model="dataTemp.approveName" />
</a-form-model-item>
</a-col>
<a-col :span="8">
<a-form-model-item ref="name" label="抄送人类型" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }"
prop="name">
<a-select v-model="dataTemp.noticeType" style="width: 100%" placeholder="请选择"
option-filter-prop="children" show-search :allowClear="true" :filter-option="filterOption">
<a-select-option v-for="item in recipientTypeData" :key="item.key" :value="item.key"
:label="item.value">{{
item.value }}</a-select-option>
</a-select>
</a-form-model-item>
</a-col>
<a-col :span="8">
<a-form-model-item ref="name" v-if="dataTemp.noticeType == 'ORDER_ROLE'" label="指定角色选择"
:label-col="{ span: 6 }" :wrapper-col="{ span: 16 }" prop="name">
<a-select mode="multiple" v-model="dataTemp.roleIdList" style="width: 100%" placeholder="请选择领用人"
option-filter-prop="children" show-search :allowClear="true" :filter-option="filterOption">
<a-select-option v-for="item in roleData" :key="item.id" :value="item.id" :label="item.roleName">{{
item.roleName }}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item ref="name" v-if="dataTemp.noticeType == 'ORDER_PERSON'" label="指定人员选择"
:label-col="{ span: 6 }" :wrapper-col="{ span: 16 }" prop="name">
<a-select mode="multiple" v-model="dataTemp.employeeIdList" style="width: 100%" placeholder="请选择领用人"
option-filter-prop="children" show-search :allowClear="true" :filter-option="filterOption">
<a-select-option v-for="item in allemployee" :key="item.id" :value="item.id"
:label="item.actualName">{{ item.actualName }}</a-select-option>
</a-select>
</a-form-model-item>
</a-col>
</a-row>
<div class="form-designer">
<a-row :gutter="16">
<a-col :span="6">
<a-card title="组件库">
<a-button class="atd-btn" v-for="item in assemblyData" :key="item.key" :icon="item.icon"
type="dashed" @click="addComponent(item.key)">{{
item.value }}</a-button>
</a-card>
</a-col>
<a-col :span="12">
<a-card title="表单设计区">
<draggable v-model="formComponents" @end="onDragEnd" handle=".handle">
<transition-group>
<div v-for="(component, index) in formComponents" :key="index" class="form-component"
:class="{ 'selected': selectedComponent === component, 'hover-effect': true }"
@click="selectComponent(component)">
<div class="handle">
<a-row type="flex" justify="space-between" align="middle">
<a-col :span="6">
<span>{{ component.props.label || '未命名' }}</span>
</a-col>
<a-col :span="14">
<component :is="component.type" v-bind="component.props" />
</a-col>
<a-col :span="4">
<a-popconfirm title="确定删除当前控件吗?" ok-text="确定" cancel-text="取消"
@confirm="removeComponent(index)">
<a-button type="link" v-if="formComponents.length > 1">删除</a-button>
</a-popconfirm>
</a-col>
</a-row>
</div>
</div>
</transition-group>
</draggable>
</a-card>
</a-col>
<a-col :span="6">
<a-card title="表单设置">
<a-form v-if="selectedComponent">
<a-form-item label="名称">
<a-input v-model="selectedComponent.props.label" />
</a-form-item>
<a-form-item label="提示文字">
<a-input v-model="selectedComponent.props.placeholder" />
</a-form-item>
<a-form-item v-if="selectedComponent.type === 'a-select'" label="选择数据来源">
<a-select v-model="selectedComponent.dataInfType" style="width: 100%" placeholder="请选择"
option-filter-prop="children" show-search :allowClear="true"
:filter-option="filterOption" @change="getChangeSelect">
<a-select-option v-for="item in selectData" :key="item.key" :value="item.key"
:label="item.value">{{ item.value }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="是否必填">
<a-switch v-model="selectedComponent.props.required" />
</a-form-item>
</a-form>
</a-card>
</a-col>
</a-row>
</div>
</a-modal>
</template>
<script>
import draggable from 'vuedraggable';
export default {
components: { draggable, },
props: {
dataTemp: {
type: Array,
},
visibleEdit: {
type: Boolean,
},
},
data() {
return {
formComponents: [],
allemployee: [],
roleData: [],
visible: false,
selectedComponent: null,
recipientTypeData: [{ key: 'SELF', value: '发起人自己' },
{ key: 'ORDER_PERSON', value: '指定人员' },
{ key: 'ORDER_ROLE', value: '指定角色' }, { key: 'DEP_LEADER', value: '部门主管' }, { key: 'CENTER_LEADER', value: '中心负责人' },],//抄送人类型
assemblyData: [{ key: 'TEXT', value: '输入框', icon: 'file-text' }, { key: 'LIST', value: '选择器', icon: 'select' }, { key: 'NUMBER', value: '数字框', icon: 'number' }, { key: 'DATE', value: '日期选择', icon: 'carry-out' }, { key: 'MONEY', value: '金额输入', icon: 'money-collect' }],
selectData: [{ key: 'EMP', value: '通讯录员工' }, { key: 'DEP', value: '部门' }, { key: 'STORE', value: '仓库' }, { key: 'ADMIN', value: '仓库管理员' }, { key: 'ASSET', value: '资产' },],
};
},
watch: {
visibleEdit(oldVal, newVal) {
this.visible = true
console.log(this.dataTemp.fieldVOS);
this.dataTemp.noticeType = this.dataTemp.noticeType.name
//重置数据类型
if (this.dataTemp.employeeIdList) {
this.dataTemp.employeeIdList = this.dataTemp.employeeIdList.map(item => parseInt(item, 10));
} else {
this.dataTemp.employeeIdList = []
}
if (this.dataTemp.roleIdList) {
this.dataTemp.roleIdList = this.dataTemp.roleIdList.map(item => parseInt(item, 10));
} else {
this.dataTemp.roleIdList = []
}
//获取员工接口
this.getselects()
//获取所有角色
this.getRoleDataList()
let typeDate = { TEXT: 'a-input', LIST: 'a-select', NUMBER: 'a-input-number', DATE: 'a-date-picker', MONEY: 'a-input-number' }
let requirdData = { 0: false, 1: true }
if (this.dataTemp.fieldVOS.length > 0) {
//回显的数据处理
this.formComponents = this.dataTemp.fieldVOS
this.formComponents.forEach(item => {
if (item.dataType) {
item.dataInfType = item.dataType.name
}
item.fieldTitle = item.fieldType.name,
item.type = typeDate[item.fieldType.name],
item.props = { label: item.fieldName, placeholder: item.tip, required: requirdData[item.required], style: { width: '100%' } }
});
}
//处理流程,接口好上传,这里是用不到的
this.dataTemp.nodeVOS.forEach(item => {
item.approveType = item.approveType.name
})
console.log(this.formComponents);
}
},
methods: {
filterOption(input, option) {
return (
option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
)
},
getChangeSelect(value) {
//视图不更新,强制更新视图
this.$nextTick(() => {
this.$forceUpdate();
})
},
//点击取消
closeCancel() {
this.visible = false
this.formComponents = []
this.selectedComponent = null
},
addComponent(type) {
const component = {
type: '',
props: {},
fieldTitle: '',
dataInfType: undefined,//select数据类型
};
switch (type) {
case 'TEXT':
component.type = 'a-input';
component.fieldTitle = 'TEXT';
component.props = { placeholder: '请输入', label: '文本框', style: { width: '100%' }, required: true };
break;
case 'LIST':
component.type = 'a-select';
component.fieldTitle = 'LIST';
component.props = { placeholder: '请选择', label: '选择框', style: { width: '100%' }, required: true };
break;
case 'NUMBER':
component.type = 'a-input-number';
component.fieldTitle = 'NUMBER';
component.props = { placeholder: '请输入', label: '数字输入', style: { width: '100%' }, required: true };
break;
case 'MONEY':
component.type = 'a-input-number';
component.fieldTitle = 'MONEY';
component.props = { placeholder: '请输入', label: '金额输入', style: { width: '100%' }, step: 0.01, required: true };
break;
case 'DATE':
component.type = 'a-date-picker';
component.fieldTitle = 'DATE';
component.props = { placeholder: '请选择', label: '日期', style: { width: '100%' }, required: true };
break;
default:
break;
}
this.formComponents.push(component);
this.selectComponent(component);
},
removeComponent(index) {
this.formComponents.splice(index, 1);
if (this.selectedComponent && this.formComponents.indexOf(this.selectedComponent) === -1) {
this.selectedComponent = null;
}
},
onDragEnd(event) {
// 拖拽结束后的处理逻辑
},
selectComponent(component) {
this.selectedComponent = component;
},
//获取所有角色
getRoleDataList() {
this.$api.getAllrole().then(res => {
if (res.code == 1) {
this.roleData = res.data.flatMap(item => item.roleList);
console.log(this.roleData);
}
})
},
//获取全部员工
getselects() {
this.$api.employeegetall().then(res => {
if (res.code == 1) {
this.allemployee = res.data
console.log(this.allemployee);
}
})
},
//点击提交
submitClick() {
if (!this.dataTemp.approveName) {
return this.$message.warning('请完善审批表单名称!')
}
if (!this.dataTemp.noticeType) {
return this.$message.warning('请完善抄送人类型!')
}
if (this.dataTemp.noticeType == 'ORDER_PERSON' && this.dataTemp.employeeIdList.length == 0) {
return this.$message.warning('请选择抄送人员!')
}
if (this.dataTemp.noticeType == 'ORDER_ROLE' && this.dataTemp.roleIdList.length == 0) {
return this.$message.warning('请选择指定角色!')
}
if (this.formComponents.length == 0) {
return this.$message.warning('表单设计区至少选择一项!')
}
let data = []
let requirdData = { false: 0, true: 1 }
let soutNumber = 1
this.formComponents.forEach(item => {
data.push({
id: item.id,
approveBasicId: this.dataTemp.id,
fieldName: item.props.label,
required: requirdData[item.props.required],
sortNum: soutNumber++,
tip: item.props.placeholder,
fieldType: item.fieldTitle,
operatorId: JSON.parse(localStorage.getItem('UserLoginInfo')).id,//创建人
dataType: item.dataInfType
})
})
// 检查是否有 a-input 类型且 dataType 为空的情况
console.log(this.formComponents);
const hasInvalidInput = this.formComponents.some(item => item.type === 'a-select' && !item.dataInfType);
if (hasInvalidInput) {
return this.$message.warning('请将下拉选择框的数据来源补充完整!')
}
this.dataTemp.fieldVOS = data
console.log(this.dataTemp);
this.$api.getFullUpdateAPI(this.dataTemp).then(res => {
if (res.success) {
this.$message.success('保存成功')
this.closeCancel()
} else {
this.$message.error(res.msg)
}
})
},
},
};
</script>
<style scoped lang="less">
.form-designer {
padding: 20px;
}
.atd-btn {
margin: 0 10px 10px 0;
}
.form-component {
margin-bottom: 10px;
cursor: move;
transition: transform 0.5s, box-shadow 0.5s;
/* 添加过渡效果 */
}
.handle {
cursor: move;
}
.selected {
border: 1.5px solid #1890ff;
padding: 5px;
/* 蓝色边框 */
}
/* 鼠标经过时的样式 */
.hover-effect:hover {
transform: scale(1.05);
/* 放大效果 */
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.1);
/* 高亮效果 */
}
</style>
整体代码就在这里了,如果有需要的可以试一下,把接口的数据自己换一下,我这个为了传参数据处理写的比较繁琐,哈哈哈
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐



所有评论(0)