Skip to content

VitePress 主题定制完全指南

VitePress 提供了强大的主题定制能力,让你可以创建独特且专业的文档站点。本文将深入探讨 VitePress 主题定制的各个方面,从基础配置到高级自定义技巧,帮助你打造完美的文档体验。

目录

主题系统概述

VitePress 主题架构

VitePress 采用分层的主题架构:

主题层级
├── 默认主题 (Default Theme)
├── 自定义主题 (Custom Theme)
├── 主题扩展 (Theme Extensions)
└── 组件覆盖 (Component Overrides)

主题配置基础

javascript
// .vitepress/config.js
export default {
  // 基础主题配置
  themeConfig: {
    // 导航栏
    nav: [
      { text: '首页', link: '/' },
      { text: '指南', link: '/guide/' },
      { text: '配置', link: '/config/' }
    ],
    
    // 侧边栏
    sidebar: {
      '/guide/': [
        {
          text: '开始',
          items: [
            { text: '介绍', link: '/guide/introduction' },
            { text: '快速开始', link: '/guide/getting-started' }
          ]
        }
      ]
    },
    
    // 社交链接
    socialLinks: [
      { icon: 'github', link: 'https://github.com/vuejs/vitepress' }
    ],
    
    // 页脚
    footer: {
      message: '基于 MIT 许可发布',
      copyright: 'Copyright © 2025 VitePress'
    }
  }
}

CSS 变量定制

1. 颜色系统定制

css
/* .vitepress/theme/custom.css */
:root {
  /* 品牌色彩 */
  --vp-c-brand-1: #3eaf7c;
  --vp-c-brand-2: #369870;
  --vp-c-brand-3: #2d8063;
  --vp-c-brand-soft: rgba(62, 175, 124, 0.14);
  
  /* 背景色 */
  --vp-c-bg: #ffffff;
  --vp-c-bg-alt: #f6f6f7;
  --vp-c-bg-elv: #ffffff;
  --vp-c-bg-soft: #f6f6f7;
  
  /* 文本色 */
  --vp-c-text-1: rgba(60, 60, 67);
  --vp-c-text-2: rgba(60, 60, 67, 0.78);
  --vp-c-text-3: rgba(60, 60, 67, 0.56);
  
  /* 边框色 */
  --vp-c-divider: rgba(60, 60, 67, 0.29);
  --vp-c-border: rgba(60, 60, 67, 0.23);
  
  /* 控件色 */
  --vp-c-gutter: rgba(60, 60, 67, 0.12);
  --vp-c-neutral: rgba(60, 60, 67, 0.08);
  --vp-c-neutral-inverse: rgba(255, 255, 255, 0.95);
}

/* 深色模式 */
.dark {
  --vp-c-bg: #1b1b1f;
  --vp-c-bg-alt: #161618;
  --vp-c-bg-elv: #202127;
  --vp-c-bg-soft: #202127;
  
  --vp-c-text-1: rgba(255, 255, 245, 0.86);
  --vp-c-text-2: rgba(235, 235, 245, 0.6);
  --vp-c-text-3: rgba(235, 235, 245, 0.38);
  
  --vp-c-divider: rgba(84, 84, 88, 0.65);
  --vp-c-border: rgba(82, 82, 89, 0.68);
}

2. 字体系统定制

css
:root {
  /* 字体族 */
  --vp-font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --vp-font-family-mono: 'Fira Code', Menlo, Monaco, Consolas, 'Courier New', monospace;
  
  /* 字体大小 */
  --vp-font-size-root: 16px;
  --vp-font-size-xs: 12px;
  --vp-font-size-sm: 14px;
  --vp-font-size-md: 16px;
  --vp-font-size-lg: 18px;
  --vp-font-size-xl: 20px;
  
  /* 行高 */
  --vp-line-height-xs: 1.4;
  --vp-line-height-sm: 1.5;
  --vp-line-height-md: 1.6;
  --vp-line-height-lg: 1.7;
}

/* 自定义字体加载 */
@font-face {
  font-family: 'CustomFont';
  src: url('./fonts/custom-font.woff2') format('woff2');
  font-display: swap;
}

/* 应用自定义字体 */
.custom-font {
  font-family: 'CustomFont', var(--vp-font-family-base);
}

