前端拖拽生成页面的思路

5,783 阅读5分钟

页面

Untitled.png

设计思路

  1. 设计数据结构
  2. 确定技术路线
  3. 阅读原型图,确定复用的业务组件
  4. 与后端沟通可行性

设计数据结构

设计数据结构之前,要考虑 1.后续迭代升级 2.权限字段 3.针对不同组件的配置

/**
 * displayName: 中文名称
 * name: 英文名称
 * type: 表单组件类型
 * value: 配置项的值
 * options: 配置可选项
 */
// => 页面数据 Object
const JSON_SCHEMA = {
  name: '模板内容', // 页面名称
  thumb: '', // 页面缩略图
  permission: [], // 权限
  config: [
    // 页面配置信息
    {
      displayName: '尺寸',
      name: 'dimension',
      value: [
        {
          displayName: '屏幕大小',
          name: 'recommend',
          type: 'select',
          value: 0,
          options: [
            {
              name: '375*667',
              value: 0
            },
            {
              name: '820*1180',
              value: 1
            }
          ]
        },
        {
          displayName: '宽度',
          name: 'width',
          type: 'number',
          value: 375
        },
        {
          displayName: '高度',
          name: 'height',
          type: 'number',
          value: 667
        }
      ]
    }
  ],
  components: [
    // 组件配置
    {
      // 组件配置数据
      base: {
        moduleName: 'Downline',
        version: '1.0.0'
      },
      name: '图片魔方',
      id: 1,
      height: 200, // 估算最低高度,用户前台分批渲染组件
      config: [
        {
          displayName: '内容配置',
          name: 'componentConfig',
          value: [
            {
              displayName: '链接',
              name: 'url',
              type: 'string',
              value: 'https://……'
            }
          ]
        },
        {
          displayName: '样式配置',
          name: 'style',
          value: [
            {
              displayName: '背景颜色',
              name: 'background-color',
              type: 'string',
              value: '#fff'
            },
            {
              displayName: '页面边距',
              name: 'page-padding',
              type: 'number',
              value: 16
            }
          ]
        }
      ],
      componentData: {
        // 组件配置取值
        data: [
          {
            url: ''
          }
        ],
        fields: [
          {
            displayName: '选择图片',
            name: 'url',
            value: '',
            desc: '图片地址'
          }
        ],
        staticData: {},
        remoteData: {
          displayName: '远程数据',
          name: 'remoteApi',
          value: [
            {
              displayName: '接口路径',
              name: 'apiUrl',
              value: ['', '']
            }
          ]
        }
      }
    }
  ]
}

确定技术路线

使用vuedraggable 可以实现互相拖拽,

分为 3个部分

拖拽业务组件开发:

预览区

配置区

数据控制 : 由于横跨多个组件,所以用vuex, 然后不同的模板由模板的uuid作为key来放在大json里面

实际实现

拖拽业务组件开发

不同的业务组件,实际上是不同的json,这里只需要配置好json,然后for循环json

显示的时候这里原型只要求显示正方形和组件名, 这里可以在for循环里面,添加div对应的样式以及文字即可

我这里新建了一个 COMPONENTS-DATA.js 里面是一个数组 , 数据里面是这些业务组件的配置

[
	{
        "base": {
            "moduleName": "InstructionsText",
            "version": "1.0.0"
        },
        "name": "说明文本",
        "id": 1,
        "height": 20,
        type:"input",
        "fontSize":"16",
        placeholder:"请输入说明文本",
        "config": [
            {
                "displayName": "对齐方式",
                "name": "alignMode",
                "type": "radio",
                "value": "left",
                "options": [
                    {
                        "value": "left",
                        "name": "左对齐"
                    },
                    {
                        "value": "center",
                        "name": "居中"
                    },
                    {
                        "value": "right",
                        "name": "右对齐"
                    }
                ]
            },
            {
                "displayName": "字体大小",
                "name": "fontSize",
                "type": "radio",
                "value": "16",
                "options": [
                    {
                        "value": "16",
                        "name": "16像素"
                    },
                    {
                        "value": "14",
                        "name": "14像素"
                    },
                    {
                        "value": "12",
                        "name": "12像素"
                    }
                ]
            },
            {
                "displayName": "文本内容",
                "name": "text",
                value:"",
                type:"input",
            }
        ]
    },
]

