We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
vue版本裁切工具,包含预览功能
最终效果: https://qiuyaofan.github.io/vue-crop-demo/
源码地址: 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源码)
最终生成的文件结构如下图:
// 导入插件入口文件 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()
所以,vue插件注册的过程是:
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点有详细讲解
首先,确定自己的调用方式和需要暴露的参数
<vue-crop :crop-url="cropUrl1" :ratio="ratio" :height="460" :width="460" :previewJson="previewJson1" class="c-crop--preview_right" @afterCrop="afterCrop" > >
其中,@afterCrop="afterCrop"是裁切完成的回调函数,其他是属性配置
在组件src/components/VueCrop/VueCrop.vue内,可以用this.$emit('afterCrop')触发demo里的afterCrop事件
组件结构上,主要分为:裁切主体部分(VueCrop.vue),选框组件(VueCropTool.vue),裁切框宽度、位置坐标等计算(VueCropMove.js),拖拽事件注册公共js(components/utils/draggable.js)
备注:此组件不具备真实的裁切功能,最终的裁切是传递给后台去裁,你如果想扩展可以在afterCrop函数里根据坐标等信息进行处理
接下来我们对各个组件和js进行讲解
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
The text was updated successfully, but these errors were encountered:
选择图片后,再去选择更换图片,然后取消,看控制台
Sorry, something went wrong.
No branches or pull requests
最终效果: https://qiuyaofan.github.io/vue-crop-demo/
源码地址: https://github.com/qiuyaofan/vue-crop
第一步:先用vue-cli安装脚手架(不会安装的看 vue-cli官网)
第二步:创建文件
最终生成的文件结构如下图:
第三步:注册组件
1.引用所有插件:src/components/index.js
2.全局调用插件:src/main.js ( vue plugins官方文档解说install)
3.VueCrop入口文件调用VueCrop.vue:src/components/VueCrop/index.js
小结:我一开始一直有个误解,以为myPlugin.install是vue的一个方法,其实不是,他只是我们构造plugin识的一个公开方法,可以理解为原生js中的构造函数的方法:
而真正注册组件的是:Vue.component()
所以,vue插件注册的过程是:
第四步:设计开发自己的组件,构建组件结构
首先,确定自己的调用方式和需要暴露的参数
其中,@afterCrop="afterCrop"是裁切完成的回调函数,其他是属性配置
在组件src/components/VueCrop/VueCrop.vue内,可以用this.$emit('afterCrop')触发demo里的afterCrop事件
组件结构上,主要分为:裁切主体部分(VueCrop.vue),选框组件(VueCropTool.vue),裁切框宽度、位置坐标等计算(VueCropMove.js),拖拽事件注册公共js(components/utils/draggable.js)
当前裁切插件的总体思路
接下来我们对各个组件和js进行讲解
1.draggable.js是参照element里的,修改了一部分,源码如下
VueCropTool.vue使用如下
2.裁切主体部分(VueCrop.vue全部源码链接)
3.裁切框部分(VueCropTool.vue全部源码链接)
4.计算裁切框的js(VueCropMove.js全部源码链接)
The text was updated successfully, but these errors were encountered: