小站修补琐记2

上一个修补琐记太古早了,再新建一个占个坑

2026/2/28: 真の缓存 & 一些小修小补

终于有时间倒腾一下小站了,把友链抽奖机的图片改成真·缓存,并且上传了近几次比赛写了的WP

原来的缓存是D老师写的,原理是预先访问一次,然后浏览器进行“缓存”

后来想起来为什么不用blob呢,于是就写了一个真·缓存,大致如下

1
2
3
4
const response = await fetch(friend.avatar);  // 使用fetch获得资源
const blob = await response.blob(); // 将资源读取成Blob对象
const cachedURL = URL.createObjectURL(blob); // 生成blob:链接
friend.avatar = cachedURL; // 将图像链接用blob:链接替换,使用时浏览器直接从内存读取

同时更新/修正了关于页的内容和样式,在友链抽奖机中加入了求友链的说明

2026/4/13: 友链++ & 小修小补 & 一个决定?

今天加了不少友链,开心~

对图片缓存逻辑进行了一点小修,try catch了一下url不支持CORS的情况,退回到浏览器自带缓存机制

以及决定择期上线一个每咕一题页面,闲的没事可以搓几道史给大家尝尝

2026/4/17: 每咕一题调试完成上线

经过几天高强度的手搓代码,终于,每咕一题完成上线了!

alt text

本次代码保证纯手工打造,100%原汁原味(bushi

好了,其实还是让D老师帮忙写了一下HistoryAPI有关的代码的,这个东西我实在是不想吐槽了

下面展示一些技术细节

Tag组开关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
document.querySelectorAll('#index-page .tag').forEach(e => e.addEventListener("click", evt => {
// 如果所有标签都激活,则等效于所有都不激活
if (![...evt.srcElement.parentElement.children].filter(elem => elem.classList.contains('deactive')).length) {
[...evt.srcElement.parentElement.children].forEach(elem => elem.classList.add("deactive"));
}
// 切换标签激活状态
evt.srcElement.classList.toggle("deactive");
// 如果所有标签都不激活,则等效于所有都激活
if (![...evt.srcElement.parentElement.children].filter(elem => !elem.classList.contains('deactive')).length) {
[...evt.srcElement.parentElement.children].forEach(elem => elem.classList.remove("deactive"));
}
// 重置搜索页面数
pagecount = 0;
// 渲染新列表
renderProblemList()
}));

筛选功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function doSearch() {
return Object.entries(problems).filter(kv => {
let [pid, item] = kv;
// 忽略大小写搜索
if (document.getElementById("search").value && !(item.title + item.desc.replace(/<.*?>/g, '')).toLowerCase().includes(document.getElementById("search").value.toLowerCase())) {
return false;
}
// 按解出状态筛选
if (![...document.querySelectorAll("#solve-tags .tag:not(.deactive)")].map(e => e.innerText).includes(!(solveData['solved'][pid] || []).length ? '未解出' : (solveData['solved'][pid].length == item['flag'] ? '已解出' : '部分解出'))) {
return false;
}
// 按题目类型筛选
if (!item["tags"].filter(t => [...document.querySelectorAll("#category-tags .tag:not(.deactive)")].map(e => e.innerText).includes(t)).length) {
return false;
}
// 按题目特点筛选
if (!item["tags"].filter(t => [...document.querySelectorAll("#desc-tags .tag:not(.deactive)")].map(e => e.innerText).includes(t)).length) {
return false;
}
return true;
});
}

强兼博客代码样式

为了渲染WP中的内容(尤其是代码),我试了不少方式,最后还是决定手动解决WP渲染,就只剩下了一个code的highlight

经过搜索,我发现hexo的默认渲染引擎是highlight.js,并且默认移除hljs-前缀

我懒得修改hexo配置,于是就写了一段代码移除hljs-前缀,就可以使用博客主题分亮暗主题高亮了~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function removeHljsPrefix(elem) {
elem.classList.forEach(i => {
if (i.startsWith('hljs-')) {
elem.classList.replace(i, i.slice(5));
}
});
[...elem.children].forEach(sub => removeHljsPrefix(sub));
}

// 在渲染问题详情时
hljs.highlightAll();
setTimeout(() => document.querySelectorAll('pre code').forEach(
elem => {
elem.classList.add("code");
elem.parentElement.classList.add("highlight");
removeHljsPrefix(elem);
}
), 10)

hook <details> & <a>

为了在打开折叠框/下载黑盒文件前弹窗,对这两个元素进行了click事件的监听

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
function hookDetailsOpen(elem) {
elem.addEventListener("toggle", async (evt) => {
if (elem.open && (!elem.dataset.confirmed || elem.dataset.confirmed == 'false')) { // 未确认过
elem.open = false; // 将打开的details关闭
if (await showConfirm('折叠框内含有与解题思路相关度较高的内容,您确定要展开吗?', 'Spoiler Alert!')) {
elem.dataset.confirmed = true; // 确认后记录
let arr = solveData['confirmed'][currentDisplayedPid] || [];
arr.push(elem.dataset.confirmId);
solveData['confirmed'][currentDisplayedPid] = arr;
saveData();
elem.open = true; // 重新打开
}
}
})
}

document.querySelectorAll("#problem-page details").forEach(elem => hookDetailsOpen(elem));
document.querySelectorAll("a[download][data-blackbox=true]").forEach(e => e.addEventListener("click", evt => {
evt.preventDefault(); // 阻止默认下载
// 弹窗提示
showAlert('这是一个黑盒附件,请勿在解题时阅读其中内容!', '黑盒附件提醒', '!!', '我知道了', { danger: true }).then(() => {
const tempA = document.createElement('a');
tempA.href = evt.srcElement.href;
tempA.download = evt.srcElement.download;
tempA.click(); // 创建临时元素并点击下载
tempA.remove();
})
}))

好了,其实代码里还是有很多细节的,欢迎查看源码哦~

2026/4/21: 更多窗口支持地址栏回退

在每咕一题中,题目页是支持地址栏回退的,这使得移动端可以通过返回按钮(或等效手势)退出问题页。

然而其他窗口对于这种操作并不支持,于是基于又爱又恨的HistoryAPI,在尽可能少改动代码的前提下,为弹出modal增加这一功能。

我选择了使用popstate来完成,核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let popStateHandle = null;
function registerPopStateHandle(handle) {
popStateHandle = handle;
history.pushState(null, null, '');
}

function cleanupPopStateHandle(){
if(popStateHandle){
popStateHandle = null;
history.go(-1);
}
}

window.addEventListener("popstate", ()=>{
if(popStateHandle){
const handle = popStateHandle;
popStateHandle = null;
handle();
}
})
  • popStateHandle是注册的返回键按下时触发的函数,用于关闭窗口
  • registerPopStateHandle是一个帮助函数,集成了注册handle和pushState两步
  • cleanupPopStateHandle是一个帮助函数,用于在handle未触发时清理handle和历史栈
  • 最后的addEventListener注册监听函数,当注册了handle时,则调用它,注意这里清除handle和调用handle的顺序,先清除再调用使得handle中会调用cleanup的情况下不会过度清理历史栈

使用时,在弹出窗口处通过registerPopStateHandle注册handle,并在所有关闭窗口的地方(或者最后关闭窗口逻辑中)加入cleanupPopStateHandle来确保不污染历史栈和下一次操作,这样就可以通过返回键关闭窗口了~