Newer
Older
dxCard-admin / src / components / Preview / src / Functional.vue
YFJ on 23 Sep 14 KB 项目推送
<script lang="tsx">
  import { defineComponent, ref, unref, computed, reactive, watchEffect } from 'vue';
  import { CloseOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons-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 props = {
    show: {
      type: Boolean as PropType<boolean>,
      default: false,
    },
    imageList: {
      type: [Array] as PropType<string[]>,
      default: null,
    },
    index: {
      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,
    emits: ['img-load', 'img-error'],
    setup(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,
        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;
        };
      }

      const getScaleStep = computed(() => {
        const scaleStep = props?.scaleStep ?? 0;
        if (scaleStep ?? (0 > 0 && scaleStep < 100)) {
          return scaleStep / 100;
        } else {
          return imgState.imgScale / 10;
        }
      });

      // 监听鼠标滚轮
      function scrollFunc(e: any) {
        e = e || window.event;
        e.delta = e.wheelDelta || -e.detail;

        e.preventDefault();
        if (e.delta > 0) {
          // 滑轮向上滚动
          scaleFunc(getScaleStep.value);
        }
        if (e.delta < 0) {
          // 滑轮向下滚动
          scaleFunc(-getScaleStep.value);
        }
      }
      // 缩放函数
      function scaleFunc(num: number) {
        if (imgState.imgScale <= 0.2 && num < 0) return;
        imgState.imgScale += num;
        // update-begin--author:liaozhiyang---date:20250722---for:【QQYUN-13162】图片预览点击缩小一下就没了
        if (imgState.imgScale < 0.2) {
          imgState.imgScale = 0.2;
        }
        // update-end--author:liaozhiyang---date:20250722---for:【QQYUN-13162】图片预览点击缩小一下就没了
      }

      // 旋转图片
      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 = (e: Event) => {
          if (imgState.currentUrl !== url) {
            const ele: any[] = 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 = (e: Event) => {
          const ele: EventTarget[] = e.composedPath();
          ele &&
            emit('img-error', {
              index: imgState.currentIndex,
              dom: ele[0] as HTMLImageElement,
              url,
            });
          imgState.status = StatueEnum.FAIL;
        };
      }

      // 关闭
      function handleClose(e: MouseEvent) {
        e && e.stopPropagation();
        close();
      }

      function close() {
        imgState.show = false;
        // 移除火狐浏览器下的鼠标滚动事件
        document.body.removeEventListener('DOMMouseScroll', scrollFunc);
        // 恢复火狐及Safari浏览器下的图片拖拽
        document.ondragstart = null;
      }

      // 图片复原
      function resume() {
        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;
        },
      });

      // 上一页下一页
      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`,
          maxWidth: props.defaultWidth ? 'unset' : '100%',
        };
      });

      const getIsMultipleImage = computed(() => {
        const { imageList } = props;
        return imageList.length > 1;
      });

      watchEffect(() => {
        if (props.show) {
          init();
        }
        if (props.imageList) {
          initState();
        }
      });

      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}>
            <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(-getScaleStep.value)}>
              <img src={unScaleSvg} />
            </div>
            <div class={`${prefixCls}__controller-item`} onClick={() => scaleFunc(getScaleStep.value)}>
              <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} onClick={handleMaskClick}>
              <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>
          )
        );
      };
    },
  });
</script>
<style lang="less">
  .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;
      }
    }
  }
</style>