小站修补琐记

因为自己非常追求细节,同时也想分享一下开发过程中的琐事与技巧,特此设立“小站修补琐记”板块,持续更新!

2025.10 + 2025.11

明暗切换

Cactus模板内置了几种颜色主题,但是只能通过配置文件切换,不能动态切换。

动态切换明暗的核心是通过对html元素(或者body元素)添加如light/dark这样的class,再在CSS分别写入对应的颜色等规则完成的

经过观察,CSS使用Stylus编译,颜色被放在了themes/cactus/css/_color下的各个文件里,以变量形式存储,而Stylus支持块级导入,所以,我们可以把导入从这样

1
@import "_colors/" + $colors

写成

1
2
3
4
.light
@import "_colors/" + $colors-light
.night
@import "_colors/" + $colors-night

$colors这个变量在$_variables.styl里面定义,进行相应修改即可

但仅仅这样在编译的时候会报错,原因是在CSS的定义中用到了darken之类的函数,这些函数会在编译时执行,然后硬编码进生成的.css文件中,然而块级定义导致这些变量没有明确的值,无法执行函数,故报错终止。

最好的办法就是将这些定义的.light/.night分开定义,比如,把下面的

1
2
*::-webkit-scrollbar-thumb:active
background-color: darken($color-scrollbar, 30%)

写成

1
2
3
4
5
*::-webkit-scrollbar-thumb:active
.night &
background-color: darken($color-scrollbar, 30%)
.light &
background-color: darken($color-scrollbar, 30%)

但是我不保证这种方法的可行性,因为我一开始不知道Stylus可以块级导入,就手动把所有的变量加上-light-/-dark-中缀,并将颜色直接硬编码在style.styl里面(所以才说拉了坨大的,而且有一大堆漏的)

然后就是增加切换按钮和持久化存储功能,这里通过修改模板layout.ejs实现

head.ejs里面添加一段JavaScript,用于切换主题

1
2
3
4
5
6
colorScheme = localStorage.getItem('color-scheme') || (matchMedia('(prefers-color-scheme: dark)').matches?"night":"light")
if(colorScheme==="light"){
let classlist = document.querySelector("html").classList
classlist.remove('night')
classlist.add('light')
}

先尝试从localStorage里面读取是否已有主题,如果没有,就根据媒体查询同步浏览器主题。为了防止JS莫名其妙的炸掉,在html上加了.night,所以如果选择亮主题的话就要切换。

然后是切换按钮,在layout.ejsbody下面添加

1
2
3
4
<div style="position: fixed; right: 0; bottom: 0; z-index: 999; margin: 10px;">
<button id="sun" style="border: none; background: none; color:white; cursor: pointer;" onclick="switchColorScheme('light')"><svg style="width: 25px; height: 25px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path fill="none" d="M0 0h24v24H0z"></path><path d="M12 18C8.68629 18 6 15.3137 6 12C6 8.68629 8.68629 6 12 6C15.3137 6 18 8.68629 18 12C18 15.3137 15.3137 18 12 18ZM12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16ZM11 1H13V4H11V1ZM11 20H13V23H11V20ZM3.51472 4.92893L4.92893 3.51472L7.05025 5.63604L5.63604 7.05025L3.51472 4.92893ZM16.9497 18.364L18.364 16.9497L20.4853 19.0711L19.0711 20.4853L16.9497 18.364ZM19.0711 3.51472L20.4853 4.92893L18.364 7.05025L16.9497 5.63604L19.0711 3.51472ZM5.63604 16.9497L7.05025 18.364L4.92893 20.4853L3.51472 19.0711L5.63604 16.9497ZM23 11V13H20V11H23ZM4 11V13H1V11H4Z"></path></svg></button>
<button id="moon" style="border: none; background: none; color: black; cursor: pointer;" onclick="switchColorScheme('night')"><svg style="width: 25px; height: 25px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path fill="none" d="M0 0h24v24H0z"></path><path d="M11.3807 2.01886C9.91573 3.38768 9 5.3369 9 7.49999C9 11.6421 12.3579 15 16.5 15C18.6631 15 20.6123 14.0843 21.9811 12.6193C21.6613 17.8537 17.3149 22 12 22C6.47715 22 2 17.5228 2 12C2 6.68514 6.14629 2.33869 11.3807 2.01886Z"></path></svg></button>
</div>

在里面的图标是从这个网站复制的矢量图

以及切换逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
colorScheme = localStorage.getItem('color-scheme') || (matchMedia('(prefers-color-scheme: dark)').matches?"night":"light")
if(colorScheme==='light'){
document.getElementById("sun").style.display = "none"
document.getElementById("moon").style.display = ""
}else{
document.getElementById("sun").style.display = ""
document.getElementById("moon").style.display = "none"
}
function switchColorScheme(scheme){
localStorage.setItem("color-scheme", scheme)
if(scheme==='light'){
document.getElementById("sun").style.display = "none"
document.getElementById("moon").style.display = ""
let classlist = document.querySelector("html").classList
classlist.remove('night')
classlist.add('light')
}else{
document.getElementById("sun").style.display = ""
document.getElementById("moon").style.display = "none"
let classlist = document.querySelector("html").classList
classlist.add('night')
classlist.remove('light')
}
}

接下来就可以自如切换啦~

返回按钮

有一点小瑕疵,因为我加的返回按钮是通过history.go(-1)实现的,所以在这里切换主题后通过返回按钮返回后需要刷新才会生效,并且在通过目录跳转过的情况下这个按钮会返回上一个锚点,而不是上一个界面。
已修复,现在返回按钮通过location.href = document.referrer || '/'实现(但还是有bug,如果从一篇文章跳到另一篇,用返回键就会来回跳转,或许应该用SessionStorage?)

左上角的博客名称

最开始是没想适配手机的,但是后面还是适配了

添加没什么,主要讲讲如何适配

首先,在电脑这种大地方,在左上角显示并不多余,而在手机的首页和关于页等地,有一个博客名称,故在这些页隐藏;在文章页,屏幕比较窄的话图标会和字重叠,干脆显示成一整行,中间有一个过渡态,就是刚刚穿过文字显示边界的时候,和“返回”按钮挨得特别近,不好看,就让它挨着左上角了

其实对右边那个工具栏我也有修改,让它翻下去不会消失,但是页面变窄之后会和左上角的重合,就是以此分界过渡态和横条状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// is_post() 时
@media screen and (max-width: 1468px) { // 过渡态
.hide-on-phone{
margin: 0px !important;
border-bottom-left-radius: 0px !important;
border-top-right-radius: 0px !important;
border-top: none !important;
border-left: none !important;
}
}
@media screen and (max-width: 630px) { // 横条状态
.hide-on-phone{
margin: 0 !important;
width: 100%;
border: none !important;
border-radius: 0 !important;
border-bottom: gray 1px solid !important;
}
}

// !is_post() 时
@media screen and (max-width: 1468px) { // 直接隐藏
.hide-on-phone{
display: none !important;
}
}

随机学校

这个是一个很简单的JavaScript动态界面,每次会用Math.floor(Math.random()*3)生成随机数0/1/2,然后用document.getElementById(...).innerText = ...完成动态修改

并且为了防止小孩误食防止真的有人去报考中国邮电大学百度百科传送门,更新加上了红色波浪线警示,并且将鼠标放置在其上时会显示提示(前面的这个也有),并且可以点击刷新(前面的没有),并且人性化地考虑到一顿狂点可能会被选中而影响美观,特意添加了user-select: none(前面那个也没有)

1
2
3
4
5
6
7
8
......目前就读于<span id="studyIn" style="text-decoration: underline wavy red; cursor: pointer; user-select: none;" title="Not Truth"></span>......

<script>
studyIn = ["北京邮电学院", "中国邮电大学", "北京等通知大学"]
randomStudyIn = ()=>{document.getElementById("studyIn").innerText = studyIn[Math.floor(Math.random()*3)]}
randomStudyIn()
document.getElementById("studyIn").addEventListener("click", randomStudyIn)
</script>

图裂修复

这个似乎是hexo的特性,网上的解决方法好像都要额外下载内容,我就不!反正图片已经被移动到正确的地方了,那我就写一个JavaScript修复就是了嘛

就目前而言一切正常,可以放心食用

1
2
3
4
5
6
7
8
// 修复图炸的情况
(new Array(...document.getElementsByTagName('img'))).forEach(
img=>{
if(img.src.split('/')[3]!='images'&&!img.src.startsWith(location.href.split("#")[0])){
console.log(`"${decodeURI(img.src.split('/').slice(-1))}" 貌似炸了`)
img.src=img.src.split('/').slice(-1)
}
})