3. 布局尺寸定制

css
:root {
  /* 容器宽度 */
  --vp-layout-max-width: 1440px;
  --vp-content-max-width: 688px;
  --vp-sidebar-width-small: 272px;
  --vp-sidebar-width-medium: 320px;
  
  /* 间距系统 */
  --vp-space-xs: 4px;
  --vp-space-sm: 8px;
  --vp-space-md: 16px;
  --vp-space-lg: 24px;
  --vp-space-xl: 32px;
  --vp-space-2xl: 48px;
  
  /* 圆角 */
  --vp-border-radius-xs: 2px;
  --vp-border-radius-sm: 4px;
  --vp-border-radius-md: 6px;
  --vp-border-radius-lg: 8px;
  --vp-border-radius-xl: 12px;
  
  /* 阴影 */
  --vp-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.06);
  --vp-shadow-2: 0 3px 12px rgba(0, 0, 0, 0.07), 0 1px 4px rgba(0, 0, 0, 0.07);
  --vp-shadow-3: 0 12px 32px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.08);
}

组件定制

1. 导航栏定制

vue
<!-- .vitepress/theme/components/CustomNavBar.vue -->
<template>
  <div class="custom-nav">
    <div class="nav-container">
      <!-- Logo -->
      <div class="nav-logo">
        <img src="/logo.svg" alt="Logo" />
        <span class="nav-title">{{ site.title }}</span>
      </div>
      
      <!-- 导航菜单 -->
      <nav class="nav-menu">
        <div 
          v-for="item in nav" 
          :key="item.text"
          class="nav-item"
          :class="{ active: isActive(item.link) }"
        >
          <a :href="item.link" class="nav-link">
            {{ item.text }}
          </a>
          
          <!-- 下拉菜单 -->
          <div v-if="item.items" class="nav-dropdown">
            <div 
              v-for="subItem in item.items"
              :key="subItem.text"
              class="nav-dropdown-item"
            >
              <a :href="subItem.link">{{ subItem.text }}</a>
            </div>
          </div>
        </div>
      </nav>
      
      <!-- 搜索和主题切换 -->
      <div class="nav-actions">
        <SearchBox />
        <ThemeToggle />
        <SocialLinks />
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useData, useRouter } from 'vitepress'
import SearchBox from './SearchBox.vue'
import ThemeToggle from './ThemeToggle.vue'
import SocialLinks from './SocialLinks.vue'

const { site, theme } = useData()
const router = useRouter()

const nav = computed(() => theme.value.nav || [])

const isActive = (link) => {
  return router.route.path.startsWith(link)
}
</script>

<style scoped>
.custom-nav {
  position: sticky;
  top: 0;
  z-index: 10;
  background: var(--vp-c-bg);
  border-bottom: 1px solid var(--vp-c-divider);
  backdrop-filter: blur(8px);
}

.nav-container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  max-width: var(--vp-layout-max-width);
  margin: 0 auto;
  padding: 0 var(--vp-space-lg);
  height: 64px;
}

.nav-logo {
  display: flex;
  align-items: center;
  gap: var(--vp-space-sm);
  font-weight: 600;
  font-size: var(--vp-font-size-lg);
}

.nav-logo img {
  width: 32px;
  height: 32px;
}

.nav-menu {
  display: flex;
  align-items: center;
  gap: var(--vp-space-lg);
}

.nav-item {
  position: relative;
}

.nav-link {
  color: var(--vp-c-text-1);
  text-decoration: none;
  font-weight: 500;
  transition: color 0.2s ease;
}

.nav-link:hover,
.nav-item.active .nav-link {
  color: var(--vp-c-brand-1);
}

.nav-dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  background: var(--vp-c-bg-elv);
  border: 1px solid var(--vp-c-border);
  border-radius: var(--vp-border-radius-md);
  box-shadow: var(--vp-shadow-2);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px);
  transition: all 0.2s ease;
  min-width: 200px;
}

