主题
实战:把这一页的界面改掉
你现在看的这一页,跟站里其他页长得不一样:背景是桌面上那张
1r.webp,主色从默认蓝换成了紫,正文变成一张深色玻璃卡片,标题是亮色渐变色,下面还有个能点着玩的组件。整件事只动了 4 个文件(1 张图 + 1 个主题入口 + 1 个样式文件 + 1 个组件),而且其他页面一点没变。这节课把这份改动完整拆开:先看效果,再讲原理,最后给你可以直接抄的代码。
🔀 配套的另一套皮肤:同一套机制还能生出完全不同的味道——去看第二套皮肤:终端风,那一页的组件代码一行都没改。
一、这一页到底哪里不一样
| 部位 | 默认主题的样子 | 这一页的样子 |
|---|---|---|
| 页面背景 | 纯白 / 纯黑 | 1r.webp 背景图 + 一层深色渐变(把图压暗,但不遮住) |
| 品牌主色 | VitePress 默认蓝 | #c4b5fd 淡紫(侧边栏高亮、链接、选中态一起变) |
| 正文区域 | 贴在页面上的文字 | 深色半透明圆角卡片 + 毛玻璃 |
| 一级标题 | 黑色文字 | 紫→品红→橙的亮色渐变文字 |
| 顶栏/侧边栏/页脚 | 不透明 | 深色毛玻璃,能透出后面的图 |
| 额外内容 | 无 | 一个可点击的 Vue 组件(下面会用到) |
看一下被换掉的那几个颜色变量(鼠标划过来时会变亮):
--vp-c-brand-1
--vp-c-brand-2
--vp-c-brand-3
--vp-c-brand-soft
标题渐变
pageClass: ui-demo darkCSS 变量作用域Vue 组件
二、原理:一个页面是怎么被"组装"出来的
先看这条链路,这是理解一切主题改动的关键:
text
docs/stage2/ui-custom.md
│ ① markdown-it 编译
▼
一个 Vue 单文件组件(正文 = 组件模板)
│ ② 套进主题的 Layout(顶栏 + 侧边栏 + 正文 + 页脚)
▼
Layout 根节点 + 主题的 CSS(一堆 --vp-c-* 变量 + 组件样式)
│ ③ 浏览器渲染
▼
你看到的页面三个结论直接决定了你能怎么改:
- 正文是组件模板,所以 markdown 里能写 HTML、能写
<DemoCard />这种组件标签——它就是 Vue。 - 页面外面套着 Layout,所以改"页面外壳"(背景、导航、侧边栏)要在主题这一层动,而不是在
.md里动。 - 颜色几乎全是 CSS 变量(
--vp-c-brand-1、--vp-c-bg…),所以换肤最快的方式不是覆盖样式,而是只改变量。
于是"改 UI"就有了三层,从轻到重:
| 层次 | 改什么 | 适合做什么 | 改动量 |
|---|---|---|---|
| ① 变量层 | --vp-c-* | 换主色、圆角、字体 | 几行 CSS |
| ② 样式层 | 自己的 custom.css | 背景图、卡片、动效 | 几十行 CSS |
| ③ 结构层 | Vue 组件 + 主题入口 | 加新组件、改布局插槽 | 需要写组件 |
这一页三层都用上了,但只作用于这一页——靠的是下面这个机制。
三、为什么"只有这一页变了"
VitePress 会把 markdown 顶部 frontmatter 里的 pageClass 加到页面根节点 .Layout 上(源码位置:node_modules/vitepress/dist/client/theme-default/Layout.vue 第 34 行)。所以:
md
---
pageClass: ui-demo dark
---
# 这一页的标题编译后页面根节点就变成 <div class="Layout ui-demo dark">,而我所有样式都写成 .Layout.ui-demo ...,于是天然只命中这一页。
这里还顺手用了一个偷懒但很实用的技巧:dark 是 VitePress 默认主题自己的类名(正常深色模式时挂在 <html> 上),它里面把一整套文字/背景/边框的 CSS 变量换成了深色版。我把它跟着 pageClass 一起写在页面根节点上,就等于"只把这一棵树切换成深色",一个字都不用自己抄——正文、表格、代码块、提示框全都自动适配深色。
再加上 CSS 变量是在这棵子树上重设的,变量会跟着"就近生效"——这就是为什么这页的链接、高亮全是紫色,翻到别的页又是默认蓝、白底。
💡 这就是 CSS 的两条基本功在起作用:选择器限定作用范围 + 变量的作用域是 DOM 子树。忘了的话回去看 CSS:页面皮肤。
四、动手改:四步
第 1 步:把图片放进 docs/public
docs/public/1r.webp → 构建后可通过 /1r.webp 访问docs/public/ 里的文件会原样复制到站点根目录,不参与打包、不改文件名。所以 CSS 里直接写 url('/1r.webp') 就行(注意是站点根路径,不是相对路径)。
第 2 步:建主题入口 docs/.vitepress/theme/index.js
js
import DefaultTheme from 'vitepress/theme'
import './custom.css'
import DemoCard from './components/DemoCard.vue'
export default {
extends: DefaultTheme, // 继承官方默认主题,而不是 fork 一份
enhanceApp({ app }) {
app.component('DemoCard', DemoCard) // 全局注册:任何 .md 都能用
}
}只要这个文件存在,VitePress 就会自动使用你的主题,不需要改 config.mjs。
第 3 步:写 docs/.vitepress/theme/custom.css
关键就是两段(完整文件有 200 行左右,这里抽主干):
css
/* 单页主题:只命中 pageClass 里带 ui-demo 的页面 */
.Layout.ui-demo {
/* ① 重设主色变量(深色底上要用更亮的一档,不然看不清) */
--vp-c-brand-1: #c4b5fd;
--vp-c-brand-2: #a78bfa;
--vp-c-brand-3: #7c4dff;
/* ② 背景:深色渐变蒙版 + 图片,两层叠在一起 */
background-image:
linear-gradient(180deg, rgba(20,15,36,.42), rgba(18,14,32,.74)),
url('/1r.webp');
background-size: cover, cover;
background-attachment: fixed, fixed;
}为什么一定要加蒙版?因为 1r.webp 是偏亮的浅紫图,文字直接压上去会看不清。这里的做法是用深色蒙版压暗、但保留图片的层次(0.42 → 0.74 的渐变,上浅下深),而不是拿一层白色把它糊掉——背景图 + 半透明蒙版是最常用的组合拳,"压多暗"取决于图片亮度和正文对比度要求。
注意深色底上的两个细节:主色要往亮处调一档(#c4b5fd 而不是 #6d4aff),否则紫字在暗底上发灰;正文卡片的背景用 rgba(20,16,36,.62) 这种半透明深色,让底下的图隐约透出来,玻璃感就来自这里。
剩下的部分同理:.Layout.ui-demo .VPNav 换深色毛玻璃、.Layout.ui-demo .VPDoc .content-container 变成深色卡片、.Layout.ui-demo h1 用 background-clip: text 做渐变文字。
第 4 步(可选):写一个组件并注册
docs/.vitepress/theme/components/DemoCard.vue:
vue
<script setup>
import { ref } from 'vue'
defineProps({ title: String, tag: { type: String, default: '示例组件' } })
const count = ref(0) // 响应式数据:变了页面会自己更新
</script>
<template>
<div class="demo-card">
<span class="demo-card__tag">{{ tag }}</span>
<h3 class="demo-card__title">{{ title }}</h3>
<p class="demo-card__desc"><slot /></p>
<button class="demo-card__btn" @click="count++">点我一下 · 已点击 {{ count }} 次</button>
</div>
</template>注册之后,在 markdown 里就能这么写(下面这块就是真的在跑的组件,点一下试试):
DemoCard.vue
这个卡片是 Vue 组件,不是图片
props 传进来的是标题和标签,按钮里的数字是组件内部的响应式状态——所以你点它,数字会变,页面不会刷新。
它同时演示了三件事:props(外面传参)、slot(标签内容由使用方填)、响应式(ref 一变,视图自动更新)。这就是"网页不是静态文档,而是程序"的直接证据。
五、去浏览器里验证(这一步别跳)
- 按
F12→ Elements,找到最外层<div class="Layout ui-demo dark">,往上能看到.VPNav、.VPSidebar,往下能看到.VPDoc > .container > .content-container——这就是上面所有选择器命中的元素。留意那个dark类:它是让这一棵树变深色的开关。 - 右侧 Styles 面板里搜
--vp-c-brand-1,能看到同一变量在:root(全局)和.Layout.ui-demo(本页)各有一份定义,本页那份生效了。这就是"作用域最近优先"的现场。 - 选中
.Layout.ui-demo,在 Styles 里把rgba(20,15,36,.42)的透明度往.9调,你会看到背景图被压得更暗——这层蒙版是控制"图片看得清多少"的唯一旋钮。
💡 动手实验 1:把
.Layout.ui-demo里的--vp-c-brand-1改成绿色系(比如#6ee7b7),刷新看看侧边栏高亮和链接颜色是不是一起变了。再想想:为什么改一个变量能变这么多地方?💡 动手实验 2:把蒙版第一层的透明度从
.42改成.15,图片会变得很抢眼;再改成.9,图片基本消失。找一个你能接受的位置,想想"可读性"和"氛围"怎么平衡。💡 动手实验 3:新建一个
docs/stage2/my-ui.md,frontmatter 写pageClass: ui-demo dark,随便写点内容,访问/stage2/my-ui。你应该免费获得了同一套皮肤——因为样式是按类名匹配的,不是按文件名。
六、小结
text
要改的地方 改哪里 作用范围
──────────────────────────────────────────────────────────────────
颜色/圆角/字号 → CSS 变量 --vp-c-* → 写了变量的那棵子树
深浅(亮/暗) → 借主题的 .dark 类 → 挂在哪个节点就管哪棵子树
背景/卡片/动效 → 自己的 custom.css → 选择器限定到哪就管到哪
新组件/新结构 → Vue 组件 + theme/index.js → 注册后可全局使用
"只改一页"的开关 → frontmatter pageClass → 挂到 .Layout 根节点上三句话记住这节课:
- 改样式先找变量,改变量比覆盖样式干净得多;
- 样式靠选择器划定范围,
.Layout.ui-demo就是这一页的"围栏"; - 继承默认主题 + 叠加自己的东西,官方升级时你不会被打散架。
想再往下走,下一站是 F12 开发者工具精要:上面这些操作全都能在浏览器里现场试;如果你更想弄懂"页面的骨架是怎么搭的",回到 HTML:网页的骨架。
📎 本课对应知识库阅读:知识库里的《常见 Web 安全响应头与加固》——同一个页面,你从"改样式"的角度看是 CSS,从"攻击者"的角度看就是样式注入与点击劫持的舞台。