教你用vue开发拖拽式商品标签设计系统

1,001 阅读5分钟

小弟初次在掘金上撰文,如有不足还请各位看官多多保函。

国际惯例

github:github.com/SXX19950910…
预览:sxx19950910.github.io/manifest-de…

很久很久以前,在一个月黑风高的夜晚,有一群人独自的坐在办公室工作,殊不知是为了设计一个商品标签而操劳到那么晚,于是乎,一位优秀的程序员为解决他们的烦恼而研发了一个商品标签系统,使他们可以更加便利的做出他们想要的内容。哈哈哈哈哈,没错~ 那个人就是我。2333~

系统设计的关键点(难点)

  1. 如何将拖拽的东西变成一个组件放到画板之中。
  2. 怎样动态修改标签元素。
  3. 自由拖拽一个标签元素该如何实现。
  4. 如何拖拽组件尺寸的大小。
  5. 怎么将设计好的标签模板保存为图片。
以上的几点可以说是整个项目的灵魂所在,缺一不可,请大家耐心看完。

如何将拖拽的东西变成一个组件放到画板之中

话不多说先上代码

<template>
  <div ref="drag" :id="aimId" class="drag-warp" :class="activeClass" :style="dragStyle" @click.stop="handleSetCurrent" @mousedown="handleMouseDown" @contextmenu="handleContextMenu">
    <component :is="componentObject.type" v-bind="componentObject.props" :element-id="componentObject.id" :update-id="componentObject.updateId" @complete="init" />
    <drag-resize v-if="resizeVisible" :component-object="componentObject" @resize-down="handleResizeDown"/>
  </div>
</template>

相信已经有人看出来了解决问题的关键,没错!就是component组件,vue自带的component组件是一个非常神器的组件,通过给该组件的is属性传值即可渲染成对应实体组件。
好了,知道怎么渲染组件了,那我们下一步该如何去生成对应的组件呢?先给大家上个图。

如图所示,js代码如下

    // 拖拽到画板之中所执行的事件
    onAdd(el) {
        const { clientX, clientY } = el.originalEvent;
        // 获取组件名称
        const componentId = el.item.getAttribute('data-component-id');
        const props = {
          position: {
            clientX,
            clientY,
          },
        };
        // 是否在画板的范围之类
        const xArea = clientX < this.right && clientX > this.left;
        const yArea = clientY < this.bottom && clientX > this.top;
        if (xArea && yArea) {
          this.$store.dispatch('components/addComponent', { componentId, props });
        }
      },

由于拖拽面板,与拖拽的目标画板不是在同一个组件之中,为了方便状态管理所以我们所做的一切操作都是基于vuex来进行的。

怎样动态修改标签元素

想要动态修改标签元素就和吃饭一样简单,简单来说改变state就是改变组件属性,只需要将一些关键属性放入state之中即可。
放一个图片组件的代码

<template>
  <img ref="img" class="barcode" :class="elementId" :style="getStyle" alt="barcode" src draggable="false" />
</template>

<script>
  import barcode from 'jsbarcode';
  import { on, off } from '@/utils/dom.js';
  import { mapGetters } from 'vuex';
  export default {
    props: {
      elementId: {
        type: String,
        default: '',
      },
      component: {
        type: Object,
        default() {
          return {};
        },
      },
      bodyHeight: {
        type: Number,
        default: 40,
      },
      lineWidth: {
        type: Number,
        default: 2,
      },
      format: {
        type: String,
        default: 'CODE128',
      },
      displayValue: {
        type: String,
        default: '1',
      },
      data: {
        type: String,
        default: '',
      },
    },
    data() {
      return {
      };
    },
    computed: {
      ...mapGetters(['activeComponent', 'storeList']),
      currentComponent() {
        return this.storeList.find((item) => item.id === this.activeComponent);
      },
      getStyle() {
        return {
          maxWidth: '100%',
          verticalAlign: 'middle',
          userSelect: 'none',
        };
      },
    },
    destroyed() {
      // this.clearListener();
    },
    mounted() {
      this.init();
    },
    methods: {
      complete() {
        this.$emit('complete');
      },
      clearListener() {
        const that = this;
        off(that.$refs.img, 'load', that.complete);
      },
      init() {
        this.complete();
        const { elementId, bodyHeight, lineWidth, displayValue, format, data } = this;
        barcode(`.${elementId}`, data, {
          format,
          width: lineWidth,
          height: bodyHeight,
          textMargin: 10,
          displayValue: displayValue === '1',
        });
      },
    },
  };
