Skip to content

VitePress系列教程:内容展示优化#5

首页组件

如果我们觉得首页内容比较单调,想自已设计点可交互的内容,我们可以通过以下方案追加样式内容

我们在首页index.md中添加内容

详细信息
md
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
title: GuiZhi # 标签页选项卡片显示名称

hero:
  name: "归至晨光拾阶行"
  text: "DawnStair"
  tagline: 晨光熹微踏阶行,一行一步一风景。
  image:
    src: logo-gz.png
    alt: GZ(GuiZhi)
  actions:
    - theme: brand
      text: Markdown Examples
      link: /markdown-examples
    - theme: alt
      text: API Examples
      link: /api-examples

features:
  - title: 晨光意象
    icon: 🌅
    details: 晨光是一天中最柔和的光线,不刺眼、不炽热,却足以照亮前行的每一级台阶。它代表着开始、希望与可能性。在光影交替的边界,事物刚刚显露出轮廓,尚未被正午的强光抹去棱角——那是一天之中最适合观察与出发的时刻。
  
  - title: 拾阶而行
    icon: 🪜
    details: 拾阶是一种朴素的行走方式,不跳跃、不飞跃,一步接一步地向上。每一级台阶都被认真踏过,没有哪一步可以被省略。这种节奏既不是急切的奔跑,也不是停滞的观望,而是一种稳定而持续的前进,带着对每一步的尊重与确认。
  
  - title: 归至之境
    icon: 🧘
    details: 归至指向一种返回本源的状态。它不是前进,也不是后退,而是向内收束、回到最朴素的出发点。在纷繁复杂之后,主动选择一种更简单、更纯粹的存在方式——不添加多余的东西,也不刻意减损什么,只是回到事物本来的样子。
  
  - title: 晨昏交界
    icon: 🌄
    details: 晨光出现的时刻,正是黑夜与白昼的交界。这个时刻充满张力——旧的事物尚未完全退去,新的事物正在生成。站在交界处的人,既看得见来路,也望得见去路。这种双重视野让人既保持清醒的回顾,又拥有开放的展望,不偏执于任何一端。
  
  - title: 步步生长
    icon: 🌿
    details: 拾阶而上的过程,本质上是持续累积的过程。每一级台阶都记录着一次迈步,每一次迈步都留下一个印记。台阶不会因为被踏过就消失,相反,它们构成了完整的路径。一个人走过的台阶越多,路径就越清晰,越能够被后来者看见和辨认。
  
  - title: 光阶同行
    icon: ✨
    details: 当晨光与台阶相遇,行走就不仅仅是行走——每一步都有了光的陪伴。光线勾勒出台阶的边缘,让路径变得清晰可辨;而台阶则将光线承接、折射,让光有了具体的形状和落点。行走者、光线与台阶三者共同构成了一幅完整而宁静的画面。
---

添加的内容是能直接染到首页的Features下,Footer上的空白部分的,由于vitePress使用的是markdown it解析的md,所以也能直接写html标签

将新增./guide/writting/parts/footer-show.md
md
# Python 语言

## Python 基础知识
<div class="card-grid">
  <div class="category-card">
    <div class="card-title">Python 核心编程</div>
    <div class="card-desc">Python 编程的核心知识点。</div>
  </div>
  <div class="category-card">
    <div class="card-title">Python 高级开发</div>
    <div class="card-desc">内置函数、内置模块、网络编程、并发编程、装饰器、迭代器、生成器、魔法函数,都是进阶内容。</div>
  </div>
  <div class="category-card">
    <div class="card-title">Python 开发桌面程序</div>
    <div class="card-desc">tkinter 非常方便,兼容性也不错,而且是 Python 自带,用来写小工具非常合适。</div>
  </div>
  <div class="category-card">
    <div class="card-title">Python 自动化办公</div>
    <div class="card-desc">Python 非常适合编写小脚本提高办公的效率。</div>
  </div>
</div>

