实现拖拽低代码功能可配置并展示

323 阅读1分钟

效果图:

​编辑

 ​编辑

 配置页面主体代码:

左边控件模块以及中间内容模块:

主要采用vue-drag-resize插件实现拖拽功能,本来是运用的grid-layout实现拖拽,运用更简单,但是需求又改了,可能需要每个控件在图片上,所以改用了vue-drag-resize。

vue-drag-resize循环layout数组,这个对象数组是我们所有控件的详细配置,将此对象数组记载的宽,高,x轴定位,y轴定位全同步在控件中。并在左边控件栏添加click事件 将每个控件的item信息添加到layout数组里,并在vue-drag-resize里添加mousedown事件(为什么要用mousedown,因为click事件在这里用有bug click事件会在拖拽之前)触发唤出右边配置模块

<template>
  <el-container class="JNPF-Portal">
    <el-aside width="250px" class="left-box">
      <el-scrollbar class="aside-scrollbar">
        <div class="components-list">
          <div class="components-part">
            <el-collapse>
              <el-collapse-item>
                <template slot="title">
                  <div class="eqpcomponents-title">设备控件</div>
                </template>
                <div class="list">
                  <div v-for="(item, index) in eqpList" :key="index" class="components-item" @click="addComponent(item)">
                    <div class="components-body">
                      <i class="icon-ym icon-ym-scheduleExample" />
                      {{ item.Name }}
                    </div>
                  </div>
                </div>
              </el-collapse-item>
            </el-collapse>
            <div class="components-title">标签控件</div>
            <div class="list">
              <div class="components-item" @click="addComponent(viewItem)">
                <div class="components-body">
                  <i class="icon-ym icon-ym-scheduleExample" />
                  {{ viewItem.title }}
                </div>
              </div>
              <div class="components-item" @click="addComponent(videoItem)">
                <div class="components-body">
                  <i class="icon-ym icon-ym-scheduleExample" />
                  {{ videoItem.title }}
                </div>
              </div>
              <div class="components-item" @click="addComponent(textItem)">
                <div class="components-body">
                  <i class="icon-ym icon-ym-scheduleExample" />
                  {{ textItem.title }}
                </div>
              </div>
            </div>
          </div>
        </div>
      </el-scrollbar>
    </el-aside>
    <el-main class="center-box">
      <div class="action-bar">

        <el-button icon="el-icon-video-play" type="text" @click="preview" size="medium">
          预览</el-button>
        <el-button class="delete-btn" icon="el-icon-delete" type="text" @click="empty" size="medium">清空</el-button>
      </div>
      <div ref="addheight" class="layout-area" :style="canvasInfo">

        <vue-drag-resize v-for="item in layout" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :i="item.i" :z="item.z"
          :isResizable="true" @mousedown.native="handleClick(item)" :class="{ 'active-item': item.i === activeId }"
          class="dragResize" @dragstop="changeData" @resizestop="changeData">
          <img height="100%" :alt="item.title" width="100%" :src="baseUrl + item.EquPicture[0].url"
            v-if="item.jnpfKey == 'img'" />
          <p v-if="item.jnpfKey == 'img' && item.titleShow" style="text-align: center;font-size: 18px;">{{ item.title }}
          </p>

          <div height="100%" width="100%" v-if="item.jnpfKey == 'view'"
            :style="[{ color: item.color }, { fontSize: item.fontSize }]">
            {{ item.title }}:{{ item.points ? item.points : '请配置测点字段' }} {{ item.unit ? item.unit : '单位' }}
          </div>
          <div height="100%" width="100%" v-if="item.jnpfKey == 'text'"
            :style="[{ color: item.color }, { fontSize: item.fontSize }]">
            {{ item.title }}
          </div>
          <div v-if="item.jnpfKey == 'video'" class="video">
            <i class="el-icon-video-camera-solid"></i>
          </div>
          <div class="mask"></div>
          <span title="复制" class="drawing-item-copy" @click="addComponent(item)">
            <i class="el-icon-copy-document"></i></span>
          <span title="删除" class="drawing-item-delete" @click="handleRemoveItem(item.i)">
            <i class="el-icon-delete"></i></span>
        </vue-drag-resize>
        <!-- 空白时显示 -->
        <div v-show="!layout.length" class="empty-info">
          <img :src="require('@/assets/images/emptyPortal.png')" alt="" class="empty-img">
        </div>
      </div>
    </el-main>
    <right-panel :active-data="activeData" :canvasInfo="canvasInfo" />
    <Preview :visible.sync="previewVisible" :layout="layout" />
  </el-container>
