缩放体验比较糟糕,仅供参考。

image.png

代码

请忽略个别业务组件,关注裁切逻辑。
总结:利用 mousedown 确定鼠标的初始化位置,利用 mousemove 随时获取当前鼠标的位置,利用和初始化位置数据的差值,来移动容器。mouseup 重置。

  1. <template>
  2. <div class="cover-image-cropper">
  3. <div class="cropper-container">
  4. <div class="cropper-container-left">
  5. <div class="origin-image">
  6. <img-picker
  7. ref="imgPicker"
  8. customClass="cover-image-cropper-picker"
  9. :imgKey="originPicKey"
  10. :defaultImg="defaultImg"
  11. :formDataKey="selfFormDataKey"
  12. @picked="setImgUrl"
  13. @delete-click="deleteCurrentSelectedImage"
  14. />
  15. </div>
  16. <div v-show="currentGraphIndex !== ''" class="cropper-bg" :style="originImgDomStyle"></div>
  17. <img
  18. v-show="currentGraphIndex !== ''"
  19. :src="articleForm.origin_image_url"
  20. class="clip-image"
  21. :style="[originImgDomStyle, clipImageStyle]"
  22. alt="原始图片"
  23. />
  24. <div
  25. v-show="currentGraphIndex !== ''"
  26. class="drag-resize-box"
  27. :style="dragResizeBoxStyle"
  28. @mousedown.stop.self="commonMousedownEvent($event)"
  29. @mousemove.stop.self="dragResizeBoxMousemoveEvent($event)"
  30. @mouseup.stop.self="isMousedownType = false"
  31. >
  32. <i
  33. v-for="(item, index) in zoomPoints"
  34. class=" zoom-point"
  35. :class="item.customClass"
  36. :key="index"
  37. @mousedown.stop.self="commonMousedownEvent($event)"
  38. @mousemove.stop.self="zoomPointMousemoveEvent($event, index)"
  39. @mouseup.stop.self="isMousedownType = false"
  40. ></i>
  41. </div>
  42. </div>
  43. <div class="cropper-container-right">
  44. <div class="ratio-graph-lists">
  45. <div
  46. v-for="(item, index) in ratioLists"
  47. class="single-item"
  48. :class="{
  49. active: currentGraphIndex === index,
  50. disabled: !hasOriginPic,
  51. cropped: item.croppedPosition.length,
  52. }"
  53. :style="item.style"
  54. :data-radio="item.text"
  55. @click="handleSelectedClick(index)"
  56. >
  57. {{ item.text }}
  58. </div>
  59. </div>
  60. <div class="save-btn" :class="{ disabled: !hasOriginPic }" @click="saveCropperImage">
  61. 保存图片
  62. </div>
  63. </div>
  64. </div>
  65. <!-- 图片选择弹窗 -->
  66. <picture-select-dialog />
  67. </div>
  68. </template>
  69. <script>
  70. import { ImgPicker, PictureSelectDialog } from '@/components'
  71. export default {
  72. name: 'CoverImageCropper',
  73. props: {
  74. articleForm: {
  75. type: Object || String,
  76. },
  77. },
  78. data() {
  79. return {
  80. ratioLists: [
  81. {
  82. text: '1:1',
  83. initPosition: [
  84. { x: 0, y: 0 },
  85. { x: 70, y: 0 },
  86. { x: 70, y: 70 },
  87. { x: 0, y: 70 },
  88. ],
  89. croppedPosition: [],
  90. initSize: { width: 70, height: 70 },
  91. style: { width: '70px', height: '70px' },
  92. key: '1_1',
  93. extraValue: { prefix: '', suffix: '_1_1' },
  94. },
  95. {
  96. text: '3:2',
  97. initPosition: [
  98. { x: 0, y: 0 },
  99. { x: 105, y: 0 },
  100. { x: 105, y: 70 },
  101. { x: 0, y: 70 },
  102. ],
  103. croppedPosition: [],
  104. initSize: { width: 105, height: 70 },
  105. style: { width: '105px', height: '70px' },
  106. key: '3_2',
  107. extraValue: { prefix: '', suffix: '_3_2' },
  108. },
  109. {
  110. text: '7:5',
  111. initPosition: [
  112. { x: 0, y: 0 },
  113. { x: 97, y: 0 },
  114. { x: 97, y: 70 },
  115. { x: 0, y: 70 },
  116. ],
  117. croppedPosition: [],
  118. initSize: { width: 97, height: 70 },
  119. style: { width: '97px', height: '70px' },
  120. key: '7_5',
  121. extraValue: { prefix: '', suffix: '_7_5' },
  122. },
  123. {
  124. text: '17:10',
  125. initPosition: [
  126. { x: 0, y: 0 },
  127. { x: 118, y: 0 },
  128. { x: 118, y: 70 },
  129. { x: 0, y: 70 },
  130. ],
  131. croppedPosition: [],
  132. initSize: { width: 118, height: 70 },
  133. style: { width: '118px', height: '70px' },
  134. key: '17_10',
  135. extraValue: { prefix: '', suffix: '_17_10' },
  136. },
  137. {
  138. text: '21:10',
  139. initPosition: [
  140. { x: 0, y: 0 },
  141. { x: 143, y: 0 },
  142. { x: 143, y: 70 },
  143. { x: 0, y: 70 },
  144. ],
  145. croppedPosition: [],
  146. initSize: { width: 143, height: 70 },
  147. style: { width: '143px', height: '70px' },
  148. key: '21_10',
  149. extraValue: { prefix: '', suffix: '_21_10' },
  150. },
  151. ],
  152. // 当前比例尺下标
  153. currentGraphIndex: '',
  154. // imgPicker key
  155. originPicKey: 'coverImageCropper',
  156. // formdata keys
  157. selfFormDataKey: ['cover_img_id', 'origin_image_url'],
  158. // 原图尺寸
  159. originImgSize: {},
  160. // 原图初始化位置
  161. originImgPosition: {},
  162. // 原图 DOM 样式
  163. originImgDomStyle: {},
  164. // 裁切图片 DOM 样式
  165. clipImageStyle: {},
  166. // 拖拽、缩放容器 DOM 样式
  167. dragResizeBoxStyle: {},
  168. // 当前裁切后图片的位置信息
  169. currentPositionCoordinates: [],
  170. // 持续长按中
  171. isMousedownType: false,
  172. // 鼠标按下位置
  173. originPosition: {
  174. x: null,
  175. y: null,
  176. },
  177. zoomPoints: [
  178. { customClass: 'top-left-point' },
  179. { customClass: 'top-right-point' },
  180. { customClass: 'bottom-right-point' },
  181. { customClass: 'bottom-left-point' },
  182. ],
  183. // 待提交的数据
  184. submitMsg: {},
  185. }
  186. },
  187. components: {
  188. ImgPicker,
  189. PictureSelectDialog,
  190. },
  191. computed: {
  192. defaultImg() {
  193. return {
  194. id: this.articleForm.cover_img_id,
  195. file_path: this.articleForm.origin_image_url,
  196. }
  197. },
  198. hasOriginPic() {
  199. return this.articleForm.origin_image_url !== ''
  200. },
  201. },
  202. watch: {
  203. hasOriginPic: {
  204. handler(newVal, oldVal) {
  205. if (!newVal) {
  206. this.currentGraphIndex = ''
  207. this.isMousedownType = false
  208. this.ratioLists.map((item) => {
  209. item.croppedPosition.length = 0
  210. })
  211. }
  212. },
  213. immediate: true,
  214. },
  215. currentGraphIndex: {
  216. handler(newVal, oldVal) {
  217. if (newVal !== '') {
  218. this.$nextTick(() => {
  219. // 已经选取裁切图逻辑
  220. const currentGraphCroppedPosition = this.ratioLists[newVal].croppedPosition
  221. if (currentGraphCroppedPosition.length) {
  222. this.updateCropperView(currentGraphCroppedPosition)
  223. this.currentPositionCoordinates = this.ratioLists[newVal].croppedPosition
  224. } else {
  225. // 未选取裁切图逻辑
  226. const originImgDom = this.$refs.imgPicker.$refs.img
  227. const offsetWidth = originImgDom.offsetWidth
  228. const offsetHeight = originImgDom.offsetHeight
  229. const offsetTop = originImgDom.offsetTop
  230. const offsetLeft = originImgDom.offsetLeft
  231. /**
  232. * originImgSize
  233. * 原图长宽尺寸
  234. */
  235. this.originImgSize = {
  236. width: offsetWidth,
  237. height: offsetHeight,
  238. }
  239. /**
  240. * originImgPosition
  241. * 原图初始化位置
  242. */
  243. this.originImgPosition = {
  244. top: offsetTop,
  245. left: offsetLeft,
  246. }
  247. /**
  248. * originImgDomStyle
  249. * 原图长宽样式
  250. */
  251. this.originImgDomStyle = {
  252. top: offsetTop + 1 + 'px',
  253. left: offsetLeft + 1 + 'px',
  254. width: offsetWidth + 'px',
  255. height: offsetHeight + 'px',
  256. }
  257. /**
  258. * currentPositionCoordinates
  259. * 当前位置坐标
  260. */
  261. let currentInitPosition = JSON.parse(
  262. JSON.stringify(this.ratioLists[newVal].initPosition)
  263. )
  264. if (offsetTop > 0 || offsetLeft > 0) {
  265. currentInitPosition.map((item) => {
  266. if (offsetTop > 0) {
  267. item.y = item.y + offsetTop
  268. }
  269. if (offsetLeft > 0) {
  270. item.x = item.x + offsetLeft
  271. }
  272. })
  273. }
  274. this.currentPositionCoordinates = currentInitPosition
  275. /**
  276. * 根据最新坐标信息更新视图
  277. */
  278. this.updateCropperView(this.currentPositionCoordinates)
  279. }
  280. })
  281. }
  282. },
  283. },
  284. },
  285. methods: {
  286. setImgUrl(data) {
  287. const { file_id, file_path } = data[this.originPicKey]
  288. this.articleForm.cover_img_id = file_id
  289. this.articleForm.origin_image_url = file_path
  290. },
  291. getClipImageStyle(currentPositionCoordinates) {
  292. // polygon(0px 0px, 100px 0px, 100px 100px, 0px 100px)
  293. const selfOriginImgPosition = this.originImgPosition
  294. return {
  295. 'clip-path': `polygon(
  296. ${currentPositionCoordinates
  297. .map(
  298. (item, index) =>
  299. item.x -
  300. this.originImgPosition.left +
  301. 'px' +
  302. ' ' +
  303. (item.y - this.originImgPosition.top) +
  304. 'px' +
  305. (index < currentPositionCoordinates.length - 1 ? ', ' : '')
  306. )
  307. .join('')}
  308. )`,
  309. }
  310. },
  311. /**
  312. * 获取拖动、缩放容器的位置样式
  313. * @param {array} currentPositionCoordinates - 当前位置信息
  314. *
  315. * @return {object} 返回 style 对象
  316. */
  317. getDragResizeBoxStyle(currentPositionCoordinates) {
  318. return {
  319. top: currentPositionCoordinates[0].y + 1 + 'px',
  320. left: currentPositionCoordinates[0].x + 1 + 'px',
  321. width: currentPositionCoordinates[1].x - currentPositionCoordinates[0].x + 'px',
  322. height: currentPositionCoordinates[3].y - currentPositionCoordinates[0].y + 'px',
  323. }
  324. },
  325. /**
  326. * 获取 mousedown 事件出发点的位置坐标
  327. * @param {object} evt - 鼠标点击事件对象
  328. */
  329. commonMousedownEvent(evt) {
  330. this.isMousedownType = true
  331. this.originPosition.x = evt.offsetX
  332. this.originPosition.y = evt.offsetY
  333. },
  334. /**
  335. * 裁切图片拖拽事件
  336. * @param {object} evt - 拖拽事件对象
  337. */
  338. dragResizeBoxMousemoveEvent(evt) {
  339. // console.log('***', evt.offsetX)
  340. if (this.isMousedownType) {
  341. const selfOriginImgSize = this.originImgSize
  342. const selfOriginImgPosition = this.originImgPosition
  343. const selfCurrentPositionCoordinates = this.currentPositionCoordinates
  344. const x = evt.offsetX
  345. const y = evt.offsetY
  346. const distanceX = x - this.originPosition.x
  347. const distanceY = y - this.originPosition.y
  348. if (
  349. distanceX + selfCurrentPositionCoordinates[0].x < selfOriginImgPosition.left ||
  350. distanceY + selfCurrentPositionCoordinates[0].y < selfOriginImgPosition.top ||
  351. distanceX + selfCurrentPositionCoordinates[1].x >
  352. selfOriginImgSize.width + selfOriginImgPosition.left ||
  353. distanceY + selfCurrentPositionCoordinates[3].y >
  354. selfOriginImgSize.height + selfOriginImgPosition.top
  355. ) {
  356. return false
  357. } else {
  358. this.currentPositionCoordinates = selfCurrentPositionCoordinates.map((item) => {
  359. return {
  360. x: item.x + distanceX,
  361. y: item.y + distanceY,
  362. }
  363. })
  364. this.updateCropperView(this.currentPositionCoordinates)
  365. }
  366. }
  367. },
  368. /**
  369. * 裁切图片缩放事件
  370. * 根据 mousemove 缩放图片
  371. * @param {object} evt - 拖拽事件对象
  372. * @param {number} currentPointIndex - 拖拽的 point,0: ↖️, 1: ↗️, 2: ↘️, 3: ↙️
  373. */
  374. zoomPointMousemoveEvent(evt, currentPointIndex) {
  375. if (this.isMousedownType) {
  376. const x = evt.offsetX
  377. const distance = x - this.originPosition.x
  378. // 判读是否可以进行容器缩放
  379. if (this.zoomCanPass(currentPointIndex, distance)) {
  380. return false
  381. } else {
  382. this.currentPositionCoordinates = this.currentPositionCoordinates.map((item, index) => {
  383. let isEven = (currentPointIndex + 1) % 2 === 0
  384. // 当前缩放触发方向
  385. // x,y 坐标都变动
  386. if (currentPointIndex === index) {
  387. return {
  388. x: item.x + distance,
  389. y: item.y + distance,
  390. }
  391. }
  392. // 顺时针下一位
  393. if ((currentPointIndex + 1) % 4 === index) {
  394. return {
  395. x: isEven ? item.x + distance : item.x,
  396. y: isEven ? item.y : item.y + distance,
  397. }
  398. }
  399. // 对角方向
  400. if ((currentPointIndex + 2) % 4 === index) {
  401. return {
  402. x: item.x,
  403. y: item.y,
  404. }
  405. }
  406. // 逆时针上一位
  407. if ((currentPointIndex + 3) % 4 === index)
  408. return {
  409. x: isEven ? item.x : item.x + distance,
  410. y: isEven ? item.y + distance : item.y,
  411. }
  412. })
  413. this.updateCropperView(this.currentPositionCoordinates)
  414. }
  415. }
  416. },
  417. /**
  418. * 判断缩放是否可执行
  419. * @param {number} currentPointIndex - 拖拽的 point,0: ↖️, 1: ↗️, 2: ↘️, 3: ↙️
  420. * @param {number} distance - 缩放的大小值
  421. *
  422. * @return {boolean} 返回是否可执行的布尔值
  423. */
  424. zoomCanPass(currentPointIndex, distance) {
  425. const selfCurrentPositionCoordinates = this.currentPositionCoordinates
  426. const selfOriginImgSize = this.originImgSize
  427. const selfOriginImgPosition = this.originImgPosition
  428. switch (currentPointIndex) {
  429. case 0:
  430. return
  431. distance + selfCurrentPositionCoordinates[0].x < selfOriginImgPosition.left ||
  432. distance + selfCurrentPositionCoordinates[0].y < selfOriginImgPosition.top
  433. break
  434. case 1:
  435. distance + selfCurrentPositionCoordinates[1].x >
  436. selfOriginImgSize.width + selfOriginImgPosition.left ||
  437. distance + selfCurrentPositionCoordinates[1].y < selfOriginImgPosition.top
  438. break
  439. case 2:
  440. distance + selfCurrentPositionCoordinates[2].x >
  441. selfOriginImgSize.width + selfOriginImgPosition.left ||
  442. distance + selfCurrentPositionCoordinates[2].y >
  443. selfOriginImgSize.height + selfOriginImgPosition.top
  444. break
  445. case 3:
  446. distance + selfCurrentPositionCoordinates[3].x < selfOriginImgPosition.left ||
  447. distance + selfCurrentPositionCoordinates[3].y >
  448. selfOriginImgSize.height + selfOriginImgPosition.top
  449. break
  450. default:
  451. return false
  452. }
  453. },
  454. /**
  455. * 根据最新坐标信息更新视图
  456. * @param {object} selfCurrentPositionCoordinates - 当前裁切后图片的位置信息
  457. */
  458. updateCropperView(selfCurrentPositionCoordinates) {
  459. /**
  460. * clip-image
  461. * 利用 css3 属性 clip-path
  462. */
  463. this.clipImageStyle = this.getClipImageStyle(selfCurrentPositionCoordinates)
  464. /**
  465. * dragResizeBoxStyle
  466. * 用于拖动、缩放的元素
  467. */
  468. this.dragResizeBoxStyle = this.getDragResizeBoxStyle(selfCurrentPositionCoordinates)
  469. },
  470. /**
  471. * 点击选中当前比例
  472. * @param {number} index - 当前比例的下标
  473. */
  474. handleSelectedClick(index) {
  475. if (this.hasOriginPic && this.currentGraphIndex !== index) {
  476. this.currentGraphIndex = index
  477. this.isMousedownType = false
  478. }
  479. },
  480. /**
  481. * 保存当前裁切比例下的图片位置信息
  482. */
  483. saveCropperImage() {
  484. const currentGraph = this.ratioLists[this.currentGraphIndex]
  485. const singleCoverScale = {}
  486. // 保存裁切后的位置信息
  487. currentGraph.croppedPosition = this.currentPositionCoordinates
  488. // 表单提交所需要的比例信息
  489. singleCoverScale.x = currentGraph.croppedPosition[0].x
  490. singleCoverScale.y = currentGraph.croppedPosition[0].y
  491. singleCoverScale.width = currentGraph.croppedPosition[1].x - currentGraph.croppedPosition[0].x
  492. singleCoverScale.heigt = currentGraph.croppedPosition[3].y - currentGraph.croppedPosition[0].y
  493. Object.assign(singleCoverScale, currentGraph.extraValue)
  494. console.log('保存图片', singleCoverScale)
  495. if (!this.submitMsg[currentGraph.key]) {
  496. this.submitMsg[currentGraph.key] = singleCoverScale
  497. } else {
  498. this.$delete(this.submitMsg, currentGraph.key)
  499. }
  500. },
  501. /**
  502. * 删除当前选中图回调函数
  503. * @param {array, string} formDataKey - 待删除图片表单项属性 key
  504. */
  505. deleteCurrentSelectedImage(formDataKey) {
  506. if (Array.isArray(formDataKey)) {
  507. formDataKey.forEach((singleKey) => {
  508. this.$set(this.articleForm, singleKey, '')
  509. })
  510. } else {
  511. this.$set(this.articleForm, formDataKey, '')
  512. }
  513. },
  514. },
  515. }
  516. </script>
  517. <style lang="scss" scoped>
  518. .cover-image-cropper {
  519. position: relative;
  520. width: 100%;
  521. height: auto;
  522. .cropper-container {
  523. display: flex;
  524. &-left {
  525. position: relative;
  526. .origin-image {
  527. .img-picker.cover-image-cropper-picker {
  528. width: 256px;
  529. height: 176px;
  530. line-height: 176px;
  531. .delete-icon-zIndex {
  532. z-index: 9;
  533. }
  534. }
  535. }
  536. .cropper-bg,
  537. .clip-image {
  538. position: absolute;
  539. top: 1px;
  540. left: 1px;
  541. }
  542. .cropper-bg {
  543. width: 100%;
  544. background: rgba(0, 0, 0, 0.35);
  545. }
  546. .clip-image {
  547. width: 100%;
  548. }
  549. .drag-resize-box {
  550. position: absolute;
  551. cursor: move;
  552. .zoom-point {
  553. position: absolute;
  554. width: 4px;
  555. height: 4px;
  556. background-color: #000;
  557. &.top-left-point {
  558. top: -2px;
  559. left: -2px;
  560. cursor: nwse-resize;
  561. }
  562. &.top-right-point {
  563. top: -2px;
  564. right: -2px;
  565. cursor: nesw-resize;
  566. }
  567. &.bottom-right-point {
  568. bottom: -2px;
  569. right: -2px;
  570. cursor: nwse-resize;
  571. }
  572. &.bottom-left-point {
  573. bottom: -2px;
  574. left: -2px;
  575. cursor: nesw-resize;
  576. }
  577. }
  578. }
  579. }
  580. &-right {
  581. position: relative;
  582. margin-left: 85px;
  583. &::before {
  584. content: ' ';
  585. position: absolute;
  586. top: 26px;
  587. left: -42px;
  588. width: 1px;
  589. height: 112px;
  590. background: rgba(240, 242, 245, 1);
  591. }
  592. .ratio-graph-lists {
  593. position: relative;
  594. display: flex;
  595. flex-wrap: wrap;
  596. .single-item {
  597. position: relative;
  598. margin-left: 22px;
  599. border: 1px solid rgba(216, 220, 230, 1);
  600. line-height: 70px;
  601. text-align: center;
  602. font-size: 14px;
  603. font-family: PingFangSC-Regular, PingFang SC;
  604. color: rgba(144, 147, 153, 1);
  605. background: rgba(240, 242, 245, 1);
  606. cursor: pointer;
  607. &:nth-of-type(1) {
  608. margin-left: 0;
  609. }
  610. /** 兼容小屏幕,换行处理 */
  611. @media (max-width: 1443px) {
  612. &:nth-of-type(4) {
  613. margin-left: 0;
  614. }
  615. &:nth-of-type(4),
  616. &:nth-of-type(5) {
  617. margin-top: 35px;
  618. }
  619. }
  620. &.active {
  621. border-color: rgba(25, 137, 250, 1);
  622. color: rgba(25, 137, 250, 1);
  623. background-color: rgba(240, 242, 245, 1);
  624. }
  625. &.disabled {
  626. pointer-events: none;
  627. }
  628. &.cropped {
  629. &::after {
  630. content: '已获取';
  631. position: absolute;
  632. bottom: -27px;
  633. left: 50%;
  634. margin-left: -26px;
  635. border-radius: 9px;
  636. width: 52px;
  637. height: 18px;
  638. line-height: 18px;
  639. text-align: center;
  640. font-size: 12px;
  641. font-family: PingFangSC-Regular, PingFang SC;
  642. color: rgba(4, 134, 254, 1);
  643. background: rgba(224, 240, 255, 1);
  644. }
  645. }
  646. }
  647. }
  648. .save-btn {
  649. margin-top: 70px;
  650. border-radius: 4px;
  651. width: 96px;
  652. height: 36px;
  653. line-height: 36px;
  654. text-align: center;
  655. font-size: 14px;
  656. font-family: PingFangSC-Regular, PingFang SC;
  657. color: rgba(255, 255, 255, 1);
  658. background: rgba(25, 137, 250, 1);
  659. cursor: pointer;
  660. &.disabled {
  661. color: rgba(144, 147, 153, 1);
  662. background-color: rgba(240, 242, 245, 1);
  663. pointer-events: none;
  664. }
  665. @media (max-width: 1443px) {
  666. margin-top: 35px;
  667. }
  668. }
  669. }
  670. }
  671. }
  672. </style>

