VueBloghyhero6

基于VUE,实现拖拽 + 配置方式生成数据大屏,低代码使用案例实现原理解析

2023-10-13 / 2023-10-13 / 1485次浏览

https://colaiven.gitee.io/cola-designer-doc/start/
是该项目的示例网站文档地址

开源版在线体验地址:http://colaiven.gitee.io/cola-designer/#/design (没钱买服务器暂时只能体验前端,联合后端可自行拉取代码运行体验) 前端仓库:https://gitee.com/colaiven/cola-designer 后端仓库:https://gitee.com/colaiven/cola-designer-java

好奇心的起因是一位群友,他接到了低代码任务,要求他最好7天拉一个拖拽平台出来,他寻求案例的时候,我也就顺手看了看,业务逻辑上还算是需要代码拆分和构建的完成后,我预计针对特定公司的特定平台进行使用。

我大体对代码进行了逻辑分析,大概摸清了它的实现基础。所以下面就简单浅析下前端的核心渲染逻辑。

关于低代码的具体一些使用案例,结合了昨天简单的看了一个低代码拉取平台的案例。 它的一些个具体实现方案和思路,我结合了GPT的说法,大概如下:

当涉及到拖拽生成大屏时,通常的业务逻辑如下:

数据准备:首先,你需要准备好用于生成大屏的数据。这可以是来自数据库、API接口或其他数据源的数据。确保你有足够的数据来填充大屏的各个组件和图表。

设计大屏布局:在开始拖拽生成大屏之前,你需要设计大屏的整体布局。考虑大屏的结构和所需的组件,例如图表、表格、文本框等。确定它们在大屏上的位置、大小和相互关系。

拖拽组件:在大屏编辑界面中,为用户提供可拖拽的组件。这些组件可以是预定义的,也可以是用户自定义的。用户可以从组件库中选择一个组件,并将其拖动到大屏的指定位置。

放置组件:一旦用户拖动组件到大屏上,你需要将该组件放置到相应的位置,并进行布局和调整。这可以通过JavaScript或其他前端框架来实现。

配置组件属性:对于每个放置在大屏上的组件,用户通常希望能够配置其属性,例如数据源、样式、交互行为等。你需要提供一个界面,让用户可以编辑和配置每个组件的属性。

数据绑定:将大屏上的组件与准备好的数据进行绑定。这意味着将数据源中的数据传递给相应的组件,并确保它们正确地显示和更新。

实时预览和保存:在用户编辑大屏的过程中,你可以提供实时预览功能,让用户看到他们所做的更改。同时,你还应该提供保存功能,使用户能够保存他们创建的大屏布局和配置。

生成大屏:一旦用户完成了对大屏的设计和配置,你可以将其生成为最终的大屏展示页面。这可能涉及将用户所做的更改转换为HTML、CSS和JavaScript代码,并将其部署到一个Web服务器上供访问。

需要注意的是,具体的业务逻辑可能因项目需求而有所不同。上述步骤提供了一个一般化的流程,但在实际实现中,你可能需要根据项目的具体要求进行适当的调整和定制。

这个只是在一个广义层面上的宽泛逻辑设计

接上面 我重点想问的是第八步,它这个生成代码是如何生成的

在第八步中,生成大屏的代码可以根据具体需求和技术栈的不同而有所变化。下面是一种常见的生成代码的方法:

收集用户在编辑界面上的布局和配置信息,包括组件的位置、大小、样式、数据源等。

根据用户的布局信息,生成对应的HTML结构。可以使用HTML模板或动态地在JavaScript中构建HTML元素。

将用户配置的样式信息应用到相应的HTML元素上,比如设置组件的颜色、字体、背景等。

根据用户配置的数据源,将数据绑定到对应的组件上。这可能涉及将数据动态地填充到HTML模板中或使用JavaScript来更新组件的内容。

如果有交互行为,例如点击事件或动画效果,需要将相应的JavaScript代码添加到生成的HTML文件中。

最后,将生成的HTML代码保存为一个文件,或将其作为字符串返回给用户。

需要注意的是,生成的代码应该是经过优化和安全处理的。这包括对用户输入进行验证和过滤,防止潜在的安全漏洞,以及对生成的代码进行压缩和精简,以提高加载速度和性能。

另外,生成代码的具体实现方式也取决于所使用的技术栈和框架。不同的前端框架(如React、Vue.js、Angular等)可能有自己的代码生成机制和工具,可以简化这个过程。

总结来说,生成大屏的代码是根据用户在编辑界面上的布局和配置信息,将其转换为相应的HTML、CSS和JavaScript代码,并进行优化和安全处理。

