小弟初次在掘金上撰文,如有不足还请各位看官多多保函。
国际惯例
github:github.com/SXX19950910…
预览:sxx19950910.github.io/manifest-de…
很久很久以前,在一个月黑风高的夜晚,有一群人独自的坐在办公室工作,殊不知是为了设计一个商品标签而操劳到那么晚,于是乎,一位优秀的程序员为解决他们的烦恼而研发了一个商品标签系统,使他们可以更加便利的做出他们想要的内容。哈哈哈哈哈,没错~ 那个人就是我。2333~
系统设计的关键点(难点)
- 如何将拖拽的东西变成一个组件放到画板之中。
- 怎样动态修改标签元素。
- 自由拖拽一个标签元素该如何实现。
- 如何拖拽组件尺寸的大小。
- 怎么将设计好的标签模板保存为图片。
以上的几点可以说是整个项目的灵魂所在,缺一不可,请大家耐心看完。
如何将拖拽的东西变成一个组件放到画板之中
话不多说先上代码
<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属性传值即可渲染成对应实体组件。
好了,知道怎么渲染组件了,那我们下一步该如何去生成对应的组件呢?先给大家上个图。
// 拖拽到画板之中所执行的事件
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);
},
紧张的码字阶段终于结束了,感觉写的还不够详细,尤其是一些细节写的可能还不够到位,我只能将一些关键点和大家讲一下,因为一个问题的答案有很多种,我只给出了我的解答,如果大家有更好的方式还请多多指教。
望大家多多鼓励,共勉!