VitePress系列教程:内容展示优化#5
首页组件
如果我们觉得首页内容比较单调,想自已设计点可交互的内容,我们可以通过以下方案追加样式内容
我们在首页index.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
# 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中进行引用
<!--@include: ./guide/writting/parts/footer-show.md -->提示
还可以使用tailwindcss之类预设样式类,需要安装拓展插件才可以
Vue SFC组件
我们可以自己写一个SFC,注册到工程中,然后在md中使用
SFC VUE 介绍
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
详细信息
<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组件
详细信息
// 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 Theme3、在首页index.md或当前页使用组件
<Counter/>Count: 0
可以看到Counter组件成功被染出来,并且点击按钮能响应式变化数字
所以,对于复杂的交互组件,我们可以通过自定义SFC,然后在theme/index.js中注册组件,并在md中使用组件,达到想要的复杂交互效果
Vue style scoped
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首页给我们预留了很多插槽,通过插槽我们可以将自定义组件染到想要的位置
使用插槽案例
尝试将一个组件放到Hero上方
1、在components目录下新建目录clock-filp,在该目录下新建FlipNum.vue和ClockFlip.vue
FlipNum.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
<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方法
npm add vue -D3、在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
<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,输入以下内容
import Theme from 'vitepress/theme'
export default {
...Theme
}步骤2:
在 theme 目录下创建 style 目录,style 目录下创建 var.css
:root {
--vp-home-hero-name-color: red;
}步骤3:
在 theme/index.ts 下引入 style/var.css
import Theme from 'vitepress/theme'
import './style/var.css'
export default {
...Theme
}底部说明文字: 可以看到标题颜色已经变成设定的红色了 可以加点渐变色来让整体效果好看点,渐变色可以从这个网站获取
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代码块
.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 中设置的 hero 和 features
关于首页标题和图标的样式在 VPHomeHero 组件中
做个预告,可以看到下面有个
Content组件,可以自定义首页下半部分的内容,将会是下篇文章讲的内容,敬请期待~
然后在VPHero中的源码中可以看到首页布局的真面目了!我们先看到标题的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属性
.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,也就是绿色按钮,总共有三种模式:brand、alt、sponsor
VPButton 源码
通过分析源码,可以看到button的样式控制,通过传入的theme,计算动态classes,然后传给组件
这里就是brand模式下的button样式,可以看到使用了三个css变量,我们在var.css中对着三个样式进行改动