</template>
<script>
import VueDragResize from 'vue-drag-resize'
import { deepClone } from '@/utils'
import { getDeviceList } from '@/api/iotSupervision'
import Preview from './Preview'
import RightPanel from './RightPanel'
const defaultConf = {
  layouyId: 100,
  layout: []
}

export default {
  name: 'JNPF-IotDesign',
  props: ['conf'],
  components: {
    Preview,
    RightPanel,
    VueDragResize
  },
  data() {
    return {
      eqpList: [],
      baseUrl: process.env.VUE_APP_BASE_API,
      viewItem: {
        jnpfKey: 'view',
        title: '标签输入',
        points: null,
        unit: null,
        eqp: null,
        color: '#888',
        fontSize: '18px',
        w: 250,
        h: 40,
        z: 10,
        minW: 6,
        minH: 4,
        maxW: 12,
        maxH: 6
      },
      textItem: {
        jnpfKey: 'text',
        title: '文本输入',
        color: '#888',
        fontSize: '18px',
        w: 250,
        h: 40,
        z: 10,
        minW: 6,
        minH: 4,
        maxW: 12,
        maxH: 6
      },
      videoItem: {
        jnpfKey: 'video',
        title: '视频控件',
        w: 320,
        h: 200,
        z: 15,
        src: '',
      },
      viewStyle: {},
      layout: [],
      eqplayout: [],
      activeId: null,
      activeData: null,
      previewVisible: false,
      config: {},
      changeDataId: 1,
      canvasInfo: {
        width: '1000px',
        height: '750px'
      },
    }
  },
  watch: {
    layout: {
      handler(val) {
        if (val.length === 0) this.config.layouyId = 100
      },
      deep: true
    },
  },
  created() {
    this.init()
  },
  mounted() {
    if (typeof this.conf === 'object' && this.conf !== null) {
      this.config = this.conf
    } else {
      this.config = deepClone(defaultConf)
      this.config.layouyId = 100
    }
    this.layout = this.config.layout || []
  },
  methods: {
    init() {
      getDeviceList().then(res => {
        res.data[0].children.forEach(item => {
          console.log(item, '999');
          item.EquPicture = JSON.parse(item.EquPicture)
          item.icon = 'icon-ym icon-ym-generator-notice',
            item.jnpfKey = 'img',
            item.title = item.Name,
            item.titleShow = true,
            item.w = 200,
            item.h = 200,
            item.z = 5,
            item.minW = 4,
            item.minH = 7,
            item.maxW = 12,
            item.maxH = 7
        })
        this.eqpList = res.data[0].children

      })
    },
    getData() {
      return new Promise((resolve, reject) => {
        this.config.layout = this.layout
        this.config.canvasInfo = this.canvasInfo
        resolve({ formData: this.config, target: 1 })
      })
    },
    addComponent(item) {
      let clone = deepClone(item)
      let x = 0, y = 0, i = this.config.layouyId
      if (this.layout.length) {
        let lastItem = this.layout[this.layout.length - 1]
        y = lastItem.y + lastItem.h
      }
      let row = { ...clone, i, x, y }

      this.layout.push(row)
      this.activeId = this.config.layouyId
      this.activeData = row
      this.config.layouyId++
    },
    handleRemoveItem(i) {
      this.layout = this.layout.filter(item => item.i !== i);

      this.activeId = null
      this.activeData = {}
    },
    empty() {
      this.$confirm('确定要清空所有吗?', '提示', { type: 'warning' }).then(() => {
        this.layout = []
        this.config.layouyId = 100
        this.activeId = null
        this.activeData = {}
      }).catch(() => { })
    },
    preview() {
      this.previewVisible = true
    },
    handleClick(item) {
      this.activeId = item.i
      this.activeData = item
    },
    resizedEvent(i) {
      this.$refs['eChart' + i] && this.$refs['eChart' + i][0] && this.$refs['eChart' + i][0].chart && this.$refs['eChart' + i][0].chart.resize()
    },
    changeData(data) {
      this.$set(this.activeData, 'x', data.left)
      this.$set(this.activeData, 'y', data.top)
      this.$set(this.activeData, 'w', data.width)
      this.$set(this.activeData, 'h', data.height)
    },
    addheight() {
      const num = parseInt(window.getComputedStyle(this.$refs.addheight).height)
      this.$refs.addheight.style.height = num + 50 + 'px'
    }
  }
}
</script>