## Python Web 开发
<div class="card-grid">
  <div class="category-card">
    <div class="card-title">网站开发 - 前端基础</div>
    <div class="card-desc">快速入门 Python web 前端开发。</div>
  </div>
  <div class="category-card">
    <div class="card-title">网站开发 - flask 框架</div>
    <div class="card-desc">flask 是一个非常易于学习的框架,适合一步步深入的学习,最终搭建复杂的应用。</div>
  </div>
  <div class="category-card">
    <div class="card-title">网站开发 - 后台管理系统</div>
    <div class="card-desc">flask + layui 快速搭建后台管理系统</div>
  </div>
  <div class="category-card">
    <div class="card-title">网站开发 - 正心论坛</div>
    <div class="card-desc">利用 flask 框架知识,结合业务知识,完成正心论坛的项目实战。</div>
  </div>
</div>

## Python 数据科学
<div class="card-grid">
  <div class="category-card">
    <div class="card-title">Python - 数据分析</div>
    <div class="card-desc">通往数据科学的第一步</div>
  </div>
  <div class="category-card">
    <div class="card-title">Python - 机器学习</div>
    <div class="card-desc">深度挖掘数据的价值</div>
  </div>
</div>

<style>
/* 分类大标题样式 */
.section-main-title {
  font-size: 28px;
  font-weight: 600;
  margin: 40px 0 12px;
  color: #222;
}
.section-sub-title {
  font-size: 22px;
  font-weight: 500;
  margin: 32px 0 16px;
  color: #333;
}

/* 卡片网格容器,自动自适应列数 */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
  gap: 20px;
  margin-bottom: 24px;
}

/* 卡片基础样式 */
.category-card {
  padding: 28px 24px;
  border-radius: 12px;
  background-color: #f7f7f8;
  transition: all 0.24s ease;
  cursor: pointer;
  border: 1px solid transparent;
}
/* 悬浮蓝色半透美化 */
.category-card:hover {
  background-color: rgba(41, 118, 227, 0.12);
  border-color: rgba(41, 118, 227, 0.2);
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(41, 118, 227, 0.08);
}

/* 卡片标题文字 */
.card-title {
  font-size: 18px;
  font-weight: 500;
  color: #1e293b;
  margin-bottom: 10px;
}
/* 卡片描述小字 */
.card-desc {
  font-size: 14px;
  color: #666;
  line-height: 1.6;
}

/* ========== 深色模式适配 ========== */
:root.dark .section-main-title {
  color: #eee;
}
:root.dark .section-sub-title {
  color: #ddd;
}
:root.dark .category-card {
  background-color: #242428;
}
:root.dark .category-card:hover {
  background-color: rgba(70, 145, 245, 0.18);
  border-color: rgba(70, 145, 245, 0.25);
}
:root.dark .card-title {
  color: #e8e8ed;
}
:root.dark .card-desc {
  color: #a0a0b0;
}
</style>

然后再 index.md中进行引用

md
<!--@include: ./guide/writting/parts/footer-show.md -->

提示

还可以使用tailwindcss之类预设样式类,需要安装拓展插件才可以

Vue SFC组件

我们可以自己写一个SFC,注册到工程中,然后在md中使用

SFC VUE 介绍
md
Vue SFC(Single-File Component,单文件组件)是 Vue.js 特有的以**.vue**为扩展名的文件格式,将**模板(HTML)、逻辑(JS/TS)和样式(CSS)**封装在单一文件中,用于构建模块化、可复用的组件 。

### 核心结构

- `<template>`:定义组件的 HTML 结构(必选,通常需有唯一根节点)
- `<script>`:编写组件逻辑(数据、方法、生命周期等,必选;支持 ES Module 导出)
- `<style>`:定义组件样式(可选;支持 `scoped` 实现局部作用域,也可使用 Sass/Less 等预处理器)

### 关键特性

- **模块化开发**:相关代码紧耦合,提升维护性与可读性
- **工程化支持**:需通过构建工具(如 Vite、Webpack + vue-loader)编译为浏览器可执行代码
- **高级能力**:支持热重载、响应式样式(Vue 3 中 via `v-bind` in CSS)、TypeScript 及预处理器集成 

该格式是 Vue 生态的标准组件写法,官方脚手架(如 Vite + Vue 模板)默认生成此类文件 。
[Vue SFC 介绍.md](Vue%20SFC%20%E4%BB%8B%E7%BB%8D.md)

1、在theme目录中创建components目录,然后创建Counter.vue

详细信息
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
  count.value++
}
</script>