.nav-item:hover .nav-dropdown {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.nav-dropdown-item {
  padding: var(--vp-space-sm) var(--vp-space-md);
}

.nav-dropdown-item a {
  color: var(--vp-c-text-2);
  text-decoration: none;
  display: block;
  transition: color 0.2s ease;
}

.nav-dropdown-item a:hover {
  color: var(--vp-c-brand-1);
}

.nav-actions {
  display: flex;
  align-items: center;
  gap: var(--vp-space-md);
}

@media (max-width: 768px) {
  .nav-menu {
    display: none;
  }
  
  .nav-container {
    padding: 0 var(--vp-space-md);
  }
}
</style>

2. 侧边栏定制

vue
<!-- .vitepress/theme/components/CustomSidebar.vue -->
<template>
  <aside class="custom-sidebar">
    <div class="sidebar-content">
      <!-- 搜索框 -->
      <div class="sidebar-search">
        <SearchBox />
      </div>
      
      <!-- 导航树 -->
      <nav class="sidebar-nav">
        <div 
          v-for="group in sidebarGroups" 
          :key="group.text"
          class="sidebar-group"
        >
          <h3 class="sidebar-group-title">
            {{ group.text }}
          </h3>
          
          <ul class="sidebar-group-items">
            <li 
              v-for="item in group.items"
              :key="item.link"
              class="sidebar-item"
              :class="{ 
                active: isActive(item.link),
                collapsed: item.collapsed 
              }"
            >
              <a 
                :href="item.link"
                class="sidebar-link"
                @click="handleItemClick(item)"
              >
                <span class="sidebar-link-text">{{ item.text }}</span>
                <span 
                  v-if="item.items"
                  class="sidebar-link-arrow"
                  :class="{ expanded: !item.collapsed }"
                >
                  <ChevronRightIcon />
                </span>
              </a>
              
              <!-- 子菜单 -->
              <ul 
                v-if="item.items && !item.collapsed"
                class="sidebar-sub-items"
              >
                <li 
                  v-for="subItem in item.items"
                  :key="subItem.link"
                  class="sidebar-sub-item"
                  :class="{ active: isActive(subItem.link) }"
                >
                  <a :href="subItem.link" class="sidebar-sub-link">
                    {{ subItem.text }}
                  </a>
                </li>
              </ul>
            </li>
          </ul>
        </div>
      </nav>
      
      <!-- 页面导航 */
      <div class="sidebar-toc">
        <h4 class="toc-title">本页内容</h4>
        <TableOfContents />
      </div>
    </div>
  </aside>
</template>

<script setup>
import { computed, ref } from 'vue'
import { useData, useRouter } from 'vitepress'
import SearchBox from './SearchBox.vue'
import TableOfContents from './TableOfContents.vue'
import ChevronRightIcon from './icons/ChevronRightIcon.vue'

const { theme } = useData()
const router = useRouter()

const collapsedItems = ref(new Set())

const sidebarGroups = computed(() => {
  const sidebar = theme.value.sidebar
  const currentPath = router.route.path
  
  // 根据当前路径获取对应的侧边栏配置
  for (const path in sidebar) {
    if (currentPath.startsWith(path)) {
      return sidebar[path].map(group => ({
        ...group,
        items: group.items?.map(item => ({
          ...item,
          collapsed: collapsedItems.value.has(item.link)
        }))
      }))
    }
  }
  
  return []
})

const isActive = (link) => {
  return router.route.path === link
}

const handleItemClick = (item) => {
  if (item.items) {
    if (collapsedItems.value.has(item.link)) {
      collapsedItems.value.delete(item.link)
    } else {
      collapsedItems.value.add(item.link)
    }
  }
}
</script>

<style scoped>
.custom-sidebar {
  width: var(--vp-sidebar-width-medium);
  height: 100vh;
  position: fixed;
  top: 0;
  left: 0;
  background: var(--vp-c-bg-alt);
  border-right: 1px solid var(--vp-c-divider);
  overflow-y: auto;
  z-index: 5;
}

.sidebar-content {
  padding: var(--vp-space-lg);
}

.sidebar-search {
  margin-bottom: var(--vp-space-lg);
}

.sidebar-group {
  margin-bottom: var(--vp-space-xl);
}

.sidebar-group-title {
  font-size: var(--vp-font-size-sm);
  font-weight: 600;
  color: var(--vp-c-text-1);
  margin: 0 0 var(--vp-space-md) 0;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.sidebar-group-items {
  list-style: none;
  padding: 0;
  margin: 0;
}

.sidebar-item {
  margin-bottom: var(--vp-space-xs);
}

.sidebar-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--vp-space-sm) var(--vp-space-md);
  color: var(--vp-c-text-2);
  text-decoration: none;
  border-radius: var(--vp-border-radius-sm);
  transition: all 0.2s ease;
}