右边配置栏:

配置栏就没什么需要注意的 灵活运用watch和this.$set就行

<template>
  <el-aside width="300px" class="right-box">
    <div class="cap-wrapper">组件属性</div>
    <el-scrollbar class="aside-scrollbar">
      <el-form size="small" label-width="80px" labelPosition="left" style="padding-left: 10px;padding-top: 10px;">
        <template v-if="activeData">
          <el-form-item v-if="activeData.title !== undefined" label="标题">
            <el-input v-model="activeData.title" placeholder="请输入标题" />
          </el-form-item>
          <el-form-item v-if="activeData.jnpfKey === 'view'" label="测点字段">
            <el-input v-model="activeData.points"></el-input>
          </el-form-item>
          <el-form-item v-if="activeData.jnpfKey === 'view'" label="设备编码">
            <el-input v-model="activeData.eqp" placeholder="请输入设备编码" />
            <!-- <el-select v-model="activeData.eqp" placeholder="请选择">
              <el-option v-for="item in eqplayout" :key="item.id" :label="item.Name" :value="item.id">
              </el-option>
            </el-select> -->
          </el-form-item>
          <el-form-item v-if="activeData.jnpfKey === 'view'" label="单位">
            <el-select v-model="activeData.unit" clearable placeholder="请选择">
              <el-option v-for="item in unitList" :key="item.id" :label="item.fullName" :value="item.fullName">
              </el-option>
            </el-select>
          </el-form-item>
          <el-form-item v-if="activeData.jnpfKey === 'view' || activeData.jnpfKey === 'text'" label="字体颜色">
            <el-color-picker v-model="activeData.color"></el-color-picker>
          </el-form-item>
          <el-form-item v-if="activeData.jnpfKey === 'view' || activeData.jnpfKey === 'text'" label="字体大小">
            <el-input-number v-model="fontSize"></el-input-number>
          </el-form-item>
          <el-form-item v-if="activeData.jnpfKey === 'video'" label="网址路径">
            <el-input v-model="activeData.src" placeholder="请输入src" />
          </el-form-item>
          <el-form-item v-if="activeData.jnpfKey === 'img'" label="是否显示">
            <el-switch v-model="activeData.titleShow" />
          </el-form-item>
          <el-form-item label="宽">
            <el-input-number v-model="activeData.w"></el-input-number>
          </el-form-item>
          <el-form-item label="高">
            <el-input-number v-model="activeData.h"></el-input-number>
          </el-form-item>
          <el-form-item label="x">
            <el-input-number v-model="activeData.x"></el-input-number>
          </el-form-item>
          <el-form-item label="y">
            <el-input-number v-model="activeData.y"></el-input-number>
          </el-form-item>
          <el-form-item label="层级">
            <el-input-number v-model="activeData.z"></el-input-number>
          </el-form-item>
        </template>
      </el-form>
    </el-scrollbar>
    <div class="cap-wrapper">画布参数</div>
    <el-scrollbar class="aside-scrollbar">
      <el-form size="small" label-width="80px" labelPosition="left" style="padding-left: 10px;padding-top: 10px;">
        <el-form-item label="画布宽度">
          <el-input-number v-model="width"></el-input-number>
        </el-form-item>
        <el-form-item label="画布高度">
          <el-input-number v-model="height"></el-input-number>
        </el-form-item>
      </el-form>
    </el-scrollbar>
  </el-aside>