前面的提示的路径也是动态生成的

又小小的更新一下(2026-1-3),加了一个decodeURI解码%xx,使log更像人类(bushi

1
2
3
4
5
6
7
8
9
10
> 不知道为什么图片路径可能会炸掉,在最后面写了一个简易的修复代码,不能保证一定有用
>
> 如果图炸了的话请手动拼接URL,为
>
> <span id="urlNow"></span>image-编号.png

<script>
document.getElementById("urlNow").innerText = location.href.split('#')[0]
document.getElementById("urlNow").innerText += location.href.split('#')[0].slice(-1)==='/'?'':'/'
</script>

至于为什么是image-编号.png,是因为在VSCode的Markdown编辑器里粘贴图片的默认命名就是这个,还是要以实际为准

2026.1

back按钮

突然想捣腾一下小站了,把back按钮使用sessionStorage存储,这下不至于在同一页的锚点上来回跳了,
但是这样的操作成功地往浏览器自己的History里面拉了一坨大的:每次点back会被认为是一次“新页面”

综合考虑下,打算给自己的网页加一个FeatureConfig页

FeatureConfig页

说搞就搞!

首先在首页加一个Feature按钮,找到面包条在_partial/header.ejs里生成

面包条的内容来源于theme的config,进去修改,并且__应该是翻译函数,所以为了用不到的i18n,改一下对应翻译的内容

然后hexo g && hexo s一下看看效果

好!接下来就是添加可选的特性了!

特性依旧使用localStorage存储

先来一个localStorage检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<p id="support-storage" class="unknow">正在检测你的浏览器是否支持localStorage……🔎</p>

<style>
.unknow{
border: 1px hsla(54, 88%, 24%, 1.00) solid;
color: hsla(54, 88%, 24%, 1.00);
background-color: hsla(54, 90%, 85%, 1.00);
border-radius: 5px;
padding: 5px;
width: fit-content;
}
.support{
border: 1px #077207 solid;
color: #077207;
background-color: #b4fbb4;
border-radius: 5px;
padding: 5px;
width: fit-content;
}
.unsupport{
border: 1px #720707 solid;
color: #720707;
background-color: #fbb4b4;
border-radius: 5px;
padding: 5px;
width: fit-content;
}
</style>

<script>
function test(){
try{
localStorage.setItem('test', 'ok');
var result = localStorage.getItem('test') == 'ok';
localStorage.removeItem('test');
return result;
}catch(error){
console.error(error);
return false;
}
}
let result = test();
document.getElementById('support-storage').classList.remove('unknow')
if(result){
document.getElementById('support-storage').innerText = '你的浏览器支持localStorage!🎉🎉🎉'
document.getElementById('support-storage').classList.add('support')
}else{
document.getElementById('support-storage').innerText = '你的浏览器好像不支持localStorage!🤔'
document.getElementById('support-storage').classList.add('unsupport')
}
</script>

然后加上各部分的代码……(有点多,不放了)

一通倒腾,搞定了~ 修改特性传送门

并且为了方便,加了个toc属性,通过layout.ejs里面判断page.toc是否为真来强制显示toc

希望不要有bug

给图片加边框

从背景相似的地方截图根本看不清边界,给所有图片加了一个边框~

修复了一些之前产的史

如题,发现于原版Cactus有点不一样,经查发现.light selector无法生效,需替换成

1
2
3
selector
.light &
...

并且将desktop版的nav改回了仅在顶部附近显示(这里我发现阈值过小,导致无法重新显示,于是调大了阈值,并且在已经写了这么多NativeJS的情况下发现他竟然有jQuery……)

上线“友链抽奖机”

虽然一个友链都没有 ,但是“友链抽奖机”终于写完了!

这个抽奖机拥抱AI,因为我实在不想写前端了!用的D老师,效果还不错~

快去抽友链吧!

对了,把D老师之前几版也放一下,下面的都是D老师根据提示词自己写出来的~

  • V1 -> 最原始的简单提示词,看起来不错,就是AI味太大了
    提示词我想给我的博客写一个“友链抽奖机”,有一张类似牌的元素初始展示背面,点击后3D快速旋转,每转一圈随机一个友链(包括方形图片+大字名称+小字描述+加粗网址),每个友链有几种不同“品质”随机出现,用不同背景、前景色效果展示(比如默认、蓝、绿、金、红),旋转由快到慢,最后停在某个品质的友链上后就向localStorage存储一下,记录抽到了哪些友链及品质,再写一个展示抽到的友链的页面,以友链 分,显示抽到的最高品质的颜色,点击后弹出浮层,显示友链详情和抽到过的品质列表及数量,未抽中的留灰色占位,最后给出一个作弊按钮用来一键解锁所有基础友链。友链数据记录在JS对象中,可能随时间更改,编写代码时请注意这一点
  • V2 -> 让D老师接受我的“现代”审美,至少AI味没有那么大了
    提示词我想给我的博客写一个“友链抽奖机”,有一张类似牌的元素初始展示背面,点击后3D快速旋转,每转一圈随机一个友链(包括方形图片+大字名称+小字描述+加粗网址),每个友链都有五种不同“品质”随机出现,用不同背景、前景色效果展示(比如默认、绿、蓝、金、红),旋转由快匀速变慢,每次背面朝外时随机刷新背面的友链和品质,最后停在某个品质的友链上后就向localStorage存储一下,记录抽到了哪些友链及品质,再写一个展示抽到的友链的页面,以友链分,显示与抽取的卡片类似,颜色选抽到的最高品质的颜色,未抽中则为灰色,并显示???,根据品质、数量降序排列,并有各品质收集进度,有复选框来决定是否显示未抽中的,有一个搜索的功能(未抽中的也按名称参与,但仍显示???),给出一个作弊按钮“一键解锁”用来给未抽中过的友链添加一个普通品质,添加导入导出功能。最后给出一个danger的删除数据按钮。友链数据记录在JS对象中,可能随时间更改,编写代码时请注意这一点。尽可能用默认值来兼容上下文样式,并且除了卡牌、品质等适合使用圆角矩形的地方,尽可能采用尖角矩形、非渐变,按钮强调下缘的简约风格
  • V3 -> 从V2的提示词进一步修改,最终版就是从这版上改的
    提示词我想给我的博客写一个“友链抽奖机”,有一张类似牌的元素初始展示背面,点击后3D快速旋转,每转一圈随机一个友链(包括方形图片+大字名称+小字描述+加粗网址),每个友链都有五种不同“品质”随机出现,用不同背景、前景色效果展示(比如默认、绿、蓝、金、红),旋转由快匀速变慢,每次背面朝外时随机刷新背面的友链和品质,用JS实现,最后停在某个品质的友链上后就向localStorage存储一下,记录抽到了哪些友链及品质,再写一个展示抽到的友链的页面,以友链分,显示与抽取的卡片类似,颜色选抽到的最高品质的颜色,未抽中则为灰色,并显示???,根据品质、数量降序排列,并有各品质收集进度,有复选框来决定是否显示未抽中的,有一个搜索的功能(未抽中的也按名称参与,但仍显示???),给出一个作弊按钮“一键解锁”用来给未抽中过的友链添加一个普通品质,添加导入导出功能,点击后弹出浮层,左侧是较大的头像,右上是标题、说明、链接,若超出则显示滚动条,右下留一小条背景稍不同,用倾斜圆角矩形和对应颜色表示某种品质的牌,矩形里数字代表抽到的次数(注意2位数以上字体大小),若未抽到对应品质则背景为灰色、标0,矩形下方给出品质名称,注意间距。最后给出一个danger的删除数据按钮。友链数据记录在JS对象中,可能随时间更改,编写代码时请注意这一点。两个页面写在一起。我像把它加到已有的界面中,尽可能用默认值来兼容上下文样式,并且除了卡牌、品质等适合使用圆角矩形的地方,尽可能采用尖角矩形、非渐变,按钮强调下缘的简约风格

基于V3,通过反复向D老师提要求+自己修,慢慢地完成了这个“浩大工程”

alt text

(完全想象不出来自己写这么大托需要多久,我上一个纯手写的HTML前端只有798行41297字符)

不过D老师还不是万能的,比如旋转动画写不出来,偶尔听不懂人话(不过不排除是上下文过大导致的),最后修改还得是我来做

听说GPT、Gemini效果很好,不知道长城外的AI怎么样呢?

进一步改进“友链抽奖机”

突然想到会不会有人把博客名字和介绍写的特别特别长,添加了截断,并且让友链详情的url会自动滚动了!

然后还加入了友链消失和上新提示,加了一个友链全收集提示

还加了体力全恢复的时间和对应的时刻

啊吧啊吧啊吧……