Skip to content
VitePress 主题深度定制:集成 VitePress 原生不支持的功能

VitePress 主题深度定制:集成 VitePress 原生不支持的功能

VitePress

基于 实际项目,深入讲解如何深度定制 VitePress 主题,集成打字机效果、光标动画、双主题切换等原生不支持的功能

标签:
Tailwind CSS DaisyUI 动画效果
发布于 2025年12月19日

主题定制概述

VitePress 提供了良好的默认主题,但在实际项目中,我们经常需要集成一些原生不支持的功能。 通过深度定制,实现了以下高级功能:

  1. 打字机效果:首页每日一言的动态显示
  2. 光标动画:自定义光标样式和动画
  3. 运行时间计数器:显示博客运行时间
  4. 双主题系统:light/dracula 主题切换
  5. 高级网格系统:扩展的 Tailwind 网格配置

Tailwind CSS 深度配置

tailwind.config.js 详细解析

javascript
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{vue,js,ts,jsx,tsx,md}",
    "./.vitepress/**/*.{vue,js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      colors: {
        primary: {
          50: "#eff6ff",
          100: "#dbeafe",
          200: "#bfdbfe",
          300: "#93c5fd",
          400: "#60a5fa",
          500: "#3b82f6",
          600: "#2563eb",
          700: "#1d4ed8",
          800: "#1e40af",
          900: "#1e3a8a",
        },
      },
      fontFamily: {
        sans: ["Inter", "ui-sans-serif", "system-ui"],
      },
      gridColumn: {
        'span-13': 'span 13 / span 13',
        'span-14': 'span 14 / span 14',
        'span-15': 'span 15 / span 15',
        'span-16': 'span 16 / span 16',
      },
      gridRow: {
        'span-13': 'span 13 / span 13',
        'span-14': 'span 14 / span 14',
        'span-15': 'span 15 / span 15',
        'span-16': 'span 16 / span 16',
      },
      gridColumnStart: {
        '13': '13',
        '14': '14',
        '15': '15',
        '16': '16',
        '17': '17',
        '18': '18',
        '19': '19',
        '20': '20',
        '21': '21',
        '22': '22',
        '23': '23',
        '24': '24',
      },
      gridColumnEnd: {
        '13': '13',
        '14': '14',
        '15': '15',
        '16': '16',
        '17': '17',
        '18': '18',
        '19': '19',
        '20': '20',
        '21': '21',
        '22': '22',
        '23': '23',
        '24': '24',
      },
      gridRowStart: {
        '13': '13',
        '14': '14',
        '15': '15',
        '16': '16',
        '17': '17',
        '18': '18',
        '19': '19',
        '20': '20',
        '21': '21',
        '22': '22',
        '23': '23',
        '24': '24',
      },
      gridRowEnd: {
        '13': '13',
        '14': '14',
        '15': '15',
        '16': '16',
        '17': '17',
        '18': '18',
        '19': '19',
        '20': '20',
        '21': '21',
        '22': '22',
        '23': '23',
        '24': '24',
      },
    },
  },
  plugins: [require("@tailwindcss/typography"), require("daisyui")],
  daisyui: {
    themes: ["light", "dracula"],
    darkTheme: "dracula",
    base: true,
    styled: true,
    utils: true,
    prefix: "",
    logs: false,
    themeRoot: ":root",
  },
};

关键配置说明

  1. 颜色系统扩展:自定义 primary 颜色调色板
  2. 字体配置:使用 Inter 字体作为默认 sans 字体
  3. 网格系统扩展:支持 24 列网格系统,满足复杂布局需求
  4. DaisyUI 集成:配置 light/dracula 双主题系统

打字机效果实现

HomeHero.vue 组件核心代码

vue
<!-- 打字机效果容器 -->
<div class="typewriter-container min-h-[120px] flex items-center justify-center">
  <div class="typewriter-content text-center max-w-3xl mx-auto">
    <!-- 引用符号 -->
    <div class="text-4xl md:text-5xl text-primary/30 mb-4">❝</div>

    <!-- 打字机文本 -->
    <div class="typewriter-text text-xl md:text-2xl font-medium text-base-content leading-relaxed mb-4">
      <span class="typed-text">{{ typedText }}</span>
      <span class="cursor" :class="{ 'blinking': isTypingComplete }">|</span>
    </div>

    <!-- 作者信息 -->
    <div v-if="quoteAuthor" class="author-info text-base-content/70 text-sm md:text-base italic mt-6">
      —— {{ quoteAuthor }}
    </div>
  </div>