<template>
  <div class="counter-wrap">
    <p class="counter">Count: {{ count }}</p>
    <button class="i-click" @click="increment">Increment</button>
  </div>
</template>

<!-- Vue 中 <style scoped> 的含义是‌将样式限定在当前组件内生效,
    通过编译时注入唯一数据属性(data-v-xxxx)实现组件级样式隔离,防止全局污染和命名冲突‌。‌‌ -->
<style >
/* 外层容器,增加间距 */
.counter-wrap {
  margin: 1.5rem 0;
}

.counter {
  font-size: 1.5rem;
  margin-bottom: 1rem;
  font-weight: 500;
  letter-spacing: 0.5px;
}

/* 按钮基础美化 + 明暗主题变量 */
:root {
  --btn-i-primary: #00bfa5;
  --btn-i-primary-hover: #00a892;
  --btn-i-text: #ffffff;
  --btn-i-shadow: rgba(0, 191, 165, 0.25);
}
/* 暗色适配 */
:root.dark {
  --btn-i-primary: #00d1b5;
  --btn-i-primary-hover: #00e0c1;
  --btn-i-text: #111111;
  --btn-i-shadow: rgba(0, 209, 181, 0.2);
}

button.i-click {
  padding: 0.6rem 1.4rem;
  font-size: 1rem;
  cursor: pointer;
  border: none;
  border-radius: 8px;
  background-color: var(--btn-i-primary);
  color: var(--btn-i-text);
  font-weight: 500;
  /* 平滑过渡所有属性 */
  transition: all 0.22s ease;
  box-shadow: 0 2px 8px var(--btn-i-shadow);
}

/* 悬浮效果 */
button.i-click:hover {
  background-color: var(--btn-i-primary-hover);
  transform: translateY(-2px);
  box-shadow: 0 4px 12px var(--btn-i-shadow);
}

/* 点击按压效果 */
button.i-click:active {
  transform: translateY(0);
  box-shadow: 0 1px 4px var(--btn-i-shadow);
}

/* 禁用状态预留(可选) */
button.i-click:disabled {
  opacity: 0.6;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}
</style>

2、在.vitepress/theme/index.ts'中注册Counter.vue组件

详细信息
ts
// https://vitepress.dev/guide/custom-theme
import { h } from 'vue'
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import './style.css'
import './custome01/styles.css'

import ElementPlus from 'element-plus'
// 全局引入Element Plus完整样式
import 'element-plus/dist/index.css'
// Element Plus 暗黑主题,自动跟随页面dark类切换
import 'element-plus/theme-chalk/dark/css-vars.css'


// @ts-ignore
import Counter from './components/Counter.vue'
// @ts-ignore
import ClockFlip from './components/clock-flip/ClockFlip.vue'
// @ts-ignore
import PromoteBanner from './components/PromoteBanner.vue'

import './custome01/custom.css' // 直接导入不行:import 'tailwindcss'

// 引入头像组件
//@ts-ignore
import UserAvatar from './components/UserAvatar.vue'

// 导入计时组件
// @ts-ignore
import SiteRuntime from './components/SiteRuntime.vue'

export default {
  extends: DefaultTheme,
  Layout: () => {
    return h(DefaultTheme.Layout, null, {
      // https://vitepress.dev/guide/extending-default-theme#layout-slots
      'home-hero-info-after': () => h(ClockFlip),
      'doc-footer-before': () => h(PromoteBanner),
      // 使用插槽显示头像组件
      'nav-bar-content-after': () => h(UserAvatar),
      // 'layout-bottom': () => h(SiteRuntime),
      // 全局自定义页脚插槽,替换默认footer
      'layout-bottom': () => h('div', { class: 'custom-footer' }, [
        // 1. 运行计时组件
        h('p', { class: 'text-center text-gray-600 dark:text-gray-400 my-2' }, '归至而行,初心一道.'),
        // 2. 自定义标语
        h(SiteRuntime),
        // 3. 版权 + ICP
        h('p', { class: 'text-center text-gray-600 dark:text-gray-500 leading-relaxed' },
          `Copyright © 2026.07.17-${new Date().getFullYear()} @guizhi. All rights reserved  | 渝ICP备2026005423号-1`
        )
      ])
    })
  },
  enhanceApp({ app, router, siteData }) {
    app.component("Counter", Counter),
    app.component("ClockFlip", ClockFlip),
    // 全局挂载ElementPlus,全局所有md页面、自定义组件均可直接使用el-xxx组件
    app.use(ElementPlus)
    // ...
  }
} satisfies Theme

3、在首页index.md或当前页使用组件

md
<Counter/>

Count: 0

可以看到Counter组件成功被染出来,并且点击按钮能响应式变化数字

所以,对于复杂的交互组件,我们可以通过自定义SFC,然后在theme/index.js中注册组件,并在md中使用组件,达到想要的复杂交互效果

Vue style scoped
md
Vue 中 `<style scoped>` 的含义是将**样式限定在当前组件内生效,通过编译时注入唯一数据属性(`data-v-xxxx`)实现组件级样式隔离,防止全局污染和命名冲突**

### 核心机制与原理

- **作用本质**:启用后,该组件内的 CSS 规则仅匹配当前组件模板渲染的 DOM 元素,不会影响到其他组件或全局页面 。
- **实现方式**:Vue 编译器(via `vue-loader`/`@vue/compiler-sfc`)在构建阶段执行两步操作:
  
  1. 为模板中**每个元素**自动添加唯一哈希属性,如 `data-v-f3f3eg9`
  2. 将所有 CSS 选择器重写为追加该属性选择器的形式,例如 `.btn` 变为 `.btn[data-v-f3f3eg9]`
- **编译示例**
  
```md
 /* 源码 */
<style scoped>
.title { color: red; }
</style>

/* 编译后实际输出 */
.title[data-v-f3f3eg9] { color: red; }
```
  对应 HTML 元素:`<h1 class="title" data-v-f3f3eg9>...</h1>`

### 关键特性与边界

1. **隔离范围**:样式默认不穿透到子组件内部元素(除非子组件根元素继承了父组件的 `data-v` 属性,即子组件根节点会被父 scoped 样式影响);
2. **穿透需求**:若需修改子组件内部样式,须使用深度选择器(Vue 3 推荐 `:deep(.child)`,Vue 2 支持 `/deep/``::v-deep``>>>`),编译器会将其转换为跨组件的有效选择器组合 ;
3. **性能影响**:因依赖属性选择器匹配,渲染性能略低于普通类选择器,但在现代浏览器中差异极小,可忽略不计;
4. **适用场景**:适用于绝大多数业务组件,尤其大型项目中可显著降低样式冲突风险;全局通用样式(如重置、变量、布局基线)仍应放在无 `scoped``<style>` 或独立 CSS 文件中 。

### 注意事项

- `scoped`**单文件组件(SFC)**特有语法,普通 `.css` 文件不支持;
- 同一组件可同时存在 `<style>`(全局)和 `<style scoped>`(局部),二者共存时局部优先级由选择器特异性决定,非自动覆盖;
- 动态生成的 DOM(如通过 `v-html` 插入的内容)不会自动获得 `data-v` 属性,scoped 样式对其无效,需手动处理或使用深度选择器谨慎覆盖 。

使用首页预留插槽

现在我们已经能做到在Features下Footer上添加自定义内容了,但是我有办法将自定义内容加到Header下 Hero 上吗

答案是可以的,vitepress首页给我们预留了很多插槽,通过插槽我们可以将自定义组件染到想要的位置

使用插槽案例

2026-07-19 星期日
15
15
:
18
18
:
45
45

尝试将一个组件放到Hero上方

1、在components目录下新建目录clock-filp,在该目录下新建FlipNum.vueClockFlip.vue

FlipNum.vue
vue
<template>
  <div class="num-box" :class="{flip: flipAnim}">
    <div class="num-top">{{ numStr }}</div>
    <div class="num-bottom">{{ numStr }}</div>
  </div>
</template>

<script setup>
import { computed, ref, watch } from 'vue'
const props = defineProps(['num'])
const flipAnim = ref(false)
const numStr = computed(() => String(props.num).padStart(2, '0'))

watch(() => props.num, () => {
  flipAnim.value = true
  setTimeout(() => flipAnim.value = false, 400)
})
</script>

