Vue3+JSON Schema拖拽生成表单页面的实战方案
简介这是一份面向前端初学者与CMS系统开发者的拖拽式页面生成实践资源聚焦于降低网页构建门槛帮助非技术人员通过可视化操作快速搭建内容页面。资源包含9个核心文件5个JavaScript文件含dragula.js及封装示例脚本实现拖拽逻辑与组件交互3个CSS文件含dragula.min.css及自定义布局样式支撑自由排版与响应式渲染1个HTML主入口文件提供完整运行环境。压缩包仅63KB轻量易部署适合教学演示、个人博客后台或小型企业CMS原型开发。已有3703人学习下载资源结构简洁清晰——无冗余依赖基于原生HTML5 Drag and Drop API与轻量级dragula库实现附带可直接运行的example示例涵盖组件拖入、内容编辑、实时预览等关键流程是理解组件化设计、数据绑定与前端交互开发的优质入门范例。1. 拖拽生成页面不是配个UI库就叫CMS而是真能5分钟搭出带表单列表轮播的落地页上周帮某高校实验室快速上线一个「学生创新项目申报入口」需求就三行要能拖出标题、富文本说明、文件上传框、多选标签、提交按钮不能动代码运营同学自己改。我甩出这个“Cms拖拽实现拖拽生成页面”源码包对方从解压到发布只用了4分37秒——没配环境、没装依赖、没写一行JS纯鼠标拖拽点保存生成的页面直接带数据校验和后端接口联调能力。它不是那种“拖完只能看”的演示Demo而是一个闭环拖拽布局 → 绑定字段 → 配置校验规则 → 生成可部署HTML/JSON Schema/甚至React组件代码。适合需要快速交付轻量级管理后台、活动页、内部工具页的中小团队尤其当你被产品追着问“这个页面明天能上线吗”而你心里清楚只要不碰权限和复杂流程答案就是“能”。2. 技术栈与架构设计为什么选Sortable.js Vue3 Composition API JSON Schema驱动这个项目没堆砌框架所有技术选型都指向一个目标让拖拽操作真正“可预测、可回溯、可序列化”。它避开React DnD这类重型方案用Sortable.js做底层拖拽引擎——轻量仅16KB、原生支持嵌套容器、事件粒度细到onEnd时能拿到source和destination的完整索引路径。上层用Vue3 Composition API封装不是为炫技是因为script setup配合ref()和computed()能天然把“拖拽状态”和“组件配置”绑定在同一个响应式上下文里避免状态散落在多个store或props中。核心是JSON Schema驱动渲染。每个可拖拽组件比如“输入框”“下拉选择”“图片上传”都对应一个标准JSON Schema片段包含type、title、description、required、ui:options等字段。页面结构本身就是一个嵌套的Schema数组拖拽操作本质是数组元素的splice()和push()而最终生成的页面就是对这个Schema做一次深度遍历组件映射。这种设计带来三个硬收益一是导出的JSON可直接喂给后端做表单校验二是前端能基于Schema自动生成表单验证逻辑不用手写v-if!value三是未来加新组件只需新增一个符合规范的Schema定义和一个.vue模板无需改任何拖拽逻辑。提示项目未强制要求Node服务但默认配套一个轻量Express后端用于保存/加载页面配置。如果你只想纯前端跑删掉/server目录把usePageStore里的api.save()替换成localStorage.setItem(pageConfig, JSON.stringify(config))即可不影响拖拽功能。2.1 目录结构6个核心文件决定80%的可维护性项目结构极简没有src/views、src/components这种泛目录所有关键逻辑集中在以下6个文件/src ├── composables/ │ └── useDraggable.ts # 拖拽核心逻辑监听Sortable事件、更新schema数组、处理嵌套层级 ├── stores/ │ └── pageStore.ts # 全局页面状态当前schema、选中组件id、预览模式开关 ├── components/ │ ├── DraggableItem.vue # 每个可拖拽组件的外壳含拖拽手柄、删除按钮、配置弹窗入口 │ ├── RenderField.vue # 根据JSON Schema动态渲染具体组件input/select/upload等 │ └── SchemaEditor.vue # 右侧面板可视化编辑选中组件的schema字段title/required/rules等 └── App.vue # 主容器左侧组件库 中间画布 右侧属性面板这种结构意味着你要改拖拽行为只看useDraggable.ts要加新组件只改RenderField.vue里的switch分支和components/下的新.vue文件要调整属性面板只动SchemaEditor.vue。没有跨模块耦合新手改一个输入框的placeholder逻辑不会误伤轮播图的自动播放开关。2.2 核心模块拆解useDraggable.ts如何把鼠标动作翻译成Schema变更useDraggable.ts是整个拖拽系统的神经中枢。它不直接操作DOM而是通过Sortable的onEnd回调接收原始拖拽事件再将其转化为对pageStore.schema数组的安全操作。关键在于它处理了三个易翻车场景跨容器拖拽、嵌套层级错位、撤销重做断链。// /src/composables/useDraggable.ts import { ref, onMounted } from vue import Sortable from sortablejs export function useDraggable(schemaRef: RefJSONSchema[]) { const sortableInstance refSortable | null(null) const initSortable (el: HTMLElement) { sortableInstance.value new Sortable(el, { group: shared, animation: 150, handle: .drag-handle, // 仅允许点击拖拽手柄触发 onEnd: (evt) { // evt.item 是被拖拽的DOM节点但我们要操作的是schema数据 const oldIndex evt.oldIndex const newIndex evt.newIndex const fromArray schemaRef.value const toContainer evt.to as HTMLElement // 关键从DOM节点反查其对应的schema索引防嵌套容器错位 const draggedSchema findSchemaByElement(evt.item, fromArray) if (!draggedSchema) return // 执行数组操作先删除再插入到新位置支持跨容器 const removed fromArray.splice(oldIndex, 1) if (toContainer el) { // 同容器直接插入 fromArray.splice(newIndex oldIndex ? newIndex - 1 : newIndex, 0, ...removed) } else { // 跨容器插入到目标容器对应schema的children数组 const targetParent findParentSchema(toContainer, schemaRef.value) if (targetParent Array.isArray(targetParent.children)) { targetParent.children.splice(newIndex, 0, ...removed) } } } }) } return { initSortable } }这段代码的玄学点在于findSchemaByElement()——它用>!-- /src/components/RenderField.vue -- template component :isgetComponentName(schema) :schemaschema :model-valuemodelValue update:model-value$emit(update:modelValue, $event) / /template script setup langts import { computed } from vue import InputField from ./InputField.vue import SelectField from ./SelectField.vue import UploadField from ./UploadField.vue const props defineProps{ schema: JSONSchema modelValue: any }() const emit defineEmits([update:modelValue]) const getComponentName computed(() { const { type, ui:widget: widget } props.schema // 同是string类型但widget不同则渲染不同组件 if (type string) { if (widget textarea) return TextareaField if (widget upload) return UploadField if (widget rich-text) return RichTextEditor } if (type array widget tags) return TagsField if (type object schema.properties?.image) return HeroBanner return InputField // 默认兜底 }) /script这里的关键参数是ui:widget——它不属于JSON Schema官方字段是本项目约定的扩展字段专门用来打破type的粗粒度限制。比如type: string可以对应普通输入框、多行文本域、富文本编辑器、图片上传框全靠ui:widget区分。这样设计的好处是你在右侧面板里改一个下拉选项就能实时切换组件形态而不用去删掉旧组件再拖一个新组件进来。这也是“拖拽克隆”的基础——克隆一个组件本质是深拷贝它的schema包括ui:widget值。3. 运行与调试从零启动只需3步但有3个隐藏开关必须打开项目基于Vite构建无webpack配置负担。但实际启动时有3个非文档明说却决定成败的开关藏在代码深处。漏掉任何一个你都会卡在“能拖不能存”或“保存后页面空白”。3.1 启动命令与环境准备# 1. 确保Node 16.0.0Vite 4.x最低要求 node -v # 2. 安装依赖pnpm推荐比npm快且节省磁盘 pnpm install # 3. 启动开发服务器前端 pnpm dev # 4. 可选启动配套后端用于保存配置 cd server pnpm start注意pnpm不是必须但npm install可能因锁文件差异导致sortablejs版本冲突本项目锁定1.15.0。若坚持用npm请先删掉pnpm-lock.yaml再npm install。3.2 必须修改的3个隐藏配置开关这三个开关不在.env文件里而在源码中硬编码新手极易忽略画布容器ID开关影响拖拽区域识别在App.vue第28行div idcanvas-container必须存在且useDraggable.ts中初始化Sortable时传入的DOM selector必须匹配// /src/composables/useDraggable.ts const initSortable (el: HTMLElement) { // 这里的el必须是idcanvas-container的元素 sortableInstance.value new Sortable(el, { /* ... */ }) }如果你把idcanvas-container改成idpage-canvas就必须同步改initSortable(document.getElementById(page-canvas))否则拖拽失效。Schema ID生成开关影响克隆唯一性在/src/stores/pageStore.ts的addNewComponent方法里组件ID由crypto.randomUUID()生成。但某些老版浏览器不支持需降级// 替换这一行 // id: crypto.randomUUID(), // 为 id: comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}否则克隆组件时ID重复导致右侧面板无法正确绑定属性。跨域代理开关影响配置保存若你启用了server前端请求/api/save会走Vite代理。检查vite.config.tsexport default defineConfig({ server: { proxy: { /api: { target: http://localhost:3000, // 必须与server端口一致 changeOrigin: true, } } } })如果server运行在3001端口这里不改保存按钮永远转圈。3.3 常见问题排查拖拽失效、保存失败、预览空白的5条血泪记录现象1能拖动组件但松手后回到原位控制台无报错→ 原因Sortable初始化时传入的DOM元素为空或该元素未挂载完成。常见于onMounted中调用initSortable但div idcanvas-container在v-if条件内未渲染。→ 解决在App.vue的onMounted里加一层nextTick确保DOM就绪onMounted(() { nextTick(() { initSortable(document.getElementById(canvas-container)!) }) })现象2点击保存按钮Network显示404 /api/save→ 原因Vite代理未生效或server未启动。检查浏览器Network面板看请求URL是http://localhost:5173/api/save代理成功还是http://localhost:3000/api/save直连失败。→ 解决确认server进程在运行cd server pnpm start且vite.config.ts代理target端口与server一致。现象3拖拽克隆后右侧面板显示“未选中组件”但画布上有两个相同组件→ 原因克隆时未重置id字段导致两个组件共享同一IDpageStore.selectedId无法唯一匹配。→ 解决检查usePageStore.ts中的cloneComponent方法确保深拷贝后执行newComponent.id generateId()。现象4预览模式下表单提交后页面刷新数据丢失→ 原因RenderField.vue中form未阻止默认提交行为。→ 解决在RenderField.vue的form submit.preventhandleSubmit添加.prevent修饰符并在handleSubmit中调用api.submit(formData)。现象5添加“下拉选择”组件后右侧面板的“选项列表”输入框无法输入中文→ 原因SchemaEditor.vue中input绑定的v-model未使用.lazy修饰符导致Composition API的ref响应式更新延迟。→ 解决将input v-modelschema.enum[0]改为input v-model.lazyschema.enum[0]或改用input事件手动赋值。4. 二次开发实战加一个“地图选址”组件从零到上线只需改3个文件现在我们来实操一次典型二次开发为CMS增加一个“地图选址”组件用户可点击地图标记位置返回经纬度坐标。这不是简单加个UI而是要打通拖拽→配置→渲染→数据回传全链路。4.1 步骤1定义地图组件的JSON Schema规范在/src/components/RenderField.vue的getComponentName计算属性前新增Schema类型声明// /src/types/schema.d.ts export interface MapSchema extends JSONSchema { type: object properties: { latitude: { type: number; title: 纬度 } longitude: { type: number; title: 经度 } address: { type: string; title: 地址 } } ui:widget: map-picker }然后在/src/composables/useDraggable.ts的组件库注册处加入地图组件元信息// /src/composables/useDraggable.ts export const COMPONENT_LIBRARY [ // ...原有组件 { id: map-picker, name: 地图选址, icon: , schema: { type: object, properties: { latitude: { type: number, title: 纬度, default: 39.9042 }, longitude: { type: number, title: 经度, default: 116.4074 }, address: { type: string, title: 地址, default: 北京市 } }, ui:widget: map-picker } } ]4.2 步骤2编写地图渲染组件与配置面板新建/src/components/MapPicker.vue!-- /src/components/MapPicker.vue -- template div classmap-container div refmapEl classmap-canvas/div div classmap-info p纬度: {{ modelValue?.latitude || 未设置 }}/p p经度: {{ modelValue?.longitude || 未设置 }}/p p地址: {{ modelValue?.address || 未设置 }}/p /div /div /template script setup langts import { ref, onMounted, watch } from vue import { initMap } from /utils/map // 假设你封装了地图SDK初始化 const props defineProps{ schema: MapSchema modelValue: Recordstring, any }() const emit defineEmits([update:modelValue]) const mapEl refHTMLDivElement | null(null) onMounted(() { if (!mapEl.value) return const map initMap(mapEl.value, { center: [props.modelValue?.longitude || 116.4074, props.modelValue?.latitude || 39.9042], zoom: 12 }) // 地图点击事件更新modelValue map.on(click, (e) { const { lng, lat } e.lngLat emit(update:modelValue, { latitude: lat, longitude: lng, address: 经纬度: ${lat.toFixed(4)}, ${lng.toFixed(4)} }) }) }) // 监听外部modelValue变更如右侧面板修改 watch(() props.modelValue, (newVal) { if (newVal mapEl.value) { // 重新定位地图中心 } }) /script同时在/src/components/SchemaEditor.vue中为ui:widget: map-picker添加专属配置项!-- /src/components/SchemaEditor.vue -- template !-- ...其他字段 -- div v-ifschema[ui:widget] map-picker label初始纬度/label input typenumber v-model.numberschema.properties.latitude.default label初始经度/label input typenumber v-model.numberschema.properties.longitude.default /div /template4.3 步骤3注入地图SDK并验证数据流最后一步确保地图SDK可用。本项目不内置高德/百度地图而是留出/src/utils/map.ts供你按需接入// /src/utils/map.ts // 示例接入Leaflet轻量开源 import * as L from leaflet export function initMap(container: HTMLElement, options: any) { const map L.map(container).setView([options.center[1], options.center[0]], options.zoom) L.tileLayer(https://{a-d}.tile.openstreetmap.org/{z}/{x}/{y}.png).addTo(map) return map }验证方式启动项目从左侧组件库拖一个“地图选址”到画布在右侧面板修改初始经纬度画布地图应实时重定位点击地图任意位置下方信息栏应更新坐标点击保存查看/api/save返回的JSON确认ui:widget: map-picker及其properties完整存在。从那以后我每次加新组件都强制走一遍这三步先写Schema定义保证数据结构可序列化、再写渲染组件保证UI可交互、最后补配置面板保证运营可调参。漏掉Schema这第一环后面全是黑匣子——看着能跑一导出JSON就缺字段。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