</template>
<script>
import draggable from 'vuedraggable'
import { getSelectorAll } from '@/api/system/menu'
import { getDataInterfaceSelector } from '@/api/systemData/dataInterface'
import InterfaceDialog from '@/components/Process/PropPanel/InterfaceDialog'
import { getUnitList } from '@/api/IotDesign'
export default {
  props: ['activeData', 'canvasInfo'],
  components: { draggable, InterfaceDialog },
  data() {
    return {
      areaVisible: false,
      currentIndex: 0,
      menuList: [],
      dataInterfaceOptions: [],
      unitList: [],
      fontSize: 0,
      width: 0,
      height: 0,
    }
  },
  created() {
    this.getMenuList()
    this.getDataInterfaceSelector()
    this.getUnitList()
  },
  watch: {
    'activeData.color': function (newVal, oldVal) {
      this.$set(this.activeData, 'color', newVal)
    },
    'activeData.fontSize': function (newVal, oldVal) {
      console.log('123123');
      this.fontSize = parseInt(this.activeData.fontSize)
    },
    'canvasInfo.width': {
      handler(newVal) {
        this.width = parseInt(this.canvasInfo.width)
      },
      immediate: true,
      // todo
    },
    'canvasInfo.height': {
      handler(newVal) {
        this.height = parseInt(this.canvasInfo.height)
      },
      immediate: true,
      // todo
    },
    fontSize: {
      handler(newVal) {
        this.$set(this.activeData, 'fontSize', newVal + 'px')
      },
    },
    width: {
      handler(newVal) {
        this.$set(this.canvasInfo, 'width', newVal + 'px')
      },
    },
    height: {
      handler(newVal) {
        this.$set(this.canvasInfo, 'height', newVal + 'px')
      },
    },
  },
  methods: {
    // 获取单位
    async getUnitList() {
      const res = await getUnitList()
      this.unitList = res.data.list
    },
    getMenuList() {
      getSelectorAll({ category: 'Web' }).then(res => {
        this.menuList = res.data.list
      })
    },
    getDataInterfaceSelector() {
      getDataInterfaceSelector().then(res => {
        this.dataInterfaceOptions = res.data
      })
    },
    getSelectValue(data, i) {
      if (!data[0]) {
        this.$set(this.activeData.list, i, {
          fullName: '',
          id: '',
          urlAddress: '',
          icon: '',
          iconBackgroundColor: '',
          type: '',
          propertyJson: '',
          linkTarget: '_self',
          enCode: ''
        })
      } else {
        let iconBackgroundColor = ''
        if (data[1].propertyJson) {
          let propertyJson = JSON.parse(data[1].propertyJson)
          iconBackgroundColor = propertyJson.iconBackgroundColor || ''
        }
        this.$set(this.activeData.list, i, {
          fullName: data[1].fullName,
          id: data[1].id,
          urlAddress: data[1].urlAddress,
          type: data[1].type,
          propertyJson: data[1].propertyJson,
          linkTarget: data[1].linkTarget,
          enCode: data[1].enCode,
          icon: data[1].icon,
          iconBackgroundColor: iconBackgroundColor
        })
      }
    },
    addSelectItem() {
      this.activeData.list.push({
        fullName: '',
        id: '',
        urlAddress: '',
        icon: '',
        iconBackgroundColor: '',
        type: '',
        propertyJson: '',
        linkTarget: '_self',
        enCode: ''
      })
    },
    delSelectItem(index) {
      if (this.activeData.list.length < 3) {
        this.$message({
          message: '选项最少要保留两项',
          type: 'warning'
        });
        return
      }
      this.activeData.list.splice(index, 1)
    },
    addDataBoardItem() {
      this.activeData.list.push({ fullName: "", num: '', dataType: 'static', propsApi: '', icon: "" })
    },
    delDataBoardItem(index) {
      if (this.activeData.list.length < 3) {
        this.$message({
          message: '选项最少要保留两项',
          type: 'warning'
        });
        return
      }
      this.activeData.list.splice(index, 1)
    },
    openIconsDialog(index) {
      this.iconsVisible = true
      this.currentIndex = index
    },

    showData(option) {
      this.areaVisible = true
      this.$nextTick(() => {
        this.$refs.JSONArea.init(option)
      })
    },
    updateOption(data) {
      let option = data ? JSON.parse(data) : {}
      this.activeData.option = option
    },
    dataTypeChange() {
      this.activeData.propsApi = ''
      this.activeData.propsName = ''
    },
    propsUrlChange(data, index) {
      if (!data || !data.length) {
        this.activeData.list[index].propsApi = ''
        this.activeData.list[index].propsName = ''
        return
      }
      this.activeData.list[index].propsApi = data[0]
      this.activeData.list[index].propsName = data[1].fullName
    },
    propsApiChange(val, item) {
      if (!val) {
        this.activeData.propsApi = ''
        this.activeData.propsName = ''
        return
      }
      this.activeData.propsApi = val
      this.activeData.propsName = item.fullName
    }
  }
}
</script>

展示页面代码:

基本就和配置页面中间内容模块一样 设置成禁止拖拽 禁止缩放就行

区别是获取存在后端的json字符串进行赋值配置

其中业务逻辑多是处理了测点获取接口数据的项目需求 (功能需求要求的 可以不看)