</script>

<style lang="scss">
  .barcode {
    max-width: 100%;
    vertical-align: middle;
    user-select: none;
  }
</style>

该组件绑定了vuex中的state,那么我们只需要在其他地方修改对应组件的state即可修改组件。

自由拖拽一个标签元素该如何实现 && 如何拖拽组件尺寸的大小

简单来说就是监听鼠标事件然后改变组件的top,left值,注意元素定位一定要脱离文档流哟,上代码!

<template>
  <div ref="drag" :id="aimId" class="drag-warp" :class="activeClass" :style="dragStyle" @click.stop="handleSetCurrent" @mousedown="handleMouseDown" @contextmenu="handleContextMenu">
    <component :is="componentObject.type" v-bind="componentObject.props" :element-id="componentObject.id" :update-id="componentObject.updateId" @complete="init" />
    <drag-resize v-if="resizeVisible" :component-object="componentObject" @resize-down="handleResizeDown"/>
  </div>
</template>

<script>
  import {on, off} from '@/utils/dom';
  import { debounce } from 'throttle-debounce';
  import { mapGetters } from 'vuex';
  export default {
    name: 'Drag',
    props: {
      componentObject: {
        type: Object,
        default() {
          return {};
        },
      },
      isInstance: {
        type: Boolean,
        default: false,
      },
      aimId: {
        type: String,
        default: '',
      },
      defaultX: {
        type: Number,
        default: 0,
      },
      defaultY: {
        type: Number,
        default: 0,
      },
      updateId: {
        type: String,
        default: '',
      },
      default: {
        type: Object,
        default() {
          return {};
        },
      },
    },
    data() {
      return {
        x: '',
        y: '',
        downX: '',
        downY: '',
        resizeDownX: '',
        resizeDownY: '',
        downWidth: '',
        downHeight: '',
        offsetLeft: '',
        offsetTop: '',
        resizeOffsetRight: '',
        resizeOffsetBottom: '',
        board: {},
        active: false,
        defaultWidth: '',
        defaultHeight: '',
        width: '',
        height: '',
        debounceUpdateComponent: Function,
      };
    },
    destroyed() {
      this.clearAllListener();
    },
    computed: {
      ...mapGetters(['activeComponent']),
      dragStyle() {
        const { width, height, y, x, isLine } = this;
        return {
          width: `${width}px`,
          height: `${height}px`,
          top: `${y}px`,
          left: `${x}px`,
          padding: isLine ? '0' : '0 10px 0 0',
          overflow: isLine ? 'unset' : 'hidden',
        };
      },
      activeClass() {
        const result = [];
        const {id = ''} = this.activeComponent;
        if (id === this.componentObject.id) {
          result.push('is-active');
        }
        if (this.componentObject.type === 'BarcodeUi') {
          result.push('barcode-ui');
        }
        if (this.isLine) {
          result.push('line-ui');
        }
        return result;
      },
      isLine() {
        return this.componentObject.type === 'XLineUi' || this.componentObject.type === 'YLineUi' || this.componentObject.type === 'RectangleUi';
      },
      resizeDisabledY() {
        return this.componentObject.type === 'XLineUi';
      },
      resizeDisabledX() {
        return this.componentObject.type === 'YLineUi';
      },
      resizeVisible() {
        return this.activeClass.includes('is-active');
      },
    },
    mounted() {
      this.debounceUpdateComponent = debounce(200, this.moveEnd);
    },
    methods: {
      init() {
        this.initLayoutScheme();
      },
      initLayoutScheme() {
        // 页面元素加载完成之后执行的事件。
        const $drag = this.$refs.drag;
        const isInstance = this.isInstance;
        const element = $drag.firstElementChild;
        const defaultData = this.default;
        const canvas = document.querySelector('.drag-canvas-warp.board-canvas');
        const { width, height } = $drag.getBoundingClientRect();
        const { defaultX, defaultY } = this;
        const { top, left } = element.getBoundingClientRect();
        this.board = canvas.getBoundingClientRect();
        this.offsetLeft = left;
        this.offsetTop = top;
        this.defaultHeight = height;
        this.defaultWidth = width;
        this.width = width;
        if (isInstance) {
          this.x = defaultData.x;
          this.y = defaultData.y;
          this.width = defaultData.width;
          this.height = defaultData.height || '';
        } else {
          this.x = defaultX - left;
          this.y = defaultY - top;
        }
        this.$nextTick(() => {
          this.debounceUpdateComponent();
        });
      },
      clearAllListener() {
        off(document, 'mousemove', this.handleResizeMove);
        off(document, 'mouseup', this.handleResizeUp);
        off(document, 'mousemove', this.handleMouseMove);
        off(document, 'mouseup', this.handleMouseUp);
      },
      handleMouseDown(e) {
        // 记录初始点击的坐标值以及添加一些监听事件。
        const $drag = e.path.find((item) => item.className.includes('drag-warp'));
        const { top, left } = $drag.getBoundingClientRect();
        this.downX = e.clientX - left;
        this.downY = e.clientY - top;
        on(document, 'mousemove', this.handleMouseMove);
        on(document, 'mouseup', this.handleMouseUp);
        this.handleSetCurrent();
      },
      handleContextMenu() {
        this.clearAllListener();
      },
      handleSetCurrent() {
        // 设置组件的选中状态
        const { id = '' } = this.componentObject;
        this.$store.dispatch('components/setActive', id);
      },
      handleMouseMove(e) {
        // 元素在画板中拖拽的事件
        const aim = this.aimId;
        const clientX = e.clientX;
        const clientY = e.clientY;
        const boardHeight = this.board.height;
        const boardWidth = this.board.width;
        const x = clientX - this.offsetLeft - this.downX;
        const y = clientY - this.offsetTop - this.downY;
        const $element = document.getElementById(aim);
        const { width, height } = $element.getBoundingClientRect();
        if (x <= 0) {
          this.x = 0;
        } else if ((width + x) >= boardWidth) {
          this.x = boardWidth - width;
        } else {
          this.x = x;
        }
        if (y <= 0) {
          this.y = 0;
        } else if ((height + y) >= boardHeight) {
          this.y = boardHeight - height;
        } else {
          this.y = y;
        }
        this.debounceUpdateComponent();
      },
      handleMouseUp() {
        off(document, 'mousemove', this.handleMouseMove);
        off(document, 'mouseup', this.handleMouseUp);
      },
      moveEnd() {
        // 拖拽结束后,将组件的信息同步到vuex之中
        setTimeout(() => {
          const { x, y } = this;
          const dragData = {
            id: this.aimId,
            x,
            y,
            instance: true,
            width: this.width || 0,
            height: this.height || 0,
            position: {
              clientX: x,
              clientY: y,
            },
          };
          this.$emit('move-end', dragData);
        });
      },
      handleResizeUp() {
        off(document, 'mousemove', this.handleResizeMove);
        off(document, 'mouseup', this.handleResizeUp);
      },
      handleResizeMove(e) {
        // 拖拽尺寸的各种计算,这个我打算后面细讲
        const { clientX, clientY } = e;
        const defaultWidth = this.defaultWidth;
        const defaultHeight = this.defaultHeight;
        const downWidth = this.downWidth;
        const downHeight = this.downHeight;
        const moveX = clientX - this.resizeDownX;
        const moveY = clientY - this.resizeDownY;
        const offsetRight = this.resizeOffsetRight;
        const offsetBottom = this.resizeOffsetBottom;
        const width  = downWidth + moveX;
        const height = downHeight + moveY;
        const heightLimit = height <= defaultHeight;
        const widthLimit = width <= defaultWidth;
        const xEdge = moveX >= offsetRight;
        const yEdge = moveY >= offsetBottom;
        if (!this.resizeDisabledX) {
          if (widthLimit) {
            this.width = defaultWidth;
          } else if (xEdge) {
            this.width = downWidth + offsetRight;
          } else {
            this.width = width;
          }
        }
        if (!this.resizeDisabledY) {
          if (heightLimit) {
            this.height = defaultHeight;
          } else if (yEdge) {
            this.height = downHeight + offsetBottom;
          } else {
            this.height = height;
          }
        }
        this.debounceUpdateComponent();
      },
      handleResizeDown(e) {
        const $drag = this.$refs.drag;
        // 获取拖拽组件的宽高
        const { width, height } = $drag.getBoundingClientRect();
        this.resizeOffsetRight = this.board.width - $drag.offsetLeft - width;
        this.resizeOffsetBottom = this.board.height - $drag.offsetTop - height;
        this.resizeDownX = e.clientX;
        this.resizeDownY = e.clientY;
        this.downWidth = width;
        this.downHeight = height;
        // 添加监听事件
        on(document, 'mousemove', this.handleResizeMove);
        on(document, 'mouseup', this.handleResizeUp);
        e.stopPropagation();
      },
    },
  };
