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