hevue-img-preview.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. <!--
  2. * @Author: 贺永胜
  3. * @Date: 2021-04-19 16:39:30
  4. * @email: 1378431028@qq.com
  5. * @LastEditors: 贺永胜
  6. * @LastEditTime: 2023-03-28 16:28:25
  7. * @Description: file content
  8. -->
  9. <!--
  10. * __----~~~~~~~~~~~------___
  11. * . . ~~//====...... __--~ ~~
  12. * -. \_|// |||\\ ~~~~~~::::... /~
  13. * ___-==_ _-~o~ \/ ||| \\ _/~~-
  14. * __---~~~.==~||\=_ -_--~/_-~|- |\\ \\ _/~
  15. * _-~~ .=~ | \\-_ '-~7 /- / || \ /
  16. * .~ .~ | \\ -_ / /- / || \ /
  17. * / ____ / | \\ ~-_/ /|- _/ .|| \ /
  18. * |~~ ~~|--~~~~--_ \ ~==-/ | \~--===~~ .\
  19. * ' ~-| /| |-~\~~ __--~~
  20. * |-~~-_/ | | ~\_ _-~ /\
  21. * / \ \__ \/~ \__
  22. * _--~ _/ | .-~~____--~-/ ~~==.
  23. * ((->/~ '.|||' -_| ~~-/ , . _||
  24. * -_ ~\ ~~---l__i__i__i--~~_/
  25. * _-~-__ ~) \--______________--~~
  26. * //.-~~~-~_--~- |-------~~~~~~~~
  27. * //.-~~~--\
  28. * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  29. *
  30. * 神兽保佑 永无BUG
  31. -->
  32. <template>
  33. <transition name="fade">
  34. <div
  35. class="hevue-imgpreview-wrap"
  36. id="hevue-imgpreview-wrap"
  37. v-if="visible"
  38. ref="heImg"
  39. @mouseup="removeMove('pc')"
  40. @touchend="removeMove('mobile')"
  41. @click.stop="clickMask"
  42. >
  43. <div class="he-img-wrap">
  44. <div
  45. class="heimgfont hevue-img-status-icon rotate-animation"
  46. v-show="imgState === 1"
  47. >
  48. &#xe6b1;
  49. </div>
  50. <!-- <div class="heimgfont loading">&#xe6b1;</div> -->
  51. <img
  52. :src="imgurl"
  53. ref="heImView"
  54. @click.stop=""
  55. v-show="imgState === 2"
  56. class="he-img-view"
  57. :style="
  58. 'transform: scale(' +
  59. imgScale +
  60. ') rotate(' +
  61. imgRotate +
  62. 'deg);margin-top:' +
  63. imgTop +
  64. 'px;margin-left:' +
  65. imgLeft +
  66. 'px;' +
  67. maxWH
  68. "
  69. @mousedown="addMove"
  70. @touchstart="addMoveMobile"
  71. />
  72. <!-- 图片加载失败 -->
  73. <div class="heimgfont hevue-img-status-icon" v-show="imgState === 3">
  74. &#xec0d;
  75. </div>
  76. <!-- 关闭按钮 -->
  77. <div
  78. class="heimgfont he-close-icon"
  79. @click.stop="close({ way: 'closeBtn' })"
  80. v-if="closeBtn"
  81. >
  82. &#xe608;
  83. </div>
  84. <!-- 左箭头 -->
  85. <div
  86. class="arrow arrow-left heimgfont"
  87. @click.stop="toogleImg(false, 'btn')"
  88. v-if="arrowBtn && multiple"
  89. >
  90. &#xe620;
  91. </div>
  92. <!-- 右箭头 -->
  93. <div
  94. class="arrow arrow-right heimgfont"
  95. @click.stop="toogleImg(true, 'btn')"
  96. v-if="arrowBtn && multiple"
  97. >
  98. &#xe60d;
  99. </div>
  100. <!-- 控制条 -->
  101. <div class="he-control-bar-wrap" v-if="controlBar">
  102. <div class="he-control-bar" @click.stop>
  103. <!-- 缩小 -->
  104. <div
  105. class="he-control-btn heimgfont"
  106. @click.stop="scaleFunc(-0.15)"
  107. >
  108. &#xe65e;
  109. </div>
  110. <!-- 放大 -->
  111. <div class="he-control-btn heimgfont" @click.stop="scaleFunc(0.15)">
  112. &#xe65d;
  113. </div>
  114. <!-- 复位 -->
  115. <div
  116. class="he-control-btn heimgfont"
  117. v-show="isFull"
  118. @click.stop="imgToggle"
  119. >
  120. &#xe698;
  121. </div>
  122. <!-- 复位 -->
  123. <div
  124. class="he-control-btn heimgfont"
  125. v-show="!isFull"
  126. @click.stop="imgToggle"
  127. >
  128. &#xe86b;
  129. </div>
  130. <!-- 左转 -->
  131. <div class="he-control-btn heimgfont" @click.stop="rotateFunc(-90)">
  132. &#xe670;
  133. </div>
  134. <!-- 右转 -->
  135. <div class="he-control-btn heimgfont" @click.stop="rotateFunc(90)">
  136. &#xe66f;
  137. </div>
  138. <!-- 下载 -->
  139. <!-- <div class="he-control-btn heimgfont" @click.stop="downloadIamge">
  140. &#xe694;
  141. </div> -->
  142. </div>
  143. </div>
  144. <!-- 页码指示器 -->
  145. <div class="he-control-num" v-if="controlBar && multiple">
  146. {{ imgIndex + 1 }} / {{ imgList.length }}
  147. </div>
  148. </div>
  149. </div>
  150. </transition>
  151. </template>
  152. <script>
  153. export default {
  154. name: 'hevue-img-preview',
  155. data() {
  156. return {
  157. // imgWidth: 0,
  158. // imgHeight: 0,
  159. visible: false, // 插件显示,默认为false
  160. imgScale: 1,
  161. imgTop: 0,
  162. imgLeft: 0,
  163. imgRotate: 0,
  164. isFull: false,
  165. maxWH: 'max-width:100%;max-height:100%;',
  166. clientX: 0,
  167. clientY: 0,
  168. imgIndex: 0,
  169. canRun: true,
  170. imgurl: '',
  171. imgState: 1,
  172. start: [{}, {}],
  173. mobileScale: 0, // 手指离开时图片的缩放比例
  174. // 以下内容为用户传入配置
  175. // show: true, // 插件显示,默认为false
  176. url: '', // 预览图片的地址
  177. nowImgIndex: 0,
  178. multiple: false,
  179. imgList: [],
  180. // 以下为可全局配置
  181. controlBar: true,
  182. closeBtn: true,
  183. arrowBtn: true,
  184. keyboard: false,
  185. clickMaskCLose: false, // 是否点击遮罩关闭,默认false
  186. closeFn: null, // 关闭回调函数
  187. changeFn: null, // 切换图片回调函数
  188. }
  189. },
  190. mounted() {
  191. this.initImg()
  192. },
  193. watch: {
  194. url() {
  195. this.initImg()
  196. },
  197. visible: {
  198. handler(newV) {
  199. if (newV) {
  200. this.$nextTick(() => {
  201. let _dom = document.getElementById('hevue-imgpreview-wrap')
  202. _dom.onmousewheel = this.scrollFunc
  203. // 火狐浏览器没有onmousewheel事件,用DOMMouseScroll代替(滚轮事件)
  204. document.body.addEventListener('DOMMouseScroll', this.scrollFunc)
  205. // 禁止火狐浏览器下拖拽图片的默认事件
  206. document.ondragstart = function () {
  207. return false
  208. }
  209. // 判断是否多图
  210. if (this.multiple) {
  211. if (Array.isArray(this.imgList) && this.imgList.length > 0) {
  212. this.imgIndex = Number(this.nowImgIndex) || 0
  213. // this.url = this.imgList[this.imgIndex]
  214. this.changeUrl(this.imgList[this.imgIndex], this.imgIndex)
  215. } else {
  216. // console.error("imgList 为空或格式不正确");
  217. }
  218. } else {
  219. this.changeUrl(this.url)
  220. }
  221. // 判断是否开启键盘事件
  222. if (this.keyboard) {
  223. document.addEventListener('keydown', this.keyHandleDebounce)
  224. }
  225. })
  226. }
  227. },
  228. immediate: true,
  229. },
  230. },
  231. methods: {
  232. show() {
  233. this.visible = true
  234. },
  235. close(data) {
  236. this.initImg()
  237. // this.maxWH = "max-width:100%;max-height:100%;";
  238. // this.isFull = false;
  239. // 移除火狐浏览器下的鼠标滚动事件
  240. document.body.removeEventListener('DOMMouseScroll', this.scrollFunc)
  241. //恢复火狐及Safari浏览器下的图片拖拽
  242. document.ondragstart = null
  243. // 移除键盘事件
  244. if (this.keyboard) {
  245. document.removeEventListener('keydown', this.keyHandleDebounce)
  246. }
  247. this.visible = false
  248. this.closeFn && this.closeFn(data)
  249. },
  250. initImg() {
  251. this.mobileScale = 1
  252. this.imgScale = 1
  253. this.imgRotate = 0
  254. this.imgTop = 0
  255. this.imgLeft = 0
  256. },
  257. /**
  258. * 切换图片
  259. * true 下一张
  260. * false 上一张
  261. */
  262. toogleImg(bool, way) {
  263. let fromIndex = this.imgIndex
  264. if (bool) {
  265. this.imgIndex++
  266. if (this.imgIndex > this.imgList.length - 1) {
  267. this.imgIndex = 0
  268. }
  269. } else {
  270. this.imgIndex--
  271. if (this.imgIndex < 0) {
  272. this.imgIndex = this.imgList.length - 1
  273. }
  274. }
  275. this.changeFn &&
  276. this.changeFn({
  277. type: bool ? 'next' : 'prev',
  278. fromImgIndex: fromIndex,
  279. fromImgUrl: this.imgList[fromIndex],
  280. toImgIndex: this.imgIndex,
  281. toImgUrl: this.imgList[this.imgIndex],
  282. way,
  283. })
  284. // this.url = this.imgList[this.imgIndex]
  285. this.changeUrl(this.imgList[this.imgIndex], this.imgIndex)
  286. },
  287. // 改变图片地址
  288. /**
  289. * @description:
  290. * @param {String} url 要显示的图片的url
  291. * @param {Number} index 当前显示当图片下标,防止用户点击切换图片过快
  292. * @return {*}
  293. */
  294. changeUrl(url, index) {
  295. this.imgState = 1
  296. let img = new Image()
  297. img.src = url
  298. img.onload = () => {
  299. // 如果加载出来图片当下标不是当前显示图片当下标,则不予显示(用户点击过快当时候,会出现用户点到第三张了,此时第一张图片才加载完当情况)
  300. if (index != undefined && index == this.imgIndex) {
  301. this.imgState = 2
  302. this.imgurl = url
  303. } else if (index == undefined) {
  304. this.imgState = 2
  305. this.imgurl = url
  306. }
  307. }
  308. img.onerror = () => {
  309. if (index != undefined && index == this.imgIndex) {
  310. this.imgState = 3
  311. } else if (index == undefined) {
  312. this.imgState = 3
  313. }
  314. }
  315. },
  316. // 旋转图片
  317. rotateFunc(deg) {
  318. this.imgRotate += deg
  319. },
  320. // 图片缩放
  321. scaleFunc(num, bool) {
  322. if (this.imgScale <= 0.2 && num < 0) return
  323. if (bool) {
  324. this.imgScale = num
  325. } else {
  326. this.imgScale += num
  327. }
  328. },
  329. // 图片原尺寸切换
  330. imgToggle() {
  331. this.initImg()
  332. if (this.isFull) {
  333. this.maxWH = 'max-width:100%;max-height:100%;'
  334. } else {
  335. this.maxWH = ''
  336. }
  337. this.isFull = !this.isFull
  338. },
  339. // 鼠标滚轮缩放
  340. scrollFunc(e) {
  341. e = e || window.event
  342. // e.returnValue = false // ie
  343. // 火狐下没有wheelDelta,用detail代替,由于detail值的正负和wheelDelta相反,所以取反
  344. e.delta = e.wheelDelta || -e.detail
  345. e.preventDefault()
  346. if (e.delta > 0) {
  347. //当滑轮向上滚动时
  348. this.scaleFunc(0.05)
  349. }
  350. if (e.delta < 0) {
  351. //当滑轮向下滚动时
  352. this.scaleFunc(-0.05)
  353. }
  354. },
  355. // 鼠标按下
  356. addMove(e) {
  357. e = e || window.event
  358. this.clientX = e.clientX
  359. this.clientY = e.clientY
  360. this.$refs.heImg.onmousemove = this.moveFunc
  361. },
  362. // 手指按下事件
  363. addMoveMobile(e) {
  364. e.preventDefault()
  365. e = e || window.event
  366. if (e.touches.length > 1) {
  367. this.start = e.touches
  368. } else {
  369. this.clientX = e.touches[0].pageX
  370. this.clientY = e.touches[0].pageY
  371. }
  372. // 添加手指拖动事件
  373. this.$refs.heImg.ontouchmove = this.moveFuncMobile
  374. },
  375. // 鼠标拖动
  376. moveFunc(e) {
  377. e = e || window.event
  378. e.preventDefault()
  379. let movementX = e.clientX - this.clientX
  380. let movementY = e.clientY - this.clientY
  381. // event.clientY;
  382. this.imgLeft += movementX * 2
  383. this.imgTop += movementY * 2
  384. this.clientX = e.clientX
  385. this.clientY = e.clientY
  386. },
  387. // 手指拖动
  388. moveFuncMobile(e) {
  389. e = e || window.event
  390. if (e.touches.length > 1) {
  391. var now = e.touches
  392. var scale =
  393. this.getDistance(now[0], now[1]) /
  394. this.getDistance(this.start[0], this.start[1])
  395. // 判断是否手指缩放过,如果缩放过,要在上次缩放的比例基础上进行缩放
  396. if (this.mobileScale) {
  397. if (scale > 1) {
  398. // 放大
  399. this.scaleFunc(scale + this.mobileScale - 1, true)
  400. } else {
  401. // 缩小
  402. this.scaleFunc(scale * this.mobileScale, true)
  403. }
  404. } else {
  405. this.scaleFunc(scale, true)
  406. }
  407. } else {
  408. let touch = e.touches[0]
  409. e.preventDefault()
  410. let movementX = touch.pageX - this.clientX
  411. let movementY = touch.pageY - this.clientY
  412. // event.clientY;
  413. this.imgLeft += movementX * 2
  414. this.imgTop += movementY * 2
  415. this.clientX = touch.pageX
  416. this.clientY = touch.pageY
  417. }
  418. },
  419. // 移除拖动事件
  420. removeMove(type) {
  421. if (type === 'pc') {
  422. this.$refs.heImg.onmousemove = null
  423. } else {
  424. this.mobileScale = this.imgScale
  425. this.$refs.heImg.ontouchmove = null
  426. }
  427. },
  428. keyHandleDebounce(e) {
  429. if (this.canRun) {
  430. // 如果this.canRun为true证明当前可以执行函数
  431. this.keyHandle(e)
  432. this.canRun = false // 执行函数后一段时间内不可再次执行
  433. setTimeout(() => {
  434. this.canRun = true // 等到了我们设定的时间之后,把this.canRun改为true,可以再次执行函数
  435. }, 300)
  436. }
  437. },
  438. // 键盘事件
  439. keyHandle(e) {
  440. e = window.event || e
  441. var key = e.keyCode || e.which || e.charCode
  442. switch (key) {
  443. case 27: // esc
  444. this.close({ way: 'esc' })
  445. break
  446. case 65: // a键-上一张
  447. if (this.multiple) {
  448. this.toogleImg(false, 'key-a')
  449. }
  450. break
  451. case 68: // d键-下一张
  452. if (this.multiple) {
  453. this.toogleImg(true, 'key-d')
  454. }
  455. break
  456. case 87: // w键-放大
  457. this.scaleFunc(0.15)
  458. break
  459. case 83: // s键-缩小
  460. this.scaleFunc(-0.15)
  461. break
  462. case 81: // q键-逆时针旋转
  463. this.rotateFunc(-90)
  464. break
  465. case 69: // e键-顺时针旋转
  466. this.rotateFunc(90)
  467. break
  468. case 82: // r键-复位键
  469. this.initImg()
  470. break
  471. default:
  472. break
  473. }
  474. },
  475. // 点击遮罩层
  476. clickMask() {
  477. if (this.clickMaskCLose) {
  478. this.close({ way: 'mask' })
  479. }
  480. },
  481. //缩放 勾股定理方法-求两点之间的距离
  482. getDistance(p1, p2) {
  483. var x = p2.pageX - p1.pageX,
  484. y = p2.pageY - p1.pageY
  485. return Math.sqrt(x * x + y * y)
  486. },
  487. /**
  488. * @description:
  489. * @param {String} imgsrc
  490. * @param {*} name
  491. * @return {*}
  492. */
  493. downloadIamge() {
  494. //下载图片地址和图片名
  495. let image = new Image()
  496. // 解决跨域 Canvas 污染问题
  497. image.setAttribute('crossOrigin', 'anonymous')
  498. image.onload = function () {
  499. let canvas = document.createElement('canvas')
  500. canvas.width = image.width
  501. canvas.height = image.height
  502. let context = canvas.getContext('2d')
  503. context.drawImage(image, 0, 0, image.width, image.height)
  504. let url = canvas.toDataURL('image/png') //得到图片的base64编码数据
  505. let a = document.createElement('a') // 生成一个a元素
  506. let event = new MouseEvent('click') // 创建一个单击事件
  507. a.download = 'photo' + +new Date() // 设置图片名称
  508. a.href = url // 将生成的URL设置为a.href属性
  509. a.dispatchEvent(event) // 触发a的单击事件
  510. }
  511. image.onerror = function (err) {
  512. console.log('图片信息不正确或图片服务器禁止访问')
  513. console.log(err)
  514. }
  515. if (this.multiple) {
  516. image.src = this.imgList[this.imgIndex]
  517. } else {
  518. image.src = this.url
  519. }
  520. },
  521. },
  522. }
  523. </script>
  524. <style scoped>
  525. @import './iconfont/iconfont.css';
  526. @import './css/default.css';
  527. </style>