.sidebar-link:hover {
  background: var(--vp-c-bg-soft);
  color: var(--vp-c-text-1);
}

.sidebar-item.active .sidebar-link {
  background: var(--vp-c-brand-soft);
  color: var(--vp-c-brand-1);
  font-weight: 500;
}

.sidebar-link-arrow {
  transition: transform 0.2s ease;
}

.sidebar-link-arrow.expanded {
  transform: rotate(90deg);
}

.sidebar-sub-items {
  list-style: none;
  padding: 0;
  margin: var(--vp-space-sm) 0 0 var(--vp-space-lg);
}

.sidebar-sub-item {
  margin-bottom: var(--vp-space-xs);
}

.sidebar-sub-link {
  display: block;
  padding: var(--vp-space-xs) var(--vp-space-md);
  color: var(--vp-c-text-3);
  text-decoration: none;
  border-radius: var(--vp-border-radius-sm);
  transition: all 0.2s ease;
  font-size: var(--vp-font-size-sm);
}

.sidebar-sub-link:hover {
  background: var(--vp-c-bg-soft);
  color: var(--vp-c-text-2);
}

.sidebar-sub-item.active .sidebar-sub-link {
  background: var(--vp-c-brand-soft);
  color: var(--vp-c-brand-1);
  font-weight: 500;
}

.sidebar-toc {
  margin-top: var(--vp-space-2xl);
  padding-top: var(--vp-space-lg);
  border-top: 1px solid var(--vp-c-divider);
}

.toc-title {
  font-size: var(--vp-font-size-sm);
  font-weight: 600;
  color: var(--vp-c-text-1);
  margin: 0 0 var(--vp-space-md) 0;
}

@media (max-width: 1024px) {
  .custom-sidebar {
    transform: translateX(-100%);
    transition: transform 0.3s ease;
  }
  
  .custom-sidebar.open {
    transform: translateX(0);
  }
}
</style>

3. 内容区域定制

vue
<!-- .vitepress/theme/components/CustomContent.vue -->
<template>
  <main class="custom-content">
    <div class="content-container">
      <!-- 面包屑导航 -->
      <Breadcrumb v-if="showBreadcrumb" />
      
      <!-- 文章头部 -->
      <header v-if="frontmatter.title" class="content-header">
        <h1 class="content-title">{{ frontmatter.title }}</h1>
        
        <div v-if="frontmatter.description" class="content-description">
          {{ frontmatter.description }}
        </div>
        
        <div v-if="showMeta" class="content-meta">
          <span v-if="frontmatter.author" class="meta-author">
            作者: {{ frontmatter.author }}
          </span>
          <span v-if="frontmatter.date" class="meta-date">
            发布于: {{ formatDate(frontmatter.date) }}
          </span>
          <span v-if="readingTime" class="meta-reading-time">
            阅读时间: {{ readingTime }}
          </span>
        </div>
        
        <div v-if="frontmatter.tags" class="content-tags">
          <span 
            v-for="tag in frontmatter.tags"
            :key="tag"
            class="content-tag"
          >
            {{ tag }}
          </span>
        </div>
      </header>
      
      <!-- 文章内容 -->
      <article class="content-body">
        <Content />
      </article>
      
      <!-- 文章底部 -->
      <footer class="content-footer">
        <!-- 上一篇/下一篇导航 -->
        <PrevNext />
        
        <!-- 编辑链接 -->
        <EditLink v-if="showEditLink" />
        
        <!-- 最后更新时间 -->
        <LastUpdated v-if="showLastUpdated" />
        
        <!-- 分享按钮 -->
        <ShareButtons v-if="showShare" />
      </footer>
    </div>
    
    <!-- 右侧目录 -->
    <aside v-if="showToc" class="content-toc">
      <TableOfContents />
    </aside>
  </main>
</template>