<template>
  <div class="layout-area red" :style="canvasInfo">
    <vue-drag-resize v-for="item in layout" :key="item.i" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :i="item.i"
      :z="item.z" :isActive="false" :isResizable="false" :isDraggable="false" class="dragResize">
      <img height="100%" :alt="item.title" width="100%" :src="baseUrl + item.EquPicture[0].url"
        v-if="item.jnpfKey == 'img'" />
      <p v-if="item.jnpfKey == 'img' && item.titleShow" style="text-align: center;font-size: 18px;">{{ item.title }}</p>
      <div height="100%" width="100%" v-if="item.jnpfKey == 'view'"
        :style="[{ color: item.color }, { fontSize: item.fontSize }]">
        {{ item.title }}:{{ item.dataPoints ? item.dataPoints : '请配置测点字段' }} {{ item.unit ? item.unit : '' }}
      </div>
      <div height="100%" width="100%" v-if="item.jnpfKey == 'text'"
        :style="[{ color: item.color }, { fontSize: item.fontSize }]">
        {{ item.title }}
      </div>
      <div v-if="item.jnpfKey == 'video'" class="video" @click="videoDialogFn(item)">
        <i class="el-icon-video-camera-solid"></i>
      </div>
    </vue-drag-resize>
    <!-- 空白时显示 -->
    <div v-show="!layout.length" class="empty-info">
      <img :src="require('@/assets/images/emptyPortal.png')" alt="" class="empty-img">
    </div>
    <el-dialog :title="title" :visible.sync="videoDialog" width="60%" @close="videoDialogClose">
      <div class="flex-f">
        <video width="70%" height="70%" id="myVideo" preload="auto" muted type="rtmp/flv"></video>
      </div>
    </el-dialog>
  </div>
</template>
<script>
import VueDragResize from 'vue-drag-resize'
import { getEqpPointsAll } from '@/api/IotDesign'
import flvjs from 'flv.js'
export default {
  props: ["layout", 'type', 'canvasInfo'],
  components: {
    VueDragResize
  },
  data() {
    return {
      baseUrl: process.env.VUE_APP_BASE_API,
      videoDialog: false,
      videoSrc: '',
      title: '',
    };
  },
  activated() { },
  watch: {
    layout: {
      deep: true,
      handler(v) {
        this.setData()
      },
      player: null,
    }
  },
  created() {
    // this.setData()
    // console.log(this.datas.data[0].values['1-PowerAlarm'],'7878787');
  },
  mounted() {
    // this.$nextTick(() => {
    //   this.createdPlay()
    // })
  },
  methods: {
    setData() {
      // 标签数组
      let arr = this.layout.filter(item => {
        return item.jnpfKey == 'view'
      })
      // 集合转化为参数
      // let newArr = []
      // arr.forEach(item => {
      //   let obj = {}
      //   console.log(!newArr.length,'777');
      //   if (!newArr.length) {
      //     obj.eqpCode = item.eqp
      //     obj.points = [item.points]
      //     newArr.push(obj)
      //   } else {
      //     newArr.forEach(it => {
      //       if (item.eqp == it.eqpCode) {
      //         it.points.push(item.points)
      //       } else {
      //         obj.eqpCode = item.eqp
      //         obj.points = [item.points]
      //         newArr.push(obj)
      //       }
      //     })
      //   }

      // }),
      let newArr = arr.reduce((acc, cur) => {
        let found = false
        for (let i = 0; i < acc.length; i++) {
          if (acc[i].eqpCode === cur.eqp) {
            acc[i].points.push(cur.points)
            found = true
            break
          }
        }
        if (!found) {
          acc.push({
            eqpCode: cur.eqp,
            points: [cur.points]
          })
        }
        return acc
      }, [])
      getEqpPointsAll(newArr).then(({ data }) => {
        this.layout.forEach(item => {
          data.forEach(it => {
            if (item.eqp == it.eqpCode) {
              Object.keys(it.values).forEach(i => {
                if (item.points == i) {
                  item.dataPoints = it.values[i]
                }
                this.$forceUpdate()
              })
            }
          })
        })
      })

    },
    videoDialogFn(item) {
      this.videoDialog = true
      // console.log(item, '999');
      // this.videoSrc = item.src
      this.title = item.title
      this.createdPlay(item.src)
    },
</script>

整个做下来还是挺有意思的 也遇到了很多bug  其中代码还可以封装得更好 比如每个控件的配置可以封装成js文件 vue-drag-resize中的控件显示也可以封装成额外的组件。

欢迎讨论