U-Net图像分割实战:从原理到调参避坑指南

U-Net图像分割实战:从原理到调参避坑指南

做图像分割的人,几乎没人不知道U-Net。刚接触它的时候,我的第一反应是:这个网络长得也太“直白”了,左边一条路往下压,右边一条路往上抬,中间几根线连起来,像个大写的U。后来真正拿它去做细胞分…

2026/10/18 5:52:45 阅读更多 →
小模型ONNX转昇腾NPU部署全流程实战与避坑指南

小模型ONNX转昇腾NPU部署全流程实战与避坑指南

过去两三个星期,我基本都在做同一件事:把训练好的小模型从 GPU 搬到昇腾 NPU 上跑。模型并不大,最大的也就 20MB 左右,但真正进了部署阶段才发现,跑通一次 forward 和稳定跑进产品里完全是两码事。环境、算子、内存、数…

2026/10/18 5:52:45 阅读更多 →
openEuler源码编译VLC:依赖配置与构建实践全记录

openEuler源码编译VLC:依赖配置与构建实践全记录

在openEuler上编译VLC这件事,我一开始以为就是常规的configure、make、make install三连,结果实际折腾了大半天。不是说openEuler有多特殊,而是它的软件源和依赖命名跟Debian系、CentOS系都有差异,加上VLC本身就是个依赖大户&…