</div>

打字机动画逻辑

typescript
// 打字机效果状态
const typedText = ref<string>('');
const isTypingComplete = ref<boolean>(false);
const quoteAuthor = ref<string>('');
const typingSpeed = 100; // 打字速度(毫秒/字符)
const pauseAfterTyping = 1000; // 打字完成后的暂停时间(毫秒)
let typingInterval: NodeJS.Timeout | null = null;

// 开始打字机动画
const startTypingAnimation = (quote: string, author: string) => {
  // 重置状态
  typedText.value = '';
  isTypingComplete.value = false;
  quoteAuthor.value = author;

  let charIndex = 0;

  // 清除之前的定时器
  if (typingInterval) {
    clearInterval(typingInterval);
    typingInterval = null;
  }

  // 开始打字
  const typeNextChar = () => {
    if (charIndex < quote.length) {
      typedText.value += quote.charAt(charIndex);
      charIndex++;
      typingInterval = setTimeout(typeNextChar, typingSpeed);
    } else {
      // 打字完成
      typingInterval = null;

      // 延迟后标记为完成
      setTimeout(() => {
        isTypingComplete.value = true;
      }, pauseAfterTyping);
    }
  };

  // 开始打字动画
  typingInterval = setTimeout(typeNextChar, typingSpeed);
};

样式实现

css
/* 打字机效果样式 */
.typewriter-container {
  position: relative;
}

.typewriter-content {
  position: relative;
  z-index: 1;
}

.typewriter-text {
  position: relative;
  min-height: 1.5em;
}

.typed-text {
  display: inline-block;
  white-space: pre-wrap;
  word-break: break-word;
}

.cursor {
  display: inline-block;
  margin-left: 2px;
  color: var(--primary);
  font-weight: bold;
  animation: cursor-blink 1s infinite;
}

.cursor.blinking {
  animation: cursor-blink 0.7s infinite;
}

@keyframes cursor-blink {
  0%, 50% {
    opacity: 1;
  }
  51%, 100% {
    opacity: 0;
  }
}

光标动画系统

cursor.mts 实现

typescript
// .vitepress/theme/hooks/cursor.mts
export const cursor = () => {
  const initCursor = () => {
    // 创建自定义光标元素
    const cursor = document.createElement('div');
    cursor.id = 'custom-cursor';
    cursor.style.cssText = `
      position: fixed;
      width: 20px;
      height: 20px;
      border: 2px solid var(--primary);
      border-radius: 50%;
      pointer-events: none;
      z-index: 9999;
      transform: translate(-50%, -50%);
      transition: transform 0.1s ease;
      mix-blend-mode: difference;
    `;
    document.body.appendChild(cursor);

    // 跟踪鼠标移动
    document.addEventListener('mousemove', (e) => {
      cursor.style.left = `${e.clientX}px`;
      cursor.style.top = `${e.clientY}px`;
    });

    // 点击效果
    document.addEventListener('mousedown', () => {
      cursor.style.transform = 'translate(-50%, -50%) scale(0.8)';
    });

    document.addEventListener('mouseup', () => {
      cursor.style.transform = 'translate(-50%, -50%) scale(1)';
    });

    // 悬停效果
    const interactiveElements = document.querySelectorAll('a, button, [role="button"]');
    interactiveElements.forEach(el => {
      el.addEventListener('mouseenter', () => {
        cursor.style.transform = 'translate(-50%, -50%) scale(1.5)';
        cursor.style.backgroundColor = 'var(--primary)';
      });
      
      el.addEventListener('mouseleave', () => {
        cursor.style.transform = 'translate(-50%, -50%) scale(1)';
        cursor.style.backgroundColor = 'transparent';
      });
    });
  };

  return { initCursor };
};

运行时间计数器

duration.mts 实现