<style scoped>
.num-box {
  width: 100%;
  height: 100%;
  position: relative;
}
.num-top, .num-bottom {
  width: 100%;
  height: 50%;
  display: grid;
  place-items: center;
  font-size: 120px;
  font-weight: 900;
  color: var(--clock-text);
  overflow: hidden;
}
.num-top {
  border-bottom: 1px solid rgba(255,255,255,0.2);
}
.num-bottom {
  transform: translateY(-1px);
}
/* 翻转动画 */
.num-box.flip .num-top {
  animation: flipTop 0.4s ease-out forwards;
}
.num-box.flip .num-bottom {
  animation: flipBottom 0.4s ease-out forwards;
}
@keyframes flipTop {
  0% { transform: rotateX(0deg); opacity: 1; }
  100% { transform: rotateX(-90deg); opacity: 0; }
}
@keyframes flipBottom {
  0% { transform: rotateX(90deg); opacity: 0; }
  100% { transform: rotateX(0deg); opacity: 1; }
}
</style>
ClockFlip.vue
vue
<template>
  <div class="clock-container">
    <div class="date-text">{{ dateStr }}</div>

    <div class="flip-wrap">
      <!-- 时 -->
      <div class="flip-card">
        <div class="flip-panel">
          <div class="flip-top">
            <span class="num-text">{{ hourStr }}</span>
          </div>
          <div class="flip-bottom">
            <span class="num-text">{{ hourStr }}</span>
          </div>
        </div>
      </div>

      <span class="split">:</span>

      <!-- 分 -->
      <div class="flip-card">
        <div class="flip-panel">
          <div class="flip-top">
            <span class="num-text">{{ minuteStr }}</span>
          </div>
          <div class="flip-bottom">
            <span class="num-text">{{ minuteStr }}</span>
          </div>
        </div>
      </div>

      <span class="split">:</span>

      <!-- 秒 -->
      <div class="flip-card">
        <div class="flip-panel" :class="{ flip: flipAnim }">
          <div class="flip-top">
            <span class="num-text">{{ secondStr }}</span>
          </div>
          <div class="flip-bottom">
            <span class="num-text">{{ secondStr }}</span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'

const now = ref(new Date())
const flipAnim = ref(false)
let timer = null

const dateStr = computed(() => {
  const d = now.value
  const weekList = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
  const year = d.getFullYear()
  const month = String(d.getMonth() + 1).padStart(2, '0')
  const day = String(d.getDate()).padStart(2, '0')
  const week = weekList[d.getDay()]
  return `${year}-${month}-${day} ${week}`
})

const hourStr = computed(() => String(now.value.getHours()).padStart(2, '0'))
const minuteStr = computed(() => String(now.value.getMinutes()).padStart(2, '0'))
const secondStr = computed(() => String(now.value.getSeconds()).padStart(2, '0'))

const updateTime = () => {
  now.value = new Date()
}

watch(secondStr, () => {
  flipAnim.value = true
  setTimeout(() => {
    flipAnim.value = false
  }, 600)
})

onMounted(() => {
  updateTime()
  timer = setInterval(updateTime, 1000)
})

onUnmounted(() => {
  clearInterval(timer)
})
</script>

<style scoped>
:root {
  --clock-bg: #f7e1ef;
  --clock-card: #f2c6e0;
  --clock-card-deeper: #e8a8c8;
  --clock-text: #d14d88;
  --clock-split: #d14d88;
  --clock-date: #c8689c;
}

:global(.dark) {
  --clock-bg: #2e1c28;
  --clock-card: #3d2836;
  --clock-card-deeper: #4a3340;
  --clock-text: #f8d8e8;
  --clock-split: #f8d8e8;
  --clock-date: #f0b8d2;
}

.clock-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 18px 24px;
  background: var(--clock-bg);
  border-radius: 16px;
  margin: 2rem 0;
}

.date-text {
  font-size: 16px;
  color: var(--clock-date);
  font-weight: 500;
  letter-spacing: 1px;
}

.flip-wrap {
  display: flex;
  align-items: center;
  gap: 10px;
}

.flip-card {
  width: 52px;
  height: 64px;
  background: var(--clock-card);
  border-radius: 10px;
  overflow: hidden;
  position: relative;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4), 0 4px 10px rgba(0, 0, 0, 0.08);
}