<script setup>
import { computed } from 'vue'
import { useData } from 'vitepress'
import Breadcrumb from './Breadcrumb.vue'
import PrevNext from './PrevNext.vue'
import EditLink from './EditLink.vue'
import LastUpdated from './LastUpdated.vue'
import ShareButtons from './ShareButtons.vue'
import TableOfContents from './TableOfContents.vue'

const { page, frontmatter, theme } = useData()

const showBreadcrumb = computed(() => 
  theme.value.breadcrumb !== false && frontmatter.value.breadcrumb !== false
)

const showMeta = computed(() => 
  frontmatter.value.author || frontmatter.value.date
)

const showEditLink = computed(() => 
  theme.value.editLink && frontmatter.value.editLink !== false
)

const showLastUpdated = computed(() => 
  theme.value.lastUpdated && frontmatter.value.lastUpdated !== false
)

const showShare = computed(() => 
  theme.value.share !== false && frontmatter.value.share !== false
)

const showToc = computed(() => 
  theme.value.outline !== false && frontmatter.value.outline !== false
)

const readingTime = computed(() => {
  const content = page.value.content
  if (!content) return null
  
  const wordsPerMinute = 200
  const words = content.split(/\s+/).length
  const minutes = Math.ceil(words / wordsPerMinute)
  
  return `${minutes} 分钟`
})

const formatDate = (date) => {
  return new Date(date).toLocaleDateString('zh-CN', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  })
}
</script>

<style scoped>
.custom-content {
  display: flex;
  max-width: var(--vp-layout-max-width);
  margin: 0 auto;
  padding: var(--vp-space-lg);
  gap: var(--vp-space-2xl);
}

.content-container {
  flex: 1;
  max-width: var(--vp-content-max-width);
}

.content-header {
  margin-bottom: var(--vp-space-2xl);
  padding-bottom: var(--vp-space-lg);
  border-bottom: 1px solid var(--vp-c-divider);
}

.content-title {
  font-size: 2.5rem;
  font-weight: 700;
  line-height: 1.2;
  color: var(--vp-c-text-1);
  margin: 0 0 var(--vp-space-md) 0;
}

.content-description {
  font-size: var(--vp-font-size-lg);
  color: var(--vp-c-text-2);
  line-height: var(--vp-line-height-md);
  margin-bottom: var(--vp-space-lg);
}

.content-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vp-space-md);
  margin-bottom: var(--vp-space-md);
  font-size: var(--vp-font-size-sm);
  color: var(--vp-c-text-3);
}

.content-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vp-space-sm);
}

.content-tag {
  padding: var(--vp-space-xs) var(--vp-space-sm);
  background: var(--vp-c-brand-soft);
  color: var(--vp-c-brand-1);
  border-radius: var(--vp-border-radius-sm);
  font-size: var(--vp-font-size-xs);
  font-weight: 500;
}

.content-body {
  margin-bottom: var(--vp-space-2xl);
}

.content-footer {
  padding-top: var(--vp-space-lg);
  border-top: 1px solid var(--vp-c-divider);
}

.content-toc {
  width: 240px;
  position: sticky;
  top: var(--vp-space-lg);
  height: fit-content;
}

@media (max-width: 1024px) {
  .custom-content {
    flex-direction: column;
  }
  
  .content-toc {
    width: 100%;
    position: static;
  }
}

@media (max-width: 768px) {
  .custom-content {
    padding: var(--vp-space-md);
  }
  
  .content-title {
    font-size: 2rem;
  }
  
  .content-meta {
    flex-direction: column;
    gap: var(--vp-space-xs);
  }
}
</style>

高级定制技巧

1. 自定义布局

vue
<!-- .vitepress/theme/layouts/CustomLayout.vue -->
<template>
  <div class="custom-layout" :class="layoutClass">
    <!-- 顶部横幅 -->
    <Banner v-if="showBanner" />
    
    <!-- 导航栏 -->
    <CustomNavBar />
    
    <!-- 主要内容区域 -->
    <div class="layout-main">
      <!-- 侧边栏 -->
      <CustomSidebar v-if="showSidebar" />
      
      <!-- 内容区域 -->
      <div class="layout-content" :class="contentClass">
        <CustomContent />
      </div>
      
      <!-- 右侧栏 -->
      <aside v-if="showAside" class="layout-aside">
        <slot name="aside" />
      </aside>
    </div>
    
    <!-- 页脚 -->
    <CustomFooter />
    
    <!-- 回到顶部按钮 -->
    <BackToTop />
    
    <!-- 移动端菜单遮罩 -->
    <div 
      v-if="showMobileOverlay"
      class="mobile-overlay"
      @click="closeMobileMenu"
    />
  </div>
</template>

<script setup>
import { computed, ref } from 'vue'
import { useData } from 'vitepress'
import Banner from '../components/Banner.vue'
import CustomNavBar from '../components/CustomNavBar.vue'
import CustomSidebar from '../components/CustomSidebar.vue'
import CustomContent from '../components/CustomContent.vue'
import CustomFooter from '../components/CustomFooter.vue'
import BackToTop from '../components/BackToTop.vue'

const { page, frontmatter, theme } = useData()

const showMobileMenu = ref(false)

const layoutClass = computed(() => ({
  'has-sidebar': showSidebar.value,
  'has-aside': showAside.value,
  'mobile-menu-open': showMobileMenu.value
}))

const contentClass = computed(() => ({
  'full-width': !showSidebar.value && !showAside.value,
  'with-sidebar': showSidebar.value,
  'with-aside': showAside.value
}))

const showBanner = computed(() => 
  theme.value.banner && frontmatter.value.banner !== false
)

const showSidebar = computed(() => 
  theme.value.sidebar && frontmatter.value.sidebar !== false
)

const showAside = computed(() => 
  theme.value.aside && frontmatter.value.aside !== false
)

const showMobileOverlay = computed(() => 
  showMobileMenu.value
)

const closeMobileMenu = () => {
  showMobileMenu.value = false
}
</script>

<style scoped>
.custom-layout {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.layout-main {
  flex: 1;
  display: flex;
  position: relative;
}

.layout-content {
  flex: 1;
  min-width: 0;
}

.layout-content.with-sidebar {
  margin-left: var(--vp-sidebar-width-medium);
}

.layout-content.with-aside {
  margin-right: 280px;
}

.layout-aside {
  width: 280px;
  position: fixed;
  top: 64px;
  right: 0;
  height: calc(100vh - 64px);
  overflow-y: auto;
  background: var(--vp-c-bg-alt);
  border-left: 1px solid var(--vp-c-divider);
  padding: var(--vp-space-lg);
}

.mobile-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 9;
  display: none;
}

@media (max-width: 1024px) {
  .layout-content.with-sidebar {
    margin-left: 0;
  }
  
  .layout-content.with-aside {
    margin-right: 0;
  }
  
  .layout-aside {
    display: none;
  }
  
  .mobile-menu-open .mobile-overlay {
    display: block;
  }
}
</style>

2. 插件系统

javascript
// .vitepress/theme/plugins/index.js
export class ThemePlugin {
  constructor(name, options = {}) {
    this.name = name
    this.options = options
    this.hooks = {}
  }
  
  // 注册钩子
  hook(name, callback) {
    if (!this.hooks[name]) {
      this.hooks[name] = []
    }
    this.hooks[name].push(callback)
  }
  
  // 执行钩子
  async executeHook(name, context = {}) {
    const hooks = this.hooks[name] || []
    for (const hook of hooks) {
      await hook(context)
    }
  }
}

// 代码高亮插件
export class CodeHighlightPlugin extends ThemePlugin {
  constructor(options = {}) {
    super('code-highlight', options)
    this.setupHighlight()
  }
  
  setupHighlight() {
    this.hook('content:updated', async (context) => {
      const codeBlocks = context.el.querySelectorAll('pre code')
      
      codeBlocks.forEach(block => {
        this.enhanceCodeBlock(block)
      })
    })
  }
  
  enhanceCodeBlock(block) {
    const pre = block.parentElement
    
    // 添加复制按钮
    const copyButton = document.createElement('button')
    copyButton.className = 'copy-code-button'
    copyButton.textContent = '复制'
    copyButton.onclick = () => this.copyCode(block)
    
    pre.appendChild(copyButton)
    
    // 添加行号
    if (this.options.lineNumbers) {
      this.addLineNumbers(block)
    }
    
    // 添加语言标签
    const language = this.getLanguage(block)
    if (language) {
      const langLabel = document.createElement('span')
      langLabel.className = 'code-language'
      langLabel.textContent = language
      pre.appendChild(langLabel)
    }
  }
  
