主题定制概述
VitePress 提供了良好的默认主题,但在实际项目中,我们经常需要集成一些原生不支持的功能。 通过深度定制,实现了以下高级功能:
- 打字机效果:首页每日一言的动态显示
- 光标动画:自定义光标样式和动画
- 运行时间计数器:显示博客运行时间
- 双主题系统:light/dracula 主题切换
- 高级网格系统:扩展的 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",
},
};关键配置说明
- 颜色系统扩展:自定义 primary 颜色调色板
- 字体配置:使用 Inter 字体作为默认 sans 字体
- 网格系统扩展:支持 24 列网格系统,满足复杂布局需求
- 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 主题, 实现了许多原生不支持的高级功能:
- 视觉效果增强:打字机效果、光标动画提升了用户体验
- 交互功能丰富:运行时间计数器、每日一言增加了博客的趣味性
- 主题系统完善:双主题切换支持用户个性化偏好
- 布局系统强大:扩展的网格系统支持复杂布局设计
这些定制不仅提升了博客的功能性,也展示了 VitePress 主题系统的强大扩展能力。通过合理的架构设计和代码组织,我们可以轻松集成各种自定义功能,打造独特的博客体验。
在下一篇文章中,我们将深入探讨组件化开发实践,包括自动组件注册、组件架构设计和 TypeScript 类型系统。