Skip to content

更多功能

你可能发现 Fork 的 izhichao/vitepress-theme-minimalism 启动后和我的博客相比还少了一些功能,这是因为我的博客还修改了部分 CSS 以及安装了一些第三方的插件,但这些内容不太适合直接集成到主题中。

如果你也需要这些功能可以按照以下步骤自行添加。

图注

默认情况下,VitePress 的图片下方是没有图注的,可以借助 @mdit/plugin-figure 这个插件自动为图片添加描述。

例如,![标题](https://zhichao.org/image.png) 其中的 标题 就会自动添加到图片的下方。

安装依赖

sh
pnpm add @mdit/plugin-figure -D

导入

.vitepress/config.ts
ts
import { defineConfig } from 'vitepress'
import { figure } from '@mdit/plugin-figure';

export default defineConfig({
  markdown: {
    config(md) {
      md.use(figure);
    },
  },
})

代码块图标

先前一直以为代码块图标是 VitePress 默认主题自带的,因为官方文档里就有图标,但是自己无论怎么配置就是没有图标,后面才知道原来官方文档也是用了 vitepress-plugin-group-icons 这个插件才有的图标。

安装依赖

sh
pnpm add vitepress-plugin-group-icons -d

导入

.vitepress/config.ts
ts
import { defineConfig } from 'vitepress'
import { groupIconMdPlugin, groupIconVitePlugin } from 'vitepress-plugin-group-icons'

export default defineConfig({
  markdown: {
    config(md) {
      md.use(groupIconMdPlugin)
    },
  },
  vite: {
    plugins: [
      groupIconVitePlugin()
    ],
  }
})
.vitepress/theme/index.ts
ts
import Theme from 'vitepress/theme'
import 'virtual:group-icons.css'

export default Theme

容器样式

默认的 VitePress 容器样式过于简单,可以通过 CSS 修改一下样式

提示

提示

危险

危险

警告

警告

信息

信息

.vitepress/theme/style/custom-block.css
css
/* .vitepress/theme/style/custom-block.css */
/* 深浅色卡 */
:root {
  --custom-block-info-left: #cccccc;
  --custom-block-info-bg: #fafafa;

  --custom-block-tip-left: #009400;
  --custom-block-tip-bg: #e6f6e6;

  --custom-block-warning-left: #e6a700;
  --custom-block-warning-bg: #fff8e6;

  --custom-block-danger-left: #e13238;
  --custom-block-danger-bg: #ffebec;

  --custom-block-note-left: #4cb3d4;
  --custom-block-note-bg: #eef9fd;

  --custom-block-important-left: #a371f7;
  --custom-block-important-bg: #f4eefe;

  --custom-block-caution-left: #e0575b;
  --custom-block-caution-bg: #fde4e8;
}

.dark {
  --custom-block-info-left: #cccccc;
  --custom-block-info-bg: #474748;

  --custom-block-tip-left: #009400;
  --custom-block-tip-bg: #003100;

  --custom-block-warning-left: #e6a700;
  --custom-block-warning-bg: #4d3800;

  --custom-block-danger-left: #e13238;
  --custom-block-danger-bg: #4b1113;

  --custom-block-note-left: #4cb3d4;
  --custom-block-note-bg: #193c47;

  --custom-block-important-left: #a371f7;
  --custom-block-important-bg: #230555;

  --custom-block-caution-left: #e0575b;
  --custom-block-caution-bg: #391c22;
}

/* 标题字体大小 */
.custom-block-title {
  font-size: 16px;
}

/* info容器:背景色、左侧 */
.custom-block.info {
  border-left: 5px solid var(--custom-block-info-left);
  background-color: var(--custom-block-info-bg);
}

/* info容器:svg图 */
.custom-block.info [class*='custom-block-title']::before {
  content: '';
  background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-11v6h2v-6h-2zm0-4v2h2V7h-2z' fill='%23ccc'/%3E%3C/svg%3E");
  width: 20px;
  height: 20px;
  display: inline-block;
  vertical-align: middle;
  position: relative;
  margin-right: 4px;
  left: -5px;
  top: -1px;
}

/* 提示容器:边框色、背景色、左侧 */
.custom-block.tip {
  /* border-color: var(--custom-block-tip); */
  border-left: 5px solid var(--custom-block-tip-left);
  background-color: var(--custom-block-tip-bg);
}

/* 提示容器:svg图 */
.custom-block.tip [class*='custom-block-title']::before {
  content: '';
  background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23009400' d='M7.941 18c-.297-1.273-1.637-2.314-2.187-3a8 8 0 1 1 12.49.002c-.55.685-1.888 1.726-2.185 2.998H7.94zM16 20v1a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-1h8zm-3-9.995V6l-4.5 6.005H11v4l4.5-6H13z'/%3E%3C/svg%3E");
  width: 20px;
  height: 20px;
  display: inline-block;
  vertical-align: middle;
  position: relative;
  margin-right: 4px;
  left: -5px;
  top: -2px;
}

.custom-block.tip code {
  color: #037a03;
  background-color: #c3eec3;
}

/* 警告容器:背景色、左侧 */
.custom-block.warning {
  border-left: 5px solid var(--custom-block-warning-left);
  background-color: var(--custom-block-warning-bg);
}

/* 警告容器:svg图 */
.custom-block.warning [class*='custom-block-title']::before {
  content: '';
  background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M576.286 752.57v-95.425q0-7.031-4.771-11.802t-11.3-4.772h-96.43q-6.528 0-11.3 4.772t-4.77 11.802v95.424q0 7.031 4.77 11.803t11.3 4.77h96.43q6.528 0 11.3-4.77t4.77-11.803zm-1.005-187.836 9.04-230.524q0-6.027-5.022-9.543-6.529-5.524-12.053-5.524H456.754q-5.524 0-12.053 5.524-5.022 3.516-5.022 10.547l8.538 229.52q0 5.023 5.022 8.287t12.053 3.265h92.913q7.032 0 11.803-3.265t5.273-8.287zM568.25 95.65l385.714 707.142q17.578 31.641-1.004 63.282-8.538 14.564-23.354 23.102t-31.892 8.538H126.286q-17.076 0-31.892-8.538T71.04 866.074q-18.582-31.641-1.004-63.282L455.75 95.65q8.538-15.57 23.605-24.61T512 62t32.645 9.04 23.605 24.61z' fill='%23e6a700'/%3E%3C/svg%3E");
  width: 20px;
  height: 20px;
  display: inline-block;
  vertical-align: middle;
  position: relative;
  margin-right: 4px;
  left: -5px;
}

/* 危险容器:背景色、左侧 */
.custom-block.danger {
  border-left: 5px solid var(--custom-block-danger-left);
  background-color: var(--custom-block-danger-bg);
}

/* 危险容器:svg图 */
.custom-block.danger [class*='custom-block-title']::before {
  content: '';
  background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2c5.523 0 10 4.477 10 10v3.764a2 2 0 0 1-1.106 1.789L18 19v1a3 3 0 0 1-2.824 2.995L14.95 23a2.5 2.5 0 0 0 .044-.33L15 22.5V22a2 2 0 0 0-1.85-1.995L13 20h-2a2 2 0 0 0-1.995 1.85L9 22v.5c0 .171.017.339.05.5H9a3 3 0 0 1-3-3v-1l-2.894-1.447A2 2 0 0 1 2 15.763V12C2 6.477 6.477 2 12 2zm-4 9a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm8 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z' fill='%23e13238'/%3E%3C/svg%3E");
  width: 20px;
  height: 20px;
  display: inline-block;
  vertical-align: middle;
  position: relative;
  margin-right: 4px;
  left: -5px;
  top: -1px;
}

/* 提醒容器:背景色、左侧 */
.custom-block.note {
  border-left: 5px solid var(--custom-block-note-left);
  background-color: var(--custom-block-note-bg);
}

/* 提醒容器:svg图 */
.custom-block.note [class*='custom-block-title']::before {
  content: '';
  background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-11v6h2v-6h-2zm0-4v2h2V7h-2z' fill='%234cb3d4'/%3E%3C/svg%3E");
  width: 20px;
  height: 20px;
  display: inline-block;
  vertical-align: middle;
  position: relative;
  margin-right: 4px;
  left: -5px;
  top: -1px;
}

/* 重要容器:背景色、左侧 */
.custom-block.important {
  border-left: 5px solid var(--custom-block-important-left);
  background-color: var(--custom-block-important-bg);
}

/* 重要容器:svg图 */
.custom-block.important [class*='custom-block-title']::before {
  content: '';
  background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M512 981.333a84.992 84.992 0 0 1-84.907-84.906h169.814A84.992 84.992 0 0 1 512 981.333zm384-128H128v-42.666l85.333-85.334v-256A298.325 298.325 0 0 1 448 177.92V128a64 64 0 0 1 128 0v49.92a298.325 298.325 0 0 1 234.667 291.413v256L896 810.667v42.666zm-426.667-256v85.334h85.334v-85.334h-85.334zm0-256V512h85.334V341.333h-85.334z' fill='%23a371f7'/%3E%3C/svg%3E");
  width: 20px;
  height: 20px;
  display: inline-block;
  vertical-align: middle;
  position: relative;
  margin-right: 4px;
  left: -5px;
  top: -1px;
}

/* 注意容器:背景色、左侧 */
.custom-block.caution {
  border-left: 5px solid var(--custom-block-caution-left);
  background-color: var(--custom-block-caution-bg);
}

/* 注意容器:svg图 */
.custom-block.caution [class*='custom-block-title']::before {
  content: '';
  background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2c5.523 0 10 4.477 10 10v3.764a2 2 0 0 1-1.106 1.789L18 19v1a3 3 0 0 1-2.824 2.995L14.95 23a2.5 2.5 0 0 0 .044-.33L15 22.5V22a2 2 0 0 0-1.85-1.995L13 20h-2a2 2 0 0 0-1.995 1.85L9 22v.5c0 .171.017.339.05.5H9a3 3 0 0 1-3-3v-1l-2.894-1.447A2 2 0 0 1 2 15.763V12C2 6.477 6.477 2 12 2zm-4 9a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm8 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z' fill='%23e13238'/%3E%3C/svg%3E");
  width: 20px;
  height: 20px;
  display: inline-block;
  vertical-align: middle;
  position: relative;
  margin-right: 4px;
  left: -5px;
  top: -1px;
}

参考资料

容器颜色

实现原理

下面这些功能已经集成在了主题中,如果你也想给你 VitePress 添加这些功能,可以参考一下如果实现。

加载进度条

想要在切换路由时,页面顶部显示加载进度条,可以使用 BProgress

安装依赖

sh
pnpm add @bprogress/core -D

导入

.vitepress/theme/index.ts
ts
import { onMounted, onUnmounted } from 'vue';
import { EnhanceAppContext, inBrowser } from 'vitepress';
import DefaultTheme from 'vitepress/theme';
import { BProgress } from '@bprogress/core';
import '@bprogress/core/css';

export default {
  extends: DefaultTheme,
  enhanceApp({ app, router }: EnhanceAppContext) {
    if (inBrowser) {
      BProgress.configure({ showSpinner: false });
      router.onBeforeRouteChange = () => {
        BProgress.start();
      };
      router.onAfterRouteChange = () => {
        BProgress.done();
      };
    }
  }
};

样式

css
:root {
  --bprogress-color: var(--vp-c-brand-3);
}

图片缩放

medium-zoom

medium-zoom 可以非常简单的为图片添加缩放功能,但除此之外就没有任何其他功能了。如果有更多需求建议使用 fancybox。

安装依赖

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 是非常经典的图片查看库,支持放大、缩小、翻转和旋转等丰富的功能。

安装依赖

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,
      Carousel: {
        Zoomable: {
          Panzoom: {
            clickAction: false, // 禁用单击放大
            dblClickAction: PanzoomAction.IterateZoom, // 双击放大
            maxScale: 2,
            on: {
              // 单击关闭 Fancybox
              singleClick: () => {
                Fancybox.close();
              }
            }
          }
        },
        Toolbar: {
          absolute: false,
          display: {
            left: ['counter'],
            middle: ['zoomIn', 'zoomOut', 'toggle1to1', 'rotateCCW', 'rotateCW', 'flipX', 'flipY', 'reset'],
            right: ['thumbs', 'close'] // 'autoplay' 自动播放
          }
        },
        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) {
      router.onBeforeRouteChange = () => {
        destroyFancybox(); // 销毁图片查看器
      };
      router.onAfterRouteChange = () => {
        bindFancybox(); // 绑定图片查看器
      };
    }
  },
  setup() {
    onMounted(() => {
      bindFancybox();
    });
    onUnmounted(() => {
      destroyFancybox();
    });
  }
};

样式

/src/styles/fancybox.less
less
.fancybox__container {
  --fancybox-backdrop-bg: none;
  --f-caption-color: var(--vp-c-text-1);
  --f-thumb-selected-shadow: inset 0 0 0 2px var(--vp-c-brand-1);

  .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);

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

  &::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-radius: 10px;
    box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.2);
  }
}

警告

原文使用的是老版本 Fancybox v5 @fancyapps/ui@5,以上代码参考原文修改后兼容 Fancybox v6,可以直接安装最新版