.flip-panel {
  width: 100%;
  height: 100%;
  position: relative;
  perspective: 180px;
}

.flip-top,
.flip-bottom {
  width: 100%;
  height: 50%;
  overflow: hidden;
  position: relative;
  background: var(--clock-card);
}

.flip-top {
  border-bottom: 1px solid rgba(255, 255, 255, 0.35);
}

.flip-bottom {
  background: linear-gradient(
    to bottom,
    var(--clock-card),
    var(--clock-card-deeper)
  );
}

.num-text {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  font-size: 24px;
  font-weight: 800;
  color: var(--clock-text);
  line-height: 1;
}

.flip-top .num-text {
  bottom: 0;
  transform: translateX(-50%) translateY(50%);
}

.flip-bottom .num-text {
  top: 0;
  transform: translateX(-50%) translateY(-50%);
}

.split {
  font-size: 24px;
  color: var(--clock-split);
  font-weight: 800;
  margin-bottom: 4px;
}

/* 翻页动作 */
.flip-panel.flip .flip-top {
  transform-origin: center bottom;
  animation: flipTop 0.6s ease-in forwards;
}

.flip-panel.flip .flip-bottom {
  transform-origin: center top;
  animation: flipBottom 0.6s ease-out forwards;
}

@keyframes flipTop {
  0% {
    transform: rotateX(0deg);
  }
  100% {
    transform: rotateX(-90deg);
  }
}

@keyframes flipBottom {
  0% {
    transform: rotateX(90deg);
  }
  100% {
    transform: rotateX(0deg);
  }
}
</style>

2、安装vue,因为需要使用vue提供的h方法

bash
 npm add vue -D

3、在theme/index.ts中使用插槽

index.ts
,
// https://vitepress.dev/guide/custom-theme
import { h } from 'vue'
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import './style.css'
import './custome01/styles.css'

import ElementPlus from 'element-plus'
// 全局引入Element Plus完整样式
import 'element-plus/dist/index.css'
// Element Plus 暗黑主题,自动跟随页面dark类切换
import 'element-plus/theme-chalk/dark/css-vars.css'


// @ts-ignore
import Counter from './components/Counter.vue' // [!code ++]
// @ts-ignore
import ClockFlip from './components/clock-flip/ClockFlip.vue'
// @ts-ignore
import PromoteBanner from './components/PromoteBanner.vue'

import './custome01/custom.css' // 直接导入不行:import 'tailwindcss'

// 引入头像组件
//@ts-ignore
import UserAvatar from './components/UserAvatar.vue'

// 导入计时组件
// @ts-ignore
import SiteRuntime from './components/SiteRuntime.vue'

export default {
  extends: DefaultTheme,
  Layout: () => {
    return h(DefaultTheme.Layout, null, {
      // https://vitepress.dev/guide/extending-default-theme#layout-slots
      'home-hero-info-after': () => h(ClockFlip),
      'doc-footer-before': () => h(PromoteBanner),
      // 使用插槽显示头像组件
      'nav-bar-content-after': () => h(UserAvatar),
      // 'layout-bottom': () => h(SiteRuntime),
      // 全局自定义页脚插槽,替换默认footer
      'layout-bottom': () => h('div', { class: 'custom-footer' }, [
        // 1. 运行计时组件
        h('p', { class: 'text-center text-gray-600 dark:text-gray-400 my-2' }, '归至而行,初心一道.'),
        // 2. 自定义标语
        h(SiteRuntime),
        // 3. 版权 + ICP
        h('p', { class: 'text-center text-gray-600 dark:text-gray-500 leading-relaxed' },
          `Copyright © 2026.07.17-${new Date().getFullYear()} @guizhi. All rights reserved  | 渝ICP备2026005423号-1`
        )
      ])
    })
  },
  enhanceApp({ app, router, siteData }) {
    app.component("Counter", Counter),
    app.component("ClockFlip", ClockFlip),
    // 全局挂载ElementPlus,全局所有md页面、自定义组件均可直接使用el-xxx组件
    app.use(ElementPlus)
    // ...
  }
} satisfies Theme

组件已经染到Header下Hero下方了

查看插槽位置

vitepress文档并没有详细说明,我们可以通过查阅vitepress 源码来知道预留的插槽位置,文件在node_modules/vitepress/dist/client/theme-default/Layout.vue

Layout.vue
vue
<script setup lang="ts">
import { useRoute } from 'vitepress'
import { computed, provide, useSlots, watch } from 'vue'
import VPBackdrop from './components/VPBackdrop.vue'
import VPContent from './components/VPContent.vue'
import VPFooter from './components/VPFooter.vue'
import VPLocalNav from './components/VPLocalNav.vue'
import VPNav from './components/VPNav.vue'
import VPSidebar from './components/VPSidebar.vue'
import VPSkipLink from './components/VPSkipLink.vue'
import { useData } from './composables/data'
import { useCloseSidebarOnEscape, useSidebar } from './composables/sidebar'

const {
  isOpen: isSidebarOpen,
  open: openSidebar,
  close: closeSidebar
} = useSidebar()

const route = useRoute()
watch(() => route.path, closeSidebar)

useCloseSidebarOnEscape(isSidebarOpen, closeSidebar)

const { frontmatter } = useData()

const slots = useSlots()
const heroImageSlotExists = computed(() => !!slots['home-hero-image'])

provide('hero-image-slot-exists', heroImageSlotExists)
</script>

<template>
  <div v-if="frontmatter.layout !== false" class="Layout" :class="frontmatter.pageClass" >
    <slot name="layout-top" />
    <VPSkipLink />
    <VPBackdrop class="backdrop" :show="isSidebarOpen" @click="closeSidebar" />
    <VPNav>
      <template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
      <template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
      <template #nav-bar-content-before><slot name="nav-bar-content-before" /></template>
      <template #nav-bar-content-after><slot name="nav-bar-content-after" /></template>
      <template #nav-screen-content-before><slot name="nav-screen-content-before" /></template>
      <template #nav-screen-content-after><slot name="nav-screen-content-after" /></template>
    </VPNav>
    <VPLocalNav :open="isSidebarOpen" @open-menu="openSidebar" />

    <VPSidebar :open="isSidebarOpen">
      <template #sidebar-nav-before><slot name="sidebar-nav-before" /></template>
      <template #sidebar-nav-after><slot name="sidebar-nav-after" /></template>
    </VPSidebar>

    <VPContent>
      <template #page-top><slot name="page-top" /></template>
      <template #page-bottom><slot name="page-bottom" /></template>

      <template #not-found><slot name="not-found" /></template>
      <template #home-hero-before><slot name="home-hero-before" /></template>
      <template #home-hero-info-before><slot name="home-hero-info-before" /></template>
      <template #home-hero-info><slot name="home-hero-info" /></template>
      <template #home-hero-info-after><slot name="home-hero-info-after" /></template>
      <template #home-hero-actions-after><slot name="home-hero-actions-after" /></template>
      <template #home-hero-image><slot name="home-hero-image" /></template>
      <template #home-hero-after><slot name="home-hero-after" /></template>
      <template #home-features-before><slot name="home-features-before" /></template>
      <template #home-features-after><slot name="home-features-after" /></template>

      <template #doc-footer-before><slot name="doc-footer-before" /></template>
      <template #doc-before><slot name="doc-before" /></template>
      <template #doc-after><slot name="doc-after" /></template>
      <template #doc-top><slot name="doc-top" /></template>
      <template #doc-bottom><slot name="doc-bottom" /></template>

      <template #aside-top><slot name="aside-top" /></template>
      <template #aside-bottom><slot name="aside-bottom" /></template>
      <template #aside-outline-before><slot name="aside-outline-before" /></template>
      <template #aside-outline-after><slot name="aside-outline-after" /></template>
      <template #aside-ads-before><slot name="aside-ads-before" /></template>
      <template #aside-ads-after><slot name="aside-ads-after" /></template>
    </VPContent>

    <VPFooter />
    <slot name="layout-bottom" />
  </div>
  <Content v-else />
</template>

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

通过插槽名能大概猜到位置在哪,当然也能一个个试知道具体位置,结合这些插槽就能白定义出更个性化的 vitepress首页了

更改首页标题色调

默认首质展示的标题颜色是绿色,图标背景是自色,通过以下操作,可以获得跟方官网一样的炫彩配色了

步骤1:

.vitepress 目录下创建 theme 目录,theme 目录下创建 index.ts,输入以下内容

typescript
import Theme from 'vitepress/theme'

export default {
  ...Theme
}

步骤2:

theme 目录下创建 style 目录,style 目录下创建 var.css

css
:root {
  --vp-home-hero-name-color: red;
}

步骤3:

theme/index.ts 下引入 style/var.css

typescript
import Theme from 'vitepress/theme'
import './style/var.css'

export default {
  ...Theme
}

底部说明文字: 可以看到标题颜色已经变成设定的红色了 可以加点渐变色来让整体效果好看点,渐变色可以从这个网站获取

CSS代码片段

css
:root {
  /* 标题 */
  --vp-home-hero-name-color: transparent;
  --vp-home-hero-name-background: linear-gradient(135deg, #F6CEEC 10%, #D939CD 100%);

  /* 图标背景 */
  --vp-home-hero-image-background-image: linear-gradient(135deg, #F6CEEC 10%, #D939CD 100%);
  --vp-home-hero-image-filter: blur(150px);
}

右下角标签:CSS

正文文字

首页颜色源码解读

我们通过使用 var.css 文件,给根节点 root 添加了css变量,来改变首页的标题和图片的背景色,那我们要怎么确认用什么属性就能修改我希望修改的元素呢

方法一:开发者工具

通过控制台我们就能直观的看出希望修改的样式有没有使用 css 变量,以图片背景做例子

在开发者工具中,可以看到

CSS代码块

css
.image-bg {
  background-image: var(--vp-home-hero-image-background-image);
  filter: var(--vp-home-hero-image-filter);
}

段落说明

这两个变量就是我们通过 root 下注入的 css 变量,因为我们显式的修改了两个变量,所以系统优先使用我们设定的样式

方案二:看源码

拉取 vitepress 的源码,看到 node_modules/vitepress/dist/client/theme-default/Layout.vue,这个 sfc 就是文档的布局组件,三种 layout 模式都是使用的这个组件

...略(见插槽位置

看到 VPContent 组件,这里会通过 frontmatter.layout 来切换使用的布局模式,所以通过这个文件我们可以看到,三种模式对应的组件名为

  • doc: VPDoc
  • page: VPPage
  • home: VPHome

主要看 VPHome 组件

拓展,vitepress 使用的读取 md 头部信息所使用的插件是 gray-matter,感兴趣的可以查阅下使用方法

...没找着(😥)

这里就是首页模式下的布局情况,可以看到组件名就是对应的我们在 index.md 中设置的 herofeatures

关于首页标题和图标的样式在 VPHomeHero 组件中

做个预告,可以看到下面有个 Content 组件,可以自定义首页下半部分的内容,将会是下篇文章讲的内容,敬请期待~

然后在VPHero中的源码中可以看到首页布局的真面目了!我们先看到标题的CSS属性

css
.name {
  color: var(--vp-home-hero-name-color);
}

.clip {
  background: var(--vp-home-hero-name-background);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: var(--vp-home-hero-name-color);
}

可以明显看到这里使用的css变量,就是我们在var.css中设定的css变量

同理,我们看图片的css属性

css
.image-bg {
  position: absolute;
  top: 50%;
  left: 50%;
  border-radius: 50%;
  width: 192px;
  height: 192px;
  background-image: var(--vp-home-hero-image-background-image);
  filter: var(--vp-home-hero-image-filter);
  transform: translate(-50%, -50%);
}

也是我们在 var.css 中设定的 css 变量

所以,我们可以直接在 VitePress 源码中,找到我们希望更改样式的组件,观察他们的 css 样式是否使用 css 变量,然后我们在 var.css 中进行更改即可

我们根据这个方法,改一下首页的按钮样式

实践,更改首页按钮样式

首页的按钮通过 hero 下的 actions 属性控制,通过 actions.theme 控制样式,默认是 brand,也就是绿色按钮,总共有三种模式:brandaltsponsor

VPButton 源码

通过分析源码,可以看到button的样式控制,通过传入的theme,计算动态classes,然后传给组件

这里就是brand模式下的button样式,可以看到使用了三个css变量,我们在var.css中对着三个样式进行改动