Browse Source

feat(preview): add more features

为Preview组件添加新的属性及事件
无木 3 years ago
parent
commit
e23bd2696d

+ 4 - 0
CHANGELOG.zh_CN.md

@@ -1,3 +1,7 @@
+### ✨ Features
+
+- **Preview** 添加新的属性及事件
+
 ### 🐛 Bug Fixes
 
 - **ApiTreeSelect** 修复未能正确监听`params`变化的问题

+ 118 - 8
src/components/Preview/src/Functional.vue

@@ -38,13 +38,33 @@
       type: Number as PropType<number>,
       default: 0,
     },
+    scaleStep: {
+      type: Number as PropType<number>,
+    },
+    defaultWidth: {
+      type: Number as PropType<number>,
+    },
+    maskClosable: {
+      type: Boolean as PropType<boolean>,
+    },
+    rememberState: {
+      type: Boolean as PropType<boolean>,
+    },
   };
 
   const prefixCls = 'img-preview';
   export default defineComponent({
     name: 'ImagePreview',
     props,
-    setup(props: Props) {
+    emits: ['img-load', 'img-error'],
+    setup(props: Props, { expose, emit }) {
+      interface stateInfo {
+        scale: number;
+        rotate: number;
+        top: number;
+        left: number;
+      }
+      const stateMap = new Map<string, stateInfo>();
       const imgState = reactive<ImgState>({
         currentUrl: '',
         imgScale: 1,
@@ -96,6 +116,14 @@
         };
       }
 
+      const getScaleStep = computed(() => {
+        if (props.scaleStep > 0 && props.scaleStep < 100) {
+          return props.scaleStep / 100;
+        } else {
+          return imgState.imgScale / 10;
+        }
+      });
+
       // 监听鼠标滚轮
       function scrollFunc(e: any) {
         e = e || window.event;
@@ -104,11 +132,11 @@
         e.preventDefault();
         if (e.delta > 0) {
           // 滑轮向上滚动
-          scaleFunc(0.015);
+          scaleFunc(getScaleStep.value);
         }
         if (e.delta < 0) {
           // 滑轮向下滚动
-          scaleFunc(-0.015);
+          scaleFunc(-getScaleStep.value);
         }
       }
       // 缩放函数
@@ -134,11 +162,54 @@
         imgState.status = StatueEnum.LOADING;
         const img = new Image();
         img.src = url;
-        img.onload = () => {
+        img.onload = (e: Event) => {
+          if (imgState.currentUrl !== url) {
+            const ele: HTMLElement[] = e.composedPath();
+            if (props.rememberState) {
+              // 保存当前图片的缩放信息
+              stateMap.set(imgState.currentUrl, {
+                scale: imgState.imgScale,
+                top: imgState.imgTop,
+                left: imgState.imgLeft,
+                rotate: imgState.imgRotate,
+              });
+              // 如果之前已存储缩放信息,就应用
+              const stateInfo = stateMap.get(url);
+              if (stateInfo) {
+                imgState.imgScale = stateInfo.scale;
+                imgState.imgTop = stateInfo.top;
+                imgState.imgRotate = stateInfo.rotate;
+                imgState.imgLeft = stateInfo.left;
+              } else {
+                initState();
+                if (props.defaultWidth) {
+                  imgState.imgScale = props.defaultWidth / ele[0].naturalWidth;
+                }
+              }
+            } else {
+              if (props.defaultWidth) {
+                imgState.imgScale = props.defaultWidth / ele[0].naturalWidth;
+              }
+            }
+
+            ele &&
+              emit('img-load', {
+                index: imgState.currentIndex,
+                dom: ele[0] as HTMLImageElement,
+                url,
+              });
+          }
           imgState.currentUrl = url;
           imgState.status = StatueEnum.DONE;
         };
-        img.onerror = () => {
+        img.onerror = (e: Event) => {
+          const ele: EventTarget[] = e.composedPath();
+          ele &&
+            emit('img-error', {
+              index: imgState.currentIndex,
+              dom: ele[0] as HTMLImageElement,
+              url,
+            });
           imgState.status = StatueEnum.FAIL;
         };
       }
@@ -146,6 +217,10 @@
       // 关闭
       function handleClose(e: MouseEvent) {
         e && e.stopPropagation();
+        close();
+      }
+
+      function close() {
         imgState.show = false;
         // 移除火狐浏览器下的鼠标滚动事件
         document.body.removeEventListener('DOMMouseScroll', scrollFunc);
@@ -158,6 +233,19 @@
         initState();
       }
 
+      expose({
+        resume,
+        close,
+        prev: handleChange.bind(null, 'left'),
+        next: handleChange.bind(null, 'right'),
+        setScale: (scale: number) => {
+          if (scale > 0 && scale <= 10) imgState.imgScale = scale;
+        },
+        setRotate: (rotate: number) => {
+          imgState.imgRotate = rotate;
+        },
+      } as PreviewActions);
+
       // 上一页下一页
       function handleChange(direction: 'left' | 'right') {
         const { currentIndex } = imgState;
@@ -205,6 +293,7 @@
           transform: `scale(${imgScale}) rotate(${imgRotate}deg)`,
           marginTop: `${imgTop}px`,
           marginLeft: `${imgLeft}px`,
+          maxWidth: props.defaultWidth ? 'unset' : '100%',
         };
       });
 
@@ -222,6 +311,16 @@
         }
       });
 
+      const handleMaskClick = (e: MouseEvent) => {
+        if (
+          props.maskClosable &&
+          e.target &&
+          (e.target as HTMLDivElement).classList.contains(`${prefixCls}-content`)
+        ) {
+          handleClose(e);
+        }
+      };
+
       const renderClose = () => {
         return (
           <div class={`${prefixCls}__close`} onClick={handleClose}>
@@ -246,10 +345,16 @@
       const renderController = () => {
         return (
           <div class={`${prefixCls}__controller`}>
-            <div class={`${prefixCls}__controller-item`} onClick={() => scaleFunc(-0.15)}>
+            <div
+              class={`${prefixCls}__controller-item`}
+              onClick={() => scaleFunc(-getScaleStep.value)}
+            >
               <img src={unScaleSvg} />
             </div>
-            <div class={`${prefixCls}__controller-item`} onClick={() => scaleFunc(0.15)}>
+            <div
+              class={`${prefixCls}__controller-item`}
+              onClick={() => scaleFunc(getScaleStep.value)}
+            >
               <img src={scaleSvg} />
             </div>
             <div class={`${prefixCls}__controller-item`} onClick={resume}>
@@ -279,7 +384,12 @@
       return () => {
         return (
           imgState.show && (
-            <div class={prefixCls} ref={wrapElRef} onMouseup={handleMouseUp}>
+            <div
+              class={prefixCls}
+              ref={wrapElRef}
+              onMouseup={handleMouseUp}
+              onClick={handleMaskClick}
+            >
               <div class={`${prefixCls}-content`}>
                 {/*<Spin*/}
                 {/*  indicator={<LoadingOutlined style="font-size: 24px" spin />}*/}

+ 2 - 5
src/components/Preview/src/functional.ts

@@ -6,15 +6,12 @@ import { createVNode, render } from 'vue';
 let instance: ReturnType<typeof createVNode> | null = null;
 export function createImgPreview(options: Options) {
   if (!isClient) return;
-  const { imageList, show = true, index = 0 } = options;
-
   const propsData: Partial<Props> = {};
   const container = document.createElement('div');
-  propsData.imageList = imageList;
-  propsData.show = show;
-  propsData.index = index;
+  Object.assign(propsData, { show: true, index: 0, scaleStep: 100 }, options);
 
   instance = createVNode(ImgPreview, propsData);
   render(instance, container);
   document.body.appendChild(container);
+  return instance.component?.exposed;
 }

+ 19 - 0
src/components/Preview/src/typing.ts

@@ -2,6 +2,12 @@ export interface Options {
   show?: boolean;
   imageList: string[];
   index?: number;
+  scaleStep?: number;
+  defaultWidth?: number;
+  maskClosable?: boolean;
+  rememberState?: boolean;
+  onImgLoad?: (img: HTMLImageElement) => void;
+  onImgError?: (img: HTMLImageElement) => void;
 }
 
 export interface Props {
@@ -9,6 +15,19 @@ export interface Props {
   instance: Props;
   imageList: string[];
   index: number;
+  scaleStep: number;
+  defaultWidth: number;
+  maskClosable: boolean;
+  rememberState: boolean;
+}
+
+export interface PreviewActions {
+  resume: () => void;
+  close: () => void;
+  prev: () => void;
+  next: () => void;
+  setScale: (scale: number) => void;
+  setRotate: (rotate: number) => void;
 }
 
 export interface ImageProps {

+ 6 - 1
src/views/demo/feat/img-preview/index.vue

@@ -8,6 +8,7 @@
   import { defineComponent } from 'vue';
   import { createImgPreview, ImagePreview } from '/@/components/Preview/index';
   import { PageWrapper } from '/@/components/Page';
+  // import { PreviewActions } from '/@/components/Preview/src/typing';
 
   const imgList: string[] = [
     'https://picsum.photos/id/66/346/216',
@@ -18,7 +19,11 @@
     components: { PageWrapper, ImagePreview },
     setup() {
       function openImg() {
-        createImgPreview({ imageList: imgList });
+        const onImgLoad = ({ index, url, dom }) => {
+          console.log(`第${index + 1}张图片已加载,URL为:${url}`, dom);
+        };
+        // 可以使用createImgPreview返回的 PreviewActions 来控制预览逻辑,实现类似幻灯片、自动旋转之类的骚操作
+        createImgPreview({ imageList: imgList, defaultWidth: 700, rememberState: true, onImgLoad });
       }
       return { imgList, openImg };
     },