手把手教你寫vue裁切預覽元件

飯妖精發表於2018-05-03

手把手教你寫vue裁切預覽元件

vue版本裁切工具,包含預覽功能

最終效果: qiuyaofan.github.io/vue-crop-de…

原始碼地址: github.com/qiuyaofan/v…

第一步:先用vue-cli安裝腳手架(不會安裝的看 vue-cli官網

// 初始化vue-cli
vue init webpack my-plugin
複製程式碼

第二步:建立檔案

新建src/views/validSlideDemo.vue,

src/components裡新建VueCrop/index.js,VueCrop.vue,

在routes/index.js配置訪問路由(具體看github原始碼)
複製程式碼

最終生成的檔案結構如下圖:

vuecorp
裁切外掛檔案結構圖

第三步:註冊元件

1.引用所有外掛:src/components/index.js
// 匯入外掛入口檔案
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
}
複製程式碼
2.全域性呼叫外掛:src/main.js ( vue plugins官方文件解說install
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/>'
})
複製程式碼
3.VueCrop入口檔案呼叫VueCrop.vue:src/components/VueCrop/index.js
// 匯入vue
import VueCrop from './VueCrop.vue'

// Vue.js 的外掛應當有一個公開方法 install 。這個方法的第一個引數是 Vue 構造器
VueCrop.install = function (Vue) {
  // 註冊元件
  Vue.component(VueCrop.name, VueCrop)
}

export default VueCrop
複製程式碼
小結:我一開始一直有個誤解,以為myPlugin.install是vue的一個方法,其實不是,他只是我們構造plugin識的一個公開方法,可以理解為原生js中的建構函式的方法:
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)

當前裁切外掛的總體思路
  1. 裁切外掛的裁切主體由圖片,選框,預覽結構組成
  2. 選框(VueCropTool.vue)負責拖拽改變其大小,座標位置等並返回給VueCrop.vue
  3. 主體計算數值同步預覽顯示(c-crop--preview)
  4. 主體觸發呼叫頁面(VueCropDemo.vue)的afterCrop事件,從而傳遞引數返回裁切後的url,left,top,bottom,right,x,y,w,h等

備註:此元件不具備真實的裁切功能,最終的裁切是傳遞給後臺去裁,你如果想擴充套件可以在afterCrop函式里根據座標等資訊進行處理

接下來我們對各個元件和js進行講解

1.draggable.js是參照element裡的,修改了一部分,原始碼如下
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)
  })
}

複製程式碼
VueCropTool.vue使用如下
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)
	}
})
複製程式碼
2.裁切主體部分(VueCrop.vue全部原始碼連結
//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>


複製程式碼
3.裁切框部分(VueCropTool.vue全部原始碼連結
//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>
複製程式碼
4.計算裁切框的js(VueCropMove.js全部原始碼連結
// 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~

相關文章