  copyCode(block) {
    const code = block.textContent
    navigator.clipboard.writeText(code).then(() => {
      // 显示复制成功提示
      this.showCopySuccess()
    })
  }
  
  addLineNumbers(block) {
    const lines = block.textContent.split('\n')
    const lineNumbers = document.createElement('div')
    lineNumbers.className = 'line-numbers'
    
    lines.forEach((_, index) => {
      const lineNumber = document.createElement('span')
      lineNumber.textContent = index + 1
      lineNumbers.appendChild(lineNumber)
    })
    
    block.parentElement.appendChild(lineNumbers)
  }
  
  getLanguage(block) {
    const className = block.className
    const match = className.match(/language-(\w+)/)
    return match ? match[1] : null
  }
  
  showCopySuccess() {
    // 实现复制成功提示
    const toast = document.createElement('div')
    toast.className = 'copy-toast'
    toast.textContent = '代码已复制到剪贴板'
    document.body.appendChild(toast)
    
    setTimeout(() => {
      document.body.removeChild(toast)
    }, 2000)
  }
}

// 图片懒加载插件
export class LazyLoadPlugin extends ThemePlugin {
  constructor(options = {}) {
    super('lazy-load', options)
    this.observer = null
    this.setupLazyLoad()
  }
  
  setupLazyLoad() {
    this.hook('content:updated', (context) => {
      this.observeImages(context.el)
    })
    
    this.createObserver()
  }
  
  createObserver() {
    this.observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          this.loadImage(entry.target)
          this.observer.unobserve(entry.target)
        }
      })
    }, {
      rootMargin: '50px'
    })
  }
  
  observeImages(container) {
    const images = container.querySelectorAll('img[data-src]')
    images.forEach(img => {
      this.observer.observe(img)
    })
  }
  
  loadImage(img) {
    const src = img.dataset.src
    if (src) {
      img.src = src
      img.removeAttribute('data-src')
      img.classList.add('loaded')
    }
  }
}

3. 主题配置系统

javascript
// .vitepress/theme/config/theme-config.js
export class ThemeConfig {
  constructor() {
    this.config = {
      // 布局配置
      layout: {
        navbar: true,
        sidebar: true,
        aside: true,
        footer: true
      },
      
      // 样式配置
      style: {
        colorScheme: 'auto', // light, dark, auto
        primaryColor: '#3eaf7c',
        fontFamily: 'Inter',
        borderRadius: '6px'
      },
      
      // 功能配置
      features: {
        search: true,
        darkMode: true,
        editLink: true,
        lastUpdated: true,
        prevNext: true,
        outline: true
      },
      
      // 插件配置
      plugins: {
        codeHighlight: {
          enabled: true,
          lineNumbers: true,
          copyButton: true
        },
        lazyLoad: {
          enabled: true,
          placeholder: '/images/placeholder.svg'
        }
      }
    }
  }
  
  // 合并配置
  merge(userConfig) {
    this.config = this.deepMerge(this.config, userConfig)
    return this
  }
  
  // 深度合并对象
  deepMerge(target, source) {
    const result = { ...target }
    
    for (const key in source) {
      if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
        result[key] = this.deepMerge(target[key] || {}, source[key])
      } else {
        result[key] = source[key]
      }
    }
    
    return result
  }
  
  // 获取配置值
  get(path, defaultValue = null) {
    const keys = path.split('.')
    let current = this.config
    
    for (const key of keys) {
      if (current && typeof current === 'object' && key in current) {
        current = current[key]
      } else {
        return defaultValue
      }
    }
    
    return current
  }
  
  // 设置配置值
  set(path, value) {
    const keys = path.split('.')
    const lastKey = keys.pop()
    let current = this.config
    
    for (const key of keys) {
      if (!(key in current) || typeof current[key] !== 'object') {
        current[key] = {}
      }
      current = current[key]
    }
    
    current[lastKey] = value
  }
  
  // 应用样式配置
  applyStyles() {
    const root = document.documentElement
    const style = this.get('style')
    
    if (style.primaryColor) {
      root.style.setProperty('--vp-c-brand-1', style.primaryColor)
    }
    
    if (style.fontFamily) {
      root.style.setProperty('--vp-font-family-base', style.fontFamily)
    }
    
    if (style.borderRadius) {
      root.style.setProperty('--vp-border-radius-md', style.borderRadius)
    }
  }
}