那问一个比较细节的问题 echarts 这是一个图表框架,就是我使用拖拽,那么这个拖拽是如何实现从一个图表的组件然后展示到它的页面上呢
对于echarts这样的图表框架,通常可以通过以下步骤实现从一个图表组件拖拽展示到页面上:

准备图表组件:首先,你需要准备好echarts的图表组件。这可以是事先定义好的模板或自定义的图表组件。每个组件应该包含图表的配置和数据。

拖拽开始:当用户开始拖拽一个图表组件时,你可以在拖拽事件中获取该组件的标识或其他相关信息。这可以使用HTML5的拖拽API来实现,例如使用draggable属性和相关事件(如dragstart)。

拖拽过程:在拖拽过程中,你可以根据鼠标的位置或拖拽元素的相对位置,实时更新图表组件的位置。这可以通过CSS的transform属性或JavaScript来实现。

放置图表组件:当用户放置图表组件时,你需要确定放置的位置,并将图表组件添加到页面的相应位置。这可以通过计算鼠标位置或放置容器的位置来实现。

初始化图表:一旦图表组件被放置到页面上,你需要初始化echarts图表实例,并将其绑定到放置的容器上。这可以使用echarts提供的初始化方法,并传递相关的配置和数据。

更新图表数据:如果你希望图表能够根据用户的拖拽位置或其他参数进行动态更新,你可以监听拖拽事件或其他交互事件,并在事件发生时更新图表的数据或配置。

渲染图表:最后,你需要调用echarts的渲染方法,将图表实例渲染到页面上。这会将图表的可视化呈现给用户。

需要注意的是,具体的实现方式可能会因使用的技术栈、框架或库的不同而有所差异。例如,如果你使用了Vue.js或React等前端框架,可能会结合相应的生命周期钩子或组件状态来实现拖拽和图表展示的逻辑。

总结来说,通过拖拽图表组件,你可以根据用户的操作将图表组件的配置和数据展示到页面上,并通过图表框架(如echarts)来实现图表的渲染和更新。

可以看到的是到这里为止 GPT 给出的方案策略还是比较的宽泛,那么真正的细节还是要去抓代码,这边我不死心让它给出了一个图表也就是echarts的一个案例使用 Vue 来进行一个构建。

当使用ECharts图表框架和Vue.js进行拖拽时,你可以按照以下步骤实现从一个图表组件到页面上的展示:

首先,确保你已经在项目中引入了ECharts和Vue.js的相关依赖。

创建一个Vue组件,用于展示图表,比如Chart.vue:

<template>
  <div ref="chartContainer" style="width: 100%; height: 400px;"></div>
</template>

<script>
import echarts from 'echarts';

export default {
  mounted() {
    this.chart = echarts.init(this.$refs.chartContainer);
  },
  methods: {
    renderChart(chartData) {
      this.chart.setOption(chartData);
    }
  }
}
</script>

在需要拖拽的组件中引入Chart.vue,并在拖拽事件中传递图表数据:

<template>
  <div>
    <div
      class="draggable"
      draggable="true"
      @dragstart="dragStart"
    >
      拖拽图表
    </div>
    <Chart ref="chart" />
  </div>
</template>

<script>
import Chart from '@/components/Chart.vue';

export default {
  components: {
    Chart
  },
  methods: {
    dragStart(event) {
      const chartData = {
        // 在这里根据需要设置图表的数据和配置项
      };
      event.dataTransfer.setData('chartData', JSON.stringify(chartData));
    }
  }
}
</script>

<style>
.draggable {
  /* 设置拖拽元素样式 */
}
</style>

在目标页面中接收拖拽事件并渲染图表:

<template>
  <div @drop="drop" @dragover="allowDrop">
    <Chart ref="chart" />
  </div>
</template>

<script>
import Chart from '@/components/Chart.vue';

export default {
  components: {
    Chart
  },
  methods: {
    drop(event) {
      event.preventDefault();
      const chartData = JSON.parse(event.dataTransfer.getData('chartData'));
      this.$refs.chart.renderChart(chartData);
    },
    allowDrop(event) {
      event.preventDefault();
    }
  }
}
</script>

这是一个 GPT 给出的简单的没有通用的一个示例,算是最基础的情况下实现了一个拖拽

在页面代码中我找到了专门绘制代码的页面

<div v-for="(item,index) in cacheComponents" :key="item.id"
class="cptDiv" :style="{width:Math.round(item.cptWidth)+'px', height:Math.round(item.cptHeight)+'px',
top:Math.round(item.cptY)+'px',left:Math.round(item.cptX)+'px',
zIndex: currentCptIndex === index ? 1800 : item.cptZ}" :ref="'div'+item.cptKey+index"
@mousedown="showConfigBar($event,item,index)" tabindex="0">

