| <template> | 
|   <transition name="viewer-fade"> | 
|     <div tabindex="-1" ref="el-image-viewer__wrapper" class="el-image-viewer__wrapper" :style="{ 'z-index': viewerZIndex }"> | 
|       <div class="el-image-viewer__mask" @click.self="handleMaskClick"></div> | 
|       <!-- CLOSE --> | 
|       <span class="el-image-viewer__btn el-image-viewer__close" @click="hide"> | 
|         <i class="el-icon-close"></i> | 
|       </span> | 
|       <!-- ARROW --> | 
|       <template v-if="!isSingle"> | 
|         <span | 
|           class="el-image-viewer__btn el-image-viewer__prev" | 
|           :class="{ 'is-disabled': !infinite && isFirst }" | 
|           @click="prev"> | 
|           <i class="el-icon-arrow-left"/> | 
|         </span> | 
|         <span | 
|           class="el-image-viewer__btn el-image-viewer__next" | 
|           :class="{ 'is-disabled': !infinite && isLast }" | 
|           @click="next"> | 
|           <i class="el-icon-arrow-right"/> | 
|         </span> | 
|       </template> | 
|       <!-- ACTIONS --> | 
|       <div class="el-image-viewer__btn el-image-viewer__actions"> | 
|         <div class="el-image-viewer__actions__inner"> | 
|           <i class="el-icon-zoom-out" @click="handleActions('zoomOut')"></i> | 
|           <i class="el-icon-zoom-in" @click="handleActions('zoomIn')"></i> | 
|           <i class="el-image-viewer__actions__divider"></i> | 
|           <i :class="mode.icon" @click="toggleMode"></i> | 
|           <i class="el-image-viewer__actions__divider"></i> | 
|           <i class="el-icon-refresh-left" @click="handleActions('anticlocelise')"></i> | 
|           <i class="el-icon-refresh-right" @click="handleActions('clocelise')"></i> | 
|         </div> | 
|       </div> | 
|       <!-- CANVAS --> | 
|       <div class="el-image-viewer__canvas"> | 
|         <img | 
|           v-for="(url, i) in urlList" | 
|           v-if="i === index" | 
|           ref="img" | 
|           class="el-image-viewer__img" | 
|           :key="url" | 
|           :src="currentImg" | 
|           :style="imgStyle" | 
|           @load="handleImgLoad" | 
|           @error="handleImgError" | 
|           @mousedown="handleMouseDown"> | 
|       </div> | 
|     </div> | 
|   </transition> | 
| </template> | 
|   | 
| <script> | 
| import { on, off } from 'element-ui/src/utils/dom'; | 
| import { rafThrottle, isFirefox } from 'element-ui/src/utils/util'; | 
| import { PopupManager } from 'element-ui/src/utils/popup'; | 
|   | 
| const Mode = { | 
|   CONTAIN: { | 
|     name: 'contain', | 
|     icon: 'el-icon-full-screen' | 
|   }, | 
|   ORIGINAL: { | 
|     name: 'original', | 
|     icon: 'el-icon-c-scale-to-original' | 
|   } | 
| }; | 
|   | 
| const mousewheelEventName = isFirefox() ? 'DOMMouseScroll' : 'mousewheel'; | 
|   | 
| export default { | 
|   name: 'elImageViewer', | 
|   | 
|   props: { | 
|     urlList: { | 
|       type: Array, | 
|       default: () => [] | 
|     }, | 
|     zIndex: { | 
|       type: Number, | 
|       default: 2000 | 
|     }, | 
|     onSwitch: { | 
|       type: Function, | 
|       default: () => {} | 
|     }, | 
|     onClose: { | 
|       type: Function, | 
|       default: () => {} | 
|     }, | 
|     initialIndex: { | 
|       type: Number, | 
|       default: 0 | 
|     }, | 
|     appendToBody: { | 
|       type: Boolean, | 
|       default: true | 
|     }, | 
|     maskClosable: { | 
|       type: Boolean, | 
|       default: true | 
|     } | 
|   }, | 
|   | 
|   data() { | 
|     return { | 
|       index: this.initialIndex, | 
|       isShow: false, | 
|       infinite: true, | 
|       loading: false, | 
|       mode: Mode.CONTAIN, | 
|       transform: { | 
|         scale: 1, | 
|         deg: 0, | 
|         offsetX: 0, | 
|         offsetY: 0, | 
|         enableTransition: false | 
|       } | 
|     }; | 
|   }, | 
|   computed: { | 
|     isSingle() { | 
|       return this.urlList.length <= 1; | 
|     }, | 
|     isFirst() { | 
|       return this.index === 0; | 
|     }, | 
|     isLast() { | 
|       return this.index === this.urlList.length - 1; | 
|     }, | 
|     currentImg() { | 
|       return this.urlList[this.index]; | 
|     }, | 
|     imgStyle() { | 
|       const { scale, deg, offsetX, offsetY, enableTransition } = this.transform; | 
|       const style = { | 
|         transform: `scale(${scale}) rotate(${deg}deg)`, | 
|         transition: enableTransition ? 'transform .3s' : '', | 
|         'margin-left': `${offsetX}px`, | 
|         'margin-top': `${offsetY}px` | 
|       }; | 
|       if (this.mode === Mode.CONTAIN) { | 
|         style.maxWidth = style.maxHeight = '100%'; | 
|       } | 
|       return style; | 
|     }, | 
|     viewerZIndex() { | 
|       const nextZIndex = PopupManager.nextZIndex(); | 
|       return this.zIndex > nextZIndex ? this.zIndex : nextZIndex; | 
|     } | 
|   }, | 
|   watch: { | 
|     index: { | 
|       handler: function(val) { | 
|         this.reset(); | 
|         this.onSwitch(val); | 
|       } | 
|     }, | 
|     currentImg(val) { | 
|       this.$nextTick(_ => { | 
|         const $img = this.$refs.img[0]; | 
|         if (!$img.complete) { | 
|           this.loading = true; | 
|         } | 
|       }); | 
|     } | 
|   }, | 
|   methods: { | 
|     hide() { | 
|       this.deviceSupportUninstall(); | 
|       this.onClose(); | 
|     }, | 
|     deviceSupportInstall() { | 
|       this._keyDownHandler = e => { | 
|         e.stopPropagation(); | 
|         const keyCode = e.keyCode; | 
|         switch (keyCode) { | 
|           // ESC | 
|           case 27: | 
|             this.hide(); | 
|             break; | 
|           // SPACE | 
|           case 32: | 
|             this.toggleMode(); | 
|             break; | 
|           // LEFT_ARROW | 
|           case 37: | 
|             this.prev(); | 
|             break; | 
|           // UP_ARROW | 
|           case 38: | 
|             this.handleActions('zoomIn'); | 
|             break; | 
|           // RIGHT_ARROW | 
|           case 39: | 
|             this.next(); | 
|             break; | 
|           // DOWN_ARROW | 
|           case 40: | 
|             this.handleActions('zoomOut'); | 
|             break; | 
|         } | 
|       }; | 
|       this._mouseWheelHandler = rafThrottle(e => { | 
|         const delta = e.wheelDelta ? e.wheelDelta : -e.detail; | 
|         if (delta > 0) { | 
|           this.handleActions('zoomIn', { | 
|             zoomRate: 0.015, | 
|             enableTransition: false | 
|           }); | 
|         } else { | 
|           this.handleActions('zoomOut', { | 
|             zoomRate: 0.015, | 
|             enableTransition: false | 
|           }); | 
|         } | 
|       }); | 
|       on(document, 'keydown', this._keyDownHandler); | 
|       on(document, mousewheelEventName, this._mouseWheelHandler); | 
|     }, | 
|     deviceSupportUninstall() { | 
|       off(document, 'keydown', this._keyDownHandler); | 
|       off(document, mousewheelEventName, this._mouseWheelHandler); | 
|       this._keyDownHandler = null; | 
|       this._mouseWheelHandler = null; | 
|     }, | 
|     handleImgLoad(e) { | 
|       this.loading = false; | 
|     }, | 
|     handleImgError(e) { | 
|       this.loading = false; | 
|       e.target.alt = '加载失败'; | 
|     }, | 
|     handleMouseDown(e) { | 
|       if (this.loading || e.button !== 0) return; | 
|   | 
|       const { offsetX, offsetY } = this.transform; | 
|       const startX = e.pageX; | 
|       const startY = e.pageY; | 
|       this._dragHandler = rafThrottle(ev => { | 
|         this.transform.offsetX = offsetX + ev.pageX - startX; | 
|         this.transform.offsetY = offsetY + ev.pageY - startY; | 
|       }); | 
|       on(document, 'mousemove', this._dragHandler); | 
|       on(document, 'mouseup', ev => { | 
|         off(document, 'mousemove', this._dragHandler); | 
|       }); | 
|   | 
|       e.preventDefault(); | 
|     }, | 
|     handleMaskClick() { | 
|       if (this.maskClosable) { | 
|         this.hide(); | 
|       } | 
|     }, | 
|     reset() { | 
|       this.transform = { | 
|         scale: 1, | 
|         deg: 0, | 
|         offsetX: 0, | 
|         offsetY: 0, | 
|         enableTransition: false | 
|       }; | 
|     }, | 
|     toggleMode() { | 
|       if (this.loading) return; | 
|   | 
|       const modeNames = Object.keys(Mode); | 
|       const modeValues = Object.values(Mode); | 
|       const index = modeValues.indexOf(this.mode); | 
|       const nextIndex = (index + 1) % modeNames.length; | 
|       this.mode = Mode[modeNames[nextIndex]]; | 
|       this.reset(); | 
|     }, | 
|     prev() { | 
|       if (this.isFirst && !this.infinite) return; | 
|       const len = this.urlList.length; | 
|       this.index = (this.index - 1 + len) % len; | 
|     }, | 
|     next() { | 
|       if (this.isLast && !this.infinite) return; | 
|       const len = this.urlList.length; | 
|       this.index = (this.index + 1) % len; | 
|     }, | 
|     handleActions(action, options = {}) { | 
|       if (this.loading) return; | 
|       const { zoomRate, rotateDeg, enableTransition } = { | 
|         zoomRate: 0.2, | 
|         rotateDeg: 90, | 
|         enableTransition: true, | 
|         ...options | 
|       }; | 
|       const { transform } = this; | 
|       switch (action) { | 
|         case 'zoomOut': | 
|           if (transform.scale > 0.2) { | 
|             transform.scale = parseFloat((transform.scale - zoomRate).toFixed(3)); | 
|           } | 
|           break; | 
|         case 'zoomIn': | 
|           transform.scale = parseFloat((transform.scale + zoomRate).toFixed(3)); | 
|           break; | 
|         case 'clocelise': | 
|           transform.deg += rotateDeg; | 
|           break; | 
|         case 'anticlocelise': | 
|           transform.deg -= rotateDeg; | 
|           break; | 
|       } | 
|       transform.enableTransition = enableTransition; | 
|     } | 
|   }, | 
|   mounted() { | 
|     this.deviceSupportInstall(); | 
|     if (this.appendToBody) { | 
|       document.body.appendChild(this.$el); | 
|     } | 
|     // add tabindex then wrapper can be focusable via Javascript | 
|     // focus wrapper so arrow key can't cause inner scroll behavior underneath | 
|     this.$refs['el-image-viewer__wrapper'].focus(); | 
|   }, | 
|   destroyed() { | 
|     // if appendToBody is true, remove DOM node after destroy | 
|     if (this.appendToBody && this.$el && this.$el.parentNode) { | 
|       this.$el.parentNode.removeChild(this.$el); | 
|     } | 
|   } | 
| }; | 
| </script> |