vitepress动态导航

前言

我需要根据动态设置导航。
比如根据不同的路由设置不同的顶部导航。
但是vitepress并没有相关配置,但是我们可以通过拦截主题修改全局状态来完成这个功能!

核心知识

创建文件,xxx/docs/.vitepress/theme/index.js
注意文件名和路径不能写错,意思是覆盖默认主题配置!

import DefaultTheme from "vitepress/theme";
import { watch } from 'vue'

export default {
    extends: DefaultTheme,
    enhanceApp({ app, router, siteData }) {
        watch(
            () => router.route.path,
            (val) => {
                // 动态修改站点数据(毕竟使用useData不生效),比如这里动态设置navbar
                siteData.value = {
                    ...siteData.value,
                    themeConfig: {
                        ...siteData.value.themeConfig,
                        nav: [
                            { text: "Html", link: "/vue/intro" },
                            { text: "Css", link: "/vuex/intro" },
                        ]
                    }
                }
            }
        );
    },
};

完整试例

创建导航配置

xxx/docs/.vitepress/theme/helper/nav-group.ts

import holyTrinityImg from "../assets/img/docss/holy-trinity.webp";
import feFwLibImg from "../assets/img/docss/fe-fw-lib.png";

const navGroup = {
  holyTrinity: {
    title: "前端三剑客",
    content: "html、css、js的爱恨情仇",
    icon: holyTrinityImg,
    nav: [
      { text: "Html", link: "/html" },
      { text: "Css", link: "/css" },
    ],
  },
  feFwAndLib: {
    title: "前端框架与库",
    content: "让你事半功倍",
    icon: feFwLibImg,
    nav: [
      { text: "Vue", link: "/vue" },
      { text: "Vuex", link: "/vuex" },
    ],
  },
};

export default navGroup;

工具函数

xxx/docs/.vitepress/theme/helper/utils.ts

import _ from "lodash";
import navGroupTemp from "./nav-group";

// 接受navGrouplink,返回对应的第一个sidebar(既第一篇文档)
export const getFirstSideBarLink = (navGrouplink, allSidebar, baseUrl) => {
  const sidebar = allSidebar[navGrouplink + "/"];
  let firstSidebar;
  const handler = (items) => {
    for (const it of items) {
      if (it.items) handler(it.items);
      else firstSidebar = it;
      break;
    }
  };
  handler(sidebar.items);
  let link;
  if (firstSidebar) {
    let base = baseUrl;
    base = base.slice(0, -1);
    link = base + navGrouplink + "/" + firstSidebar.link;
  }
  return link;
};

// 根据当前路由 获取 对应的顶部导航
export const getNavs = (router, baseUrl) => {
  let navGroup = _.cloneDeep(navGroupTemp);
  const { path } = router.route;
  const index = baseUrl === "/" ? 1 : 2;
  const navItemlink = "/" + path.split("/")[index];
  for (const key in navGroup) {
    const match = navGroup[key].nav?.find((it) => it.link === navItemlink);
    if (match) return navGroup[key].nav; // 返回找到的根键,例如 'holyTrinity'
  }
};

自定义主题:home页

xxx/docs/.vitepress/theme/home.vue

<template>
  <div class="home">
    <div class="other"></div>
    <div class="docss">
      <template v-for="key in Object.keys(navGroup)" :key="key">
        <div class="docs">
          <img :src="navGroup[key].icon" />
          <div class="content">{{ navGroup[key].content }}</div>
          <div class="title" @click="goByNavGroup(navGroup[key])">
            {{ navGroup[key].title }}
          </div>
        </div>
      </template>
    </div>
  </div>
</template>

<script setup name="customHome" lang="ts">
// @ts-nocheck
import _ from 'lodash';
import {ref} from "vue";
import navGroupTemp from "./helper/nav-group";
import { useRouter, useData } from "vitepress";
import {getFirstSideBarLink} from "./helper/utils"

const navGroup = ref(_.cloneDeep(navGroupTemp));
const { site, theme } = useData()
const router = useRouter();
const goByNavGroup = (item) => {
  const [firstNav] = item.nav;
  const link = getFirstSideBarLink(firstNav.link, theme.value.sidebar, site.value.base);
  router.go(link);
};
</script>

<style scoped lang="scss">
.home {
  width: 100%;
  max-width: 1024px;
  background-color: rgba($color: #000000, $alpha: 0.01);
  height: 100vh;
  margin: auto;

  .other {
    height: 100px;
  }

  .docss {
    margin: auto;
    display: flex;
    flex-wrap: wrap;

    .docs {
      padding: 0 16px;
      // height: 180px;
      margin-bottom: 20px;
      cursor: pointer;

      img {
        width: 100%;
        border-radius: 4px;
        background-color: rgba($color: #000000, $alpha: 0.1);
      }

      .title {
        text-align: center;
        font-weight: 700;
      }

      .content {
        text-align: center;
        font-size: 13px;
      }
    }

    // 媒体查询:当屏幕宽度小于600px时
    @media screen and (max-width: 600px) {
      .docs {
        width: 50%;

        img {
          height: 90px;
        }
      }
    }

    // 媒体查询:当屏幕宽度在400px到600px之间时
    @media screen and (min-width: 600px) and (max-width: 800px) {
      .docs {
        width: 33%;

        img {
          height: 110px;
        }
      }
    }

    // 媒体查询:当屏幕宽度大于800px时
    @media screen and (min-width: 800px) {
      .docs {
        width: 25%;

        img {
          height: 130px;
        }
      }
    }
  }
}
</style>

自定义主题: 入口

xxx/docs/.vitepress/theme/index.ts

import _ from "lodash";
import { watch } from "vue";
import Home from "./home.vue";
import DefaultTheme from "vitepress/theme";
import { getFirstSideBarLink, getNavs } from "./helper/utils";

export default {
  extends: DefaultTheme,
  enhanceApp({ app, router, siteData }) {
    watch(
      () => router.route.path,
      (val) => {
        // 获取基础nav
        const nav = getNavs(router, siteData.value.base);
        // 动态修改站点数据(毕竟使用useData不生效),比如这里动态设置navbar
        if (nav) {
          // 补充完整nav的link,并设置合适的active高亮
          nav.forEach((it) => {
            const res = getFirstSideBarLink(
              it.link,
              siteData.value.themeConfig.sidebar,
              ""
            );
            it.link = res;
            it.activeMatch = "/" + it.link.split("/")[1] + "/";
          });
          siteData.value = {
            ...siteData.value,
            themeConfig: {
              ...siteData.value.themeConfig,
              nav,
            },
          };
        }
      }
    );
    app.component("customHome", Home);
  },
};

其他

注意我使用了侧边栏自动生成插件 vitepress-sidebar

xxx/docs/.vitepress/config.mts

import { defineConfig } from 'vitepress'
import { withSidebar } from 'vitepress-sidebar';
import fs from "node:fs";
import path from "node:path";

const vitePressOptions = defineConfig({
  title: "丁知识库",
  description: "A VitePress Site",
  base: "/vitepress",
  cleanUrls: true,
  themeConfig: {
    nav: [],
    sidebar: [],
    // socialLinks: [
    //   { icon: 'github', link: 'https://github.com/vuejs/vitepress' }
    // ]
  }
})

// 获取都有哪些项目
const projectRoot = path.resolve(process.cwd());
const docsPath = path.resolve(projectRoot, 'docs');
const ignoreDir = ['.vitepress', '.DS_Store'];
const docs = fs.readdirSync(docsPath, { withFileTypes: true }).filter(it => {
  const isIgnore = ignoreDir.includes(it.name)
  const isFile = fs.statSync(path.resolve(it.parentPath, it.name)).isFile();
  return !isIgnore && !isFile;
});

const vitePressSidebarOptions = docs.map(it => ({
  documentRootPath: 'docs',
  scanStartPath: it.name,
  basePath: `/${it.name}/`,
  resolvePath: `/${it.name}/`,
  useTitleFromFileHeading: true,
  collapsed: true,
  rootGroupCollapsed: true,
  useFolderLinkFromSameNameSubFile: true, // 如果此值为true,则当存在与文件夹同名的子文件时,将在文件夹中创建一个链接,用于导航至该文件,而该文件不会显示在子项中。
  // folderLinkNotIncludesFileName: true
}))

export default defineConfig(withSidebar(vitePressOptions, vitePressSidebarOptions));

预览效果

posted @ 2024-12-09 00:59  丁少华  阅读(21)  评论(0编辑  收藏  举报