vue + ant-design-vue + vuedraggable 实现可视化表单设计器
Vue 可视化表单设计器:从0到1实现拖拽式表单配置
本文将基于 Vue + Ant Design Vue + vuedraggable,手把手教你实现一个可视化表单设计器,支持组件拖拽、属性配置、表单预览与保存。
一、项目介绍
1. 核心用途
通过拖拽组件快速生成表单,无需编写代码即可配置:
- 单行文本、多行文本、数字等基础字段
- 下拉选择、单选/复选框组等选择类字段
- 日期选择、文件上传、开关等特殊字段
- 自定义字段标签、必填规则、占位提示等属性
2. 技术栈
技术/框架 | 用途 |
---|---|
Vue 2.x | 前端框架(核心逻辑承载) |
Ant Design Vue | UI组件库(提供表单组件、按钮、模态框等) |
vuedraggable | 拖拽插件(实现组件拖拽、字段排序) |
JavaScript | 逻辑处理(拖拽事件、属性更新、表单保存) |
二、核心功能与视觉效果
先通过3张截图直观了解设计器的功能模块,后续将逐一实现这些效果:
1. 初始设计界面(截图1)
- 顶部工具栏:保存表单、预览表单、输入表单标题
- 左侧组件栏:提供9类可拖拽组件(单行文本、多行文本、数字等,剩余可自定义)
- 中间画布:拖拽组件的目标区域,初始显示“从左侧拖拽组件到此处”
2. 组件属性配置(截图2)
- 拖拽组件到画布后,右侧属性面板自动激活
- 可配置字段标签(如“单行文本1”)、字段名称(如“checkbox_6”)
- 选择类组件(如复选框组)支持添加/删除选项(如“选项1”“选项2”)
- 支持配置“是否必填”“占位提示”等基础规则
3. 表单预览效果(截图3)
- 点击“预览”按钮,打开模态框展示最终表单样式
- 预览界面与实际填写界面一致,支持查看字段布局和交互效果
- 提供“取消”“确定”按钮,模拟表单提交流程
三、分步实现教程
步骤1:环境准备
-
安装依赖:
# 安装 Ant Design Vue(UI组件) npm install ant-design-vue@1.7.8 --save # 安装 vuedraggable(拖拽功能) npm install vuedraggable@2.24.3 --save
-
在
main.js
全局引入 Ant Design Vue:import Vue from 'vue'; import Antd from 'ant-design-vue'; import 'ant-design-vue/dist/antd.css'; Vue.use(Antd);
步骤2:搭建入口页面(FormDesignView.vue)
入口页面负责承载表单设计器,处理路由参数(编辑/新建表单)和页面导航,对应文档中的 FormDesignView.vue
。
代码实现
<template><div class="form-design-page"><!-- 页面头部:标题、返回按钮、副标题 --><a-page-header title="表单设计器" sub-title="自定义督导表单,支持拖拽配置字段" @back="handleGoBack" /><!-- 表单设计器容器(白色背景+阴影,提升视觉体验) --><div class="designer-container"><form-designer :form-id="formId" @save-success="handleSaveSuccess" @cancel="handleGoBack" /></div></div>
</template><script>
// 引入核心表单设计器组件
import FormDesigner from './components/form-design/FormDesigner';export default {name: 'FormDesignView',components: { FormDesigner },data() {return {// 从路由参数获取formId(编辑场景),新建时为nullformId: this.$route.query.formId || null}},methods: {// 返回上一页handleGoBack() {this.$router.go(-1);},// 表单保存成功后的回调(接收设计器传递的formId和表单名称)handleSaveSuccess(formId, formName) {// 提示保存成功this.$message.success(`表单【${formName}】保存成功`);// 新建表单时,更新formId并同步到路由(避免刷新丢失)if (!this.formId) {this.formId = formId;this.$router.push({path: '/form-design',query: { formId } // 路由携带formId,支持后续编辑});}}}
}
</script><style scoped>
.form-design-page {padding: 16px;background-color: #f5f7fa; /* 页面背景色,区分内容区域 */min-height: 100vh;
}
.designer-container {margin-top: 20px;background-color: #fff;border-radius: 4px;box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05); /* 轻微阴影,提升层次感 */padding: 20px;
}
</style>
步骤3:实现核心设计器(FormDesigner.vue)
这是整个项目的核心,包含“左侧组件拖拽”“中间画布渲染”“右侧属性配置”“预览/保存”四大模块,对应文档中的 FormDesigner.vue
。
3.1 模板结构(Template)
先搭建页面骨架,分为工具栏、左侧组件栏、中间画布、右侧属性面板、预览模态框:
<template><div class="form-designer"><a-card title="表单设计器"><!-- 1. 顶部工具栏:保存、预览、表单标题输入 --><div class="designer-toolbar"><a-button type="primary" @click="saveForm">保存表单</a-button><a-button style="margin-left: 10px" @click="previewForm">预览</a-button><a-input v-model="formTitle" placeholder="请输入表单标题" style="width: 300px; margin-left: 20px" /></div><!-- 2. 核心容器:左侧组件栏 + 中间画布 + 右侧属性面板 --><div class="designer-container"><!-- 左侧:可拖拽组件列表 --><div class="designer-components"><h3>表单组件</h3><div class="component-item" v-for="item in componentList" :key="item.type" draggable @dragstart="handleDragStart(item)"><a-icon :type="item.icon" /><span>{{ item.name }}</span></div></div><!-- 中间:画布(拖拽目标区域 + 已添加字段) --><div class="designer-canvas" @dragover.prevent @drop="handleDrop" ><!-- 表单标题(为空时显示“未命名表单”) --><div class="canvas-title">{{ formTitle || '未命名表单' }}</div><!-- 已添加的字段(支持拖拽排序) --><draggable v-model="formFields" :options="{animation: 200, handle: '.form-item-header', ghostClass: 'form-item-ghost' }"@end="handleDragEnd" class="no-select"><!-- 循环渲染已添加的字段 --><div class="form-item" v-for="(field, index) in formFields" :key="field.id"><!-- 字段头部(可拖拽、编辑、删除、上下移动) --><div class="form-item-header no-select"><a-icon type="menu" style="cursor: move; margin-right: 8px;" /><span>{{ field.label }}</span><div class="form-item-actions"><a-icon type="up" @click="moveField(index, 'up')" /> <!-- 上移 --><a-icon type="down" @click="moveField(index, 'down')" /> <!-- 下移 --><a-icon type="edit" @click="editField(index)" /> <!-- 编辑属性 --><a-icon type="delete" @click="deleteField(index)" /> <!-- 删除字段 --></div></div><!-- 字段预览(画布中显示禁用状态,避免编辑干扰) --><div class="form-item-preview"><!-- 单行文本 --><template v-if="field.type === 'text'"><a-input disabled v-model="field.value" :placeholder="field.placeholder" /></template><!-- 多行文本 --><template v-if="field.type === 'textarea'"><a-textarea disabled v-model="field.value" rows="3" :placeholder="field.placeholder" /></template><!-- 其他组件(数字、下拉、单选等)按此格式添加,参考文档完整代码 --></div></div></draggable><!-- 画布为空时的提示 --><div v-if="formFields.length === 0" class="empty-canvas"><a-icon type="plus-circle" /><p>从左侧拖拽组件到此处</p></div></div><!-- 右侧:组件属性配置面板(仅选中字段时显示) --><div class="designer-properties" v-if="currentField"><h3>组件属性</h3><a-form layout="vertical"><!-- 字段标签 --><a-form-item label="字段标签"><a-input v-model="currentField.label" /></a-form-item><!-- 字段名称(用于表单提交的key) --><a-form-item label="字段名称"><a-input v-model="currentField.name" /></a-form-item><!-- 是否必填 --><a-form-item label="是否必填"><a-switch v-model="currentField.required" /></a-form-item><!-- 占位提示 --><a-form-item label="占位提示"><a-input v-model="currentField.placeholder" /></a-form-item><!-- 选择类组件(下拉/单选/复选)的选项配置 --><template v-if="['select', 'radio', 'checkbox'].includes(currentField.type)"><a-form-item label="选项配置"><a-button type="dashed" style="width: 100%" @click="addOption"><a-icon type="plus" /> 添加选项</a-button><!-- 循环渲染选项 --><div v-for="(option, i) in currentField.options" :key="i" class="option-item"><a-input v-model="option.label" placeholder="选项文本" style="width: 45%; margin-right: 10px" /><a-input v-model="option.value" placeholder="选项值" style="width: 45%" /><a-icon type="close" @click="removeOption(i)" style="margin-left: 10px; cursor: pointer" /></div></a-form-item></template><!-- 其他属性(如文本最大长度、上传类型)按此格式添加,参考文档完整代码 --></a-form></div></div></a-card><!-- 3. 预览模态框(点击“预览”时打开) --><a-modal title="表单预览" :visible="previewVisible" @cancel="previewVisible = false" width="600px":footer="[{ text: '取消', onClick: () => (previewVisible = false) }, { text: '确定', onClick: () => (previewVisible = false) }]"><a-form :model="previewFormData"><!-- 循环渲染预览字段(与画布逻辑类似,但不禁用) --><a-form-item v-for="(field, index) in formFields" :key="field.id" :label="field.label" :required="field.required"><!-- 单行文本(预览时可编辑) --><template v-if="field.type === 'text'"><a-input v-model="field.value" :placeholder="field.placeholder" /></template><!-- 其他组件预览逻辑,参考文档完整代码 --></a-form-item></a-form></a-modal></div>
</template>
3.2 逻辑处理(Script)
实现拖拽事件、属性更新、保存预览等核心逻辑:
<script>
// 引入拖拽组件
import draggable from 'vuedraggable';export default {components: { draggable },data() {return {formTitle: '', // 表单标题formFields: [], // 已添加的字段列表componentList: [{ type: 'text', name: '单行文本', icon: 'edit' },{ type: 'textarea', name: '多行文本', icon: 'align-left' },{ type: 'number', name: '数字', icon: 'calculator' },{ type: 'select', name: '下拉选择', icon: 'down' },{ type: 'radio', name: '单选框组', icon: 'check-circle' },{ type: 'checkbox', name: '复选框组', icon: 'check-square' },{ type: 'date', name: '日期选择', icon: 'calendar' },{ type: 'upload', name: '文件上传', icon: 'upload' },{ type: 'switch', name: '开关', icon: 'swap' }],currentField: null, // 当前选中的字段(用于属性配置)previewVisible: false, // 预览模态框显示状态previewFormData: {} // 预览表单数据}},methods: {// 1. 拖拽开始:生成新字段的默认配置handleDragStart(component) {// 生成唯一字段ID(避免重复)const fieldId = `field_${Date.now()}_${Math.floor(Math.random() * 1000)}`;// 新字段的默认配置const newField = {id: fieldId,type: component.type, // 组件类型(如text、radio)label: `${component.name} ${this.formFields.length + 1}`, // 默认标签(如“单行文本1”)name: `${component.type}_${this.formFields.length + 1}`, // 默认字段名(如“text_1”)required: false, // 默认非必填placeholder: `请输入${component.name}`, // 默认占位提示...this.getDefaultFieldProps(component.type) // 组件专属默认属性(如选项、最大长度)};// 存储拖拽的字段数据(用于拖拽释放时获取)event.dataTransfer.setData('field', JSON.stringify(newField));},// 2. 拖拽释放:将新字段添加到画布handleDrop(event) {const fieldData = event.dataTransfer.getData('field');if (fieldData) {const newField = JSON.parse(fieldData);this.formFields.push(newField); // 添加到字段列表this.currentField = { ...newField }; // 自动选中新字段,方便配置属性}},// 3. 获取组件专属默认属性(如单选框默认2个选项)getDefaultFieldProps(type) {const props = {};switch (type) {case 'text':case 'textarea':props.value = ''; // 文本类默认值为空props.maxLength = 100; // 默认最大长度100break;case 'select':case 'radio':case 'checkbox':props.value = undefined; // 选择类默认未选中props.options = [{ label: '选项1', value: '1' }, { label: '选项2', value: '2' }]; // 默认2个选项break;case 'upload':props.fileList = []; // 上传类默认无文件props.accept = 'image/*'; // 默认支持图片上传props.maxCount = 1; // 默认最多上传1个文件break;default:break;}return props;},// 4. 编辑字段:选中字段并加载属性editField(index) {this.currentField = { ...this.formFields[index] }; // 深拷贝,避免直接修改原数据},// 5. 删除字段:弹窗确认后删除deleteField(index) {this.$confirm({title: '确认删除',content: '确定要删除这个字段吗?',onOk: () => {this.formFields.splice(index, 1); // 从列表中删除// 若删除的是当前选中字段,清空属性面板if (this.currentField && this.formFields.findIndex(f => f.id === this.currentField.id) === -1) {this.currentField = null;}}});},// 6. 字段上下移动moveField(index, direction) {if (direction === 'up' && index > 0) {// 上移:与前一个字段交换位置[this.formFields[index], this.formFields[index - 1]] = [this.formFields[index - 1], this.formFields[index]];} else if (direction === 'down' && index < this.formFields.length - 1) {// 下移:与后一个字段交换位置[this.formFields[index], this.formFields[index + 1]] = [this.formFields[index + 1], this.formFields[index]];}this.formFields = [...this.formFields]; // 触发数组更新},// 7. 为选择类组件添加选项addOption() {if (!this.currentField.options) this.currentField.options = [];this.currentField.options.push({label: `新选项${this.currentField.options.length + 1}`,value: (this.currentField.options.length + 1).toString()});},// 8. 删除选择类组件的选项removeOption(index) {this.currentField.options.splice(index, 1);},// 9. 保存表单:校验 + 生成表单数据saveForm() {// 校验:表单标题不能为空if (!this.formTitle) {this.$message.error('请输入表单标题');return;}// 校验:至少添加一个字段if (this.formFields.length === 0) {this.$message.error('请添加表单字段');return;}// 生成最终表单数据(可提交到后端存储)const formData = {title: this.formTitle,fields: this.formFields};console.log('保存的表单数据:', formData);// 触发父组件的保存成功回调(传递表单ID和名称,实际项目需后端返回formId)this.$emit('save-success', 'form_' + Date.now(), this.formTitle);},// 10. 预览表单:初始化预览数据并打开模态框previewForm() {if (this.formFields.length === 0) {this.$message.error('请添加表单字段');return;}this.previewFormData = {};// 初始化预览数据(为空)this.formFields.forEach(field => {this.previewFormData[field.name] = '';});this.previewVisible = true; // 打开预览模态框},// 11. 字段排序结束:打印日志(可扩展后端同步排序)handleDragEnd() {console.log('字段排序已更新:', this.formFields.map(field => field.id));}},// 监听currentField变化:实时更新画布中的字段属性watch: {currentField: {handler(newVal) {if (newVal) {// 找到当前字段在列表中的索引const index = this.formFields.findIndex(f => f.id === newVal.id);if (index !== -1) {this.formFields.splice(index, 1, { ...newVal }); // 更新字段属性}}},deep: true // 深度监听(监听对象内部属性变化)}}
}
</script>
3.3 样式美化(Style)
通过CSS优化布局和交互体验,确保与截图效果一致:
<style scoped>
/* 设计器整体容器 */
.form-designer {padding: 20px;
}/* 顶部工具栏 */
.designer-toolbar {margin-bottom: 20px;display: flex;align-items: center;
}/* 核心容器(三栏布局) */
.designer-container {display: flex;gap: 20px;height: calc(100vh - 180px); /* 固定高度,超出滚动 */
}/* 左侧组件栏 */
.designer-components {width: 200px;border: 1px solid #e8e8e8;border-radius: 4px;padding: 10px;overflow-y: auto; /* 组件过多时滚动 */
}/* 单个组件项 */
.component-item {padding: 10px;margin-bottom: 10px;border: 1px solid #e8e8e8;border-radius: 4px;cursor: move;display: flex;align-items: center;gap: 8px;background: #fff;
}
/* 组件项 hover 效果 */
.component-item:hover {border-color: #1890ff; /* AntD主题色 */background: #f0f7ff;
}/* 中间画布 */
.designer-canvas {flex: 1; /* 占满剩余宽度 */border: 1px dashed #e8e8e8;border-radius: 4px;padding: 20px;overflow-y: auto;background: #fafafa;
}/* 画布标题 */
.canvas-title {text-align: center;font-size: 18px;font-weight: bold;margin-bottom: 30px;
}/* 单个字段容器 */
.form-item {background: #fff;padding: 15px;margin-bottom: 15px;border-radius: 4px;border: 1px solid #e8e8e8;
}/* 字段拖拽占位样式 */
.form-item-ghost {border: 1px dashed #1890ff !important;background-color: #e6f7ff !important;opacity: 0.8;
}/* 字段头部(可拖拽区域) */
.form-item-header {display: flex;justify-content: space-between;margin-bottom: 10px;color: #1890ff;align-items: center;cursor: move;
}/* 字段操作按钮组(上下移、编辑、删除) */
.form-item-actions {display: flex;gap: 5px;
}
.form-item-actions .anticon {cursor: pointer;font-size: 14px;
}/* 右侧属性面板 */
.designer-properties {width: 300px;border: 1px solid #e8e8e8;border-radius: 4px;padding: 10px;overflow-y: auto;
}/* 画布为空时的提示 */
.empty-canvas {height: 100%;display: flex;flex-direction: column;align-items: center;justify-content: center;color: #999;
}
.empty-canvas .anticon {font-size: 48px;margin-bottom: 10px;
}/* 选项配置项(如单选框的选项) */
.option-item {display: flex;align-items: center;margin-top: 10px;
}/* 禁止文本选中(避免拖拽时选中文本) */
.no-select {-webkit-user-select: none; /* Chrome/Safari */-moz-user-select: none; /* Firefox */-ms-user-select: none; /* IE/Edge */user-select: none; /* 标准属性 */
}
</style>
四、总结与优化方向
1. 已实现功能
- ✅ 拖拽组件生成表单
- ✅ 自定义字段属性(标签、必填、占位提示等)
- ✅ 字段排序、编辑、删除
- ✅ 表单预览与保存校验