无木
2021-07-26 e23bd2696da945291a9b652f1af39ad1936f376b
提交 | 用户 | age
305630 1 <script lang="tsx">
V 2   import { defineComponent, ref, unref, computed, reactive, watchEffect } from 'vue';
3   import { Props } from './typing';
4   import { CloseOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
5   import resumeSvg from '/@/assets/svg/preview/resume.svg';
6   import rotateSvg from '/@/assets/svg/preview/p-rotate.svg';
7   import scaleSvg from '/@/assets/svg/preview/scale.svg';
8   import unScaleSvg from '/@/assets/svg/preview/unscale.svg';
9   import unRotateSvg from '/@/assets/svg/preview/unrotate.svg';
10
11   enum StatueEnum {
12     LOADING,
13     DONE,
14     FAIL,
15   }
16   interface ImgState {
17     currentUrl: string;
18     imgScale: number;
19     imgRotate: number;
20     imgTop: number;
21     imgLeft: number;
22     currentIndex: number;
23     status: StatueEnum;
24     moveX: number;
25     moveY: number;
26     show: boolean;
27   }
28   const props = {
29     show: {
30       type: Boolean as PropType<boolean>,
31       default: false,
32     },
33     imageList: {
34       type: [Array] as PropType<string[]>,
35       default: null,
36     },
37     index: {
38       type: Number as PropType<number>,
39       default: 0,
40     },
e23bd2 41     scaleStep: {
42       type: Number as PropType<number>,
43     },
44     defaultWidth: {
45       type: Number as PropType<number>,
46     },
47     maskClosable: {
48       type: Boolean as PropType<boolean>,
49     },
50     rememberState: {
51       type: Boolean as PropType<boolean>,
52     },
305630 53   };
V 54
55   const prefixCls = 'img-preview';
56   export default defineComponent({
57     name: 'ImagePreview',
58     props,
e23bd2 59     emits: ['img-load', 'img-error'],
60     setup(props: Props, { expose, emit }) {
61       interface stateInfo {
62         scale: number;
63         rotate: number;
64         top: number;
65         left: number;
66       }
67       const stateMap = new Map<string, stateInfo>();
305630 68       const imgState = reactive<ImgState>({
V 69         currentUrl: '',
70         imgScale: 1,
71         imgRotate: 0,
72         imgTop: 0,
73         imgLeft: 0,
74         status: StatueEnum.LOADING,
75         currentIndex: 0,
76         moveX: 0,
77         moveY: 0,
78         show: props.show,
79       });
80
81       const wrapElRef = ref<HTMLDivElement | null>(null);
82       const imgElRef = ref<HTMLImageElement | null>(null);
83
84       // 初始化
85       function init() {
86         initMouseWheel();
87         const { index, imageList } = props;
88
89         if (!imageList || !imageList.length) {
90           throw new Error('imageList is undefined');
91         }
92         imgState.currentIndex = index;
93         handleIChangeImage(imageList[index]);
94       }
95
96       // 重置
97       function initState() {
98         imgState.imgScale = 1;
99         imgState.imgRotate = 0;
100         imgState.imgTop = 0;
101         imgState.imgLeft = 0;
102       }
103
104       // 初始化鼠标滚轮事件
105       function initMouseWheel() {
106         const wrapEl = unref(wrapElRef);
107         if (!wrapEl) {
108           return;
109         }
110         (wrapEl as any).onmousewheel = scrollFunc;
111         // 火狐浏览器没有onmousewheel事件,用DOMMouseScroll代替
112         document.body.addEventListener('DOMMouseScroll', scrollFunc);
113         // 禁止火狐浏览器下拖拽图片的默认事件
114         document.ondragstart = function () {
115           return false;
116         };
117       }
118
e23bd2 119       const getScaleStep = computed(() => {
120         if (props.scaleStep > 0 && props.scaleStep < 100) {
121           return props.scaleStep / 100;
122         } else {
123           return imgState.imgScale / 10;
124         }
125       });
126
305630 127       // 监听鼠标滚轮
V 128       function scrollFunc(e: any) {
129         e = e || window.event;
130         e.delta = e.wheelDelta || -e.detail;
131
132         e.preventDefault();
133         if (e.delta > 0) {
134           // 滑轮向上滚动
e23bd2 135           scaleFunc(getScaleStep.value);
305630 136         }
V 137         if (e.delta < 0) {
138           // 滑轮向下滚动
e23bd2 139           scaleFunc(-getScaleStep.value);
305630 140         }
V 141       }
142       // 缩放函数
143       function scaleFunc(num: number) {
144         if (imgState.imgScale <= 0.2 && num < 0) return;
145         imgState.imgScale += num;
146       }
147
148       // 旋转图片
149       function rotateFunc(deg: number) {
150         imgState.imgRotate += deg;
151       }
152
153       // 鼠标事件
154       function handleMouseUp() {
155         const imgEl = unref(imgElRef);
156         if (!imgEl) return;
157         imgEl.onmousemove = null;
158       }
159
160       // 更换图片
161       function handleIChangeImage(url: string) {
162         imgState.status = StatueEnum.LOADING;
163         const img = new Image();
164         img.src = url;
e23bd2 165         img.onload = (e: Event) => {
166           if (imgState.currentUrl !== url) {
167             const ele: HTMLElement[] = e.composedPath();
168             if (props.rememberState) {
169               // 保存当前图片的缩放信息
170               stateMap.set(imgState.currentUrl, {
171                 scale: imgState.imgScale,
172                 top: imgState.imgTop,
173                 left: imgState.imgLeft,
174                 rotate: imgState.imgRotate,
175               });
176               // 如果之前已存储缩放信息,就应用
177               const stateInfo = stateMap.get(url);
178               if (stateInfo) {
179                 imgState.imgScale = stateInfo.scale;
180                 imgState.imgTop = stateInfo.top;
181                 imgState.imgRotate = stateInfo.rotate;
182                 imgState.imgLeft = stateInfo.left;
183               } else {
184                 initState();
185                 if (props.defaultWidth) {
186                   imgState.imgScale = props.defaultWidth / ele[0].naturalWidth;
187                 }
188               }
189             } else {
190               if (props.defaultWidth) {
191                 imgState.imgScale = props.defaultWidth / ele[0].naturalWidth;
192               }
193             }
194
195             ele &&
196               emit('img-load', {
197                 index: imgState.currentIndex,
198                 dom: ele[0] as HTMLImageElement,
199                 url,
200               });
201           }
305630 202           imgState.currentUrl = url;
V 203           imgState.status = StatueEnum.DONE;
204         };
e23bd2 205         img.onerror = (e: Event) => {
206           const ele: EventTarget[] = e.composedPath();
207           ele &&
208             emit('img-error', {
209               index: imgState.currentIndex,
210               dom: ele[0] as HTMLImageElement,
211               url,
212             });
305630 213           imgState.status = StatueEnum.FAIL;
V 214         };
215       }
216
217       // 关闭
218       function handleClose(e: MouseEvent) {
219         e && e.stopPropagation();
e23bd2 220         close();
221       }
222
223       function close() {
305630 224         imgState.show = false;
V 225         // 移除火狐浏览器下的鼠标滚动事件
226         document.body.removeEventListener('DOMMouseScroll', scrollFunc);
227         // 恢复火狐及Safari浏览器下的图片拖拽
228         document.ondragstart = null;
229       }
230
231       // 图片复原
232       function resume() {
233         initState();
234       }
e23bd2 235
236       expose({
237         resume,
238         close,
239         prev: handleChange.bind(null, 'left'),
240         next: handleChange.bind(null, 'right'),
241         setScale: (scale: number) => {
242           if (scale > 0 && scale <= 10) imgState.imgScale = scale;
243         },
244         setRotate: (rotate: number) => {
245           imgState.imgRotate = rotate;
246         },
247       } as PreviewActions);
305630 248
V 249       // 上一页下一页
250       function handleChange(direction: 'left' | 'right') {
251         const { currentIndex } = imgState;
252         const { imageList } = props;
253         if (direction === 'left') {
254           imgState.currentIndex--;
255           if (currentIndex <= 0) {
256             imgState.currentIndex = imageList.length - 1;
257           }
258         }
259         if (direction === 'right') {
260           imgState.currentIndex++;
261           if (currentIndex >= imageList.length - 1) {
262             imgState.currentIndex = 0;
263           }
264         }
265         handleIChangeImage(imageList[imgState.currentIndex]);
266       }
267
268       function handleAddMoveListener(e: MouseEvent) {
269         e = e || window.event;
270         imgState.moveX = e.clientX;
271         imgState.moveY = e.clientY;
272         const imgEl = unref(imgElRef);
273         if (imgEl) {
274           imgEl.onmousemove = moveFunc;
275         }
276       }
277
278       function moveFunc(e: MouseEvent) {
279         e = e || window.event;
280         e.preventDefault();
281         const movementX = e.clientX - imgState.moveX;
282         const movementY = e.clientY - imgState.moveY;
283         imgState.imgLeft += movementX;
284         imgState.imgTop += movementY;
285         imgState.moveX = e.clientX;
286         imgState.moveY = e.clientY;
287       }
288
289       // 获取图片样式
290       const getImageStyle = computed(() => {
291         const { imgScale, imgRotate, imgTop, imgLeft } = imgState;
292         return {
293           transform: `scale(${imgScale}) rotate(${imgRotate}deg)`,
294           marginTop: `${imgTop}px`,
295           marginLeft: `${imgLeft}px`,
e23bd2 296           maxWidth: props.defaultWidth ? 'unset' : '100%',
305630 297         };
V 298       });
299
300       const getIsMultipleImage = computed(() => {
301         const { imageList } = props;
302         return imageList.length > 1;
303       });
304
305       watchEffect(() => {
306         if (props.show) {
307           init();
308         }
309         if (props.imageList) {
310           initState();
311         }
312       });
e23bd2 313
314       const handleMaskClick = (e: MouseEvent) => {
315         if (
316           props.maskClosable &&
317           e.target &&
318           (e.target as HTMLDivElement).classList.contains(`${prefixCls}-content`)
319         ) {
320           handleClose(e);
321         }
322       };
305630 323
V 324       const renderClose = () => {
325         return (
326           <div class={`${prefixCls}__close`} onClick={handleClose}>
327             <CloseOutlined class={`${prefixCls}__close-icon`} />
328           </div>
329         );
330       };
331
332       const renderIndex = () => {
333         if (!unref(getIsMultipleImage)) {
334           return null;
335         }
336         const { currentIndex } = imgState;
337         const { imageList } = props;
338         return (
339           <div class={`${prefixCls}__index`}>
340             {currentIndex + 1} / {imageList.length}
341           </div>
342         );
343       };
344
345       const renderController = () => {
346         return (
347           <div class={`${prefixCls}__controller`}>
e23bd2 348             <div
349               class={`${prefixCls}__controller-item`}
350               onClick={() => scaleFunc(-getScaleStep.value)}
351             >
305630 352               <img src={unScaleSvg} />
V 353             </div>
e23bd2 354             <div
355               class={`${prefixCls}__controller-item`}
356               onClick={() => scaleFunc(getScaleStep.value)}
357             >
305630 358               <img src={scaleSvg} />
V 359             </div>
360             <div class={`${prefixCls}__controller-item`} onClick={resume}>
361               <img src={resumeSvg} />
362             </div>
363             <div class={`${prefixCls}__controller-item`} onClick={() => rotateFunc(-90)}>
364               <img src={unRotateSvg} />
365             </div>
366             <div class={`${prefixCls}__controller-item`} onClick={() => rotateFunc(90)}>
367               <img src={rotateSvg} />
368             </div>
369           </div>
370         );
371       };
372
373       const renderArrow = (direction: 'left' | 'right') => {
374         if (!unref(getIsMultipleImage)) {
375           return null;
376         }
377         return (
378           <div class={[`${prefixCls}__arrow`, direction]} onClick={() => handleChange(direction)}>
379             {direction === 'left' ? <LeftOutlined /> : <RightOutlined />}
380           </div>
381         );
382       };
383
384       return () => {
385         return (
386           imgState.show && (
e23bd2 387             <div
388               class={prefixCls}
389               ref={wrapElRef}
390               onMouseup={handleMouseUp}
391               onClick={handleMaskClick}
392             >
305630 393               <div class={`${prefixCls}-content`}>
V 394                 {/*<Spin*/}
395                 {/*  indicator={<LoadingOutlined style="font-size: 24px" spin />}*/}
396                 {/*  spinning={true}*/}
397                 {/*  class={[*/}
398                 {/*    `${prefixCls}-image`,*/}
399                 {/*    {*/}
400                 {/*      hidden: imgState.status !== StatueEnum.LOADING,*/}
401                 {/*    },*/}
402                 {/*  ]}*/}
403                 {/*/>*/}
404                 <img
405                   style={unref(getImageStyle)}
406                   class={[
407                     `${prefixCls}-image`,
408                     imgState.status === StatueEnum.DONE ? '' : 'hidden',
409                   ]}
410                   ref={imgElRef}
411                   src={imgState.currentUrl}
412                   onMousedown={handleAddMoveListener}
413                 />
414                 {renderClose()}
415                 {renderIndex()}
416                 {renderController()}
417                 {renderArrow('left')}
418                 {renderArrow('right')}
419               </div>
420             </div>
421           )
422         );
423       };
424     },
425   });
426 </script>
427 <style lang="less">
428   .img-preview {
429     position: fixed;
430     top: 0;
431     right: 0;
432     bottom: 0;
433     left: 0;
434     z-index: @preview-comp-z-index;
435     background: rgba(0, 0, 0, 0.5);
436     user-select: none;
437
438     &-content {
439       display: flex;
440       width: 100%;
441       height: 100%;
442       color: @white;
443       justify-content: center;
444       align-items: center;
445     }
446
447     &-image {
448       cursor: pointer;
449       transition: transform 0.3s;
450     }
451
452     &__close {
453       position: absolute;
454       top: -40px;
455       right: -40px;
456       width: 80px;
457       height: 80px;
458       overflow: hidden;
459       color: @white;
460       cursor: pointer;
461       background-color: rgba(0, 0, 0, 0.5);
462       border-radius: 50%;
463       transition: all 0.2s;
464
465       &-icon {
466         position: absolute;
467         top: 46px;
468         left: 16px;
469         font-size: 16px;
470       }
471
472       &:hover {
473         background-color: rgba(0, 0, 0, 0.8);
474       }
475     }
476
477     &__index {
478       position: absolute;
479       bottom: 5%;
480       left: 50%;
481       padding: 0 22px;
482       font-size: 16px;
483       background: rgba(109, 109, 109, 0.6);
484       border-radius: 15px;
485       transform: translateX(-50%);
486     }
487
488     &__controller {
489       position: absolute;
490       bottom: 10%;
491       left: 50%;
492       display: flex;
493       width: 260px;
494       height: 44px;
495       padding: 0 22px;
496       margin-left: -139px;
497       background: rgba(109, 109, 109, 0.6);
498       border-radius: 22px;
499       justify-content: center;
500
501       &-item {
502         display: flex;
503         height: 100%;
504         padding: 0 9px;
505         font-size: 24px;
506         cursor: pointer;
507         transition: all 0.2s;
508
509         &:hover {
510           transform: scale(1.2);
511         }
512
513         img {
514           width: 1em;
515         }
516       }
517     }
518
519     &__arrow {
520       position: absolute;
521       top: 50%;
522       display: flex;
523       align-items: center;
524       justify-content: center;
525       width: 50px;
526       height: 50px;
527       font-size: 28px;
528       cursor: pointer;
529       background-color: rgba(0, 0, 0, 0.5);
530       border-radius: 50%;
531       transition: all 0.2s;
532
533       &:hover {
534         background-color: rgba(0, 0, 0, 0.8);
535       }
536
537       &.left {
538         left: 50px;
539       }
540
541       &.right {
542         right: 50px;
543       }
544     }
545   }
546 </style>