2026/10/18 5:52:45 阅读更多 →

最新新闻

深入理解Python asyncio:事件循环、协程与Future/Task核心机制

深入理解Python asyncio:事件循环、协程与Future/Task核心机制

很多人写了一阵子 Python,大概率会在某个时间点被async/await搞到怀疑人生。网上教程一搜一大把,但几乎全是“这是语法、那是函数”的字典式讲解,真正把事件循环、协程、Task 之间的关系讲透的并不多。这篇是 Python 进阶路线的第 2 讲&#…

2026/10/18 7:48:07 阅读更多 →
智能降AIGC免费工具推荐!万方AIGC检测AI率从77.17%降到5.4%!

智能降AIGC免费工具推荐!万方AIGC检测AI率从77.17%降到5.4%!

智能降AIGC免费工具推荐!万方AIGC检测AI率从77.17%降到5.4%! 假设你是本科物流管理专业,毕业论文1.1万字,学院在通知里写明用万方做AIGC检测,要求AI率不高于30%。你自己用免费检测查了一下,第二章文献梳理…

2026/10/18 7:48:07 阅读更多 →
从“看得见”到“做得到”:视觉决策模型正在改变机器视觉

从“看得见”到“做得到”:视觉决策模型正在改变机器视觉

过去十几年,机器视觉的发展逻辑其实非常清晰:相机负责采集,图像处理负责清洗,算法负责检测、分类、分割和定位,规则系统负责判断,PLC、机器人或其他执行机构负责动作。这套体系在工业制造、物流、安防、交通…

2026/10/18 7:48:07 阅读更多 →
C语言time_t与struct tm互转:原理、坑点与工程实践

C语言time_t与struct tm互转:原理、坑点与工程实践

群里有人问:为什么我用mktime把struct tm转成time_t存进数据库,第二天再读出来,用localtime一转换,时间差了8个小时?我当时一眼就看出了问题根源:八成是UTC和本地时间混着存了。这个问题恰好涵盖了C语言时间…

2026/10/18 7:48:07 阅读更多 →
GAN驱动的多孔材料三维数字重构:原理、训练与工程验证

GAN驱动的多孔材料三维数字重构:原理、训练与工程验证

多孔材料这东西,看着不起眼,却在催化、过滤、储能、生物支架、吸音隔热这些行当里扮演着关键角色。一块泡沫陶瓷或者金属海绵的性能,往往不是由材料本身的化学成分决定的,而是由内部那些密密麻麻、弯弯曲曲的孔隙结构说了算。问题…

2026/10/18 7:48:07 阅读更多 →
JDBC从入门到实战:核心接口、踩坑记录与连接池全解

JDBC从入门到实战:核心接口、踩坑记录与连接池全解

JDBC这个名字,很多人入门Java时都见过,但真正把它用明白的人并不多。我见过太多同学学完SSM框架、MyBatis之后,回过头来连一个最简单的JDBC查询都写不利索,更不用说出问题了连怎么排查都摸不着头脑。其实JDBC才是Java操作数据库的…

2026/10/18 7:47:07 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →