Selaa lähdekoodia

perf: add createImgPreview func (#713)

jinmao88 3 vuotta sitten
vanhempi
commit
b7c7c46853

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
src/assets/svg/preview/p-rotate.svg


+ 1 - 0
src/assets/svg/preview/resume.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595307154239" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7317" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><defs><style type="text/css"></style></defs><path d="M316 672h60c4.4 0 8-3.6 8-8V360c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v304c0 4.4 3.6 8 8 8zM512 622c22.1 0 40-17.9 40-39 0-23.1-17.9-41-40-41s-40 17.9-40 41c0 21.1 17.9 39 40 39zM512 482c22.1 0 40-17.9 40-39 0-23.1-17.9-41-40-41s-40 17.9-40 41c0 21.1 17.9 39 40 39z" p-id="7318" fill="#ffffff"></path><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32z m-40 728H184V184h656v656z" p-id="7319" fill="#ffffff"></path><path d="M648 672h60c4.4 0 8-3.6 8-8V360c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v304c0 4.4 3.6 8 8 8z" p-id="7320" fill="#ffffff"></path></svg>

+ 1 - 0
src/assets/svg/preview/scale.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595307195033" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8116" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><defs><style type="text/css"></style></defs><path d="M887.081 904.791a25.8 25.8 0 0 1-18.376-7.619L705.618 734.075l-4.163 3.369c-58.255 47.18-131.522 73.16-206.32 73.16-181.07 0-328.377-147.308-328.377-328.367 0-181.068 147.308-328.376 328.377-328.376 181.063 0 328.376 147.308 328.376 328.376 0 77.072-27.412 152.07-77.169 211.17l-3.522 4.173 162.719 162.744a25.846 25.846 0 0 1 7.639 18.432 26.081 26.081 0 0 1-26.051 26.045l-0.046-0.01zM495.13 205.957c-152.336 0-276.27 123.935-276.27 276.27 0 152.33 123.934 276.27 276.27 276.27 152.34 0 276.275-123.94 276.275-276.27 0-152.335-123.935-276.27-276.275-276.27z" fill="#ffffff" p-id="8117"></path><path d="M626.545 508.355h-262.83a26.127 26.127 0 0 1 0-52.255h262.83a26.127 26.127 0 0 1 0 52.255z" fill="#ffffff" p-id="8118"></path><path d="M495.13 639.77a26.127 26.127 0 0 1-26.128-26.128v-262.83a26.127 26.127 0 0 1 52.255 0v262.835a26.127 26.127 0 0 1-26.127 26.123z" fill="#ffffff" p-id="8119"></path></svg>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
src/assets/svg/preview/unrotate.svg


+ 1 - 0
src/assets/svg/preview/unscale.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595308005241" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9878" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><defs><style type="text/css"></style></defs><path d="M750.3 198.7C598 46.4 351.1 46.4 198.7 198.7s-152.3 399.2 0 551.5C345.1 896.6 578.8 902.3 732 767.3l172.1 172.1 35.4-35.4-172.1-171.9c135-153.2 129.3-387-17.1-533.4z m39.3 403.8c-17.1 42.1-42.2 80-74.7 112.4-32.5 32.5-70.3 57.6-112.4 74.7-40.7 16.5-83.8 24.9-128 24.9s-87.2-8.4-128-24.9c-42.1-17.1-80-42.2-112.4-74.7s-57.6-70.3-74.7-112.4c-16.5-40.7-24.9-83.8-24.9-128s8.4-87.2 24.9-128c17.1-42.1 42.2-80 74.7-112.4s70.3-57.6 112.4-74.7c40.7-16.5 83.8-24.9 128-24.9s87.2 8.4 128 24.9c42.1 17.1 80 42.2 112.4 74.7 32.5 32.5 57.6 70.3 74.7 112.4 16.5 40.7 24.9 83.8 24.9 128s-8.4 87.3-24.9 128zM671 502H271v-50h400v50z" fill="#ffffff" p-id="9879"></path></svg>

+ 1 - 0
src/components/Preview/index.ts

@@ -1 +1,2 @@
 export { default as ImagePreview } from './src/Preview.vue';
+export { createImgPreview } from './src/functional';

+ 22 - 0
src/components/Preview/src/functional.ts

@@ -0,0 +1,22 @@
+import ImgPreview from './index';
+import { isClient } from '/@/utils/is';
+
+import type { Options, Props } from './types';
+
+import { createVNode, render } from 'vue';
+
+let instance: any = 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;
+
+  instance = createVNode(ImgPreview, propsData);
+  render(instance, container);
+  document.body.appendChild(container);
+}

+ 118 - 0
src/components/Preview/src/index.less