// 使用示例
const themeConfig = new ThemeConfig()
  .merge({
    style: {
      primaryColor: '#007bff',
      fontFamily: 'Roboto'
    },
    features: {
      search: false
    }
  })

export default themeConfig

实战案例

1. 企业文档站点主题

javascript
// .vitepress/config.js - 企业主题配置
export default {
  title: '企业产品文档',
  description: '专业的企业级产品文档站点',
  
  themeConfig: {
    // 企业 Logo 和品牌
    logo: '/logo-enterprise.svg',
    siteTitle: '企业文档中心',
    
    // 企业色彩方案
    primaryColor: '#1890ff',
    
    // 导航配置
    nav: [
      { text: '产品介绍', link: '/product/' },
      { text: 'API 文档', link: '/api/' },
      { text: '最佳实践', link: '/best-practices/' },
      { text: '支持中心', link: '/support/' }
    ],
    
    // 企业功能
    features: {
      feedback: true,
      analytics: true,
      search: {
        provider: 'algolia',
        options: {
          appId: 'YOUR_APP_ID',
          apiKey: 'YOUR_API_KEY',
          indexName: 'docs'
        }
      }
    },
    
    // 页脚信息
    footer: {
      message: '© 2025 企业名称. 保留所有权利.',
      links: [
        { text: '隐私政策', link: '/privacy' },
        { text: '服务条款', link: '/terms' },
        { text: '联系我们', link: '/contact' }
      ]
    }
  }
}

2. 开源项目文档主题

javascript
// .vitepress/config.js - 开源项目主题配置
export default {
  title: 'OpenSource Project',
  description: '开源项目文档',
  
  themeConfig: {
    // GitHub 集成
    repo: 'username/project',
    repoLabel: 'GitHub',
    editLinks: true,
    editLinkText: '在 GitHub 上编辑此页',
    
    // 社交链接
    socialLinks: [
      { icon: 'github', link: 'https://github.com/username/project' },
      { icon: 'twitter', link: 'https://twitter.com/username' },
      { icon: 'discord', link: 'https://discord.gg/invite' }
    ],
    
    // 贡献者展示
    contributors: true,
    
    // 版本管理
    versions: [
      { text: 'v2.0', link: '/v2/' },
      { text: 'v1.0', link: '/v1/' }
    ],
    
    // 开源特色功能
    features: {
      stargazers: true,
      issues: true,
      discussions: true
    }
  }
}

性能优化

1. 样式优化

css
/* 关键 CSS 内联 */
:root {
  /* 只包含首屏必需的 CSS 变量 */
  --vp-c-brand-1: #3eaf7c;
  --vp-c-bg: #ffffff;
  --vp-c-text-1: rgba(60, 60, 67);
}

/* 非关键 CSS 异步加载 */
@media print {
  /* 打印样式 */
}

/* 预加载字体 */
@font-face {
  font-family: 'Inter';
  src: url('./fonts/inter.woff2') format('woff2');
  font-display: swap;
}

/* 优化动画性能 */
.smooth-transition {
  transition: transform 0.2s ease;
  will-change: transform;
}

.smooth-transition:hover {
  transform: translateY(-2px);
}

2. JavaScript 优化

javascript
// 组件懒加载
const LazyComponent = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})

// 防抖搜索
import { debounce } from 'lodash-es'

const debouncedSearch = debounce((query) => {
  // 执行搜索
}, 300)

// 虚拟滚动(大列表优化)
import { RecycleScroller } from 'vue-virtual-scroller'

export default {
  components: {
    RecycleScroller
  }
}

总结

VitePress 主题定制提供了强大的灵活性,让你可以创建独特的文档体验。关键要点:

  1. 渐进式定制:从 CSS 变量开始,逐步深入到组件定制
  2. 组件化思维:将复杂功能拆分为可复用的组件
  3. 性能优先:始终考虑加载性能和用户体验
  4. 可维护性:保持代码结构清晰,便于后续维护

通过合理运用这些技巧,你可以打造出既美观又实用的文档站点。

参考资源

vitepress开发指南