previewImage.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. <template>
  2. <transition name="viewer-fade">
  3. <div
  4. ref="wrapper"
  5. :tabindex="-1"
  6. class="el-image-viewer__wrapper"
  7. :style="{'z-index': zIndex}"
  8. >
  9. <div class="el-image-viewer__mask" @click.self="hideOnClickModal && hide()"></div>
  10. <!-- CLOSE -->
  11. <span class="el-image-viewer__btn el-image-viewer__close" @click="hide">
  12. <i class="el-icon">
  13. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
  14. <path
  15. fill="currentColor"
  16. d="M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"
  17. ></path>
  18. </svg>
  19. </i>
  20. </span>
  21. <!-- ARROW -->
  22. <template v-if="!isSingle">
  23. <span
  24. class="el-image-viewer__btn el-image-viewer__prev"
  25. :class="{ 'is-disabled': !infinite && isFirst }"
  26. @click="prev"
  27. >
  28. <i class="el-icon">
  29. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
  30. <path
  31. fill="currentColor"
  32. d="M609.408 149.376 277.76 489.6a32 32 0 0 0 0 44.672l331.648 340.352a29.12 29.12 0 0 0 41.728 0 30.592 30.592 0 0 0 0-42.752L339.264 511.936l311.872-319.872a30.592 30.592 0 0 0 0-42.688 29.12 29.12 0 0 0-41.728 0z"
  33. ></path>
  34. </svg>
  35. </i>
  36. </span>
  37. <span
  38. class="el-image-viewer__btn el-image-viewer__next"
  39. :class="{ 'is-disabled': !infinite && isLast }"
  40. @click="next"
  41. >
  42. <i class="el-icon">
  43. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
  44. <path
  45. fill="currentColor"
  46. d="M340.864 149.312a30.592 30.592 0 0 0 0 42.752L652.736 512 340.864 831.872a30.592 30.592 0 0 0 0 42.752 29.12 29.12 0 0 0 41.728 0L714.24 534.336a32 32 0 0 0 0-44.672L382.592 149.376a29.12 29.12 0 0 0-41.728 0z"
  47. ></path>
  48. </svg>
  49. </i>
  50. </span>
  51. </template>
  52. <!-- ACTIONS -->
  53. <div
  54. class="el-image-viewer__btn el-image-viewer__actions"
  55. v-if="!isVideo(urlList[index])"
  56. >
  57. <div class="el-image-viewer__actions__inner">
  58. <i class="el-icon" @click="handleActions('zoomOut')"
  59. ><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
  60. <path
  61. fill="currentColor"
  62. d="m795.904 750.72 124.992 124.928a32 32 0 0 1-45.248 45.248L750.656 795.904a416 416 0 1 1 45.248-45.248zM480 832a352 352 0 1 0 0-704 352 352 0 0 0 0 704M352 448h256a32 32 0 0 1 0 64H352a32 32 0 0 1 0-64"
  63. ></path></svg
  64. ></i>
  65. <i class="el-icon" @click="handleActions('zoomIn')"
  66. ><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
  67. <path
  68. fill="currentColor"
  69. d="m795.904 750.72 124.992 124.928a32 32 0 0 1-45.248 45.248L750.656 795.904a416 416 0 1 1 45.248-45.248zM480 832a352 352 0 1 0 0-704 352 352 0 0 0 0 704m-32-384v-96a32 32 0 0 1 64 0v96h96a32 32 0 0 1 0 64h-96v96a32 32 0 0 1-64 0v-96h-96a32 32 0 0 1 0-64z"
  70. ></path></svg
  71. ></i>
  72. <i class="el-image-viewer__actions__divider"></i>
  73. <i class="el-icon" @click="toggleMode"
  74. ><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
  75. <path
  76. fill="currentColor"
  77. d="m160 96.064 192 .192a32 32 0 0 1 0 64l-192-.192V352a32 32 0 0 1-64 0V96h64zm0 831.872V928H96V672a32 32 0 1 1 64 0v191.936l192-.192a32 32 0 1 1 0 64zM864 96.064V96h64v256a32 32 0 1 1-64 0V160.064l-192 .192a32 32 0 1 1 0-64l192-.192zm0 831.872-192-.192a32 32 0 0 1 0-64l192 .192V672a32 32 0 1 1 64 0v256h-64z"
  78. ></path></svg
  79. ></i>
  80. <i class="el-image-viewer__actions__divider"></i>
  81. <i class="el-icon" @click="handleActions('anticlocelise')"
  82. ><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
  83. <path
  84. fill="currentColor"
  85. d="M289.088 296.704h92.992a32 32 0 0 1 0 64H232.96a32 32 0 0 1-32-32V179.712a32 32 0 0 1 64 0v50.56a384 384 0 0 1 643.84 282.88 384 384 0 0 1-383.936 384 384 384 0 0 1-384-384h64a320 320 0 1 0 640 0 320 320 0 0 0-555.712-216.448z"
  86. ></path></svg
  87. ></i>
  88. <i class="el-icon" @click="handleActions('clocelise')"
  89. ><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
  90. <path
  91. fill="currentColor"
  92. d="M784.512 230.272v-50.56a32 32 0 1 1 64 0v149.056a32 32 0 0 1-32 32H667.52a32 32 0 1 1 0-64h92.992A320 320 0 1 0 524.8 833.152a320 320 0 0 0 320-320h64a384 384 0 0 1-384 384 384 384 0 0 1-384-384 384 384 0 0 1 643.712-282.88z"
  93. ></path></svg
  94. ></i>
  95. <i class="el-icon" @click="handleDownload">
  96. <svg data-v-d2e47025="" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
  97. <path fill="currentColor" d="M160 832h704a32 32 0 1 1 0 64H160a32 32 0 1 1 0-64m384-253.696 236.288-236.352 45.248 45.248L508.8 704 192 387.2l45.248-45.248L480 584.704V128h64z"></path>
  98. </svg>
  99. </i>
  100. </div>
  101. </div>
  102. <!-- CANVAS -->
  103. <div class="el-image-viewer__canvas">
  104. <template v-for="(url, i) in urlList" :key="i">
  105. <img
  106. v-if="i == index && !isVideo(url)"
  107. ref="media"
  108. :src="url"
  109. :style="mediaStyle"
  110. class="el-image-viewer__img"
  111. @load="handleMediaLoad"
  112. @error="handleMediaError"
  113. @mousedown="handleMouseDown"
  114. />
  115. <video
  116. v-if="i == index && isVideo(url)"
  117. ref="media"
  118. :src="url"
  119. :controls="true"
  120. :autoplay="true"
  121. :style="mediaStyle"
  122. class="el-image-viewer__img"
  123. @load="handleMediaLoad"
  124. @error="handleMediaError"
  125. @mousedown="handleMouseDown"
  126. >
  127. </video>
  128. </template>
  129. </div>
  130. </div>
  131. </transition>
  132. </template>
  133. <script>
  134. import { computed, ref, onMounted, watch, nextTick } from 'vue'
  135. import { downloadImgVideo, downloadBase64 } from '@/utils'
  136. const EVENT_CODE = {
  137. tab: 'Tab',
  138. enter: 'Enter',
  139. space: 'Space',
  140. left: 'ArrowLeft', // 37
  141. up: 'ArrowUp', // 38
  142. right: 'ArrowRight', // 39
  143. down: 'ArrowDown', // 40
  144. esc: 'Escape',
  145. delete: 'Delete',
  146. backspace: 'Backspace'
  147. }
  148. const isFirefox = function () {
  149. return !!window.navigator.userAgent.match(/firefox/i)
  150. }
  151. const rafThrottle = function (fn) {
  152. let locked = false
  153. return function (...args) {
  154. if (locked) return
  155. locked = true
  156. window.requestAnimationFrame(() => {
  157. fn.apply(this, args)
  158. locked = false
  159. })
  160. }
  161. }
  162. const Mode = {
  163. CONTAIN: {
  164. name: 'contain',
  165. icon: 'el-icon-full-screen'
  166. },
  167. ORIGINAL: {
  168. name: 'original',
  169. icon: 'el-icon-c-scale-to-original'
  170. }
  171. }
  172. const mousewheelEventName = isFirefox() ? 'DOMMouseScroll' : 'mousewheel'
  173. const CLOSE_EVENT = 'close'
  174. const SWITCH_EVENT = 'switch'
  175. export default {
  176. name: 'ElMediaViewer',
  177. props: {
  178. urlList: {
  179. type: Array,
  180. default: () => []
  181. },
  182. zIndex: {
  183. type: Number,
  184. default: 2016
  185. },
  186. initialIndex: {
  187. type: Number,
  188. default: 0
  189. },
  190. infinite: {
  191. type: Boolean,
  192. default: true
  193. },
  194. hideOnClickModal: {
  195. type: Boolean,
  196. default: true
  197. },
  198. // 下载文件名
  199. fileName: {
  200. type: String,
  201. default: ''
  202. }
  203. },
  204. emits: [CLOSE_EVENT, SWITCH_EVENT],
  205. setup(props, { emit }) {
  206. let _keyDownHandler = null
  207. let _mouseWheelHandler = null
  208. let _dragHandler = null
  209. const loading = ref(true)
  210. const index = ref(props.initialIndex)
  211. const wrapper = ref(null)
  212. const media = ref(null)
  213. const mode = ref(Mode.CONTAIN)
  214. const transform = ref({
  215. scale: 1,
  216. deg: 0,
  217. offsetX: 0,
  218. offsetY: 0,
  219. enableTransition: false
  220. })
  221. const isVideo = computed(() => (url) => {
  222. return /\.(mp4|webm|ogg)$/i.test(url)
  223. })
  224. // 处理 video 有video 时 字段
  225. const isSingle = computed(() => {
  226. // const { urlList } = props
  227. // urlList.forEach((item) => {
  228. // if (!item.type) {
  229. // item.type = item.response.type
  230. // if (item.response.thumbnailUrl) {
  231. // item.videoUrl = item.response.thumbnailUrl
  232. // }
  233. // }
  234. // })
  235. // return urlList.length <= 1
  236. return false
  237. })
  238. const isFirst = computed(() => {
  239. return index.value === 0
  240. })
  241. const isLast = computed(() => {
  242. return index.value === props.urlList.length - 1
  243. })
  244. const currentMedia = computed(() => {
  245. return props.urlList[index.value]
  246. })
  247. const isImage = computed(() => {
  248. const currentUrl = props.urlList[index.value]
  249. return currentUrl.endsWith('.jpg') || currentUrl.endsWith('.png')
  250. })
  251. const mediaStyle = computed(() => {
  252. const { scale, deg, offsetX, offsetY, enableTransition } = transform.value
  253. const style = {
  254. transform: `scale(${scale}) rotate(${deg}deg)`,
  255. transition: enableTransition ? 'transform .3s' : '',
  256. marginLeft: `${offsetX}px`,
  257. marginTop: `${offsetY}px`
  258. }
  259. if (mode.value.name === Mode.CONTAIN.name) {
  260. style.maxWidth = style.maxHeight = '100%'
  261. }
  262. return style
  263. })
  264. function hide() {
  265. deviceSupportUninstall()
  266. emit(CLOSE_EVENT)
  267. }
  268. function deviceSupportInstall() {
  269. _keyDownHandler = rafThrottle((e) => {
  270. switch (e.code) {
  271. // ESC
  272. case EVENT_CODE.esc:
  273. hide()
  274. break
  275. // SPACE
  276. case EVENT_CODE.space:
  277. toggleMode()
  278. break
  279. // LEFT_ARROW
  280. case EVENT_CODE.left:
  281. prev()
  282. break
  283. // UP_ARROW
  284. case EVENT_CODE.up:
  285. handleActions('zoomIn')
  286. break
  287. // RIGHT_ARROW
  288. case EVENT_CODE.right:
  289. next()
  290. break
  291. // DOWN_ARROW
  292. case EVENT_CODE.down:
  293. handleActions('zoomOut')
  294. break
  295. }
  296. })
  297. _mouseWheelHandler = rafThrottle((e) => {
  298. const delta = e.wheelDelta ? e.wheelDelta : -e.detail
  299. if (delta > 0) {
  300. handleActions('zoomIn', {
  301. zoomRate: 0.015,
  302. enableTransition: false
  303. })
  304. } else {
  305. handleActions('zoomOut', {
  306. zoomRate: 0.015,
  307. enableTransition: false
  308. })
  309. }
  310. })
  311. document.addEventListener('keydown', _keyDownHandler, false)
  312. document.addEventListener(mousewheelEventName, _mouseWheelHandler, false)
  313. }
  314. function deviceSupportUninstall() {
  315. document.removeEventListener('keydown', _keyDownHandler, false)
  316. document.removeEventListener(
  317. mousewheelEventName,
  318. _mouseWheelHandler,
  319. false
  320. )
  321. _keyDownHandler = null
  322. _mouseWheelHandler = null
  323. }
  324. function handleMediaLoad() {
  325. loading.value = false
  326. }
  327. function handleMediaError(e) {
  328. loading.value = false
  329. }
  330. function handleMouseDown(e) {
  331. if (loading.value || e.button !== 0) return
  332. const { offsetX, offsetY } = transform.value
  333. const startX = e.pageX
  334. const startY = e.pageY
  335. const divLeft = wrapper.value.clientLeft
  336. const divRight = wrapper.value.clientLeft + wrapper.value.clientWidth
  337. const divTop = wrapper.value.clientTop
  338. const divBottom = wrapper.value.clientTop + wrapper.value.clientHeight
  339. _dragHandler = rafThrottle((ev) => {
  340. transform.value = {
  341. ...transform.value,
  342. offsetX: offsetX + ev.pageX - startX,
  343. offsetY: offsetY + ev.pageY - startY
  344. }
  345. })
  346. document.addEventListener('mousemove', _dragHandler, false)
  347. document.addEventListener(
  348. 'mouseup',
  349. (e) => {
  350. const mouseX = e.pageX
  351. const mouseY = e.pageY
  352. if (
  353. mouseX < divLeft ||
  354. mouseX > divRight ||
  355. mouseY < divTop ||
  356. mouseY > divBottom
  357. ) {
  358. reset()
  359. }
  360. document.removeEventListener('mousemove', _dragHandler, false)
  361. },
  362. false
  363. )
  364. e.preventDefault()
  365. }
  366. function reset() {
  367. transform.value = {
  368. scale: 1,
  369. deg: 0,
  370. offsetX: 0,
  371. offsetY: 0,
  372. enableTransition: false
  373. }
  374. }
  375. function toggleMode() {
  376. if (loading.value) return
  377. const modeNames = Object.keys(Mode)
  378. const modeValues = Object.values(Mode)
  379. const currentMode = mode.value.name
  380. const index = modeValues.findIndex((i) => i.name === currentMode)
  381. const nextIndex = (index + 1) % modeNames.length
  382. mode.value = Mode[modeNames[nextIndex]]
  383. reset()
  384. }
  385. function prev() {
  386. if (isFirst.value && !props.infinite) return
  387. const len = props.urlList.length
  388. index.value = (index.value - 1 + len) % len
  389. }
  390. function next() {
  391. if (isLast.value && !props.infinite) return
  392. const len = props.urlList.length
  393. index.value = (index.value + 1) % len
  394. }
  395. function handleActions(action, options = {}) {
  396. if (loading.value) return
  397. const { zoomRate, rotateDeg, enableTransition } = {
  398. zoomRate: 0.2,
  399. rotateDeg: 90,
  400. enableTransition: true,
  401. ...options
  402. }
  403. switch (action) {
  404. case 'zoomOut':
  405. if (transform.value.scale > 0.2) {
  406. transform.value.scale = parseFloat(
  407. (transform.value.scale - zoomRate).toFixed(3)
  408. )
  409. }
  410. break
  411. case 'zoomIn':
  412. transform.value.scale = parseFloat(
  413. (transform.value.scale + zoomRate).toFixed(3)
  414. )
  415. break
  416. case 'clocelise':
  417. transform.value.deg += rotateDeg
  418. break
  419. case 'anticlocelise':
  420. transform.value.deg -= rotateDeg
  421. break
  422. }
  423. transform.value.enableTransition = enableTransition
  424. }
  425. function handleDownload () {
  426. const url = props.urlList[index.value]
  427. const isOnlineImage = url.startsWith('http://') || url.startsWith('https://') // 判断是否为在线地址
  428. const date = new Date()
  429. const filename = props.fileName || `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`
  430. isOnlineImage ? downloadImgVideo(url, filename) : downloadBase64(url, filename)
  431. }
  432. watch(currentMedia, () => {
  433. nextTick(() => {
  434. const $media = media.value
  435. if (!$media.complete) {
  436. loading.value = true
  437. }
  438. })
  439. })
  440. watch(index, (val) => {
  441. reset()
  442. emit(SWITCH_EVENT, val)
  443. })
  444. onMounted(() => {
  445. deviceSupportInstall()
  446. wrapper.value?.focus?.()
  447. })
  448. return {
  449. index,
  450. wrapper,
  451. media,
  452. isSingle,
  453. isFirst,
  454. isLast,
  455. currentMedia,
  456. isImage,
  457. isVideo,
  458. mediaStyle,
  459. mode,
  460. handleActions,
  461. prev,
  462. next,
  463. hide,
  464. toggleMode,
  465. handleMediaLoad,
  466. handleMediaError,
  467. handleDownload,
  468. handleMouseDown
  469. }
  470. }
  471. }
  472. </script>
  473. <style lang="scss" scoped>
  474. // .el-icon {
  475. // z-index: 200;
  476. // }
  477. // .el-image-viewer__btn {
  478. // overflow: hidden;
  479. // border-radius: 100px;
  480. // opacity: 1;
  481. // text-align: center;
  482. // line-height: 44px;
  483. // background-color: rgba($color: #0d0d0d, $alpha: 0.5);
  484. // }
  485. </style>