然后将这个js文件引入 vuex中作为一个变量

import COMPONENTSDATA from "@/store/COMPONENTS-DATA";
const state = {
    /* 预设模板 */
    componentList:COMPONENTSDATA,
    /* 模板配置 */
    templateJson: {
        newTemplateJSON:{
            /* 模板名称 */
            name: '',
            /* 模板图标 */
            thumb: '',
            /* 被激活的组件索引 */
            activeComponentIndex: 0,
            /* 模板的页面配置信息,存在这里 */
            config: [
                {
                    displayName: '宽度',
                    name: 'width',
                    type: 'number',
                    value: 375
                },
                {
                    displayName: '高度',
                    name: 'height',
                    type: 'number',
                    value: 667
                }
            ],
            /* 模板中的各个组件 */
            components: []
        },
        //剩下的以uuid作为key,区分不同的模板
    },
}

后续维护新增组件的类型 , 就直接在 COMPONENTS-DATA.js 中新增即可

预览区

由于预览区要加载大量的的组件,所以这里使用 懒加载以及 动态组件渲染

预览区的代码实现分为

  1. 动态组件加载
  2. vue组件来根据配置json 来生成 实际的显示组件
  <!--中间显示区-->
  <!-- 因为这个根据配置显示页面的组件,要在多个地方用到,且用来仅显示的用途 , 所以这里单独抽成一个组件
  放在了顶层的components文件夹中 -->
<div class="middle">
  <H5ShowByJSONCommitment
    :value.sync="templateJson.components"
    :group="groupB"
  ></H5ShowByJSONCommitment>
</div>

代码实现 :

<draggable
    v-model="templateJsonComponents"
    animation="300"
    :group="group"
>
  <div
      v-for="(item,index) in templateJsonComponents" :key="index" @click="handleClick(index)"
      :class="{
        active: activeComponentIndex === index && !preview,
        'preview-no-border' : preview
      }"
  >
    <component
        :is="getComponentName(item.base.moduleName)"
        :index="index"
        :templateJsonKey="templateJsonKey"
    ></component>
  </div>
</draggable>
/* 返回组件名称 */
getComponentName(moduleName) {
  return components[moduleName];
}
const components = {
  InstructionsText: () => import('@/views/enterpriseCredit/template-management/components/middleContentComponents/DescriptionText.vue'),
  divider: () => import('@/views/enterpriseCredit/template-management/components/middleContentComponents/DividerComponents.vue'),
  Instructions: () => import('@/views/enterpriseCredit/template-management/components/middleContentComponents/IndicatorComponents.vue'),
  Input: () => import('@/views/enterpriseCredit/template-management/components/middleContentComponents/InputComponents.vue'),
};

  components: {
    ...components,
    draggable
  },

其中 components 中各个vue组件就可以单独放到一个文件夹里面, 针对不同的业务json来开发不同的显示组件

配置区 :

不同的组件虽然有很多配置,且不相同, 但是可以把这些都抽象出来, 不单独针对组件一一对应 , 而是根据 input , radio 这些分类即可 , 具体要配置的东西 , 可以在 COMPONENTS-DATA.js 的config字段来控制,