</script>

<style lang="scss">
  @import "./src/style/variable";
  .drag-warp {
    position: absolute;
    cursor: pointer;
    border: 1px solid transparent;
    color: #000;
    border-radius: 2px;
    max-width: 100%;
    max-height: 100%;
    overflow: hidden;
    transition: background-color ease .36s;
    &.is-active {
      border: 1px solid $skyBlue;
      .rectangle-warp {
        border-color: $skyBlue !important;
      }
    }
    .resize-btn {
      position: absolute;
      right: -1px;
      bottom: -3px;
      font-size: 12px;
      transform: scale(.6);
      color: $skyBlue;
    }
    &:hover {
      background-color: rgba(25,143,255, 0.1);
    }
    &.line-ui {
      border: none;
      .line-resize {
        position: absolute;
        height: 100%;
        width: 10px;
        right: 0;
        top: 0;
        background-color: $skyBlue;
      }
    }
  }
</style>

如何将设计标签画板保存为图片

我们可以利用html2canvas这个库来实现该功能

 async handleSaveTemplateToImg() {
     //获取画板元素
     const $el = document.querySelector('.drag-canvas-warp.board-canvas');
     // 转换canvas
     const canvas = await html2canvas($el, {
       allowTaint: true,
       useCORS: true,
       backgroundColor: '#fff',
       width: $el.offsetWidth,
       height: $el.offsetHeight,
       dpi: window.devicePixelRatio * 2,
     });
     // 转为png格式的图片进行下载。
     const data = canvas.toDataURL('image/png');
     const blob = this.convertBase64UrlToBlob(data);
     const url = window.URL.createObjectURL(blob);
     const a = document.createElement('a');
     a.href = url;
     a.download = `商品标签模板_${new Date().getTime()}`;
     a.click();
     window.URL.revokeObjectURL(url);
   },

紧张的码字阶段终于结束了,感觉写的还不够详细,尤其是一些细节写的可能还不够到位,我只能将一些关键点和大家讲一下,因为一个问题的答案有很多种,我只给出了我的解答,如果大家有更好的方式还请多多指教。

望大家多多鼓励,共勉!