本系列教程共十篇valaxy博客全局美化教程（一）：分类、标签、归档三个页面的美化教程valaxy博客全局美化教程（二）：首页公告栏美化，新增随机文章展示板块valaxy博客全局美化教程（三）：给网页增加加载动画valaxy博客全局美化教程（四）：增加图片预览功能valaxy博客全局美化教程（五）：友链页面美化，修改了友链样式以及增加留言板valaxy博客全局美化教程（六）：留言页面增加信封展开效果valaxy博客全局美化教程（七）：新增网址导航页面valaxy博客全局美化教程（八）：页脚倒计时、搜索问题修复valaxy博客全局美化教程（九）：新增相册页面，支持使用webdav作为相册valaxy博客全局美化教程（十）：修复构建结束的时候会卡住覆盖文件覆盖文件的意思就是，这个文件已存在，删除原本全部内容，替换为下方内容，不过删之前建议备份哦覆盖scripts\build-ssg.mjsimport { spawn } from 'node:child_process' import { createRequire } from 'node:module' import { existsSync } from 'node:fs' import { resolve } from 'node:path' const BUILD_DONE_MARKERS = [ '[HOOK] build:after done', 'RSS Feed Files', ] const GRACE_MS = 800 const HEARTBEAT_MS = 45_000 const MAX_MS = 20 * 60 * 1000 const require = createRequire(import.meta.url) const valaxyBin = require.resolve('valaxy/bin/valaxy.mjs') const distIndex = resolve(process.cwd(), 'dist/index.html') const child = spawn(process.execPath, [valaxyBin, 'build', '--ssg'], { stdio: ['inherit', 'pipe', 'pipe'], env: process.env, windowsHide: true, }) let finished = false let successTimer let outputBuffer = '' function killChildTree() { if (!child.pid) return if (process.platform === 'win32') { spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true, }) return } try { child.kill('SIGKILL') } catch {} } function finishSuccess() { if (finished) return finished = true clearTimeout(maxTimer) clearTimeout(successTimer) clearInterval(heartbeatTimer) if (!existsSync(distIndex)) { console.error('\n构建流程已结束，但未找到 dist/index.html，请检查构建日志。\n') killChildTree() process.exit(1) return } console.log('\n✓ 构建已完成，正在结束进程…\n') killChildTree() setTimeout(() => process.exit(0), 200) } function finishError(code) { if (finished) return finished = true clearTimeout(maxTimer) clearTimeout(successTimer) clearInterval(heartbeatTimer) process.exit(code ?? 1) } function scheduleSuccess() { clearTimeout(successTimer) successTimer = setTimeout(finishSuccess, GRACE_MS) } function inspectOutput(text) { outputBuffer += text if (outputBuffer.length > 8192) outputBuffer = outputBuffer.slice(-8192) if (BUILD_DONE_MARKERS.some(marker => outputBuffer.includes(marker))) scheduleSuccess() } const heartbeatTimer = setInterval(() => { if (finished) return console.log('\n⏳ 仍在构建中（SSG 预渲染可能较慢，请稍候）…\n') }, HEARTBEAT_MS) const maxTimer = setTimeout(() => { if (finished) return console.error('\nSSG 构建超时（20 分钟）。\n') killChildTree() finishError(1) }, MAX_MS) child.stdout.on('data', (chunk) => { const text = chunk.toString() process.stdout.write(text) inspectOutput(text) }) child.stderr.on('data', (chunk) => { const text = chunk.toString() process.stderr.write(text) inspectOutput(text) }) child.on('error', (error) => { console.error(error) finishError(1) }) child.on('close', (code) => { if (finished) return if (code === 0 || existsSync(distIndex)) finishSuccess() else finishError(code) }) 修改文件修改package.json替换原本"build:ssg": "valaxy build --ssg",为 "build:ssg": "node scripts/build-ssg.mjs", "build:ssg:raw": "valaxy build --ssg",替换原本"build": "npm run build:ssg",为 "build": "node scripts/build-ssg.mjs",好啦到此修复完成啦，综上全部的美化教程就到此结束啦，感谢观看哦~
本系列教程共十篇valaxy博客全局美化教程（一）：分类、标签、归档三个页面的美化教程valaxy博客全局美化教程（二）：首页公告栏美化，新增随机文章展示板块valaxy博客全局美化教程（三）：给网页增加加载动画valaxy博客全局美化教程（四）：增加图片预览功能valaxy博客全局美化教程（五）：友链页面美化，修改了友链样式以及增加留言板valaxy博客全局美化教程（六）：留言页面增加信封展开效果valaxy博客全局美化教程（七）：新增网址导航页面valaxy博客全局美化教程（八）：页脚倒计时、搜索问题修复valaxy博客全局美化教程（九）：新增相册页面，支持使用webdav作为相册valaxy博客全局美化教程（十）：修复构建结束的时候会卡住效果展示支持图床相册或者webdav网盘相册，均支持加密，支持时间轴显示（图床需要自己设置时间，webdav没测试时间轴），图片预览界面和之前的预览界面一样的，安装本美化之前请先安装valaxy博客全局美化教程（四），否则可能会出奇奇怪怪的问题开始美化由于相册功能改动十分大，有涉及到三四十个新建文件，如果让你们一个个添加的话，那也太累了，所以我将所有新建文件全部打包，按照项目根目录路径打包，解压之后可以直接覆盖导入，不放心的话可以一个个导入，注意，修改相册之前记得备份一份博客文件，由于本次美化设计文件过多，一个个回退不太现实，万一出问题造成后果自己负责哦，环境变量说明一下，由于要使用webdav相册，需要在部署到托管时增加环境变量，以下是环境变量名称WEBDAV_PASSWORD值是你的webdav访问密码，记得类型选择秘钥类型哦~修改文件以下全部是修改文件的操作文件valaxy.config.ts增加import { albumWebdavConfigPlugin } from './plugins/album-webdav-config' import { albumWebdavProxy } from './plugins/album-webdav-proxy' { text: '相册', link: '/gallery', },export default defineValaxyConfig({ vite: { plugins: [vaFoucLoader({ avatar: siteConfig.author?.avatar, title: siteConfig.title, subtitle: siteConfig.subtitle, primary: '#E9CCCC', }), albumWebdavConfigPlugin(), albumWebdavProxy()], }, })一共增加三段代码，第一段头部引入，第二段增加页面入口，第三段启用相册效果，请对应位置哦，第三段是完整代码哦~文件.gitignore增加!.env.example文件netlify.toml增加redirects from = "/api/album-webdav/*" to = "/.netlify/functions/album-webdav/:splat" status = 200修改vercel.json{ "cleanUrls": true, "buildCommand": "pnpm run build", "outputDirectory": "dist", "functions": { "api/**/*.ts": { "includeFiles": "server/**,types/**,utils/**" } } }导入文件修改部分上面结束了，现在进行新建文件部分，这里给出文件下载地址点我跳转下载链接解压后文件夹里面长这样里面有一个readme.md文件，里面写了怎么使用，可以看看，也可以看我这里写的，解压之后将文件覆盖进你的项目就行了，这里的文件包含了页面文件，这里和你们讲一下index.md代码使用方法pages\gallery文件夹就是相册页面文件夹，这个文件夹内的index.md写的是相册--- layout: gallery title: 相册 icon: i-ri-gallery-line cover: https://你的图床.png comment: false albums: - bizhi - web-jiami - jiamio - web-kaif ---例如这样，下面的albums下方的四个就是相册，和这个目录下的文件夹名称要一样，每一个文件夹对应一个相册这里以bizhi举例，pages\gallery\bizhi文件夹就是bizhi相册，在这个文件夹内有一个index.md文件，这是该相册的索引文件，在里面设置照片这些--- layout: gallery-album title: 一些壁纸 date: 2025-08-18 cover: https://你的图床.png desc: 觉得好看的一些壁纸，喜欢的可以下载哦~ location: 重庆 tags: - 壁纸 comment: false encrypted: false source: local photos: - url: https://你的图床.png date: 2026-6-1 - url: https://你的图床.png date: 2026-6-1 --- 注意，所有部分都需要写，少一个都会报错（虽然日期那一行没啥用，但是我懒得改了），encrypted是设置是否启用密码的选项，如果选择启用的话还要增加一个:password: "你的密码"即使不设密码也需要encrypted: false字段哦好啦，到这里就结束啦~
本系列教程共十篇valaxy博客全局美化教程（一）：分类、标签、归档三个页面的美化教程valaxy博客全局美化教程（二）：首页公告栏美化，新增随机文章展示板块valaxy博客全局美化教程（三）：给网页增加加载动画valaxy博客全局美化教程（四）：增加图片预览功能valaxy博客全局美化教程（五）：友链页面美化，修改了友链样式以及增加留言板valaxy博客全局美化教程（六）：留言页面增加信封展开效果valaxy博客全局美化教程（七）：新增网址导航页面valaxy博客全局美化教程（八）：页脚倒计时、搜索问题修复valaxy博客全局美化教程（九）：新增相册页面，支持使用webdav作为相册valaxy博客全局美化教程（十）：修复构建结束的时候会卡住页脚添加计时功能预览效果新建文件以下是新建文件部分新增components\SakuraFooter.vue <SakuraCopyright /> 本站已经流畅运行 {{ runtime.days }}天 {{ runtime.hours }}小时 {{ runtime.minutes }}分钟 {{ runtime.seconds }}秒 <SakuraPowered v-if="footer.powered" /> <slot /> 修改文件以下是修改文件部分在valaxy.config.ts文件footer部分（在（themeconfig内部）增加以下代码 runtimeSince: '2026-6-01',最终效果 footer: { //... runtimeSince: '2026-6-01', //... },这是控制计时起始时间的搜索问题修复前景提要这个没有预览图哈，不知道你们遇到没有，反正主播遇到了，我使用搜索功能的时候老搜不到东西，很奇怪，顺便说一下怎么开启搜索功能，你直接点那个按钮他是有一个空壳哈，得先新建搜索页面pages\search新建文件index.md在里面写：--- layout: search title: 搜索 icon: i-ri-search-line cover: /path/to/your/cover-image.jpg comment: false ---然后还要开启搜索支持：本地搜索（基于 fuse.js）Valaxy 内置了基于 fuse.js 的离线搜索（须预先通过 valaxy fuse 构建生成本地缓存）。valaxy fuse 默认将 fuse 生成在 public 目录下，并在 .gitignore 中添加 public/valaxy-fuse-list.json 忽略。 在执行 valaxy build 之前，会自动执行 valaxy fuse。如果你想要使用全文搜索，需要进行如下设置 ：编辑site.config.ts文件，以下给的完整代码，注意哦export default defineSiteConfig({ search: { enable: true, type: 'fuse', }, fuse: { /** * 设置搜索的文件路径 */ // pattern: 'pages/**/*.md', options: { keys: ['title', 'tags', 'categories', 'excerpt', 'content'], /** * @default 0.6 * @see https://www.fusejs.io/api/options.html#threshold * 设置匹配阈值，越低越精确 */ // threshold: 0.6, /** * @default false * @see https://www.fusejs.io/api/options.html#ignoreLocation * 忽略位置 * 这对于搜索文档全文内容有用，若无需全文搜索，则无需设置此项 */ ignoreLocation: true, }, }, })还需要在文件中设置启用搜素：编辑site.config.ts文件，这里也是完整代码哦import { defineSiteConfig } from 'valaxy' export default defineSiteConfig({ search: { enable: true, // 设置类型为 Fuse provider: 'fuse', }, })还需要在你的 package.json 中添加 fuse 生成脚本编辑package.json文件主要加这两行："build": "npm run build:ssg", "fuse": "valaxy fuse",添加位置看下方完整代码{ "name": "yun-demo", "valaxy": { "theme": "yun" }, "scripts": { "build": "npm run build:ssg", "build:ssg": "valaxy build --ssg", "fuse": "valaxy fuse", "rss": "valaxy rss" }, "dependencies": { "valaxy": "latest", "valaxy-theme-yun": "latest" } }这样就算启用搜索了，但是我用起来有bug，这里给出修复方案修复方案这里很简单，新建一个文件即可新增components\layouts\SakuraSearchLayout.vue <RouterView v-slot="{ Component }"> <component :is="Component"> <slot name="content"> 0"> 搜索结果: {{ input }} 没有找到任何东西！ 0" overflow="auto" flex="~" w="full"> <AppLink :to="result.item.link"> <SakuraImageCard :src="result.item?.cover" class="h-full rounded-full" /> </AppLink> <AppLink :to="result.item.link"> {{ result.item.title }} </AppLink> 发布于 {{ result.item.date }} {{ result.item.excerpt }} <AppLink :to="result.item.link"> <SakuraDots class="float-right mt-10px" /> </AppLink> </slot> </component> </RouterView> 好啦，本次教程就到这了
本系列教程共十篇valaxy博客全局美化教程（一）：分类、标签、归档三个页面的美化教程valaxy博客全局美化教程（二）：首页公告栏美化，新增随机文章展示板块valaxy博客全局美化教程（三）：给网页增加加载动画valaxy博客全局美化教程（四）：增加图片预览功能valaxy博客全局美化教程（五）：友链页面美化，修改了友链样式以及增加留言板valaxy博客全局美化教程（六）：留言页面增加信封展开效果valaxy博客全局美化教程（七）：新增网址导航页面valaxy博客全局美化教程（八）：页脚倒计时、搜索问题修复valaxy博客全局美化教程（九）：新增相册页面，支持使用webdav作为相册valaxy博客全局美化教程（十）：修复构建结束的时候会卡住效果演示这个页面效果有点多，其中有一个随机网站抽卡界面，大家最好还是亲自体验一下比较好前往体验站长的小巧思，点击到处转转按钮有惊喜哦~文件修改以下是文件修改部分，共1个修改文件注意这里给的完整代码哦文件valaxy.config.ts增加 themeConfig: { //... // 导航页「随机网站跳转」抽卡视频：weight 越大越容易抽到 navigation: { randomDrawVideos: [ { url: 'https://img.naixiai.cn/2026/06/09/_compressed.mp4', weight: 1, }, { url: 'https://img.naixiai.cn/2026/06/09/_compresseddbc6ff3507fddbf4.mp4', weight: 2, }, { url: 'https://img.naixiai.cn/2026/06/09/_compressedf1fc8ccd127613ac.fr.jpeg', weight: 3, }, ], }, //... }上面这个解释一下，三个url链接是点击随机跳转按钮后的抽卡视频，最好自己准备哦，下面的是概率，挺好玩的上下两部分都需要添加，下面这部分就是给你的网页增加导航页面按钮，具体放置位置可以看看官方文档的顶栏按钮怎么写的，点我去看看 { text: '导航', link: '/navigation', },新建文件以下部分是新建文件哦，共7个文件，别漏了哦这是第一个文件，也是导航页面的索引文件，导航页面所有导航都写在里面，这里写了几个示例，可以自己按格式新增新增pages\navigation\index.md--- layout: navigation title: 导航 icon: i-ri-compass-3-line comment: false cover: https://你的图床.png # 抽卡视频与概率见 valaxy.config.ts → themeConfig.navigation.randomDrawVideos navGroups: - name: 软件资源 desc: 绿色软件、应用下载与资源分享 sites: - name: 果核剥壳 url: https://www.ghxi.com desc: 精品软件分享与绿色资源下载站 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=ghxi color: "#e67e22" - name: ZAPRO 杂铺 url: https://tmioe.com desc: 软件资源整理与工具分享博客 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=tmioe color: "#3498db" - name: 易破解 url: https://www.ypojie.com desc: 软件破解教程与资源下载社区 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=ypojie color: "#9b59b6" - name: MEFCL url: https://www.mefcl.com desc: 软件绿标资源与下载分享站 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=mefcl color: "#1abc9c" - name: 绿库吧 url: https://www.lkuba.com desc: 绿色软件与驱动程序下载 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=lkuba color: "#27ae60" - name: 优盟盒子 url: https://www.umsbox.com desc: 软件资源聚合与下载导航 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=umsbox color: "#2980b9" - name: 狗破解 url: https://www.gopojie.com desc: 破解软件与实用工具分享 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=gopojie color: "#8e44ad" - name: 小众软件 url: https://www.appinn.com desc: 发现有趣、实用的小众软件 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=appinn color: "#16a085" - name: 吾爱破解 url: https://www.52pojie.cn desc: 国内知名的软件安全与破解交流论坛 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=52pojie color: "#c0392b" - name: 异星软件 url: https://www.yxssp.com desc: 软件下载与绿色资源分享站 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=yxssp color: "#2ecc71" - name: 翻应用 url: https://www.iapps.me desc: 应用推荐、评测与下载资源 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=iapps color: "#e74c3c" - name: 软仓 url: https://www.ruancang.net desc: 在线软件库与一键下载服务 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=ruancang color: "#34495e" - name: 异次元软件站 url: https://www.iplaysoft.com desc: 软件推荐、评测与使用技巧 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=iplaysoft color: "#f39c12" - name: 天机阁 url: https://www.tianjige.xyz desc: 软件资源分享与下载汇总 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=tianjige color: "#7f8c8d" - name: 绿软部落 url: https://www.ludown.com desc: 绿色软件下载与资源整理 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=ludown color: "#2ecc71" - name: 天机阁资源 url: https://tianjg.com desc: 软件下载与资源合集站点 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=tianjg color: "#95a5a6" - name: Mac 毒 url: https://www.macdo.cn desc: macOS 软件分享与下载推荐 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=macdo color: "#636e72" - name: 海阔天空 url: https://haikuoshijie.cn desc: 软件教程、资源汇总与使用指南 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=haikuoshijie color: "#00b894" - name: 小黑资源库 url: https://xhzyku.com desc: 软件与各类资源合集分享 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=xhzyku color: "#2d3436" - name: 乐享网 url: https://www.lxapk.com desc: 软件与 APK 资源下载分享 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=lxapk color: "#fdcb6e" - name: 网络工具 desc: 测速、调试与网络诊断 sites: - name: ADB 在线执行 url: https://adb.http.gs desc: 在浏览器中执行 ADB 命令调试安卓设备 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=adb color: "#0984e3" - name: 大市伐谋测速 url: https://zhale.me desc: 多线路网站访问速度与延迟检测 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=zhale color: "#6c5ce7" - name: UA 检测 url: https://ua.233996.xyz desc: 查看当前 HTTP 请求的 User-Agent avatar: https://api.dicebear.com/7.x/shapes/svg?seed=ua233996 color: "#a29bfe" - name: IPv6 测试（CH） url: https://ipv6-test.ch desc: 检测网络 IPv6 连通性与配置状态 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=ipv6ch color: "#00cec9" - name: IPv6 测试 url: https://test-ipv6.com desc: 全面的 IPv6 连接与双栈测试 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=testipv6 color: "#00b894" - name: ITDog url: https://www.itdog.cn desc: 在线 Ping、测速与网络诊断工具集 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=itdog color: "#e17055" - name: ZoomEye url: https://www.zoomeye.org desc: 网络空间资产搜索与探测引擎 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=zoomeye color: "#d63031" - name: 路由管理入口 url: https://tplogin.cn desc: 常见家用路由器本地管理登录页 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=tplogin color: "#636e72" - name: 在线工具 desc: 格式转换、查询与趣味实用服务 sites: - name: FUUN.FUN url: https://fuun.fun desc: 奇趣网站收藏与发现导航 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=fuun color: "#fd79a8" - name: AllToAll url: https://www.alltoall.net desc: 文档与文件格式在线转换 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=alltoall color: "#74b9ff" - name: 美国地址生成 url: https://www.meiguodizhi.com desc: 生成用于测试的美国虚拟地址 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=meiguodizhi color: "#55efc4" - name: Shields.io url: https://shields.io desc: 为开源项目生成状态徽章与标签 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=shields color: "#2d3436" - name: OnlineConvert url: https://www.online-convert.com desc: 支持多种格式的文件在线转换 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=onlineconvert color: "#e84393" - name: SimilarSites url: https://www.similarsites.com desc: 发现与当前网站相似的服务 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=similarsites color: "#0984e3" - name: 免费网络电话 url: https://www.freeonlinephone.org desc: 在线拨打虚拟号码进行测试 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=freeonlinephone color: "#00b894" - name: Versus url: https://versus.com desc: 手机、电脑等数码产品参数对比 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=versus color: "#6c5ce7" - name: PikPak 接口 url: https://pik.bilivo.top desc: PikPak 云盘相关登录接口服务 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=pikbilivo color: "#a29bfe" - name: 音乐解析 url: https://music.zhuolin.wang desc: 在线音乐链接解析与播放 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=zhuolin color: "#fdcb6e" - name: Brie.fi url: https://brie.fi desc: 在线视频播放与分享工具 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=briefi color: "#e17055" - name: 草料二维码 url: https://cli.im desc: 将文件与内容快速生成二维码 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=cliim color: "#00cec9" - name: CodeBox url: https://www.codebox.club desc: 可爱风格的二维码生成工具 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=codebox color: "#ff7675" - name: 图床服务 desc: 图片上传托管与外链 sites: - name: 聚合图床 url: https://www.superbed.cn desc: 多后端聚合的图片上传托管服务 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=superbed color: "#6c5ce7" - name: Postimages url: https://postimages.org desc: 免费图床，支持图片上传与外链 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=postimages color: "#0984e3" - name: 图床 APICUI url: https://picui.cn desc: 稳定高速的图片托管与 CDN 服务 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=picui color: "#00b894" - name: 设计绘画 desc: 灵感采集、素材管理与绘画参考 sites: - name: 花瓣网 url: https://huaban.com desc: 设计灵感采集与素材分享社区 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=huaban color: "#e84393" - name: MuseDAM url: https://musedam.cc desc: 设计素材管理与团队协作平台 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=musedam color: "#6c5ce7" - name: Billfish url: https://www.billfish.vip desc: 设计素材云管理与本地整理工具 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=billfish color: "#0984e3" - name: Pixcall url: https://pixcall.com desc: 图片素材采集同步与分类管理 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=pixcall color: "#00cec9" - name: Bodies in Motion url: https://www.bodiesinmotion.photo desc: 人体动态姿势与动作参考图库 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=bodiesinmotion color: "#636e72" - name: Line of Action url: https://line-of-action.com desc: 速写练习与姿势参考计时工具 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=lineofaction color: "#e17055" - name: POSEMANIACS url: https://www.posemaniacs.com desc: 3D 人体肌肉与姿势参考模型 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=posemaniacs color: "#d63031" - name: Magma 绘画 url: https://magma.com desc: 多人协作在线绘画白板 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=magma color: "#fd79a8" - name: 配色工具 desc: 调色板生成与设计配色灵感 sites: - name: 设计达人配色 url: https://www.shejidaren.com desc: 设计师分享的标准配色方案参考 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=shejidaren color: "#e84393" - name: Colourcode url: https://www.toptal.com desc: 可视化调色与配色板在线生成 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=toptal color: "#204ecf" - name: Coolors url: https://coolors.co desc: 流行配色方案生成与浏览 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=coolors color: "#00cec9" - name: UI Gradients url: https://uigradients.com desc: 精选 UI 界面渐变色搭配资源 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=uigradients color: "#6c5ce7" - name: Color Hunt url: https://colorhunt.co desc: 设计师社区分享的调色板合集 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=colorhunt color: "#fd79a8" - name: Color Drop url: https://colordrop.io desc: 简洁优雅的配色灵感收集站 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=colordrop color: "#a29bfe" - name: Adobe Color url: https://color.adobe.com desc: Adobe 官方色轮与配色方案工具 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=adobecolor color: "#ff0000" - name: 文档笔记 desc: 写作、笔记与知识管理 sites: - name: ONLYOFFICE url: https://docspace-ztghdm.onlyoffice.com desc: 在线协作文档与办公套件空间 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=onlyoffice color: "#ff6f3d" - name: 秘塔写作猫 url: https://xiezuocat.com desc: AI 辅助写作、润色与文本优化 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=xiezuocat color: "#0984e3" - name: Notion url: https://www.notion.so desc: 全能笔记、知识库与团队协作 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=notion color: "#2d3436" - name: flomo url: https://flomoapp.com desc: 轻量卡片式灵感笔记与备忘 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=flomo color: "#00b894" - name: Pinbox url: https://withpinbox.com desc: 网页收藏夹与稍后读管理工具 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=pinbox color: "#e17055" - name: Cubox url: https://cubox.pro desc: 智能书签阅读与知识收集助手 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=cubox color: "#6c5ce7" - name: Excalidraw url: https://excalidraw.com desc: 手绘风格白板与流程图绘制 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=excalidraw color: "#636e72" - name: 白描 OCR url: https://baimiaoapp.com desc: 在线图片文字识别与提取 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=baimiao color: "#00cec9" - name: 智能翻译 url: https://www.fanyi1234.com desc: 多语言在线翻译与文本转换 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=fanyi1234 color: "#74b9ff" - name: 燃烧词汇 url: https://burningvocabulary.cn desc: 英语四六级真题与词汇学习 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=burningvocabulary color: "#fdcb6e" - name: 图片处理 desc: 压缩、放大、抠图与修图 sites: - name: Docsmall url: https://docsmall.com desc: 免费在线图片压缩与 PDF 处理 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=docsmall color: "#0984e3" - name: Recompressor url: https://recompressor.com desc: 智能图像压缩与体积优化 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=recompressor color: "#00b894" - name: BigJPG url: https://bigjpg.com desc: AI 驱动的图片无损放大 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=bigjpg color: "#6c5ce7" - name: Vector Magic url: https://vectormagic.com desc: 位图一键转换为矢量图 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=vectormagic color: "#e17055" - name: Clipping Magic url: https://clippingmagic.com desc: 在线智能去除图片背景 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=clippingmagic color: "#00cec9" - name: Inpaint url: https://theinpaint.com desc: 在线去除水印与画面杂物修复 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=theinpaint color: "#a29bfe" - name: 美图秀秀 Web url: https://meitu.com desc: 在线图片美化、滤镜与编辑 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=meitu color: "#fd79a8" - name: 清图 url: https://qingtu.cn desc: 图片清晰化与画质增强工具 avatar: https://api.dicebear.com/7.x/shapes/svg?seed=qingtu color: "#2d3436" --- 第二个文件新增components\NavigationSiteList.vue 📌 {{ group.name }} {{ group.desc }} {{ site.name }} {{ site.desc }} 第三新增components\layouts\SakuraNavigationLayout.vue <SakuraTitle :fm="frontmatter" /> <RouterView v-slot="{ Component }"> <component :is="Component"> <NavigationRandomDraw :sites="allSites" :draw-videos="randomDrawVideos" /> <NavigationSiteList :nav-groups="navGroups" /> </component> </RouterView> 第四新增layouts\navigation.vue <SakuraNavigationLayout /> 第五新增types\navigation.tsexport interface NavDrawVideo { url: string /** 抽取权重，数值越大出现概率越高 */ weight: number } export interface NavSiteItem { name: string url: string desc?: string avatar?: string color?: string siteshot?: string } export interface NavSiteGroup { name?: string desc?: string sites: NavSiteItem[] }第六新增utils\parseNavDrawVideos.tsimport type { NavDrawVideo } from '../types/navigation' export function parseNavDrawVideos(input: unknown): NavDrawVideo[] { if (!Array.isArray(input)) return [] const result: NavDrawVideo[] = [] for (const item of input) { if (typeof item === 'string' && item.length > 0) { result.push({ url: item, weight: 1 }) continue } if (!item || typeof item !== 'object') continue const record = item as { url?: unknown, weight?: unknown } if (typeof record.url !== 'string' || !record.url.length) continue const weight = typeof record.weight === 'number' && record.weight > 0 ? record.weight : 1 result.push({ url: record.url, weight }) } return result }第七新增components\NavigationRandomDraw.vue {{ site.name }} {{ triggerLabel }} {{ triggerLabel }} 从本页全部导航中随机抽取 <Teleport to="body"> {{ selectedSite.name }} {{ selectedSite.url }} 前往访问 暂不跳转 </Teleport> 真是一场酣畅淋漓的美化，不是吗~
本系列教程共十篇valaxy博客全局美化教程（一）：分类、标签、归档三个页面的美化教程valaxy博客全局美化教程（二）：首页公告栏美化，新增随机文章展示板块valaxy博客全局美化教程（三）：给网页增加加载动画valaxy博客全局美化教程（四）：增加图片预览功能valaxy博客全局美化教程（五）：友链页面美化，修改了友链样式以及增加留言板valaxy博客全局美化教程（六）：留言页面增加信封展开效果valaxy博客全局美化教程（七）：新增网址导航页面valaxy博客全局美化教程（八）：页脚倒计时、搜索问题修复valaxy博客全局美化教程（九）：新增相册页面，支持使用webdav作为相册valaxy博客全局美化教程（十）：修复构建结束的时候会卡住话不多说，先看效果 页面下滑时信封自动展开，上划自动闭合，可以自己换图片，但是尺寸要一样哈，由于涉及到图床，大家尽量只下载图片，不要直接使用站长的链接，因为站长不能保证什么时候就失效了给出所有图片的图床链接：https://pic1.imgdb.cn/item/6a26e5483c9809430d376f88.png https://pic1.imgdb.cn/item/6a26e5483c9809430d376f87.png https://pic1.imgdb.cn/item/6a26e5483c9809430d376f85.png https://pic1.imgdb.cn/item/6a26e5483c9809430d376f86.png从上到下分别是信纸、顶部信封、底部信封和横条，下面链接和这里一样，大家最好修改一下，测试用着可以，不要生产环境使用哦新增文件新增文件咯，只有一个文件，有点长哦新增文件components\layouts\SakuraCommentLayout.vue <SakuraPage class="sakura-comment-page"> <RouterView v-slot="{ Component }"> <component :is="Component"> ✨来自{{ authorName }}的留言: {{ msg }} {{ bottomText }} ✨来自{{ authorName }}的留言: {{ msg }} {{ bottomText }} <SakuraComment /> </component> </RouterView> </SakuraPage> 提一句留言页面要新建页面，index.md文件如下配置：--- layout: comment title: 不留下点什么吗 icon: i-ri-chat-1-line nav: false cover: https://你的图床.jpg ---具体怎么创建页面看第一章教程末尾，顺便提一句，页面的访问方法，比如pages文件夹有一个comment文件夹，comment文件夹中有index.md文件，这就是一个页面了，在浏览器打开页面的方式就是：https://你的域名.com/comment。
本系列教程共十篇valaxy博客全局美化教程（一）：分类、标签、归档三个页面的美化教程valaxy博客全局美化教程（二）：首页公告栏美化，新增随机文章展示板块valaxy博客全局美化教程（三）：给网页增加加载动画valaxy博客全局美化教程（四）：增加图片预览功能valaxy博客全局美化教程（五）：友链页面美化，修改了友链样式以及增加留言板valaxy博客全局美化教程（六）：留言页面增加信封展开效果valaxy博客全局美化教程（七）：新增网址导航页面valaxy博客全局美化教程（八）：页脚倒计时、搜索问题修复valaxy博客全局美化教程（九）：新增相册页面，支持使用webdav作为相册valaxy博客全局美化教程（十）：修复构建结束的时候会卡住效果演示原本的友链页面比较单调，也没有留言板，就改了一下新增文件以下是新增文件部分哦，共4个文件第一个比较特殊，说明一下，公告部分再次修改const rules = [ { icon: '🎉', text: '本站支持交换友链，在您提出申请之前，请将本站添加到友链' }, { icon: '🍵', text: '为了保障本站用户，本站仅支持个人网站的友链申请' }, { icon: '💡', text: '申请本站友链需要拥有独立域名（非免费域名），建议开启全站 HTTPS' }, { icon: '⌛', text: '如果友情链接已经添加，请保持网站的正常访问，会定期清理僵尸网站' }, { icon: '🐚', text: '网站有一定的实质性内容和主题，不能是空壳网站和练手网站' }, { icon: '💕', text: '感谢您对本站的支持，如果您已经满足上述要求，请在下方表单提交友链申请~~~' }, ]这里面的中文可以自由修改下面这部分是自己站点的友链信息： 本站友链信息如下： 站点名称：{{ siteInfo.name }} 站点链接：{{ siteInfo.url }} 站长头像：{{ siteInfo.avatar }} 站点描述：{{ siteInfo.desc }} 站点截图：{{ siteInfo.siteshot }}正常情况会自动获取配置文件的链接、名称、头像等东西自动获取，但是如果你发现获取的信息不对，或者你就是想不一样，可以修改，举个例子，我要修改站点截图，就将{{ siteInfo.siteshot }}整个替换为你想加的链接，完整示例如下： 站点截图：https://你的图床.png还有，开头的：const props = withDefaults(defineProps<{ siteshot?: string }>(), { siteshot: 'https://你的图床.png', })图片链接记得替换好啦，开始新增第一个文件吧新增 components\FriendLinkNotice.vue ✦ 加入本站友链方式 参照以下格式留言即可 YML 📋 {{ index + 1 }}{{ line }} 🐱 + {{ rule.icon }} {{ rule.text }} 📍 本站友链信息如下： 站点名称：{{ siteInfo.name }} 站点链接：{{ siteInfo.url }} 站长头像：{{ siteInfo.avatar }} 站点描述：{{ siteInfo.desc }} 站点截图：{{ siteInfo.siteshot }} 第二个~下方文件内部有一行代码图床要替换哦，不换也行，上面自己站点信息站点截图链接自己写就行，不影响const previewFallback = 'https://你的图床.png'好啦，开始新增~新增components\SakuraLinks.vue 📌 {{ group.name }} 分类描述 🌸：{{ group.desc }} {{ link.blog }} {{ link.desc }} 第三~新增components\layouts\SakuraLinksLayout.vue <SakuraTitle :fm="frontmatter" /> <RouterView v-slot="{ Component }"> <component :is="Component"> <SakuraLinks :links="frontmatter.links" :link-groups="frontmatter.linkGroups" :random="frontmatter.random" :error-img="frontmatter.errorImg" /> <FriendLinkNotice :siteshot="frontmatter.cover" /> </component> </RouterView> 这是友链信息的索引文件咯，所有的友链在这里配置，这个应该不用过多介绍吧，跟着模版新增就行，格式都给你写好了的，copy~新增pages\links\index.md--- layout: links title: 来加入我们叭 icon: i-ri-links-line cover: https://你的图床.png comment: true linkGroups: - name: 紧密相连 desc: 这里是站长自己的网站 links: - url: https://yybb.us/ avatar: https://tele-tuchuang.pages.dev/file/50837ffeb21853209d7e0.png name: aiovtue's notion blog: aiovtue's notion desc: 所爱隔山海，山海不可平 color: "#0078e7" siteshot: https://r2tc.20030327.xyz/file/博客/主题/1780643226230_wallhaven-9d1yjk.png - url: https://20030327.xyz avatar: https://r2tc.20030327.xyz/file/博客/主题/1780643776417_1780643686584.png name: AIOVTUE blog: AIOVTUE desc: 雨是神的烟花 color: "#0078e7" siteshot: https://r2tc.20030327.xyz/file/博客/主题/1780643226230_wallhaven-9d1yjk.png - name: 推荐网站 desc: 这里是最熟悉的小伙伴 links: - url: https://example.com avatar: https://api.dicebear.com/7.x/adventurer/svg?seed=xeu name: demo blog: demo desc: demo color: "#6b8cff" ---好啦，到此结束
本系列教程共十篇valaxy博客全局美化教程（一）：分类、标签、归档三个页面的美化教程valaxy博客全局美化教程（二）：首页公告栏美化，新增随机文章展示板块valaxy博客全局美化教程（三）：给网页增加加载动画valaxy博客全局美化教程（四）：增加图片预览功能valaxy博客全局美化教程（五）：友链页面美化，修改了友链样式以及增加留言板valaxy博客全局美化教程（六）：留言页面增加信封展开效果valaxy博客全局美化教程（七）：新增网址导航页面valaxy博客全局美化教程（八）：页脚倒计时、搜索问题修复valaxy博客全局美化教程（九）：新增相册页面，支持使用webdav作为相册valaxy博客全局美化教程（十）：修复构建结束的时候会卡住效果预览简单介绍一下，我用的时候这个博客没有图片预览，就加了一个，大概效果是这样的，底部有预览栏，可以点击切换，移动端可以滑动切换，支持图片缩放，效果还是不错的，对了，后续有一个相册页面，如果想要加入相册页面的话，这个部分必须得加哦，否则可能会出小问题，因为这两个东西是一起写的，虽然二者的图片预览是单独分开的，但是说不好哦~开始美化修改文件修改文件部分共2个文件需要修改下面这个是css文件，控制组件特效的，默认文件时空的，直接新增即可在styles\index.scss文件增加以下代码/* 文章图片点击放大 */ .markdown-body img, .prose img { cursor: zoom-in; } html.image-gallery-open { overflow: hidden; }下面也是修改哦，下面给的是纯净代码，记得格式哦~文件site.config.ts的export default defineSiteConfig({})内增加以下代码 // 关闭自带的图片预览 mediumZoom: { enable: false, },解释一下，官方说自带了一个预览功能，我也没怎么研究，因为说的是很基本，不如自己加~这里就是给他关掉，放置两个一起生效（虽然压根就没开，其实大概不加这个也行）新增文件下面是新增文件部分啦，本部分共新增5个文件，有点长哦，别漏啦新增App.vue <ImageGalleryViewer /> 第二个文件~新增utils\imageGallery.tsimport { ref, shallowRef } from 'vue' export interface GalleryImage { src: string alt: string } export const galleryVisible = ref(false) export const galleryImages = shallowRef<GalleryImage[]>([]) export const galleryIndex = ref(0) function getImageSrc(img: HTMLImageElement) { return img.currentSrc || img.src || img.getAttribute('data-src') || '' } function isGalleryImage(img: HTMLImageElement) { if (!img.closest('.markdown-body')) return false if (img.closest('pre, code, .sakura-comment, .twikoo, nav, header, footer, .sakura-navbar, .sakura-post-nav')) return false const src = getImageSrc(img) if (!src || src.startsWith('data:')) return false return true } function collectImages(container: Element) { return Array.from(container.querySelectorAll<HTMLImageElement>('img')) .filter(isGalleryImage) .map(el => ({ el, src: getImageSrc(el), alt: el.alt || '', })) } export function openGallery(images: GalleryImage[], index: number) { if (!images.length) return galleryImages.value = images galleryIndex.value = Math.min(Math.max(index, 0), images.length - 1) galleryVisible.value = true document.body.style.overflow = 'hidden' } export function closeGallery() { galleryVisible.value = false document.body.style.overflow = '' } export function prevImage() { if (galleryIndex.value > 0) galleryIndex.value -= 1 } export function nextImage() { if (galleryIndex.value < galleryImages.value.length - 1) galleryIndex.value += 1 } export function goToImage(index: number) { if (index < 0 || index >= galleryImages.value.length) return galleryIndex.value = index } function handleImageClick(event: Event) { if (!(event.target instanceof Element)) return const img = event.target.closest('img') if (!(img instanceof HTMLImageElement)) return if (!isGalleryImage(img)) return const article = img.closest('.markdown-body') if (!article) return const list = collectImages(article) const index = list.findIndex(item => item.el === img) if (index === -1) return event.preventDefault() event.stopPropagation() openGallery( list.map(item => ({ src: item.src, alt: item.alt })), index, ) } let initialized = false export function initImageGallery() { if (initialized || typeof document === 'undefined') return initialized = true document.addEventListener('click', handleImageClick, true) }第三个咯~ ##### 新增components\ImageGalleryViewer.vue <Teleport to="body"> <Transition name="image-gallery-fade"> × <path d="M14.5 6.5 9 12l5.5 5.5" /> <path d="M9.5 6.5 15 12l-5.5 5.5" /> <div class="image-gallery__viewport" :class="{ 'is-zoomed': isZoomed || isPinching, 'is-dragging': isDragging, 'is-pinching': isPinching, }" @click.self="onMobileBackdropClose" @pointerdown="onPointerDown" @pointermove="onPointerMove" @pointerup="onPointerUp" @pointercancel="onPointerUp" @wheel.prevent="onWheel" @touchstart="onTouchStart" @touchmove.prevent="onTouchMove" @touchend="onTouchEnd" @touchcancel="onTouchCancel" > {{ current.alt }} {{ counter }} </Transition> </Teleport> 第四第四~新增utils\useLightboxSwipe.tsexport interface LightboxSwipeHandlers { onTouchStart: (event: TouchEvent) => void onTouchEnd: (event: TouchEvent) => void onTouchCancel: () => void } export function createLightboxSwipeHandlers(options: { enabled: () => boolean onPrev: () => void onNext: () => void shouldIgnore?: () => boolean threshold?: number }): LightboxSwipeHandlers { const threshold = options.threshold ?? 48 let touchStartX = 0 let touchStartY = 0 let tracking = false function reset() { tracking = false } function onTouchStart(event: TouchEvent) { if (!options.enabled() || options.shouldIgnore?.()) return if (event.touches.length !== 1) return touchStartX = event.touches[0].clientX touchStartY = event.touches[0].clientY tracking = true } function onTouchEnd(event: TouchEvent) { if (!tracking) return tracking = false if (!options.enabled() || options.shouldIgnore?.()) return const touch = event.changedTouches[0] if (!touch) return const deltaX = touch.clientX - touchStartX const deltaY = touch.clientY - touchStartY if (Math.abs(deltaX) < threshold) return if (Math.abs(deltaY) > Math.abs(deltaX)) return if (deltaX > 0) options.onPrev() else options.onNext() } return { onTouchStart, onTouchEnd, onTouchCancel: reset, } }最后一个啦 ##### 新增utils\useLightboxZoom.tsimport { computed, ref, type Ref } from 'vue' const MIN_SCALE = 1 const MAX_SCALE = 4 const DOUBLE_TAP_SCALE = 2 const DBLCLICK_TOUCH_GUARD_MS = 700 const DRAG_START_THRESHOLD = 6 const WHEEL_ZOOM_INTENSITY = 0.002 function getTouchDistance(touches: TouchList) { if (touches.length < 2) return 0 const dx = touches[0].clientX - touches[1].clientX const dy = touches[0].clientY - touches[1].clientY return Math.hypot(dx, dy) } function getTouchMidpoint(touches: TouchList) { return { x: (touches[0].clientX + touches[1].clientX) / 2, y: (touches[0].clientY + touches[1].clientY) / 2, } } export function useLightboxZoom(imageRef: Ref<HTMLImageElement | null>) { const currentScale = ref(MIN_SCALE) const isDragging = ref(false) const isPinching = ref(false) const isWheelZooming = ref(false) const panOffset = ref({ x: 0, y: 0 }) const isZoomed = computed(() => currentScale.value > MIN_SCALE + 0.01) let dragStartX = 0 let dragStartY = 0 let startPanX = 0 let startPanY = 0 let pendingDrag = false let activePointerId: number | null = null let dragCaptureTarget: HTMLElement | null = null let pinchStartDistance = 0 let pinchStartScale = MIN_SCALE let pinchStartPan = { x: 0, y: 0 } let pinchStartFocal = { x: 0, y: 0 } let pinchLayoutCenter = { x: 0, y: 0 } let suppressDblClickUntil = 0 let wheelZoomEndTimer: ReturnType<typeof setTimeout> | undefined function markTouchGesture() { suppressDblClickUntil = Date.now() + DBLCLICK_TOUCH_GUARD_MS } const imageTransformStyle = computed(() => { if (currentScale.value <= MIN_SCALE && panOffset.value.x === 0 && panOffset.value.y === 0) return {} const interacting = isDragging.value || isPinching.value || isWheelZooming.value return { transform: `translate(${panOffset.value.x}px, ${panOffset.value.y}px) scale(${currentScale.value})`, transformOrigin: 'center center', transition: interacting ? 'none' : 'transform 0.25s ease', } }) function getLayoutCenter() { const img = imageRef.value if (!img) return null const rect = img.getBoundingClientRect() if (rect.width === 0 || rect.height === 0) return null return { x: rect.left + rect.width / 2 - panOffset.value.x, y: rect.top + rect.height / 2 - panOffset.value.y, } } function adjustPanForScaleChange( focalX: number, focalY: number, oldScale: number, newScale: number, ) { const center = getLayoutCenter() if (!center) return panOffset.value = { x: focalX - center.x - (focalX - center.x - panOffset.value.x) * (newScale / oldScale), y: focalY - center.y - (focalY - center.y - panOffset.value.y) * (newScale / oldScale), } } function setScaleAt(focalX: number, focalY: number, newScale: number) { const oldScale = currentScale.value const clamped = Math.min(MAX_SCALE, Math.max(MIN_SCALE, newScale)) if (Math.abs(clamped - oldScale) < 0.001) return adjustPanForScaleChange(focalX, focalY, oldScale, clamped) currentScale.value = clamped } function resetZoomState() { currentScale.value = MIN_SCALE isDragging.value = false isPinching.value = false isWheelZooming.value = false pendingDrag = false activePointerId = null dragCaptureTarget = null panOffset.value = { x: 0, y: 0 } pinchStartDistance = 0 } function releasePointerCapture() { if (dragCaptureTarget && activePointerId !== null) { try { dragCaptureTarget.releasePointerCapture(activePointerId) } catch { // pointer may already be released } } activePointerId = null dragCaptureTarget = null } function zoomImageAt(clientX: number, clientY: number) { if (isZoomed.value) { resetZoomState() return } const img = imageRef.value if (!img) return const rect = img.getBoundingClientRect() if (rect.width === 0 || rect.height === 0) return setScaleAt(clientX, clientY, DOUBLE_TAP_SCALE) } function onImageDblClick(event: MouseEvent) { if (Date.now() < suppressDblClickUntil) { event.preventDefault() return } pendingDrag = false isDragging.value = false releasePointerCapture() zoomImageAt(event.clientX, event.clientY) } function onPointerDown(event: PointerEvent) { if (event.pointerType === 'touch') markTouchGesture() if (!isZoomed.value || isPinching.value || event.button !== 0) return pendingDrag = true activePointerId = event.pointerId dragCaptureTarget = event.currentTarget as HTMLElement dragStartX = event.clientX dragStartY = event.clientY startPanX = panOffset.value.x startPanY = panOffset.value.y } function onPointerMove(event: PointerEvent) { if (!pendingDrag && !isDragging.value) return if (activePointerId !== null && event.pointerId !== activePointerId) return if (pendingDrag && !isDragging.value) { const dx = event.clientX - dragStartX const dy = event.clientY - dragStartY if (Math.hypot(dx, dy) < DRAG_START_THRESHOLD) return isDragging.value = true pendingDrag = false if (dragCaptureTarget) dragCaptureTarget.setPointerCapture(event.pointerId) } panOffset.value = { x: startPanX + event.clientX - dragStartX, y: startPanY + event.clientY - dragStartY, } } function onPointerUp(event: PointerEvent) { if (activePointerId !== null && event.pointerId !== activePointerId) return pendingDrag = false isDragging.value = false releasePointerCapture() } function onWheel(event: WheelEvent) { event.preventDefault() isWheelZooming.value = true clearTimeout(wheelZoomEndTimer) wheelZoomEndTimer = setTimeout(() => { isWheelZooming.value = false }, 120) const newScale = Math.min( MAX_SCALE, Math.max(MIN_SCALE, currentScale.value * Math.exp(-event.deltaY * WHEEL_ZOOM_INTENSITY)), ) if (newScale <= MIN_SCALE + 0.02) { resetZoomState() return } setScaleAt(event.clientX, event.clientY, newScale) } function onTouchStart(event: TouchEvent) { markTouchGesture() if (event.touches.length !== 2) return isPinching.value = true isDragging.value = false pendingDrag = false releasePointerCapture() pinchStartDistance = getTouchDistance(event.touches) pinchStartScale = currentScale.value pinchStartPan = { ...panOffset.value } pinchStartFocal = getTouchMidpoint(event.touches) const center = getLayoutCenter() if (center) pinchLayoutCenter = { ...center } event.preventDefault() } function onTouchMove(event: TouchEvent) { if (event.touches.length === 2 && isPinching.value) { const distance = getTouchDistance(event.touches) if (pinchStartDistance <= 0) return const focal = getTouchMidpoint(event.touches) const newScale = Math.min( MAX_SCALE, Math.max(MIN_SCALE, pinchStartScale * (distance / pinchStartDistance)), ) const ratio = newScale / pinchStartScale panOffset.value = { x: focal.x - pinchLayoutCenter.x - (pinchStartFocal.x - pinchLayoutCenter.x - pinchStartPan.x) * ratio + (focal.x - pinchStartFocal.x), y: focal.y - pinchLayoutCenter.y - (pinchStartFocal.y - pinchLayoutCenter.y - pinchStartPan.y) * ratio + (focal.y - pinchStartFocal.y), } currentScale.value = newScale event.preventDefault() } } function onTouchEnd(event: TouchEvent) { markTouchGesture() if (!isPinching.value || event.touches.length >= 2) return isPinching.value = false pinchStartDistance = 0 if (currentScale.value <= MIN_SCALE + 0.02) resetZoomState() } function onTouchCancel() { markTouchGesture() if (isPinching.value) { isPinching.value = false pinchStartDistance = 0 if (currentScale.value <= MIN_SCALE + 0.02) resetZoomState() } } return { isZoomed, isDragging, isPinching, imageTransformStyle, resetZoomState, onImageDblClick, onPointerDown, onPointerMove, onPointerUp, onWheel, onTouchStart, onTouchMove, onTouchEnd, onTouchCancel, } }说一嘴，上面这个一行一个空格，其实本来不这样，但是复制过来自己加上了，但是不影响，就是有点不雅观~好啦到此结束咯~
本系列教程共十篇valaxy博客全局美化教程（一）：分类、标签、归档三个页面的美化教程valaxy博客全局美化教程（二）：首页公告栏美化，新增随机文章展示板块valaxy博客全局美化教程（三）：给网页增加加载动画valaxy博客全局美化教程（四）：增加图片预览功能valaxy博客全局美化教程（五）：友链页面美化，修改了友链样式以及增加留言板valaxy博客全局美化教程（六）：留言页面增加信封展开效果valaxy博客全局美化教程（七）：新增网址导航页面valaxy博客全局美化教程（八）：页脚倒计时、搜索问题修复valaxy博客全局美化教程（九）：新增相册页面，支持使用webdav作为相册valaxy博客全局美化教程（十）：修复构建结束的时候会卡住效果预览老规矩，先看效果 前置步骤本效果基于valaxy内置的foucGuard服务，需要先开启，全名叫FOUC（Flash of Unstyled Content）防护配置。效果是，通过在 中内联 body { opacity: 0 !important } 隐藏页面，并通过 JS 监测所有样式表加载完成后，移除该隐藏样式标签以显示页面，防止首屏样式闪烁和样式分批渲染的问题。简单来说就是在你的页面加载完成之前一直白屏，以防加载出乱码来，但是白屏不太美观，所以我加入了加载动画先开启服务：这是控制该组件的代码enabled（默认 true）：是否启用 FOUC 防护maxDuration（默认 5000）：最大等待时间（毫秒），作为 CSS 加载失败时的安全兜底。设为 0 可禁用超时兜底启用方式如下：修改valaxy.config.ts文件，在其中export default defineValaxyConfig({})部分增加以下代码： build: { ssgForPagination: false, foucGuard: { enabled: true, maxDuration: 5000, }, },最终的效果是这样的，别加错了：import { defineValaxyConfig } from 'valaxy' export default defineValaxyConfig({ //... build: { ssgForPagination: false, foucGuard: { enabled: true, maxDuration: 5000, }, }, //... })好啦，到这里服务就已经开起来，现在重启服务，刷新页面就能看到白屏了开始美化修改文件以下部分是修改文件，不是新增哦~文件valaxy.config.ts增加以下代码，下面是最终效果，注意这增加了两部分哦import { vaFoucLoader } from './plugins/va-fouc-loader' import siteConfig from './site.config'上面是第一部分，下面是第二部分，都要添加哦，而且给出的是完整代码，注意不要重复哦export default defineValaxyConfig({ //.... vite: { plugins: [vaFoucLoader({ avatar: siteConfig.author?.avatar, title: siteConfig.title, subtitle: siteConfig.subtitle, primary: '#E9CCCC', })], }, //..... })新增文件下面是新增文件部分，一共新增1个文件~新增文件plugins\va-fouc-loader.tsimport type { Plugin } from 'vite' export interface VaFoucLoaderOptions { avatar?: string title?: string subtitle?: string primary?: string } function buildSiteName(options: VaFoucLoaderOptions) { const title = options.title?.trim() const subtitle = options.subtitle?.trim() if (title && subtitle) return `${title}-${subtitle}` return title || subtitle || '' } const FOUC_STYLE = ' ' function escapeHtml(value: string) { return value .replace(/&/g, '&') .replace(/"/g, '"') .replace(/</g, '<') .replace(/>/g, '>') } function buildLoaderStyle(primary = '#E9CCCC') { const safePrimary = escapeHtml(primary) return ` ` } function buildLoaderMarkup(options: VaFoucLoaderOptions) { const avatar = options.avatar ? ` ` : '' const text = escapeHtml(buildSiteName(options)) return ` ${avatar} ${text} ` } const LOADER_SCRIPT = ` ;(function () { var HIDE_CLS = 'is-hidden' var LOADER_ID = 'va-fouc-loader' var hidden = false function hideLoader() { if (hidden) return hidden = true var loader = document.getElementById(LOADER_ID) if (!loader) return loader.classList.add(HIDE_CLS) setTimeout(function () { loader.remove() var style = document.getElementById('va-fouc-loader-style') if (style) style.remove() }, 300) } function appReady() { var app = document.getElementById('app') return !!(app && app.childElementCount > 0) } function tryHide() { if (document.readyState === 'complete' && appReady()) hideLoader() } window.addEventListener('load', function () { tryHide() if (!hidden) { var tries = 0 var timer = setInterval(function () { tries++ tryHide() if (hidden || tries >= 120) clearInterval(timer) }, 50) } }) setTimeout(hideLoader, 8000) })() <\/script>` export function vaFoucLoader(options: VaFoucLoaderOptions = {}): Plugin { const loaderStyle = buildLoaderStyle(options.primary) const loaderMarkup = buildLoaderMarkup(options) const loaderHead = `${loaderStyle}${LOADER_SCRIPT}` return { name: 'va-fouc-loader', transformIndexHtml: { order: 'post', handler(html) { if (html.includes('id="va-fouc-loader"')) return html let next = html if (next.includes(FOUC_STYLE)) next = next.replace(FOUC_STYLE, `${FOUC_STYLE}${loaderHead}`) else next = next.replace(' ', ` ${loaderHead}`) return next.replace(' ', ` ${loaderMarkup}`) }, }, } }好啦，本次教程又结束啦
本系列教程共十篇valaxy博客全局美化教程（一）：分类、标签、归档三个页面的美化教程valaxy博客全局美化教程（二）：首页公告栏美化，新增随机文章展示板块valaxy博客全局美化教程（三）：给网页增加加载动画valaxy博客全局美化教程（四）：增加图片预览功能valaxy博客全局美化教程（五）：友链页面美化，修改了友链样式以及增加留言板valaxy博客全局美化教程（六）：留言页面增加信封展开效果valaxy博客全局美化教程（七）：新增网址导航页面valaxy博客全局美化教程（八）：页脚倒计时、搜索问题修复valaxy博客全局美化教程（九）：新增相册页面，支持使用webdav作为相册valaxy博客全局美化教程（十）：修复构建结束的时候会卡住最终效果展示老样子，先看看最终效果左侧部分是公告栏，会自动识别连接，点击跳转，右侧部分是随机文章，会在这部分显示随机文章，并且会随时间自动切换，当然也可以滚轮手动切换开始美化那么久开始咯，大部分操作和上一篇类似，不会的可以回去看，这里不赘述了修改文件这部分内容是修改配置文件哦，本部分修改文件1这里我就是将themeConfig: {},一起给出了，注意区份哦文件valaxy.config.ts新增代码： themeConfig: { //... notice: { rotateInterval: 5000, title: '公告栏', sections: [ { label: '--- 主域名 ---', lines: [ 'daily.yybb.us', 'hexo.yybb.us', // 也支持对象：{ text: '显示文字', url: 'https://example.com' } ], }, { label: '--- 备用域名 ---', lines: [ 'vercel.yybb.us', 'aiovtue.onrender.com', 'aiovtue.zeabur.app', ], }, ], }, //... }修改文件到此结束啦，下面是新增文件新增文件以下是新增文件类型，本部分共新增文件1个，有点长哦新建文件components\SakuraNoticeBoard.vue {{ noticeTitle }} {{ section.label }} {{ line.text }} {{ line.text }} <RouterLink class="notice-board-wrap__article-link" :to="currentPost.path" :aria-label="`查看文章：${currentPost.title}`" /> 0 ? '后' : '前'}${Math.abs(dot.offset)}篇`" :aria-selected="dot.offset === 0" role="tab" @click="goToDot(dot.offset)" /> <Transition name="notice-post-fade" mode="out-in"> </Transition> <Transition name="notice-post-fade" mode="out-in"> <SakuraPostDate :date="currentPost.updated || currentPost.date" class="post-date order-1" pb-4 text-sm /> {{ currentPost.title }} <SakuraPostMeta pb-2 class="order-3" :post="currentPost" /> <SakuraPostExcerpt v-if="currentPost.excerpt" pb-2 class="order-4" :excerpt="currentPost.excerpt" /> </Transition> 暂无文章 好啦，刷新一下终端即可看到效果啦，不会的回去看第一篇最后部分
本系列教程共十篇valaxy博客全局美化教程（一）：分类、标签、归档三个页面的美化教程valaxy博客全局美化教程（二）：首页公告栏美化，新增随机文章展示板块valaxy博客全局美化教程（三）：给网页增加加载动画valaxy博客全局美化教程（四）：增加图片预览功能valaxy博客全局美化教程（五）：友链页面美化，修改了友链样式以及增加留言板valaxy博客全局美化教程（六）：留言页面增加信封展开效果valaxy博客全局美化教程（七）：新增网址导航页面valaxy博客全局美化教程（八）：页脚倒计时、搜索问题修复valaxy博客全局美化教程（九）：新增相册页面，支持使用webdav作为相册valaxy博客全局美化教程（十）：修复构建结束的时候会卡住最初部署简介简单介绍一下valaxy，Valaxy = V + Galaxy 旨在成为下一代静态博客框架，提供更好的热更新与用户加载体验、更强大更便捷的自定义开发可能性。附上开源地址valaxy本系列教程使用的主题是Sakura主题，附上开源地址sakura在开始教程之前先看一下最终的页面效果先：开始部署本教程基于windows端选择一个合适的地方新建一个文件夹。由于后期这个文件夹会变得非常大，所以建议一开始就选择合适的位置，推荐路径不要出现中文，虽然我弄下来没啥问题。使用管理员身份打开powershell终端虽然官方文档没要求必须管理员运行，但是我拉取文件的时候遇到一些问题，所有还是建议用一下。在终端cd到你的文件夹以防你不懂，加入你刚刚创建的文件夹路径是这个C:\demo\demo,那么你只需要输入cd加上你的文件夹路径即可，示例：cd C:\demo\demo。安装valaxy和主题该操作基于电脑安装了pnpm或npm的基础上，没安装的搜一下教程，或者看这个pnpm安装教程(主播也是百度找的~) 推荐安装pnpm，因为我用的这个，你们使用另一个的话后面可能会出一些奇奇怪怪的问题，毕竟我没用过 在刚刚的终端运行以下命令pnpm create valaxy，运行之后程序会开始创建流程会经理一下过程这个界面是问你用来干什么，选择第一个blog作为博客这是让你选择主题，我们选第三行custom,这行的意思是自己输入主题名输入主题名sakura，本教程所有修改基于此主题，虽然没改主题文件，但是选其他主题可能会遇到奇奇怪怪的问题这是问你项目名称叫什么，就是文件夹名称，随便起一个英文就行然后会问你是否运行，输入y就行这里选择pnpm即可开始自动安装出现下面的界面就是安装完成了上面的链接就是你的网站，ctrl+鼠标点击即可在浏览器打开链接，到此可以初步预览你的博客开始美化一共分为两大步，其中要增加六个文件，修改三个文件，由于大部分教程重复，只拿第一次出现的地方进行说明，后面忘记了回来看前面分类页面分类页面公新增两个文件，别漏了哦新增文件所谓新增文件的意思就是新增文件夹，哈哈哈，这里解释一下我的写法是什么意思，博客的目录类似于项目根目录/ │ ├─ .valaxy/ ├─ .vscode/ ├─ components/ ├─ layouts/ ├─ locales/ ├─ node_modules/ ├─ pages/ ├─ public/ ├─ styles/ │ ├─ .dockerignore ├─ .editorconfig ├─ .gitignore ├─ .npmrc ├─ Dockerfile ├─ netlify.toml ├─ nginx.conf ├─ package.json ├─ pnpm-lock.yaml ├─ README.md ├─ site.config.ts ├─ tsconfig.json ├─ valaxy.config.ts └─ vercel.json这个就是你刚才创建的文件夹，拉取valaxy博客之后的文件夹目录，我写的路径是基于此根目录的，举个例子，下方的\components\SakuraCategoriesChart.vue文件的意思就是，在文件夹components内部，新建一个名为SakuraCategoriesChart.vue的文件（windows创建这种文件的办法是右键，新建文本文档，然后把文件名连同后缀更改为这个即可），接着将文件下方的代码 这里就是第二个文件啦，我就赘述了，要看清楚路径哦~新增components\layouts\SakuraCategoriesLayout.vue <SakuraPage class="sakura-categories-page"> <RouterView v-slot="{ Component }"> <component :is="Component"> <slot name="content"> {{ t('counter.categories', Array.from(categories.children).length) }} <SakuraCategoriesChart v-if="categoryStyle === 'chart'" :categories="categories.children" /> <SakuraCategories v-else :categories="categories.children" /> </slot> <slot name="posts"> <SakuraPostList w="full" :posts /> </slot> </component> </RouterView> </SakuraPage> 归档页面现在开始是归档页面新增文件，这部分新增文件2个，别漏了哦新增文件新增components\SakuraArchivesChart.vue 这是第二个哦~新增components\layouts\SakuraArchivesLayout.vue <SakuraPage class="sakura-archivers-page"> <RouterView v-slot="{ Component }"> <component :is="Component"> <slot name="content"> <SakuraArchivesChart :posts="site.postList" :start-month="startMonth" /> <SakuraTimeLine :posts="site.postList" /> <SakuraTimeLine v-else :posts="site.postList" /> </slot> </component> </RouterView> </SakuraPage> 标签页面现在开始标签页面新增文件，该部分新增文件2个，别漏了哦~新增文件新增 components\SakuraTagsChart.vue 新增components\layouts\SakuraTagsLayout.vue <SakuraPage class="sakura-tags-page"> <RouterView v-slot="{ Component }"> <component :is="Component"> <slot name="content"> {{ t('counter.tags', Array.from(tags).length) }} <SakuraTagsChart :display-length="chartLength" /> <SakuraButton v-for="([key, tag]) in Array.from(tags).sort()" :key="key" class="sakura-tag-button" :class="{ clicked: curTag === key.toString() }" @click="displayTag(key.toString())" > {{ key }} [{{ tag.count }}] </SakuraButton> <SakuraDivider icon="i-fa6-solid:water" text="文章列表" :divider="false" /> <SakuraButton v-for="([key, tag]) in Array.from(tags).sort()" :key="key" class="sakura-tag-button" :class="{ clicked: curTag === key.toString() }" @click="displayTag(key.toString())" > {{ key }} [{{ tag.count }}] </SakuraButton> <SakuraDivider icon="i-fa6-solid:water" text="文章列表" :divider="false" /> </slot> <slot name="post"> <SakuraPostList :posts /> </slot> </component> </RouterView> </SakuraPage> 好啦，所有的新增文件部分都已经结束了，但是目前效果还没启动，我们还需要改一下配置文件进行引入才行，接下来跟我一起来操作。其他文件这部分文件和上面的不一样哦，也是新增文件，操作和上面还是一样的，该部分一共新增2个文件，加油吧utils\echarts.tsimport type { ECharts } from 'echarts' export type EChartsInstance = ECharts export type EchartsLib = typeof import('echarts') export function loadEcharts() { return import('echarts') } export function createGradient( echartsLib: Awaited<ReturnType<typeof loadEcharts>>, topColor = 'rgba(128, 255, 165)', bottomColor = 'rgba(1, 191, 236)', ) { return echartsLib.graphic?.LinearGradient ? new echartsLib.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: topColor }, { offset: 1, color: bottomColor }, ]) : topColor } export function observeChartResize( el: HTMLElement | undefined, onResize: () => void, ) { if (!el) return () => {} let resizeTimer: ReturnType<typeof setTimeout> | undefined const scheduleResize = () => { clearTimeout(resizeTimer) resizeTimer = setTimeout(onResize, 100) } const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(scheduleResize) : null observer?.observe(el) window.addEventListener('resize', scheduleResize) return () => { observer?.disconnect() window.removeEventListener('resize', scheduleResize) clearTimeout(resizeTimer) } } 这是下一个文件styles\index.scss/* 统计图容器：与文章卡片一致的背景与黑色描边，横向铺满视口并保留边距 */ .sakura-stat-chart { --chart-edge: max(20px, env(safe-area-inset-left, 0px)); box-sizing: border-box; width: calc(100vw - 2 * var(--chart-edge)); max-width: none; margin-inline: calc(50% - 50vw + var(--chart-edge)); border-radius: var(--sakura-post-card-rd, 12px); background: var(--sakura-post-card-bg, var(--va-c-bg-soft)); border: 1px solid rgba(0, 0, 0, 0.85); } @media (min-width: 640px) { .sakura-stat-chart { --chart-edge: max(40px, env(safe-area-inset-left, 0px)); } } html.dark .sakura-stat-chart { border-color: var(--sakura-color-divider, rgba(255, 255, 255, 0.2)); }修改文件注意哦，这部分的文件不是新建，是在已有基础上修改，这种部分是需要从已有文件进行增加或删除部分代码，因为只占文件的一小部分，所以不适合整个复制这里解释一下下方写法的意思，当我注明是修改文件时，或者你们根据路径找过去，发现已经有一个同名文件，就说明这是修改类型，下方的文件名和上方的一样，都是路径加文件名的格式，最常见的文件修改就是valaxy.config.ts，因为这是配置文件，大部分修改都要在里面操作，简单介绍一下这个文件的部分这是初始状态的文件：// import type { UserThemeConfig } from 'valaxy-theme-sakura' import { defineValaxyConfig } from 'valaxy' // add icons what you will need const safelist = [ 'i-ri-home-line', ] /** * User Config */ export default defineValaxyConfig({ // site config see site.config.ts theme: 'sakura', themeConfig: {}, unocss: { safelist }, }) 这部分import { defineValaxyConfig } from 'valaxy'是引入部分，后续也会涉及一些引入，跟这一行代码一样，在这一行下方粘贴对应代码即可，例如，当需要新增代码import { demo} from 'demo'代码时，效果就如下方这样：import { defineValaxyConfig } from 'valaxy' import { demo} from 'demo'是不是很简单，还有一部分就是themeConfig: {},部分，这部分是主题配置部分，我一般给出的代码格式都是对好的，从下一行的第一个字符直接粘贴即可，这里下面就是一个很好的例子这里直接写valaxy.config.ts没写路径，不是忘记写了，是此文件在根目录下，打开此文件进行修改，这里我给出的是增加部分代码，有时候我也会连着themeConfig: {},一起给，注意区份哦，我一般会说明的，这里复制下列代码，将其粘贴在themeConfig: {},的{}里面即可。valaxy.config.ts的themeConfig内增加以下代码 // 分类页样式：list 列表 / chart 环状图（玫瑰图或旭日图） categories: { style: 'chart', }, // 归档页样式：list 时间线 / chart 发布统计折线面积图 archives: { style: 'chart', startMonth: '2020-01', }, // 标签页样式：list 按钮列表 / chart 柱状统计图 tagsPage: { style: 'chart', chartLength: 10, }, tags: { rainbow: false, },下面的代码是加上themeConfig: {},的效果，///...这样的类似符号表示省略代码，因为这部分可以写很多东西，所以加在哪里都行，不过注意格式。最终效果：themeConfig: { ///... // 分类页样式：list 列表 / chart 环状图（玫瑰图或旭日图） categories: { style: 'chart', }, // 归档页样式：list 时间线 / chart 发布统计折线面积图 archives: { style: 'chart', startMonth: '2020-01', }, // 标签页样式：list 按钮列表 / chart 柱状统计图 tagsPage: { style: 'chart', chartLength: 10, }, tags: { rainbow: false, }, }, 到此所有的修改就都完成了，如果你的终端服务还启动着，直接输入r即可刷新，再次前往浏览器就能看到样式了，如果终端关掉了，cd到项目目录，运行pnpm dev即可开启网页服务。提一句页面，就是分类页，归档页那些，不是一开始就有的，需要自己创建，具体位置在pages文件夹内，例如，我要创建一个分类页面，就在pages文件夹内部新建一个名为categories的文件夹（其实文件夹叫什么都可以，英文即可），如何在categories文件夹内部新建一个index.md文件，在文件内写上：--- layout: categories title: 分类 icon: i-ri-folder-line cover: https://你的图床.png comment: false ---解释一下，layout: categories这个决定了你这是什么页面，这一行代表的就是分类页面，title就是页面标题，icon: i-ri-folder-line这个是图标，cover: https://你的图床.png这个是该页面顶部的头图，comment: false这个是是否开启评论（这个需要另外配置）这里给出三个页面的index.md文件的内容，上面那个就是分类页面的下面是标签页面：--- layout: tags title: 标签 icon: i-ri-price-tag-3-line cover: https://你的图床.png comment: false ---归档页面：--- layout: archives title: 归档 icon: i-ri-archive-line cover: https://你的图床.png comment: false ---其他本篇教程不涉及的页面可以看sakura主题文档好啦，第一篇到此结束咯~有用的话打个call吧~