"config": [
    {
        "displayName": "对齐方式",
        "name": "alignMode",
        "type": "radio",
        "value": "left",
        "options": [
            {
                "value": "left",
                "name": "左对齐"
            },
            {
                "value": "center",
                "name": "居中"
            },
            {
                "value": "right",
                "name": "右对齐"
            }
        ]
    },
    {
        "displayName": "字体大小",
        "name": "fontSize",
        "type": "radio",
        "value": "16",
        "options": [
            {
                "value": "16",
                "name": "16像素"
            },
            {
                "value": "14",
                "name": "14像素"
            },
            {
                "value": "12",
                "name": "12像素"
            }
        ]
    },
    {
        "displayName": "文本内容",
        "name": "text",
        value:"",
        type:"input",
    }
]

然后可以开发一个 配置的vue组件, 循环配置, 来显示不同的 元素即可, 这样可以大大减少配置区的开发

<el-form label-position="left">
  <el-form-item v-for="(item,index) in configList" :label="item.displayName" :key="index">
    <el-radio-group v-if="item.type === 'radio'" v-model="item.value">
      <el-radio v-for="(optionItems,index) in item.options" :key="index" :label="optionItems.value">
        {{ optionItems.name }}
      </el-radio>
    </el-radio-group>
    <el-input v-if="item.type === 'input'"
              :placeholder="item.placeholder"
              v-model="item.value"></el-input>
    <el-input v-if="item.type === 'inputPlaceholder'"
              :placeholder="item.placeholderValue"
              v-model="item.placeholderValue"></el-input>
    <el-select v-if="item.type === 'select'"
               v-model="item.value"
               filterable
               remote
               :loading="loading"
               clearable
               :remote-method="(query)=>fetchOptionsForItem(item,index,query)"
               placeholder="请选择">
      <el-option
          v-for="(optionItems,index) in item.options"
          :key="index"
          :label="optionItems.name"
          :value="optionItems.value">
      </el-option>
    </el-select>
  </el-form-item>
</el-form>

配置区要修改的配置,实际上是修改我们的vuex, 显示区仅仅接收vux相应的变量, 这边配置区,可以来使用 computed来 声明一个简洁的变量, 接受配置文件和修改配置文件

configList: {
  get() {
    let result = {}
    if(this.$store.state.template.templateJson[this.templateJsonKey] && this.$store.state.template.templateJson[this.templateJsonKey].components[this.activeComponentIndex]) {
      result = this.$store.state.template.templateJson[this.templateJsonKey].components[this.activeComponentIndex].config
    }
    return result
  },
  set(value) {
    this.$store.commit('template/setComponentValue', {index: this.activeComponentIndex, value,uuid:this.templateJsonKey})
  }
},

另外要注意的是配置区 有些下拉框的数据量很大, 这些需要在配置json配置好请求地址, 等配置区mounted之后, 再依次请求相应的地址, 然后将结果放入到 配置json里面(保存配置的时候,同时要注意要去掉这些数据), 这样就可以大大减少要传输的数量了

async fetchOptionsForItem(item, index, queryKey = '') {
  const api = item.fetchConfig ? item.fetchConfig.api : null;
  // 检查是否已经对这个api发送过请求
//    && !this.apiRequestSent[api]
  if (api ) {
    try {
      const response = await request.request({
        url: api,
        method: 'get',
        params: {
          ...item.fetchConfig.params,
          [item.fetchConfig.queryKey]: queryKey,
        },
      });
      // 使用Vue.set来更新options以确保响应性
      const options = []
      response[item.fetchConfig.resListKey].forEach((optionsItem) => {
        options.push({
          value: optionsItem[item.fetchConfig.valueKey],
          name: optionsItem[item.fetchConfig.nameKey],
          ...optionsItem,
        })
      })
      this.$set(this.configList[index], 'options', options);
      // 标记这个api已请求
      this.apiRequestSent[api] = true;
    } catch (error) {
      console.error('Fetching options error:', error);
      // 错误处理
      this.$set(this.configList[index], 'options', [{value: 'error', name: '加载失败'}]);
    }
  }
},
fetchOptionsForConfigList() {
  this.configList.forEach((item, index) => {
    this.fetchOptionsForItem(item, index);
  });
},