那解释一下这段代码 cacheComponents=[ {cptTitle: '按钮', icon: 'button', cptKey: 'cpt-button', cptOptionKey: 'cpt-button-option', cptOption: {…}, …}cptHeight: (...)cptKey: "cpt-button"cptOption: (...)cptOptionKey: (...)cptTitle: (...)cptWidth: (...)cptX: (...)cptY: (...)cptZ: (...)icon: (...)id: "1fc98630-68ee-11ee-85e1-8fe06b150da2"__ob__: Observer {value: {…}, dep: Dep, vmCount: 0}get cptHeight: ƒ reactiveGetter()set cptHeight: ƒ reactiveSetter(newVal)get cptKey: ƒ reactiveGetter()set cptKey: ƒ reactiveSetter(newVal)get cptOption: ƒ reactiveGetter()set cptOption: ƒ reactiveSetter(newVal)get cptOptionKey: ƒ reactiveGetter()set cptOptionKey: ƒ reactiveSetter(newVal)get cptTitle: ƒ reactiveGetter()set cptTitle: ƒ reactiveSetter(newVal)get cptWidth: ƒ reactiveGetter()set cptWidth: ƒ reactiveSetter(newVal)get cptX: ƒ reactiveGetter()set cptX: ƒ reactiveSetter(newVal)get cptY: ƒ reactiveGetter()set cptY: ƒ reactiveSetter(newVal)get cptZ: ƒ reactiveGetter()set cptZ: ƒ reactiveSetter(newVal)get icon: ƒ reactiveGetter()set icon: ƒ reactiveSetter(newVal)get id: ƒ reactiveGetter()set id: ƒ reactiveSetter(newVal)[[Prototype]]: Object

在代码中 这个 cacheComponents 接的是一个对象, 它是一个数据的配置选项里面有大量的配置,结合我浏览器打的log就是。

cpt-button 就是对应的组件名称,你去到代码里的话,

里面就是对应的封装的组件代码。

<template>
  <div style="height: 100%">
    <button style="width: 100%;height: 100%;border: none" @click="redirect()"
          :style="{background:option.attribute.bgColor,color:option.attribute.textColor,
              borderRadius:option.attribute.radius+'px'}">
      {{cptData.value}}
    </button>
  </div>
</template>

<script>
import {getDataJson, pollingRefresh} from "@/utils/refreshCptData";

export default {
  name: "cpt-button",
  props:{option:Object},
  data() {
    return {
      cptData: {},
      uuid: null
    }
  },
  created() {
    this.uuid = require('uuid').v1();
    this.refreshCptData();
  },
  methods:{
    refreshCptData(){
      pollingRefresh(this.uuid, this.option.cptDataForm, this.loadData)
    },
    loadData(){
      getDataJson(this.option.cptDataForm).then(res => {
        this.cptData = res;
      });
    },
    redirect(){
      if (this.option.attribute.url){
        if (this.option.attribute.url.startsWith("view")){
          this.$router.push(this.option.attribute.url)
          this.$emit("reload")
        }else{
          window.open(this.option.attribute.url)
        }
      }
    }
  }
}
</script>

<style scoped>

</style>

还可以在看一个 echarts 的封装代码

<template>
  <div :id="uuid" style="width: 100%;height:100%;"></div>
</template>

<script>
import {getDataJson, pollingRefresh} from "@/utils/refreshCptData";

export default {
  name: "cpt-chart-line",
  props:{
    width:Number,
    height:Number,
    option:Object
  },
  data(){
    return {
      uuid:'',
      chartOption:{},
      chart:undefined,
      cptData:[]
    }
  },
  watch:{
    'option.attribute':{
      handler(newObj) {
        this.loadChart(newObj);
      },
      deep: true//深度监听
    },
    width(){
      this.chart.resize();
    },
    height(){
      this.chart.resize();
    }
  },
  created() {
    this.uuid = require('uuid').v1();
  },
  mounted() {
    this.chart = this.$echarts.init(document.getElementById(this.uuid));
    this.refreshCptData();
  },
  methods:{
    refreshCptData(){
      pollingRefresh(this.uuid, this.option.cptDataForm, this.loadData)
    },
    loadData() {
      getDataJson(this.option.cptDataForm).then(res => {
        this.cptData = res;
        this.loadChart(this.option.attribute);
      });
    },
    loadChart(attribute){
      const that = this;
      that.chartOption = {
        color: attribute.lineColor,
        title: {
          text: attribute.title,
          subtext: attribute.subtext,
          left: attribute.titleLeft,
          top: attribute.titleTop,
          textStyle:{
            color: attribute.titleTextColor
          },
          subtextStyle: { fontSize: 12, color: attribute.subtextColor }
        },
        grid: {
          x:10,y:30,x2:10,y2:10,
          containLabel: true
        },
        xAxis: {
          type: 'category',
          data: that.cptData.xData.split(','),
          axisLabel:{
            show: attribute.xLabelShow,
            color: attribute.xLabelColor
          },
          axisLine:{
            show: attribute.xLineShow,
            lineStyle: {
              color: attribute.xLineColor
            }
          },
          axisTick:{//x轴刻度线
            show: attribute.xTickShow
          },
        },
        yAxis: {
          type: 'value',
          axisLabel:{
            show: attribute.yLabelShow,
            color: attribute.yLabelColor
          },
          axisLine:{
            show: attribute.yLineShow,
            lineStyle:{
              color: attribute.yLineColor
            }
          },
          axisTick:{//y轴刻度线
            show: attribute.yTickShow
          },
          splitLine: {//网格线
            show: attribute.yGridLineShow
          }
        },
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'cross'
          }
        },
        series: [{
          data: that.cptData.yData.split(','),
          type: 'line',
          smooth: attribute.smooth,
          areaStyle:{
            color: new that.$echarts.graphic.LinearGradient(0, 0, 0, 1, [{
              offset: 0,
              color: attribute.areaColor1
            }, {
              offset: .34,
              color: attribute.areaColor2
            },{
              offset: 1,
              color: attribute.areaColor3
            }])
          }
        }]
      }
      that.chart.setOption(that.chartOption);
    }
  }
}
</script>

<style scoped>

</style>
至于渲染部分是在:

<component :is="item.cptKey" :ref="item.cptKey+index" :width="Math.round(item.cptWidth)"
 :height="Math.round(item.cptHeight)" :option="item.cptOption"/>

是在代码中的这个位置,关于 这个 Vue 的 component 通用渲染组件,关于这个组件GPT 给出相应的解释

在Vue中,<component />是一种常用的渲染通用组件的方法。<component />是Vue提供的一个内置组件,它可以根据动态的is属性值来渲染不同的组件。

你可以使用<component :is="componentName" />的语法,其中componentName是一个在父组件中定义的变量,用于指定要渲染的子组件。这使得你可以动态地切换或加载不同的组件。

以下是一个示例代码,展示了如何在Vue中使用<component />来渲染通用组件:

<template>
  <div>
    <component :is="currentComponent"></component>
    <button @click="changeComponent">Change Component</button>
  </div>
</template>

<script>
import FirstComponent from './FirstComponent.vue';
import SecondComponent from './SecondComponent.vue';

export default {
  components: {
    FirstComponent,
    SecondComponent
  },
  data() {
    return {
      currentComponent: 'FirstComponent'
    };
  },
  methods: {
    changeComponent() {
      // 切换要渲染的组件
      this.currentComponent = this.currentComponent === 'FirstComponent' ? 'SecondComponent' : 'FirstComponent';
    }
  }
};
</script>

在上面的代码中,我们使用<component :is="currentComponent"></component>来动态渲染currentComponent变量所指定的组件。通过点击按钮,我们可以切换要渲染的组件。

因此,<component />是Vue中一种常用的渲染通用组件的方法,它允许你根据动态变量来加载不同的组件。

所以代码通过引用加载自定义组件的方式引用编码,起码这个实现的前半部分我们实现了,就是自建或者引用大量的组件代码,然后通过 :ref  和
 <component :is="currentComponent"></component>
大量的动态组建代码。

那么 这个 涉及到拖拽生成大屏 前端逻辑部分第一部分算是解决了,还要解决后半段预览和出图实现方法
```。


`预览的页面找到了`

```<div v-for="item in designCache.components" :key="item.id"
             style="position: absolute;"
             :style="{width:Math.round(item.cptWidth)+'px',
                  height:Math.round(item.cptHeight)+'px',
                  top:Math.round(item.cptY)+'px',
                  left:Math.round(item.cptX)+'px',
                  zIndex:item.cptZ}">

 <component :is="item.cptKey"                     :width="Math.round(item.cptWidth)"
                   :height="Math.round(item.cptHeight)" @reload="loadCacheData"
                   :option="item.cptOption"/>
        </div>

核心还是送数据在预览
分析了它的代码,预览只是提供了一个新的路由把东西都整合放上去,
而导出只能导出数据,是一个cd格式的数据,或者使用 html2canvas 直接出图,这个就是 这个项目的大体逻辑,
但是只用 一周的时间 Vue2 往 Vue3 上转的话还是有点困难的。
综上 耗时一天半,这个项目的核心逻辑就大概是上面的一个情况。