typescript
// .vitepress/theme/hooks/duration.mts
export const createRuntimeCounter = (startDate: Date, dateElementId: string, timeElementId: string) => {
  const calculateRuntime = () => {
    const now = new Date();
    const diff = now.getTime() - startDate.getTime();
    
    // 计算年、月、日
    const years = Math.floor(diff / (1000 * 60 * 60 * 24 * 365));
    const months = Math.floor((diff % (1000 * 60 * 60 * 24 * 365)) / (1000 * 60 * 60 * 24 * 30));
    const days = Math.floor((diff % (1000 * 60 * 60 * 24 * 30)) / (1000 * 60 * 60 * 24));
    
    // 计算时、分、秒
    const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
    const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
    const seconds = Math.floor((diff % (1000 * 60)) / 1000);
    
    // 更新日期显示
    const dateElement = document.getElementById(dateElementId);
    if (dateElement) {
      dateElement.textContent = `${years}年${months}月${days}天`;
    }
    
    // 更新时间显示
    const timeElement = document.getElementById(timeElementId);
    if (timeElement) {
      timeElement.textContent = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
    }
  };
  
  // 初始计算
  calculateRuntime();
  
  // 每秒更新一次
  setInterval(calculateRuntime, 1000);
};

双主题系统集成

DaisyUI 主题配置

javascript
daisyui: {
  themes: ["light", "dracula"],
  darkTheme: "dracula",
  base: true,
  styled: true,
  utils: true,
  prefix: "",
  logs: false,
  themeRoot: ":root",
}

主题切换实现

vue
<!-- 主题切换组件示例 -->
<template>
  <button 
    class="btn btn-ghost btn-circle"
    @click="toggleTheme"
    :aria-label="`切换到${isDark ? '亮色' : '暗色'}主题`"
  >
    <Icon 
      :icon="isDark ? 'lucide:sun' : 'lucide:moon'" 
      class="w-5 h-5"
    />
  </button>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import Icon from './Icon.vue'

const isDark = ref(false)

const toggleTheme = () => {
  isDark.value = !isDark.value
  if (isDark.value) {
    document.documentElement.setAttribute('data-theme', 'dracula')
  } else {
    document.documentElement.setAttribute('data-theme', 'light')
  }
  // 保存主题偏好
  localStorage.setItem('theme', isDark.value ? 'dracula' : 'light')
}

onMounted(() => {
  // 读取保存的主题偏好
  const savedTheme = localStorage.getItem('theme')
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
  
  if (savedTheme === 'dracula' || (!savedTheme && prefersDark)) {
    isDark.value = true
    document.documentElement.setAttribute('data-theme', 'dracula')
  }
})
</script>

每日一言功能

dailyQuote.mts 实现

typescript
// .vitepress/theme/utils/dailyQuote.mts
import quotes from '../../src/assets/json/quotes.json'

export const getDailyQuote = async (useOnline: boolean = true): Promise<string> => {
  try {
    if (useOnline) {
      // 尝试获取在线名言
      const response = await fetch('https://api.quotable.io/random')
      if (response.ok) {
        const data = await response.json()
        return `${data.content} —— ${data.author}`
      }
    }
    
    // 使用本地名言作为回退
    const today = new Date()
    const dayOfYear = Math.floor((today.getTime() - new Date(today.getFullYear(), 0, 0).getTime()) / (1000 * 60 * 60 * 24))
    const index = dayOfYear % quotes.length
    return quotes[index]
  } catch (error) {
    // 出错时使用本地名言
    const randomIndex = Math.floor(Math.random() * quotes.length)
    return quotes[randomIndex]
  }
}

总结

通过深度定制 VitePress 主题, 实现了许多原生不支持的高级功能:

  1. 视觉效果增强:打字机效果、光标动画提升了用户体验
  2. 交互功能丰富:运行时间计数器、每日一言增加了博客的趣味性
  3. 主题系统完善:双主题切换支持用户个性化偏好
  4. 布局系统强大:扩展的网格系统支持复杂布局设计

这些定制不仅提升了博客的功能性,也展示了 VitePress 主题系统的强大扩展能力。通过合理的架构设计和代码组织,我们可以轻松集成各种自定义功能,打造独特的博客体验。

在下一篇文章中,我们将深入探讨组件化开发实践,包括自动组件注册、组件架构设计和 TypeScript 类型系统。

Last updated: