表单设计器拖拽对象时添加属性

背景:因为项目需要。自写设计器。遇到的坑在此记录

  • 使用的拖拽组件时vuedraggable。下面放上局部示例截图。
  • 坑1。draggable标签在拖拽时可以获取到被拖拽的对象属性定义
    1. 要使用 :clone, 而不是@clone。我想应该是因为draggable标签比较特。
    2. 另外在使用**:clone时要将此响应添加到from draggable标签中,也就是如果从左向右拖拽标签复制,要放到左侧标签中才会起作用。意思是在赋值时对被赋值的组件对象加工处理后才会被放到右侧标签**
js 复制代码
<template>
  <div class="form-designer">
    <!-- 左侧面板:可拖拽的控件 -->
    <div class="component-panel">
      <h3>控件库</h3>
      <draggable
        :list="availableComponents"
        :group="{ name: 'components', pull: 'clone' }"
        :clone="handleComponentClone">
        <div v-for="(comp, index) in availableComponents" :key="index" class="component-item">
          {{ comp.label }}
        </div>
      </draggable>
    </div>

    <!-- 中间画布:设计区域 -->
    <div class="design-canvas">
      <h3>表单设计区</h3>
      <draggable
        v-model="formStructure"
        :group="{ name: 'components', put: true }"
        @add="(item) => onComponentAdded(item)">
        <transition-group name="fade" tag="div" class="form-items">
          <div
            v-for="item in formStructure"
            :key="item.id"
            class="form-item"
            :class="{ selected: selectedItem === item }"
            @click="selectComponent(item)">
            <el-form>
              <component
                :is="item.type"
                v-model="formData[item.vModel]"
                :label="item.label"
                :options="item.options"
                :required="item.required" />
            </el-form>

            <!-- {{ item.label }} -->
          </div>
        </transition-group>
      </draggable>
    </div>

    <!-- 右边配置面板:属性编辑 -->
    <div class="config-panel">
      <h3>属性设置</h3>
      <div v-if="selectedItem">
        <el-form label-position="top">
          <el-form-item label="字段名称">
            <el-input v-model="selectedItem.label" />
          </el-form-item>
          <el-form-item label="绑定字段名">
            <el-input v-model="selectedItem.vModel" />
          </el-form-item>
          <el-form-item label="默认值">
            <el-input v-model="formData[selectedItem.vModel]" />
          </el-form-item>
          <el-form-item label="是否必填">
            <el-switch v-model="selectedItem.required" />
          </el-form-item>
        </el-form>
      </div>
    </div>
  </div>
</template>

<script>
import draggable from 'vuedraggable';
import DynamicInput from './components/Input.vue';
import DynamicSelect from './components/Select.vue';
import DynamicCheckboxGroup from './components/CheckboxGroup.vue';
import DynamicDatePicker from './components/DatePicker.vue';
import DynamicUpload from './components/UploadFile.vue';
import DynamicImageUpload from './components/UploadImage.vue';
import { options } from 'runjs';
import { start } from 'nprogress';
import cloneDeep from 'lodash.clonedeep';

export default {
  name: 'FormDesigner',
  components: {
    draggable,
    DynamicInput,
    DynamicSelect,
    DynamicCheckboxGroup,
    DynamicDatePicker,
    DynamicUpload,
    DynamicImageUpload
  },
  data() {
    return {
      // textarray: ['1', '2', '3'],
      // 可拖拽控件列表
      availableComponents: [
        { type: 'DynamicInput', label: '单行文本', required: false, vModel: 'text' },
        { type: 'DynamicDatePicker', label: '日期时间', required: false, vModel: 'datetime' },
        { type: 'DynamicUpload', label: '文件上传', required: false, vModel: 'c' },
        { type: 'DynamicImageUpload', label: '图片上传', required: false, vModel: 'imageupload' },
        {
          type: 'DynamicSelect',
          label: '下拉选择',
          required: false,
          vModel: 'select',
          label: '选项',
          options: ['选项1', '选项2']
        },
        {
          type: 'DynamicCheckboxGroup',
          label: '多选框组',
          required: false,
          vModel: 'checkboxgroup',
          label: '多选',
          options: ['选项A', '选项B', '选项C']
        }
      ],
      // availableComponents: [
      //   { id: 1, label: '张三', type:'1', vModel:'' },
      //   { id: 2, label: '李四', type:'2', vModel:'' }
      // ],
      // 当前设计的表单结构
      formStructure: [
        // { id: 3, label: '王五', type: '3', vModel:''},
      ],
      // 表单数据模型
      formData: {},
      // 当前选中项
      selectedItem: null
    };
  },
  methods: {
    // 控件被添加到设计区时触发
    onComponentAdded(event) {
      // console.log('add:', event);
      const newComponent = this.formStructure[event.newIndex];
      // 动态添加 id 字段
      if (!newComponent.id) {
        newComponent.id = Date.now() + Math.random().toString(36).substring(2);
      }
      this.formData[newComponent.vModel] = '';
      this.selectedItem = newComponent;
      console.log('index:', event.newIndex, 'newComponent:', newComponent, newComponent.id);
      console.log('formStructure:', this.formStructure[event.newIndex]);
    },
    // 点击控件时更新右侧配置
    selectComponent(item) {
      this.selectedItem = item;
    },
    handleComponentClone(component) {
      console.log('component:', component);
      console.log('component.label:', component.label);
      // 创建副本,避免修改原始 availableComponents 中的数据
      const clone = cloneDeep(component);
      clone.id = Date.now() + Math.random().toString(36).substring(2);
      console.log('clone:', clone);
      console.log('clone.id:', clone.id);
      console.log('clone.type:', clone.type);
      console.log('clone.label:', clone.label);
      console.log('clone.vModel:', clone.vModel);
      return clone;
    },
    deepClone(parm) {
      let dataType = Object.prototype.toString.call(parm);
      let result;
      if (dataType === '[object Array]') {
        result = [];
        for (let i = 0; i < parm.length; i++) {
          result[i] = deepClone(parm[i]);
        }
      } else if (dataType === '[object Object]') {
        result = {};
        for (const key in parm) {
          if (Object.hasOwnProperty.call(parm, key)) {
            result[key] = deepClone(parm[key]);
          }
        }
      } else {
        result = parm;
      }
      return result;
    }
  }
};
</script>

<style scoped>
.form-designer {
  display: flex;
  height: 100vh;
}

.component-panel,
.config-panel {
  width: 250px;
  padding: 10px;
  background-color: #f9f9f9;
  border-right: 1px solid #e4e4e4;
}

.design-canvas {
  flex: 1;
  padding: 10px;
  overflow-y: auto;
}

.component-item {
  padding: 8px;
  margin-bottom: 6px;
  background-color: #fff;
  border: 1px solid #ddd;
  cursor: move;
  text-align: center;
}

.form-items {
  min-height: 100px;
  border: 1px dashed #ccc;
  padding: 10px;
}

.form-item {
  border: 1px solid #eee;
  padding: 10px;
  margin-bottom: 10px;
}

.form-item.selected {
  border-color: #409eff; /* Element UI 主色调 */
  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.5); /* 可选:添加外发光效果 */
}
</style>
相关推荐
飞翔的佩奇2 小时前
Java项目:基于SSM框架实现的忘忧小区物业管理系统【ssm+B/S架构+源码+数据库+毕业论文+开题报告】
java·数据库·mysql·vue·毕业设计·ssm框架·小区物业管理系统
百锦再2 天前
Vue中对象赋值问题:对象引用被保留,仅部分属性被覆盖
前端·javascript·vue.js·vue·web·reactive·ref
一笑code2 天前
vue/微信小程序/h5 实现react的boundary
微信小程序·vue·react
eric*16882 天前
尚硅谷张天禹老师课程配套笔记
前端·vue.js·笔记·vue·尚硅谷·张天禹·尚硅谷张天禹
喜欢敲代码的程序员3 天前
SpringBoot+Mybatis+MySQL+Vue+ElementUI前后端分离版:项目搭建(一)
spring boot·mysql·elementui·vue·mybatis
海的诗篇_3 天前
前端开发面试题总结-原生小程序部分
前端·javascript·面试·小程序·vue·html
sunbyte3 天前
50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | DragNDrop(拖拽占用组件)
前端·javascript·css·vue.js·vue
skyymrj13 天前
Vue3 + Tailwind CSS 后台管理系统教程
前端·css·vue
程序猿小D13 天前
[附源码+数据库+毕业论文]基于Spring+MyBatis+MySQL+Maven+Vue实现的校园二手交易平台管理系统,推荐!
java·数据库·mysql·spring·vue·毕业设计·校园二手交易平台
伍哥的传说13 天前
react gsap动画库使用详解之text文本动画
前端·vue.js·react.js·前端框架·vue·html5·动画