Skip to content

图片缩放

方法一:medium-zoom

安装依赖

sh
pnpm add medium-zoom -D

导入

.vitepress/theme/index.ts
ts
import { onMounted, watch, nextTick } from 'vue';
import { useRoute } from 'vitepress';
import DefaultTheme from 'vitepress/theme';
import mediumZoom from 'medium-zoom';

import './styles/index.less';

export default {
  extends: DefaultTheme,
  setup() {
    const route = useRoute();
    const initZoom = () => {
      mediumZoom('.main img', { background: 'rgba(0,0,0,0.2)' });
    };
    onMounted(() => {
      initZoom();
    });
    watch(
      () => route.path,
      () => nextTick(() => initZoom())
    );
  }
};

样式

css
.medium-zoom-overlay {
  z-index: 30;
}

.medium-zoom-image {
  border-radius: 10px;
  z-index: 31;
}

方法二:fancybox

警告

如果使用原文的代码,需要安装老版本的 Fancybox v5 @fancyapps/ui@5

以下代码参考原文修改后兼容 Fancybox v6,安装最新版即可

安装依赖

sh
pnpm add @fancyapps/ui -D

导入

src/utils/fancybox.ts
ts
import { nextTick } from 'vue';
import '@fancyapps/ui/dist/fancybox/fancybox.css';

// 查找图像之前最近的标题
const findNearestHeading = (imgElement) => {
  // 获取 img 元素的父节点
  let currentElement = imgElement;
  // 循环向上查找
  while (currentElement && currentElement !== document.body) {
    // 在当前元素的前一个兄弟节点中查找 h1-h6 标签
    let previousSibling = currentElement.previousElementSibling;
    while (previousSibling) {
      if (previousSibling.tagName.match(/^H[1-6]$/)) {
        return previousSibling.textContent.replace(/\u200B/g, '').trim(); // 返回找到的标题内容
      }
      previousSibling = previousSibling.previousElementSibling;
    }
    // 如果没有找到,继续向上一级父节点查找
    currentElement = currentElement.parentElement;
  }

  return '';
};

export const bindFancybox = () => {
  nextTick(async () => {
    const { Fancybox, PanzoomAction } = await import('@fancyapps/ui'); // 采用这种导入方式是为了避免构建报错问题
    const imgs = document.querySelectorAll('.vp-doc img');
    imgs.forEach((img) => {
      const image = img as HTMLImageElement;
      if (!image.hasAttribute('data-fancybox')) {
        image.setAttribute('data-fancybox', 'gallery');
      }
      // 赋予 alt 属性
      if (!image.hasAttribute('alt') || image.getAttribute('alt') === '') {
        const heading = findNearestHeading(image);
        image.setAttribute('alt', heading);
      }
      // 赋予 data-caption 属性以便显示图片标题
      const altString = image.getAttribute('alt') || '';
      image.setAttribute('data-caption', altString);
    });

    Fancybox.bind('[data-fancybox="gallery"]', {
      Hash: false,
      caption: false,
      zoomEffect: false,
      Carousel: {
        Zoomable: {
          Panzoom: {
            clickAction: 'iterateZoom',
            maxScale: 2,
            minScale: 0.8
          }
        },
        Toolbar: {
          display: {
            left: ['counter'],
            middle: ['zoomIn', 'zoomOut', 'toggle1to1', 'rotateCCW', 'rotateCW', 'flipX', 'flipY', 'reset'],
            right: ['autoplay', 'thumbs', 'close']
          }
        },
        Thumbs: {
          type: 'classic',
          showOnStart: false
        }
      }
    });
  });
};

export const destroyFancybox = async () => {
  const { Fancybox } = await import('@fancyapps/ui');
  Fancybox.destroy();
};
.vitepress/theme/index.ts
ts
import { onMounted, onUnmounted } from 'vue';
import { EnhanceAppContext, inBrowser } from 'vitepress';
import DefaultTheme from 'vitepress/theme';
import { bindFancybox, destroyFancybox } from 'src/utils/fancybox';
import 'src/styles/fancybox.less';

export default {
  extends: DefaultTheme,
  enhanceApp({ app, router }: EnhanceAppContext) {
    if (inBrowser) {
      BProgress.configure({ showSpinner: false });
      router.onBeforeRouteChange = () => {
        BProgress.start();
        destroyFancybox(); // 销毁图片查看器
      };
      router.onAfterRouteChange = () => {
        BProgress.done();
        bindFancybox(); // 绑定图片查看器
      };
    }
  },
  setup() {
    onMounted(() => {
      bindFancybox();
    });
    onUnmounted(() => {
      destroyFancybox();
    });
  }
};

样式

/src/styles/fancybox.less
less
:root {
  --vp-c-bg-rgb: 255, 255, 255;
  --vp-c-bg-reverse-rgb: 27, 27, 31;
}

.dark {
  --vp-c-bg-rgb: 27, 27, 31;
  --vp-c-bg-reverse-rgb: 255, 255, 255;
}

.fancybox__container {
  --fancybox-backdrop-bg: none;
  .f-carousel__toolbar {
    --f-button-bg: none;
    --f-button-hover-bg: rgba(var(--vp-c-bg-reverse-rgb), 0.1);
    --f-button-color: rgba(var(--vp-c-bg-reverse-rgb), 1);
    --f-button-hover-color: rgba(var(--vp-c-bg-reverse-rgb), 1);
    --f-button-svg-disabled-opacity: 0.2;
    background: rgba(var(--vp-c-bg-rgb), 0.2);
  }

  &::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    right: 0;
    background: rgba(var(--vp-c-bg-rgb), 0.5);
    backdrop-filter: blur(10px);
  }

  .f-panzoom__content {
    object-fit: initial;
    border: solid 1px rgba(var(--vp-c-bg-reverse-rgb), 0.5);
    border-radius: 10px;
  }

  .is-classic .is-nav-selected .f-thumbs__slide__button::after {
    border-color: var(--vp-c-brand-1);
  }

  .f-caption {
    color: var(--vp-c-text-1);
  }
}