vue-cropper

使用 vue-cropper 实现同样的需求

  1. <template>
  2. <div class="cropper-wrapper">
  3. <div class="cropper-content">
  4. <div class="cropper-content-left">
  5. <div v-if="!hasOriginPic" class="origin-image">
  6. <img-picker
  7. ref="imgPicker"
  8. customClass="cover-image-cropper-picker"
  9. :imgKey="originPicKey"
  10. :defaultImg="defaultImg"
  11. :formDataKey="selfFormDataKey"
  12. @picked="setImgUrl"
  13. @delete-click="deleteCurrentSelectedImage"
  14. />
  15. </div>
  16. <div v-else class="my-cropper-box">
  17. <svg-icon
  18. iconName="icon-delete-file"
  19. className="delete-icon"
  20. :styleObject="deleteIconStyle"
  21. @handleClickEvent="deleteCurrentSelectedImage"
  22. />
  23. <vueCropper
  24. ref="cropper"
  25. :img="articleForm.cover_img"
  26. :outputSize="option.size"
  27. :info="true"
  28. :full="option.full"
  29. :canMove="option.canMove"
  30. :canMoveBox="option.canMoveBox"
  31. :fixedBox="option.fixedBox"
  32. :original="option.original"
  33. :autoCrop="option.autoCrop"
  34. :autoCropWidth="option.autoCropWidth"
  35. :autoCropHeight="option.autoCropHeight"
  36. :centerBox="option.centerBox"
  37. :high="option.high"
  38. :infoTrue="option.infoTrue"
  39. :maxImgSize="option.maxImgSize"
  40. @cropMoving="cropMoving"
  41. :enlarge="option.enlarge"
  42. :mode="option.mode"
  43. :limitMinSize="option.limitMinSize"
  44. :fixed="option.fixed"
  45. :fixedNumber="option.fixedNumber"
  46. ></vueCropper>
  47. </div>
  48. </div>
  49. <div class="cropper-content-right">
  50. <div class="ratio-graph-lists">
  51. <div
  52. v-for="(item, index) in ratioLists"
  53. class="single-item"
  54. :class="{
  55. active: currentGraphIndex === index,
  56. disabled: !hasOriginPic,
  57. cropped: item.isCropped,
  58. hasEditStyle: !item.isCropped && Object.keys(item.editStyle).length,
  59. }"
  60. :style="[item.style, !item.isCropped ? item.editStyle : '']"
  61. :data-radio="item.text"
  62. @click="handleSelectedClick(index)"
  63. >
  64. <div class="text-layer">
  65. <span>{{ item.text }}</span>
  66. </div>
  67. </div>
  68. </div>
  69. <div class="save-btn" :class="{ disabled: !hasOriginPic }" @click="saveCropperImage">
  70. 保存图片
  71. </div>
  72. </div>
  73. </div>
  74. <!-- 图片选择弹窗 -->
  75. <picture-select-dialog />
  76. </div>
  77. </template>
  78. <script>
  79. import { VueCropper } from 'vue-cropper'
  80. import { ImgPicker, PictureSelectDialog } from '@/components'
  81. export default {
  82. name: 'CoverImageCropperPlus',
  83. props: {
  84. articleForm: {
  85. type: Object || String,
  86. },
  87. },
  88. data() {
  89. return {
  90. ratioLists: [
  91. {
  92. text: '1:1',
  93. isCropped: false,
  94. croppedPosition: {},
  95. style: { width: '70px', height: '70px' },
  96. editStyle: {},
  97. scaleFactor: [1, 1],
  98. key: '1_1',
  99. extraValue: { prefix: '', suffix: '_1_1' },
  100. },
  101. {
  102. text: '3:2',
  103. isCropped: false,
  104. croppedPosition: {},
  105. style: { width: '105px', height: '70px' },
  106. editStyle: {},
  107. scaleFactor: [3, 2],
  108. key: '3_2',
  109. extraValue: { prefix: '', suffix: '_3_2' },
  110. },
  111. {
  112. text: '7:5',
  113. isCropped: false,
  114. croppedPosition: {},
  115. style: { width: '97px', height: '70px' },
  116. editStyle: {},
  117. scaleFactor: [7, 5],
  118. key: '7_5',
  119. extraValue: { prefix: '', suffix: '_7_5' },
  120. },
  121. {
  122. text: '17:10',
  123. isCropped: false,
  124. croppedPosition: {},
  125. style: { width: '118px', height: '70px' },
  126. editStyle: {},
  127. scaleFactor: [17, 10],
  128. key: '17_10',
  129. extraValue: { prefix: '', suffix: '_17_10' },
  130. },
  131. {
  132. text: '21:10',
  133. isCropped: false,
  134. croppedPosition: {},
  135. style: { width: '143px', height: '70px' },
  136. editStyle: {},
  137. scaleFactor: [21, 10],
  138. key: '21_10',
  139. extraValue: { prefix: '', suffix: '_21_10' },
  140. },
  141. ],
  142. // 当前比例尺
  143. currentGraphIndex: '',
  144. // imgPicker key
  145. originPicKey: 'CoverImageCropperPlus',
  146. // formdata keys
  147. selfFormDataKey: ['cover_img_id', 'cover_img'],
  148. // 待提交的数据
  149. submitMsg: {},
  150. deleteIconStyle: {
  151. width: '14px',
  152. height: '14px',
  153. },
  154. crap: false,
  155. option: {
  156. size: 1,
  157. full: false,
  158. outputType: 'png',
  159. canMove: false,
  160. fixedBox: false,
  161. original: false,
  162. canMoveBox: true,
  163. autoCrop: false,
  164. centerBox: true,
  165. high: false,
  166. cropData: {},
  167. enlarge: 1,
  168. mode: 'contain',
  169. maxImgSize: 2000,
  170. fixed: true,
  171. fixedNumber: [1, 1],
  172. },
  173. }
  174. },
  175. components: {
  176. VueCropper,
  177. ImgPicker,
  178. PictureSelectDialog,
  179. },
  180. computed: {
  181. defaultImg() {
  182. return {
  183. id: this.articleForm.cover_img_id,
  184. file_path: this.articleForm.cover_img,
  185. }
  186. },
  187. hasOriginPic() {
  188. return this.articleForm.cover_img !== ''
  189. },
  190. },
  191. watch: {
  192. hasOriginPic: {
  193. handler(newVal, oldVal) {
  194. if (!newVal) {
  195. this.currentGraphIndex = ''
  196. this.option.canMove = false
  197. this.ratioLists.map((item) => {
  198. item.isCropped = false
  199. item.croppedPosition = {}
  200. item.editStyle = {}
  201. })
  202. }
  203. },
  204. immediate: true,
  205. },
  206. currentGraphIndex: {
  207. handler(newVal, oldVal) {
  208. if (newVal !== '') {
  209. this.option.canMove = true
  210. this.$nextTick(() => {
  211. const selfRatioItem = this.ratioLists[newVal]
  212. const selfIsCropped = selfRatioItem.isCropped
  213. const selfPositionMsg = selfRatioItem.croppedPosition
  214. // update current cropper ratio
  215. this.option.fixedNumber = selfRatioItem.scaleFactor
  216. this.$nextTick(() => {
  217. // auto crop
  218. this.$refs.cropper.goAutoCrop()
  219. this.$nextTick(() => {
  220. // if cropped and has position message, update crop box
  221. if (selfIsCropped && selfPositionMsg) {
  222. this.$refs.cropper.cropOffsertX = selfPositionMsg.x
  223. this.$refs.cropper.cropOffsertY = selfPositionMsg.y
  224. this.$refs.cropper.cropW = selfPositionMsg.width
  225. this.$refs.cropper.cropH = selfPositionMsg.height
  226. }
  227. })
  228. })
  229. })
  230. }
  231. },
  232. immediate: true,
  233. },
  234. },
  235. methods: {
  236. /** 设置选中的图片*/
  237. setImgUrl(data) {
  238. const { file_id, file_path } = data[this.originPicKey]
  239. this.articleForm.cover_img_id = file_id
  240. this.articleForm.cover_img = file_path
  241. // 裁切组件获取图片地址
  242. this.option.img = file_path
  243. },
  244. /**
  245. * 删除当前选中图回调函数
  246. */
  247. deleteCurrentSelectedImage() {
  248. if (Array.isArray(this.selfFormDataKey)) {
  249. this.selfFormDataKey.forEach((singleKey) => {
  250. this.$set(this.articleForm, singleKey, '')
  251. })
  252. } else {
  253. this.$set(this.articleForm, this.selfFormDataKey, '')
  254. }
  255. },
  256. /** 裁切图回显 */
  257. setPreviewStyle() {
  258. this.ratioLists.map((item, index) => {
  259. const selfCoverImg = JSON.parse(JSON.stringify(this.articleForm.cover_img))
  260. const coverImgSplitArr = selfCoverImg.split('.')
  261. const covreImgSuffix = coverImgSplitArr[coverImgSplitArr.length - 1]
  262. const coverImgPrefix = coverImgSplitArr.splice(0, coverImgSplitArr.length - 1).join('.')
  263. const newICoverImg =
  264. coverImgPrefix +
  265. item.extraValue.suffix +
  266. '.' +
  267. covreImgSuffix +
  268. '?timestamp=' +
  269. new Date().getTime()
  270. item.editStyle = {
  271. background: `url(${newICoverImg}) no-repeat 0 0`,
  272. backgroundSize: 'contain',
  273. }
  274. })
  275. },
  276. /**
  277. * 点击选中当前比例
  278. * @param {number} index - 当前比例的下标
  279. */
  280. handleSelectedClick(index) {
  281. if (this.hasOriginPic && this.currentGraphIndex !== index) {
  282. this.currentGraphIndex = index
  283. }
  284. },
  285. /**
  286. * 保存当前裁切比例下的图片位置信息
  287. */
  288. saveCropperImage() {
  289. const imgScale = this.$refs.cropper.scale
  290. const { x1: cropX1, x2: cropX2, y1: cropY1, y2: cropY2 } = this.$refs.cropper.getCropAxis()
  291. const { x1: imgX1, x2: imgX2, y1: imgY1, y2: imgY2 } = this.$refs.cropper.getImgAxis()
  292. // console.log('cropBox axis: ', cropX1, cropX2, cropY1, cropY1)
  293. // console.log('image axis: ', imgX1, imgX2, imgY1, imgY2)
  294. const currentGraph = this.ratioLists[this.currentGraphIndex]
  295. currentGraph.isCropped = true
  296. // 保存裁切后的位置信息
  297. currentGraph.croppedPosition = Object.assign(
  298. {},
  299. {
  300. x: cropX1,
  301. y: cropY1,
  302. width: cropX2 - cropX1,
  303. height: cropY2 - cropY1,
  304. }
  305. )
  306. // 表单提交所需要的比例信息
  307. const singleCoverScale = {}
  308. singleCoverScale.x = Math.round((cropX1 - imgX1) / imgScale)
  309. singleCoverScale.y = Math.round((cropY1 - imgY1) / imgScale)
  310. singleCoverScale.width = Math.round((cropX2 - cropX1) / imgScale)
  311. singleCoverScale.height = Math.round((cropY2 - cropY1) / imgScale)
  312. Object.assign(singleCoverScale, currentGraph.extraValue)
  313. if (this.submitMsg[currentGraph.key]) {
  314. this.$delete(this.submitMsg, currentGraph.key)
  315. }
  316. this.$set(this.submitMsg, currentGraph.key, singleCoverScale)
  317. console.log('submitMsg', this.submitMsg)
  318. },
  319. },
  320. }
  321. </script>
  322. <style lang="scss" scoped>
  323. .test-button {
  324. display: flex;
  325. flex-wrap: wrap;
  326. }
  327. .btn {
  328. display: inline-block;
  329. line-height: 1;
  330. white-space: nowrap;
  331. cursor: pointer;
  332. background: #fff;
  333. border: 1px solid #c0ccda;
  334. color: #1f2d3d;
  335. text-align: center;
  336. box-sizing: border-box;
  337. outline: none;
  338. margin: 20px 10px 0px 0px;
  339. padding: 9px 15px;
  340. font-size: 14px;
  341. border-radius: 4px;
  342. color: #fff;
  343. background-color: #50bfff;
  344. border-color: #50bfff;
  345. transition: all 0.2s ease;
  346. text-decoration: none;
  347. user-select: none;
  348. }
  349. .cropper-content {
  350. display: flex;
  351. &-left {
  352. position: relative;
  353. width: 256px;
  354. height: 176px;
  355. .origin-image {
  356. position: absolute;
  357. top: 0;
  358. left: 0;
  359. z-index: 9;
  360. .img-picker.cover-image-cropper-picker {
  361. width: 256px;
  362. height: 176px;
  363. line-height: 176px;
  364. .delete-icon-zIndex {
  365. z-index: 9;
  366. }
  367. }
  368. }
  369. .my-cropper-box {
  370. position: relative;
  371. width: 256px;
  372. height: 176px;
  373. border: 1px dashed #dcdfe6;
  374. &:hover {
  375. border-color: #409eff;
  376. .delete-icon {
  377. visibility: visible;
  378. }
  379. }
  380. .delete-icon {
  381. visibility: hidden;
  382. z-index: 99;
  383. position: absolute;
  384. top: -7px;
  385. right: -7px;
  386. cursor: pointer;
  387. }
  388. }
  389. }
  390. &-right {
  391. position: relative;
  392. margin-left: 85px;
  393. &::before {
  394. content: ' ';
  395. position: absolute;
  396. top: 26px;
  397. left: -42px;
  398. width: 1px;
  399. height: 112px;
  400. background: rgba(240, 242, 245, 1);
  401. }
  402. .ratio-graph-lists {
  403. position: relative;
  404. display: flex;
  405. flex-wrap: wrap;
  406. /** 兼容小屏幕,换行处理 */
  407. @media (max-width: 1443px) {
  408. width: 316px;
  409. .single-item {
  410. &:nth-of-type(4) {
  411. margin-left: 0;
  412. }
  413. &:nth-of-type(4),
  414. &:nth-of-type(5) {
  415. margin-top: 35px;
  416. }
  417. }
  418. }
  419. .single-item {
  420. position: relative;
  421. margin-left: 22px;
  422. border: 1px solid rgba(216, 220, 230, 1);
  423. line-height: 70px;
  424. text-align: center;
  425. font-size: 14px;
  426. font-family: PingFangSC-Regular, PingFang SC;
  427. color: rgba(144, 147, 153, 1);
  428. background: rgba(240, 242, 245, 1);
  429. cursor: pointer;
  430. &:nth-of-type(1) {
  431. margin-left: 0;
  432. }
  433. &:hover,
  434. &.active {
  435. border-color: rgba(25, 137, 250, 1);
  436. color: rgba(25, 137, 250, 1);
  437. background-color: rgba(240, 242, 245, 1);
  438. }
  439. &.disabled {
  440. pointer-events: none;
  441. }
  442. &.cropped {
  443. &::after {
  444. content: '已获取';
  445. position: absolute;
  446. bottom: -27px;
  447. left: 50%;
  448. margin-left: -26px;
  449. border-radius: 9px;
  450. width: 52px;
  451. height: 18px;
  452. line-height: 18px;
  453. text-align: center;
  454. font-size: 12px;
  455. font-family: PingFangSC-Regular, PingFang SC;
  456. color: rgba(4, 134, 254, 1);
  457. background: rgba(224, 240, 255, 1);
  458. }
  459. }
  460. &.hasEditStyle {
  461. .text-layer {
  462. color: #fff;
  463. background-color: rgba(0, 0, 0, 0.15);
  464. }
  465. }
  466. }
  467. }
  468. .save-btn {
  469. margin-top: 70px;
  470. border-radius: 4px;
  471. width: 96px;
  472. height: 36px;
  473. line-height: 36px;
  474. text-align: center;
  475. font-size: 14px;
  476. font-family: PingFangSC-Regular, PingFang SC;
  477. color: rgba(255, 255, 255, 1);
  478. background: rgba(25, 137, 250, 1);
  479. cursor: pointer;
  480. &:hover {
  481. border-color: #369efe;
  482. background-color: #369efe;
  483. }
  484. &.disabled {
  485. color: rgba(144, 147, 153, 1);
  486. background-color: rgba(240, 242, 245, 1);
  487. pointer-events: none;
  488. }
  489. @media (max-width: 1443px) {
  490. margin-top: 35px;
  491. }
  492. }
  493. }
  494. }
  495. </style>

参考