@@ -0,0 +1,118 @@
+.img-preview {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: @preview-comp-z-index;
+  background: rgba(0, 0, 0, 0.5);
+  user-select: none;
+
+  &-content {
+    display: flex;
+    width: 100%;
+    height: 100%;
+    color: @white;
+    justify-content: center;
+    align-items: center;
+  }
+
+  &-image {
+    cursor: pointer;
+    transition: transform 0.3s;
+  }
+
+  &__close {
+    position: absolute;
+    top: -40px;
+    right: -40px;
+    width: 80px;
+    height: 80px;
+    overflow: hidden;
+    color: @white;
+    cursor: pointer;
+    background-color: rgba(0, 0, 0, 0.5);
+    border-radius: 50%;
+    transition: all 0.2s;
+
+    &-icon {
+      position: absolute;
+      top: 46px;
+      left: 16px;
+      font-size: 16px;
+    }
+
+    &:hover {
+      background-color: rgba(0, 0, 0, 0.8);
+    }
+  }
+
+  &__index {
+    position: absolute;
+    bottom: 5%;
+    left: 50%;
+    padding: 0 22px;
+    font-size: 16px;
+    background: rgba(109, 109, 109, 0.6);
+    border-radius: 15px;
+    transform: translateX(-50%);
+  }
+
+  &__controller {
+    position: absolute;
+    bottom: 10%;
+    left: 50%;
+    display: flex;
+    width: 260px;
+    height: 44px;
+    padding: 0 22px;
+    margin-left: -139px;
+    background: rgba(109, 109, 109, 0.6);
+    border-radius: 22px;
+    justify-content: center;
+
+    &-item {
+      display: flex;
+      height: 100%;
+      padding: 0 9px;
+      font-size: 24px;
+      cursor: pointer;
+      transition: all 0.2s;
+
+      &:hover {
+        transform: scale(1.2);
+      }
+
+      img {
+        width: 1em;
+      }
+    }
+  }
+
+  &__arrow {
+    position: absolute;
+    top: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 50px;
+    height: 50px;
+    font-size: 28px;
+    cursor: pointer;
+    background-color: rgba(0, 0, 0, 0.5);
+    border-radius: 50%;
+    transition: all 0.2s;
+
+    &:hover {
+      background-color: rgba(0, 0, 0, 0.8);
+    }
+
+    &.left {
+      left: 50px;
+    }
+
+    &.right {
+      right: 50px;
+    }
+  }
+}

+ 305 - 0
src/components/Preview/src/index.tsx

@@ -0,0 +1,305 @@
+import './index.less';
+
+import { defineComponent, ref, unref, computed, reactive, watchEffect } from 'vue';
+
+// @ts-ignore
+import { basicProps } from './props';
+// @ts-ignore
+import { Props } from './types';
+
+import { CloseOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
+// import { Spin } from 'ant-design-vue';
+
+import resumeSvg from '/@/assets/svg/preview/resume.svg';
+import rotateSvg from '/@/assets/svg/preview/p-rotate.svg';
+import scaleSvg from '/@/assets/svg/preview/scale.svg';
+import unScaleSvg from '/@/assets/svg/preview/unscale.svg';
+import unRotateSvg from '/@/assets/svg/preview/unrotate.svg';
+enum StatueEnum {
+  LOADING,
+  DONE,
+  FAIL,
+}
+interface ImgState {
+  currentUrl: string;
+  imgScale: number;
+  imgRotate: number;
+  imgTop: number;
+  imgLeft: number;
+  currentIndex: number;
+  status: StatueEnum;
+  moveX: number;
+  moveY: number;
+  show: boolean;
+}
+
+const prefixCls = 'img-preview';
+export default defineComponent({
+  name: 'ImagePreview',
+  props: basicProps,
+  setup(props: Props) {
+    const imgState = reactive<ImgState>({
+      currentUrl: '',
+      imgScale: 1,
+      imgRotate: 0,
+      imgTop: 0,
+      imgLeft: 0,
+      status: StatueEnum.LOADING,
+      currentIndex: 0,
+      moveX: 0,
+      moveY: 0,
+      show: props.show,
+    });
+
+    const wrapElRef = ref<HTMLDivElement | null>(null);
+    const imgElRef = ref<HTMLImageElement | null>(null);
+
+    // 初始化
+    function init() {
+      initMouseWheel();
+      const { index, imageList } = props;
+
+      if (!imageList || !imageList.length) {
+        throw new Error('imageList is undefined');
+      }
+      imgState.currentIndex = index;
+      handleIChangeImage(imageList[index]);
+    }
+
+    // 重置
+    function initState() {
+      imgState.imgScale = 1;
+      imgState.imgRotate = 0;
+      imgState.imgTop = 0;
+      imgState.imgLeft = 0;
+    }
+
+    // 初始化鼠标滚轮事件
+    function initMouseWheel() {
+      const wrapEl = unref(wrapElRef);
+      if (!wrapEl) {
+        return;
+      }
+      (wrapEl as any).onmousewheel = scrollFunc;
+      // 火狐浏览器没有onmousewheel事件,用DOMMouseScroll代替
+      document.body.addEventListener('DOMMouseScroll', scrollFunc);
+      // 禁止火狐浏览器下拖拽图片的默认事件
+      document.ondragstart = function () {
+        return false;
+      };
+    }
+
+    // 监听鼠标滚轮
+    function scrollFunc(e: any) {
+      e = e || window.event;
+      e.delta = e.wheelDelta || -e.detail;
+
+      e.preventDefault();
+      if (e.delta > 0) {
+        // 滑轮向上滚动
+        scaleFunc(0.015);
+      }
+      if (e.delta < 0) {
+        // 滑轮向下滚动
+        scaleFunc(-0.015);
+      }
+    }
+    // 缩放函数
+    function scaleFunc(num: number) {
+      if (imgState.imgScale <= 0.2 && num < 0) return;
+      imgState.imgScale += num;
+    }
+
+    // 旋转图片
+    function rotateFunc(deg: number) {
+      imgState.imgRotate += deg;
+    }
+
+    // 鼠标事件
+    function handleMouseUp() {
+      const imgEl = unref(imgElRef);
+      if (!imgEl) return;
+      imgEl.onmousemove = null;
+    }
+
+    // 更换图片
+    function handleIChangeImage(url: string) {
+      imgState.status = StatueEnum.LOADING;
+      const img = new Image();
+      img.src = url;
+      img.onload = () => {
+        imgState.currentUrl = url;
+        imgState.status = StatueEnum.DONE;
+      };
+      img.onerror = () => {
+        imgState.status = StatueEnum.FAIL;
+      };
+    }
+
+    // 关闭
+    function handleClose(e: MouseEvent) {
+      e && e.stopPropagation();
+      imgState.show = false;
+      // 移除火狐浏览器下的鼠标滚动事件
+      document.body.removeEventListener('DOMMouseScroll', scrollFunc);
+      // 恢复火狐及Safari浏览器下的图片拖拽
+      document.ondragstart = null;
+    }
+
+    // 图片复原
+    function resume() {
+      initState();
+    }
+
+    // 上一页下一页
+    function handleChange(direction: 'left' | 'right') {
+      const { currentIndex } = imgState;
+      const { imageList } = props;
+      if (direction === 'left') {
+        imgState.currentIndex--;
+        if (currentIndex <= 0) {
+          imgState.currentIndex = imageList.length - 1;
+        }
+      }
+      if (direction === 'right') {
+        imgState.currentIndex++;
+        if (currentIndex >= imageList.length - 1) {
+          imgState.currentIndex = 0;
+        }
+      }
+      handleIChangeImage(imageList[imgState.currentIndex]);
+    }
+
+    function handleAddMoveListener(e: MouseEvent) {
+      e = e || window.event;
+      imgState.moveX = e.clientX;
+      imgState.moveY = e.clientY;
+      const imgEl = unref(imgElRef);
+      if (imgEl) {
+        imgEl.onmousemove = moveFunc;
+      }
+    }
+
+    function moveFunc(e: MouseEvent) {
+      e = e || window.event;
+      e.preventDefault();
+      const movementX = e.clientX - imgState.moveX;
+      const movementY = e.clientY - imgState.moveY;
+      imgState.imgLeft += movementX;
+      imgState.imgTop += movementY;
+      imgState.moveX = e.clientX;
+      imgState.moveY = e.clientY;
+    }
+
+    // 获取图片样式
+    const getImageStyle = computed(() => {
+      const { imgScale, imgRotate, imgTop, imgLeft } = imgState;
+      return {
+        transform: `scale(${imgScale}) rotate(${imgRotate}deg)`,
+        marginTop: `${imgTop}px`,
+        marginLeft: `${imgLeft}px`,
+      };
+    });
+
+    const getIsMultipleImage = computed(() => {
+      const { imageList } = props;
+      return imageList.length > 1;
+    });
+
+    watchEffect(() => {
+      if (props.show) {
+        init();
+      }
+      if (props.imageList) {
+        initState();
+      }
+    });
+
+    const renderClose = () => {
+      return (
+        <div class={`${prefixCls}__close`} onClick={handleClose}>
+          <CloseOutlined class={`${prefixCls}__close-icon`} />
+        </div>
+      );
+    };
+
+    const renderIndex = () => {
+      if (!unref(getIsMultipleImage)) {
+        return null;
+      }
+      const { currentIndex } = imgState;
+      const { imageList } = props;
+      return (
+        <div class={`${prefixCls}__index`}>
+          {currentIndex + 1} / {imageList.length}
+        </div>
+      );
+    };
+
+    const renderController = () => {
+      return (
+        <div class={`${prefixCls}__controller`}>
+          <div class={`${prefixCls}__controller-item`} onClick={() => scaleFunc(-0.15)}>
+            <img src={unScaleSvg} />
+          </div>
+          <div class={`${prefixCls}__controller-item`} onClick={() => scaleFunc(0.15)}>
+            <img src={scaleSvg} />
+          </div>
+          <div class={`${prefixCls}__controller-item`} onClick={resume}>
+            <img src={resumeSvg} />
+          </div>
+          <div class={`${prefixCls}__controller-item`} onClick={() => rotateFunc(-90)}>
+            <img src={unRotateSvg} />
+          </div>
+          <div class={`${prefixCls}__controller-item`} onClick={() => rotateFunc(90)}>
+            <img src={rotateSvg} />
+          </div>
+        </div>
+      );
+    };
+
+    const renderArrow = (direction: 'left' | 'right') => {
+      if (!unref(getIsMultipleImage)) {
+        return null;
+      }
+      return (
+        <div class={[`${prefixCls}__arrow`, direction]} onClick={() => handleChange(direction)}>
+          {direction === 'left' ? <LeftOutlined /> : <RightOutlined />}
+        </div>
+      );
+    };
+
+    return () => {
+      return (
+        imgState.show && (
+          <div class={prefixCls} ref={wrapElRef} onMouseup={handleMouseUp}>
+            <div class={`${prefixCls}-content`}>
+              {/*<Spin*/}
+              {/*  indicator={<LoadingOutlined style="font-size: 24px" spin />}*/}
+              {/*  spinning={true}*/}
+              {/*  class={[*/}
+              {/*    `${prefixCls}-image`,*/}
+              {/*    {*/}
+              {/*      hidden: imgState.status !== StatueEnum.LOADING,*/}
+              {/*    },*/}
+              {/*  ]}*/}
+              {/*/>*/}
+              <img
+                style={unref(getImageStyle)}
+                class={[`${prefixCls}-image`, imgState.status === StatueEnum.DONE ? '' : 'hidden']}
+                ref={imgElRef}
+                src={imgState.currentUrl}
+                onMousedown={handleAddMoveListener}
+              />
+              {renderClose()}
+              {renderIndex()}
+              {renderController()}
+              {renderArrow('left')}
+              {renderArrow('right')}
+            </div>
+          </div>
+        )
+      );
+    };
+  },
+});

+ 15 - 0
src/components/Preview/src/props.ts

@@ -0,0 +1,15 @@
+import { PropType } from 'vue';
+export const basicProps = {
+  show: {
+    type: Boolean as PropType<boolean>,
+    default: false,
+  },
+  imageList: {
+    type: [Array] as PropType<string[]>,
+    default: null,
+  },
+  index: {
+    type: Number as PropType<number>,
+    default: 0,
+  },
+};

+ 30 - 0
src/components/Preview/src/types.ts

@@ -0,0 +1,30 @@
+export interface Options {
+  show?: boolean;
+  imageList: string[];
+  index?: number;
+}
+
+export interface Props {
+  show: boolean;
+  instance: Props;
+  imageList: string[];
+  index: number;
+}
+
+export interface ImageProps {
+  alt?: string;
+  fallback?: string;
+  src: string;
+  width: string | number;
+  height?: string | number;
+  placeholder?: string | boolean;
+  preview?:
+    | boolean
+    | {
+        visible?: boolean;
+        onVisibleChange?: (visible: boolean, prevVisible: boolean) => void;
+        getContainer: string | HTMLElement | (() => HTMLElement);
+      };
+}
+
+export type ImageItem = string | ImageProps;

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

@@ -1,11 +1,12 @@
 <template>
   <PageWrapper title="图片预览示例">
+    <p @click="openImg">打开图片</p>
     <ImagePreview :imageList="imgList" />
   </PageWrapper>
 </template>
 <script lang="ts">
   import { defineComponent } from 'vue';
-  import { ImagePreview } from '/@/components/Preview/index';
+  import { createImgPreview, ImagePreview } from '/@/components/Preview/index';
   import { PageWrapper } from '/@/components/Page';
 
   const imgList: string[] = [
@@ -16,7 +17,10 @@
   export default defineComponent({
     components: { PageWrapper, ImagePreview },
     setup() {
-      return { imgList };
+      function openImg() {
+        createImgPreview({ imageList: imgList });
+      }
+      return { imgList, openImg };
     },
   });
 </script>

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä