vue版本裁切工具,包含预览功能
最终效果: https://qiuyaofan.github.io/vue-crop-demo/ html
源码地址: https://github.com/qiuyaofan/vue-crop前端
// 初始化vue-cli vue init webpack my-plugin
新建src/views/validSlideDemo.vue, src/components里新建VueCrop/index.js,VueCrop.vue, 在routes/index.js配置访问路由(具体看github源码)
最终生成的文件结构以下图:vue
// 导入插件入口文件 import VueCrop from './VueCrop/index.js' const install = function (Vue, opts = {}) { /* 若是已安装就跳过 */ if (install.installed) return // 注册插件 Vue.component(VueCrop.name, VueCrop) } // 全局状况下注册插件 if (typeof window !== 'undefined' && window.Vue) { install(window.Vue) } export { install, // 此处是为了兼容在vue内单独引入这个插件,若是是main.js全局引入就能够去掉 VueCrop }
import Vue from 'vue' import App from './App' import router from './router' // 新加的:导入入口文件 import { install } from 'src/components/index.js' // 全局调用,至关于调用 `MyPlugin.install(Vue)` Vue.use(install) Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', router, components: { App }, template: '<App/>' })
// 导入vue import VueCrop from './VueCrop.vue' // Vue.js 的插件应当有一个公开方法 install 。这个方法的第一个参数是 Vue 构造器 VueCrop.install = function (Vue) { // 注册组件 Vue.component(VueCrop.name, VueCrop) } export default VueCrop
function MyPlugin(){ console.info('构造函数') } MyPlugin.prototype.install=function(vue,options){ console.info('构造器vue:'+vue); }
而真正注册组件的是:Vue.component()webpack
因此,vue插件注册的过程是:git
1.调用main.js中: import { install } from 'src/components/index.js' vue.use(install) 2.index.js添加install方法,调用Vue.component注册组件 3.组件内的index.js同全部组件的index.js同样
在此以前,能够先了解下组件的命名规范等,可参考文章 掘金:Vue前端开发规范,其中第2点有详细讲解
首先,肯定本身的调用方式和须要暴露的参数github
<vue-crop :crop-url="cropUrl1" :ratio="ratio" :height="460" :width="460" :previewJson="previewJson1" class="c-crop--preview_right" @afterCrop="afterCrop" > >
其中,@afterCrop="afterCrop"是裁切完成的回调函数,其余是属性配置web
在组件src/components/VueCrop/VueCrop.vue内,能够用this.$emit('afterCrop')触发demo里的afterCrop事件vue-cli
组件结构上,主要分为:裁切主体部分(VueCrop.vue),选框组件(VueCropTool.vue),裁切框宽度、位置坐标等计算(VueCropMove.js),拖拽事件注册公共js(components/utils/draggable.js)npm
备注:此组件不具有真实的裁切功能,最终的裁切是传递给后台去裁,你若是想扩展能够在afterCrop函数里根据坐标等信息进行处理
接下来咱们对各个组件和js进行讲解json
export default function (element, options) { const moveFn = function (event) { if (options.drag) { options.drag(event) } } // mousedown fn const downFn = function (event) { if (options.start) { // 调用参数中start函数 options.start(event) } } // mouseup fn const upFn = function (event) { document.removeEventListener('mousemove', moveFn) document.removeEventListener('mouseup', upFn) document.onselectstart = null document.ondragstart = null if (options.end) { // 调用参数中end函数 options.end(event) } } // 绑定事件 element.addEventListener('mousedown', event => { if (options.stop && options.stop(event, element) === false) { return false } document.onselectstart = function () { return false } document.ondragstart = function () { return false } document.addEventListener('mousedown', downFn) document.addEventListener('mousemove', moveFn) document.addEventListener('mouseup', upFn) }) }
draggable(this.$el.querySelector('.c-crop--drap_screen'), { start: (event) => { this.startPos = [event.x, event.y] }, drag: (event) => { this.handleDragLocation(event) }, end: (event) => { this.handleDragLocation(event) } })
//script部分 <script> import VueCropTool from './VueCropTool.vue' export default { name: 'VueCrop', data () { return { // 根据裁切后的缩放和坐标等生成的预览尺寸坐标数组 previewImgSize: null, // 图片初始数据 originImgSize: null, // 裁切框宽度 elWidth: 0, // 裁切框高度 elHeight: 0, // 裁切框top cursorTop: 0, // 裁切框left cursorLeft: 0, // 根据当前的容器宽高计算出的图片尺寸 imgH: 0, imgW: 0, // 图片url url: this.cropUrl, // 为适应当前的容器对原始图片的缩放值 scale: 1, // 根据当前选区和原始图片缩放前的尺寸,来获得最终的裁切尺寸 coord: null, // 计算出的裁切框的初始值 cropJson: { cw: null, ch: null, w: null, h: null, r: null } } }, // 暴露出去的参数,具体解释可看前文的表格 props: { cropUrl: String, // 比例 ratio: { type: null, default: false }, width: null, height: null, coordWidth: null, coordHeight: null, previewJson: { type: Array, default: function () { return [] } } }, components: { VueCropTool }, created () { }, watch: { // 监听图片路径变化 cropUrl (val) { this.url = val // setTimeout是为了兼容立刻获取尺寸获取不到的状况 setTimeout(() => { this.setSize() }, 200) } }, methods: { // 更新拖拽尺寸,大部分由裁切框组件经过@updateSize触发 drapSizeUpdate (w, h, t, l) { // 更新裁切框尺寸 this.elWidth = w this.elHeight = h this.cursorTop = t this.cursorLeft = l // 根据当前选区获取原始图片缩放前的尺寸(还原原始图片的宽高以获取最终裁切数据) this.coord = this.getCoord(l, t, w, h) // 更新预览尺寸 this.setPreviewSize(this.coord) }, // 裁切完毕回调 afterCrop () { this.$emit('afterCrop', this.coord, this.url) }, // 设置preview尺寸 setPreviewSize (coord) { if (!this.previewJson.length) { return false } let result = this.previewJson.map(data => { // 计算缩放比 let scale = data.width / coord.w return { scale, l: -scale * coord.l, t: -scale * coord.t, w: scale * this.originImgSize.w, h: scale * this.originImgSize.h } }) this.previewImgSize = result }, // 设置裁切显示的图片尺寸,存储scale值 async setSize () { if (!this.url) { return } let imgSize = await this.getSize(this.url) this.originImgSize = imgSize this.setCoordRange() this.scale = imgSize.w / this.imgW this.cursorTop = 0 this.cursorLeft = 0 let json = {...this.cropJson} json.w = this.imgW json.h = this.imgH // 有固定比例,则按比例截取 if (this.ratio) { json.r = this.ratio if (json.w > json.h) { let r = json.h * this.ratio / json.w if (r > 1) { json.ch = json.h / r json.cw = json.ch * this.ratio } else { json.ch = json.h json.cw = json.ch * this.ratio } } else { let r = json.w / this.ratio / json.h if (r > 1) { json.cw = json.w / r json.ch = json.cw / this.ratio } else { json.cw = json.w json.ch = json.cw / this.ratio } } } else { // 无比例 json.cw = json.w json.ch = json.h } // 裁切框的尺寸(/2是取一半的值,使裁切框居中并宽度为一半) this.elWidth = json.cw / 2 this.elHeight = json.ch / 2 this.cursorTop = json.ch / 4 this.cursorLeft = json.cw / 4 this.cropJson = {...json} this.drapSizeUpdate(this.elWidth, this.elHeight, this.cursorTop, this.cursorLeft) }, // 根据图片本来的尺寸比例和用户传入的尺寸宽高设置当前可显示的区域图片尺寸 setCoordRange () { var size = {...this.originImgSize} var ratio1 = this.width / this.height var ratio2 = size.r if (ratio2 > ratio1) { this.imgW = this.width this.imgH = this.width / size.r } else { this.imgH = this.height this.imgW = this.height * size.r } }, // 获取裁切后的原始坐标宽高(裁切看到的宽高不是原始图片的宽高) getCoord (l, t, w, h) { l = this.scale * l t = this.scale * t w = this.scale * w h = this.scale * h return { p0: [l, t], p1: [l + w, t], p2: [l + w, t + h], p3: [l, t + h], w: w, h: h, l: l, t: t } }, // 获取是src图片的尺寸 getSize (src) { let _this = this let img = this.$el.querySelector('#c-crop--hide_img') return new Promise(resolve => { if (src && img) { img.onload = function () { const size = _this.getSizeImg(img) resolve(size) } img.src = src } else { resolve({ w: 0, h: 0, r: 0 }) } }) }, // 获取原始图片的真实宽高、比例 getSizeImg (img) { let w = img.width let h = img.height let r = w === 0 && h === 0 ? 0 : w / h return { w: w, h: h, r: r } } }, mounted () { this.setSize() } } </script>
//script部分 <script> // 引入拖拽js import draggable from '../utils/draggable' // 引入裁切尺寸计算js import movePos from './VueCropMove' // 和VueCropMove有关,序号对应相应的操做,这些类名对应裁切框的四条边,四个角,四个边上的中点,拖拽由这12个位置进行 const dragEle = ['.c-crop--drap_eline', '.c-crop--drap_sline', '.c-crop--drap_wline', '.c-crop--drap_nline', '.c-crop--drap_e', '.c-crop--drap_s', '.c-crop--drap_w', '.c-crop--drap_n', '.c-crop--drap_ne', '.c-crop--drap_se', '.c-crop--drap_sw', '.c-crop--drap_nw'] export default { data () { return { width: this.elWidth, height: this.elHeight, top: this.cursorTop, left: this.cursorLeft, // 存储拖拽开始坐标(拖拽改变位置时) startPos: [0, 0], crop: [], // 计时器 cropTimer: null, // 存储拖拽开始坐标尺寸(拖拽改变尺寸时) startSize: null } }, props: ['elWidth', 'elHeight', 'cursorTop', 'cursorLeft', 'cropJson'], created () {}, watch: { elWidth (val) { this.width = val }, elHeight (val) { this.height = val }, cursorTop (val) { this.top = val }, cursorLeft (val) { this.left = val } }, methods: { // 拖拽更新位置 handleDragLocation (event) { let x = event.clientX let y = event.clientY this.left = x - this.startPos[0] + this.left this.top = y - this.startPos[1] + this.top this.startPos = [x, y] this.handleSize() // 更新尺寸 this.$emit('updateSize', this.width, this.height, this.top, this.left) clearTimeout(this.cropTimer) // setTimeout是为了拖拽完成才调用afterCrop this.cropTimer = setTimeout(() => { // 调用回调 this.$emit('afterCrop') }, 200) }, // 拖拽改变位置:绑定事件 dragCallLocation () { draggable(this.$el.querySelector('.c-crop--drap_screen'), { start: (event) => { this.startPos = [event.x, event.y] }, drag: (event) => { this.handleDragLocation(event) }, end: (event) => { this.handleDragLocation(event) } }) }, // 根据className获取父元素 getParentElement (p, className) { const classNames = p.className if (classNames.indexOf(className) === -1) { p = p.parentNode return this.getParentElement(p, className) } else { return p } }, // 获取拖拽的尺寸 getDragSize (event) { const el = this.$el const screen = this.$cropArea.getBoundingClientRect() const rect = el.getBoundingClientRect() let json = { x: event.clientX, y: event.clientY, t: rect.top, b: rect.bottom, l: rect.left, r: rect.right, w: rect.width, h: rect.height, screen: screen } json.ratio = json.w / json.h return json }, // 拖拽改变大小 handleDrag (event, i) { // 获取坐标 // console.info('move', i) const json = this.getDragSize(event) movePos[i](this, json, this.startSize) this.handleSize(true) this.$emit('updateSize', this.width, this.height, this.top, this.left) clearTimeout(this.cropTimer) this.cropTimer = setTimeout(() => { // 调用回调 this.$emit('afterCrop') }, 200) }, // 拖拽改变大小:绑定事件 dragCall (i) { let target = this.$el.querySelector(dragEle[i]) draggable(target, { start: (event) => { // 开始时拖拽框json this.startSize = this.getDragSize(event) }, drag: (event) => { this.handleDrag(event, i) }, end: (event) => { this.handleDrag(event, i) } }) }, // 改变位置大小 handleSize (isSize) { this.left = range2(this.left, this.width, this.cropJson.w) this.top = range2(this.top, this.height, this.cropJson.h) if (isSize) { let d1 = this.cropJson.w - this.left let d2 = this.cropJson.h - this.top // 按比例裁切 if (this.cropJson.r) { if (d1 < this.width) { this.width = d1 this.height = this.width / this.cropJson.r } else if (d2 < this.height) { this.height = d2 this.width = this.height * this.cropJson.r } } else { // 不按比例裁切 if (d1 < this.width) { this.width = d1 } if (d2 < this.height) { this.height = d2 } } } } }, mounted () { this.$cropArea = this.getParentElement(this.$el.parentNode, 'c-crop--area') // 初始化拖拽改变大小 for (var i = 0; i < dragEle.length; i++) { this.dragCall(i) } // 初始化拖拽改变位置 this.dragCallLocation() } } // 计算容许的范围 function range2 (pos, val, mainW) { return pos <= 0 ? 0 : pos > mainW - val ? mainW - val : pos } </script>
// 12种形态,四条边,边的中点,边的四个角。e:东,w:西,n:北,s:南,ne:东南以此类推 const movePos = { 0: e, 4: e, 1: s, 5: s, 2: w, 6: w, 3: n, 7: n, 8: ne, 9: se, 10: sw, 11: nw } let width, height, result, ratio // 获取某种形态类型的宽或高最大值 function getMaxSize (json, startJson, dire, type) { if (type === 'w') { switch (dire) { case 'e': case 's': case 'n': case 'ne': case 'se': return json.screen.right - json.l case 'w': case 'nw': case 'sw': return startJson.r - json.screen.left } } else if (type === 'h') { switch (dire) { case 'n': case 'nw': case 'ne': return startJson.b - json.screen.top case 's': case 'w': case 'e': case 'sw': case 'se': return json.screen.bottom - startJson.t } } } // 判断是否有ratio,返回修改后的尺寸 function setRatioSize (type, json, startJson, ratio, width, height) { if (ratio) { if (width / ratio >= height) { var maxHeight = getMaxSize(json, startJson, type, 'h') height = width / ratio if (height > maxHeight) { height = maxHeight width = height * ratio } } else { var maxWidth = getMaxSize(json, startJson, type, 'w') width = height * ratio if (width > maxWidth) { width = maxWidth height = width / ratio } } } return { width: width, height: height } } // 拖拽东边,高度是不变的,除非有比例拖拽时 function e (_this, json, startJson) { ratio = _this.cropJson.r width = range(getWidth(json, startJson, 'e'), getMaxSize(json, startJson, 'e', 'w')) if (ratio) { // 有比例时,计算高度,并对比最大值是否超出 height = range(width / ratio, getMaxSize(json, startJson, 'e', 'h')) result = setRatioSize('e', json, startJson, ratio, width, height) setSize(_this, result) } else { _this.width = width } return _this } // 拖拽南边,宽度是不变的,除非有比例拖拽时 function s (_this, json, startJson) { ratio = _this.cropJson.r height = range(getHeight(json, startJson, 's'), getMaxSize(json, startJson, 's', 'h')) if (ratio) { // 有比例时,计算宽度,并对比最大值是否超出 width = range(height * ratio, getMaxSize(json, startJson, 's', 'w')) result = setRatioSize('s', json, startJson, ratio, width, height) setSize(_this, result) } else { _this.height = height } return _this } // 如下同上,以此类推 function w (_this, json, startJson) { ratio = _this.cropJson.r width = range(getWidth(json, startJson, 'w'), getMaxSize(json, startJson, 'w', 'w')) if (ratio) { height = range(width / ratio, getMaxSize(json, startJson, 'w', 'h')) result = setRatioSize('w', json, startJson, ratio, width, height) setSize(_this, result) _this.left = getLeft(_this, json, startJson) } else { _this.width = width _this.left = rangeMax(json.x - json.screen.left, startJson.r) } return _this } function n (_this, json, startJson) { ratio = _this.cropJson.r height = range(getHeight(json, startJson, 'n'), getMaxSize(json, startJson, 'n', 'h')) if (ratio) { width = range(height * ratio, getMaxSize(json, startJson, 'n', 'w')) result = setRatioSize('n', json, startJson, ratio, width, height) setSize(_this, result) _this.top = getTop(_this, json, startJson) } else { _this.height = height _this.top = rangeMax(json.y - json.screen.top, startJson.b) } return _this } function ne (_this, json, startJson) { height = range(getHeight(json, startJson, 'n'), getMaxSize(json, startJson, 'ne', 'h')) width = range(getWidth(json, startJson, 'e'), getMaxSize(json, startJson, 'ne', 'w')) result = setRatioSize('ne', json, startJson, _this.cropJson.r, width, height) setSize(_this, result) _this.top = getTop(_this, json, startJson) return _this } function se (_this, json, startJson) { height = range(getHeight(json, startJson, 's'), getMaxSize(json, startJson, 'se', 'h')) width = range(getWidth(json, startJson, 'e'), getMaxSize(json, startJson, 'se', 'w')) result = setRatioSize('se', json, startJson, _this.cropJson.r, width, height) setSize(_this, result) return _this } function sw (_this, json, startJson) { width = range(getWidth(json, startJson, 'w'), getMaxSize(json, startJson, 'sw', 'w')) height = range(getHeight(json, startJson, 's'), getMaxSize(json, startJson, 'sw', 'h')) result = setRatioSize('sw', json, startJson, _this.cropJson.r, width, height) setSize(_this, result) _this.left = getLeft(_this, json, startJson) return _this } function nw (_this, json, startJson) { width = range(getWidth(json, startJson, 'w'), getMaxSize(json, startJson, 'nw', 'w')) height = range(getHeight(json, startJson, 'n'), getMaxSize(json, startJson, 'nw', 'h')) result = setRatioSize('nw', json, startJson, _this.cropJson.r, width, height) setSize(_this, result) _this.left = getLeft(_this, json, startJson) _this.top = getTop(_this, json, startJson) return _this } // 匹配范围 function range (value, max) { value = value > max ? max : value return value < 20 ? 20 : value } // 最大值 function rangeMax (value, max) { return value > max ? max : value } // top function getTop (_this, json, startJson) { return rangeMax(startJson.b - _this.height - json.screen.top, startJson.b) } // left function getLeft (_this, json, startJson) { return rangeMax(startJson.r - _this.width - json.screen.left, startJson.r) } // height:只存在于s||n类型 function getHeight (json, startJson, type) { return type === 'n' ? startJson.b - json.y : json.y - startJson.t } // width:只存在于w||e类型 function getWidth (json, startJson, type) { return type === 'w' ? startJson.r - json.x : json.x - startJson.l } // setSize function setSize (_this, result) { _this.width = result.width _this.height = result.height } export default movePos
今天就分享到这里啦~喜欢这个插件能够去 github star~