一不注意就又在捣鼓博客装修了,本来是打算继续在12号装修日志后面添加新的DLC,结果定睛一看,发现12号装修日志只差一个字就要突破7000字大关,眼看就要再创相同tag下文章总字数历史新高,于是最终还是选择新开了一篇13号报告。
自定义右键菜单
起因只是忽然觉得浏览器右键菜单的默认样式严重影响了我全方位欣赏博客的美丽(?),遂决定让ChatGPT老师帮我写一个符合博客主题的自定义菜单。新菜单除了复制、黏贴和刷新这些基础功能之外,还顺便将博客之前一直没有实装的上一篇、下一篇按钮也塞了进去,并增加了鼠标悬浮时预览目录的功能,这样配合上random按钮,就可以更方便我来随机回顾往期的博客。
基本框架弄出来之后,又总觉得是不是还可以再装饰点什么,思考了几分钟后灵机一动,在手机上临时下载了画加,约了个含有大量ff14角色sample图(?)的像素小豆丁橱窗,不过由于最初和画手老师沟通时忘记提透明底需求,收到图时才发现老师直接画成来白底,而改透明底的话还得再加3r。好在像素图本身颜色和线条都非常简单干净,我后面用Procreate微调整体颜色时就顺便把透明底一并抠出来了。和ChatGPT说明需求获得更新后的代码,按照自己的喜好调整图片的大小和位置,一只由穿着弦月睡袍母肥24小时站岗的右键菜单就诞生了!
请欣赏效果图!做了简单的叫醒母肥的差分。时至今日,博客里已经入住了三只像素母肥了,之后有机会的话,说不定还会继续加入新衣装版本的母肥酱!
下面是具体的代码存档,首先在\layouts\_default\baseof.html中找到开头的<main>标签,并替换成以下代码,用于遍历文章并计算当前博文的上一篇和下一篇文章。
{{ $pages := where site.RegularPages "Type" "in" site.Params.mainSections }}
{{ $pages = $pages.ByDate.Reverse }}
{{ $current := . }}
{{ $prev := false }}
{{ $next := false }}
{{ range $i,$page := $pages }}
{{ if eq $page.RelPermalink $current.RelPermalink }}
{{ if gt $i 0 }}{{ $prev = index $pages (sub $i 1) }}{{ end }}
{{ if lt $i (sub (len $pages) 1) }}{{ $next = index $pages (add $i 1) }}{{ end }}
{{ end }}
{{ end }}
<main data-prev="{{ with $prev }}{{ .RelPermalink }}{{ end }}" data-next="{{ with $next }}{{ .RelPermalink }}{{ end }}">
接下来就是制作菜单本体,为了方便后续修改,我把它们全部整合到了新建的menu.html中,并将其放在了\layouts\partials目录下,最后再在\layouts\_default\baseof.html中进行渲染。
hmtl部分↓
<!-- =========================
右键菜单装饰
两张图片提前同时加载
========================= -->
<div id="context-decor">
<img
id="context-decor-default"
class="context-decor-img"
src="睡着母肥.png"
alt=""
>
<img
id="context-decor-hover"
class="context-decor-img"
src="醒来母肥.png"
alt=""
>
</div>
<!-- =========================
右键菜单
========================= -->
<div id="custom-context-menu">
<div class="context-item disabled" id="context-copy">
复制
</div>
<div class="context-item disabled" id="context-paste">
粘贴
</div>
<div class="context-item" id="context-refresh">
刷新
</div>
<div class="context-line"></div>
<div class="context-item disabled" id="context-prev">
上一篇
</div>
<div class="context-item disabled" id="context-next">
下一篇
</div>
</div>
<!-- =========================
上一篇 / 下一篇预览
========================= -->
<div id="context-preview">
<div
class="context-preview-title"
id="context-preview-title">
</div>
<div
class="context-preview-date"
id="context-preview-date">
</div>
<div class="context-preview-label">
目录
</div>
<div
class="context-preview-toc"
id="context-preview-toc">
</div>
</div>
js部分,因为原代码行数过多,这里就不再记录经过 PJAX 包裹后的完整样式了↓
(function () {
/*
* 手机 / 平板触摸设备不启用右键菜单
*/
if (
window.matchMedia(
'(hover: none) and (pointer: coarse)'
).matches
) {
return;
}
/* =========================
获取元素
========================= */
const menu =
document.getElementById('custom-context-menu');
const copy =
document.getElementById('context-copy');
const paste =
document.getElementById('context-paste');
const refresh =
document.getElementById('context-refresh');
const prev =
document.getElementById('context-prev');
const next =
document.getElementById('context-next');
const decor =
document.getElementById('context-decor');
const decorDefault =
document.getElementById('context-decor-default');
const decorHover =
document.getElementById('context-decor-hover');
const preview =
document.getElementById('context-preview');
const previewTitle =
document.getElementById('context-preview-title');
const previewDate =
document.getElementById('context-preview-date');
const previewToc =
document.getElementById('context-preview-toc');
if (!menu) {
return;
}
/* =========================
变量
========================= */
let selectedText = '';
let activeElement = null;
let selectionStart = null;
let selectionEnd = null;
let previewTimer = null;
let previewType = null;
let pendingHash = null;
/* =========================
图片
========================= */
const defaultImage =
'https://img.naturaleki.one/icon/menuA.png';
const hoverImage =
'https://img.naturaleki.one/icon/menuB.png';
/*
* 提前预加载两张图片
*
* 这样第一次 hover 时
* 不需要再等待网络请求
*/
const preloadDefault = new Image();
const preloadHover = new Image();
preloadDefault.src = defaultImage;
preloadHover.src = hoverImage;
/*
* 切换图片
*
* 不再修改 src
* 而是切换 opacity
*
* 因此 hover 基本可以瞬间完成
*/
function setDecorHover(hover) {
if (!decorDefault || !decorHover) {
return;
}
if (hover) {
decorDefault.style.opacity = '0';
decorHover.style.opacity = '1';
} else {
decorDefault.style.opacity = '1';
decorHover.style.opacity = '0';
}
}
/* =========================
装饰图片位置
========================= */
function positionDecor() {
if (menu.style.display !== 'block') {
return;
}
const rect =
menu.getBoundingClientRect();
const width = 120;
const leftOffset = -33;
const topOffset = 84;
decor.style.left =
(rect.right - width - leftOffset) + 'px';
decor.style.top =
(rect.top - topOffset) + 'px';
}
/* =========================
显示装饰
========================= */
function showDecor() {
setDecorHover(false);
positionDecor();
decor.style.display = 'block';
}
/* =========================
隐藏装饰
========================= */
function hideDecor() {
decor.style.display = 'none';
setDecorHover(false);
}
/* =========================
隐藏整个菜单
========================= */
function hideMenu() {
menu.style.display = 'none';
hidePreview();
hideDecor();
}
/* =========================
隐藏文章预览
========================= */
function hidePreview() {
clearTimeout(previewTimer);
preview.style.display = 'none';
previewType = null;
}
/* =========================
获取上一篇 / 下一篇数据
========================= */
function getData(type) {
const box =
document.getElementById(
type === 'prev'
? 'context-prev-data'
: 'context-next-data'
);
if (!box) {
return null;
}
const url =
box.querySelector('.context-data-url');
const title =
box.querySelector('.context-data-title');
const date =
box.querySelector('.context-data-date');
const toc =
box.querySelector('.context-data-toc');
if (!url) {
return null;
}
return {
url:
url.textContent.trim(),
title:
title
? title.textContent.trim()
: '',
date:
date
? date.textContent.trim()
: '',
toc:
toc
? toc.innerHTML.trim()
: ''
};
}
/* =========================
更新上一篇 / 下一篇
========================= */
function updateNavigation() {
const main =
document.querySelector('main');
if (!main) {
return;
}
const prevUrl =
main.dataset.prev;
const nextUrl =
main.dataset.next;
if (prevUrl) {
prev.dataset.url = prevUrl;
prev.classList.remove('disabled');
} else {
delete prev.dataset.url;
prev.classList.add('disabled');
}
if (nextUrl) {
next.dataset.url = nextUrl;
next.classList.remove('disabled');
} else {
delete next.dataset.url;
next.classList.add('disabled');
}
}
/* =========================
显示文章预览
========================= */
function showPreview(type) {
const data =
getData(type);
if (!data) {
hidePreview();
return;
}
previewType = type;
clearTimeout(previewTimer);
previewTitle.textContent =
data.title || '未命名文章';
previewDate.textContent =
data.date || '';
if (data.toc) {
previewToc.innerHTML =
data.toc;
} else {
previewToc.innerHTML =
'<div class="context-preview-empty">' +
'暂无目录' +
'</div>';
}
const target =
type === 'prev'
? prev
: next;
const rect =
target.getBoundingClientRect();
preview.style.display =
'block';
const width =
preview.offsetWidth;
const height =
preview.offsetHeight;
const gap = 8;
let left;
let top = rect.top;
if (type === 'prev') {
left =
rect.left -
width -
gap;
if (left < 8) {
left = 8;
}
} else {
left =
rect.right +
gap;
if (
left + width >
window.innerWidth - 8
) {
left =
window.innerWidth -
width -
8;
}
}
if (
top + height >
window.innerHeight - 8
) {
top =
window.innerHeight -
height -
8;
}
if (top < 8) {
top = 8;
}
preview.style.left =
Math.max(8, left) + 'px';
preview.style.top =
Math.max(8, top) + 'px';
}
/* =========================
延迟隐藏预览
========================= */
function scheduleHide() {
clearTimeout(previewTimer);
previewTimer =
setTimeout(function () {
if (
!prev.matches(':hover') &&
!next.matches(':hover') &&
!preview.matches(':hover')
) {
hidePreview();
}
}, 120);
}
/* =========================
复制
========================= */
async function copyText(text) {
if (!text) {
return;
}
try {
await navigator.clipboard.writeText(text);
} catch (e) {
const textarea =
document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
textarea.style.top = '0';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
document.execCommand('copy');
} catch (e) {}
textarea.remove();
}
}
/* =========================
检查是否可以粘贴
========================= */
async function updatePaste() {
if (!activeElement) {
paste.classList.add('disabled');
return;
}
const editable =
activeElement.tagName === 'INPUT' ||
activeElement.tagName === 'TEXTAREA' ||
activeElement.isContentEditable;
if (!editable) {
paste.classList.add('disabled');
return;
}
try {
const text =
await navigator.clipboard.readText();
paste.classList.toggle(
'disabled',
!text
);
} catch (e) {
paste.classList.remove(
'disabled'
);
}
}
/* =========================
PJAX 页面跳转
========================= */
function pjaxGo(url) {
if (!url) {
return;
}
hideMenu();
if (
typeof pjax !== 'undefined' &&
pjax &&
typeof pjax.loadUrl === 'function'
) {
pjax.loadUrl(url);
} else {
window.location.href = url;
}
}
/* =========================
滚动到标题
========================= */
function scrollToHash(hash) {
if (!hash) {
return;
}
const id =
decodeURIComponent(
hash.substring(1)
);
const target =
document.getElementById(id);
if (!target) {
return;
}
setTimeout(function () {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}, 100);
}
/* =========================
目录跳转
========================= */
function goToToc(url, hash) {
if (!url || !hash) {
return;
}
pendingHash = hash;
hideMenu();
if (
typeof pjax !== 'undefined' &&
pjax &&
typeof pjax.loadUrl === 'function'
) {
pjax.loadUrl(url);
} else {
window.location.href =
url + hash;
}
}
/* =========================
菜单项目 Hover
========================= */
function setupHover(element) {
element.addEventListener(
'mouseenter',
function () {
/*
* disabled 项目不触发图片变化
*/
if (
element.classList.contains(
'disabled'
)
) {
return;
}
setDecorHover(true);
}
);
element.addEventListener(
'mouseleave',
function () {
if (
element.classList.contains(
'disabled'
)
) {
return;
}
setDecorHover(false);
}
);
}
/* =========================
右键菜单
========================= */
document.addEventListener(
'contextmenu',
function (e) {
e.preventDefault();
activeElement =
document.activeElement;
if (
activeElement &&
(
activeElement.tagName === 'INPUT' ||
activeElement.tagName === 'TEXTAREA' ||
activeElement.isContentEditable
)
) {
if (
typeof activeElement.selectionStart ===
'number'
) {
selectionStart =
activeElement.selectionStart;
selectionEnd =
activeElement.selectionEnd;
} else {
selectionStart = null;
selectionEnd = null;
}
} else {
selectionStart = null;
selectionEnd = null;
}
selectedText =
window
.getSelection()
.toString()
.trim();
copy.classList.toggle(
'disabled',
!selectedText
);
updateNavigation();
updatePaste();
hidePreview();
menu.style.display =
'block';
const width =
menu.offsetWidth;
const height =
menu.offsetHeight;
let left =
e.clientX;
let top =
e.clientY;
if (
left + width >
window.innerWidth - 8
) {
left =
window.innerWidth -
width -
8;
}
if (
top + height >
window.innerHeight - 8
) {
top =
window.innerHeight -
height -
8;
}
menu.style.left =
Math.max(8, left) + 'px';
menu.style.top =
Math.max(8, top) + 'px';
showDecor();
}
);
/* =========================
点击外部关闭
========================= */
document.addEventListener(
'click',
function (e) {
if (
!menu.contains(e.target) &&
!preview.contains(e.target)
) {
hideMenu();
}
}
);
/* =========================
滚动关闭
========================= */
document.addEventListener(
'scroll',
hideMenu
);
/* =========================
菜单 Hover
========================= */
setupHover(copy);
setupHover(paste);
setupHover(refresh);
setupHover(prev);
setupHover(next);
/* =========================
上一篇预览
========================= */
prev.addEventListener(
'mouseenter',
function () {
if (
prev.classList.contains(
'disabled'
)
) {
hidePreview();
return;
}
showPreview('prev');
}
);
prev.addEventListener(
'mouseleave',
scheduleHide
);
/* =========================
下一篇预览
========================= */
next.addEventListener(
'mouseenter',
function () {
if (
next.classList.contains(
'disabled'
)
) {
hidePreview();
return;
}
showPreview('next');
}
);
next.addEventListener(
'mouseleave',
scheduleHide
);
/* =========================
预览区域 Hover
========================= */
preview.addEventListener(
'mouseenter',
function () {
clearTimeout(previewTimer);
}
);
preview.addEventListener(
'mouseleave',
function () {
hidePreview();
}
);
/* =========================
预览目录点击
========================= */
preview.addEventListener(
'click',
function (e) {
const target =
e.target.closest('a');
if (!target) {
return;
}
const href =
target.getAttribute('href');
if (!href) {
return;
}
const currentData =
previewType
? getData(previewType)
: null;
if (!currentData) {
return;
}
if (
href.charAt(0) === '#'
) {
e.preventDefault();
e.stopPropagation();
goToToc(
currentData.url,
href
);
return;
}
try {
const targetUrl =
new URL(
href,
window.location.href
);
const currentUrl =
new URL(
currentData.url,
window.location.href
);
if (
targetUrl.pathname ===
currentUrl.pathname &&
targetUrl.hash
) {
e.preventDefault();
e.stopPropagation();
goToToc(
currentData.url,
targetUrl.hash
);
}
} catch (error) {}
}
);
/* =========================
复制
========================= */
copy.addEventListener(
'click',
async function (e) {
e.stopPropagation();
if (
copy.classList.contains(
'disabled'
)
) {
return;
}
await copyText(selectedText);
hideMenu();
}
);
/* =========================
粘贴
========================= */
paste.addEventListener(
'click',
async function (e) {
e.stopPropagation();
if (
paste.classList.contains(
'disabled'
)
) {
return;
}
if (!activeElement) {
return;
}
try {
const text =
await navigator.clipboard.readText();
if (!text) {
return;
}
activeElement.focus();
if (
activeElement.isContentEditable
) {
document.execCommand(
'insertText',
false,
text
);
} else {
const start =
selectionStart !== null
? selectionStart
: activeElement.value.length;
const end =
selectionEnd !== null
? selectionEnd
: start;
activeElement.setRangeText(
text,
start,
end,
'end'
);
activeElement.dispatchEvent(
new Event(
'input',
{
bubbles: true
}
)
);
}
} catch (e) {}
hideMenu();
}
);
/* =========================
刷新
========================= */
refresh.addEventListener(
'click',
function (e) {
e.stopPropagation();
hideMenu();
window.location.reload();
}
);
/* =========================
上一篇
========================= */
prev.addEventListener(
'click',
function (e) {
e.stopPropagation();
if (
prev.classList.contains(
'disabled'
)
) {
return;
}
pjaxGo(
prev.dataset.url
);
}
);
/* =========================
下一篇
========================= */
next.addEventListener(
'click',
function (e) {
e.stopPropagation();
if (
next.classList.contains(
'disabled'
)
) {
return;
}
pjaxGo(
next.dataset.url
);
}
);
/* =========================
PJAX 完成
========================= */
document.addEventListener(
'pjax:complete',
function () {
updateNavigation();
if (pendingHash) {
const hash =
pendingHash;
pendingHash = null;
scrollToHash(hash);
}
hideMenu();
}
);
/* =========================
PJAX 成功
========================= */
document.addEventListener(
'pjax:success',
function () {
updateNavigation();
}
);
/* =========================
窗口尺寸变化
========================= */
window.addEventListener(
'resize',
function () {
positionDecor();
}
);
window.addEventListener(
'resize',
hidePreview
);
/* =========================
初始化
========================= */
updateNavigation();
})();
css样式部分↓
#custom-context-menu {
display: none;
position: fixed;
z-index: 99999;
min-width: 155px;
padding: 5px;
background: #ECEFF4;
border: 2px solid #8aa2d3;
box-shadow: 4px 4px 0 #dae0ea;
border-radius: 0;
font-family: ipx, ipxs, sans-serif;
font-size: 13px;
color: #3f4551;
letter-spacing: .2px;
}
.context-item {
display: block;
padding: 7px 11px;
white-space: nowrap;
color: inherit;
text-decoration: none;
border: 1px solid transparent;
border-radius: 0;
}
.context-item:hover {
background: #dce5f4;
border-color: #b9c8e2;
color: #536b9b;
}
.context-item.disabled {
color: #aeb7c5;
}
.context-item.disabled:hover {
background: transparent;
border-color: transparent;
color: #aeb7c5;
}
.context-line {
height: 2px;
margin: 5px 6px;
background: #dae0ea;
}
/* =========================
右键菜单装饰图片
========================= */
#context-decor {
display: none;
position: fixed;
z-index: 99998;
width: 120px;
height: 120px;
pointer-events: none;
}
/* 两张图片完全重叠 */
.context-decor-img {
position: absolute;
left: 0;
top: 0;
display: block;
width: 100%;
height: 100%;
object-fit: contain;
image-rendering: pixelated;
}
/* 默认图片 */
#context-decor-default {
opacity: 1;
}
/* Hover 图片 */
#context-decor-hover {
opacity: 0;
}
/* =========================
上一篇 / 下一篇预览
========================= */
#context-preview {
display: none;
position: fixed;
z-index: 100000;
width: 270px;
padding: 12px 13px;
background: #ECEFF4;
border: 2px solid #8aa2d3;
box-shadow: 4px 4px 0 #dae0ea;
border-radius: 0;
font-family: ipx, ipxs, sans-serif;
font-size: 12px;
line-height: 1.7;
color: #3f4551;
}
.context-preview-title {
margin-bottom: 5px;
padding-bottom: 6px;
border-bottom: 2px dotted #b9c8e2;
font-size: 14px;
font-weight: bold;
line-height: 1.5;
color: #536b9b;
}
.context-preview-date {
margin-bottom: 9px;
color: #87aac9;
font-size: 11px;
}
.context-preview-label {
margin-bottom: 5px;
padding-bottom: 3px;
font-size: 11px;
color: #87aac9;
letter-spacing: 1px;
}
.context-preview-toc {
max-height: 220px;
overflow: auto;
}
.context-preview-toc ul {
list-style-type: disc;
padding-left: 20px;
}
.context-preview-toc ul ul {
list-style-type: circle;
}
.context-preview-toc ul ul ul {
display: none;
}
.context-preview-toc a {
color: inherit;
text-decoration: none;
}
.context-preview-toc a:hover {
color: #b17185;
text-decoration: none;
}
.context-preview-empty {
color: #aeb7c5;
}
.context-preview-toc::-webkit-scrollbar {
width: 6px;
}
.context-preview-toc::-webkit-scrollbar-track {
background: #f6f8fa;
}
.context-preview-toc::-webkit-scrollbar-thumb {
background: #b9c8e2;
}
.context-preview-toc::-webkit-scrollbar-thumb:hover {
background: #8aa2d3;
}
重写归档样式
忽然想在博客里加入一点类似代码编辑器里的文件树的元素,大致浏览了一遍目前的装修后,最终决定从归档页面下手。上一版的按钮式折叠的归档样式还是两年前装修时做的,现在来看,这个设计与如今博客的整体风格多少是有些格格不入。
把需求交代给GPT老师之后,就让它在原版的基础上帮忙改了代码,并自己手动对css做了微调。然后按照一贯的传统,给折叠箭头变化的js代码套上pjax包裹。
{{ define "main" }}
<div class="page-info">
<span>{{ i18n "archivesTotalPages" }}{{ len (where .Site.RegularPages "Type" "in" .Site.Params.mainSections) }}</span>
</div>
<div class="empty-space"></div>
<div class="archive-tree">
<!-- 根目录 -->
<div class="tree-line tree-root">
<span class="tree-arrow">▾</span>
<span>archive</span>
</div>
<!-- 2026 -->
<div class="tree-folder">
<div class="tree-line folder-line" onclick="toggleArchiveWindow('archive-2026', this)">
<span class="tree-arrow">▾</span>
<span>2026</span>
</div>
<div id="archive-2026" class="tree-files">
{{ $pages := where .Site.RegularPages "Date.Year" 2026 }}
{{ range $index, $page := $pages }}
<div class="tree-line file-line">
{{ if eq $index (sub (len $pages) 1) }}
<span class="tree-branch">└─</span>
{{ else }}
<span class="tree-branch">├─</span>
{{ end }}
<span class="tree-date">{{ $page.Date.Format "01-02" }}</span>
<a href="{{ $page.Permalink }}">
{{ $page.Title }}.md
</a>
</div>
{{ end }}
</div>
</div>
<!-- 2025 -->
<div class="tree-folder">
<div class="tree-line folder-line" onclick="toggleArchiveWindow('archive-2025', this)">
<span class="tree-arrow">▸</span>
<span>2025</span>
</div>
<div id="archive-2025" class="tree-files" style="display:none;">
{{ $pages := where .Site.RegularPages "Date.Year" 2025 }}
{{ range $index, $page := $pages }}
<div class="tree-line file-line">
{{ if eq $index (sub (len $pages) 1) }}
<span class="tree-branch">└─</span>
{{ else }}
<span class="tree-branch">├─</span>
{{ end }}
<span class="tree-date">{{ $page.Date.Format "01-02" }}</span>
<a href="{{ $page.Permalink }}">
{{ $page.Title }}.md
</a>
</div>
{{ end }}
</div>
</div>
<!-- 2024 -->
<div class="tree-folder">
<div class="tree-line folder-line" onclick="toggleArchiveWindow('archive-2024', this)">
<span class="tree-arrow">▸</span>
<span>2024</span>
</div>
<div id="archive-2024" class="tree-files" style="display:none;">
{{ $pages := where .Site.RegularPages "Date.Year" 2024 }}
{{ range $index, $page := $pages }}
<div class="tree-line file-line">
{{ if eq $index (sub (len $pages) 1) }}
<span class="tree-branch">└─</span>
{{ else }}
<span class="tree-branch">├─</span>
{{ end }}
<span class="tree-date">{{ $page.Date.Format "01-02" }}</span>
<a href="{{ $page.Permalink }}">
{{ $page.Title }}.md
</a>
</div>
{{ end }}
</div>
</div>
<!-- 2023 -->
<div class="tree-folder">
<div class="tree-line folder-line" onclick="toggleArchiveWindow('archive-2023', this)">
<span class="tree-arrow">▸</span>
<span>2023</span>
</div>
<div id="archive-2023" class="tree-files" style="display:none;">
{{ $pages := where .Site.RegularPages "Date.Year" 2023 }}
{{ range $index, $page := $pages }}
<div class="tree-line file-line">
{{ if eq $index (sub (len $pages) 1) }}
<span class="tree-branch">└─</span>
{{ else }}
<span class="tree-branch">├─</span>
{{ end }}
<span class="tree-date">{{ $page.Date.Format "01-02" }}</span>
<a href="{{ $page.Permalink }}">
{{ $page.Title }}.md
</a>
</div>
{{ end }}
</div>
</div>
<!-- 2022 -->
<div class="tree-folder">
<div class="tree-line folder-line" onclick="toggleArchiveWindow('archive-2022', this)">
<span class="tree-arrow">▸</span>
<span>2022</span>
</div>
<div id="archive-2022" class="tree-files" style="display:none;">
{{ $pages := where .Site.RegularPages "Date.Year" 2022 }}
{{ range $index, $page := $pages }}
<div class="tree-line file-line">
{{ if eq $index (sub (len $pages) 1) }}
<span class="tree-branch">└─</span>
{{ else }}
<span class="tree-branch">├─</span>
{{ end }}
<span class="tree-date">{{ $page.Date.Format "01-02" }}</span>
<a href="{{ $page.Permalink }}">
{{ $page.Title }}.md
</a>
</div>
{{ end }}
</div>
</div>
</div>
<style>
/* =================================
Archive File Tree
================================= */
.archive-tree {
font-family: ipxs, ipx, monospace;
font-size: 17px;
line-height: 1.8;
color: #555;
}
/* 每一行 */
.tree-line {
display: flex;
align-items: baseline;
min-height: 27px;
}
/* archive 根目录 */
.tree-root {
color: #8aa2d3;
font-weight: 600;
margin-bottom: 2px;
font-size: 22px;
}
/* 文件夹 */
.tree-folder {
margin-left: 18px;
}
.folder-line {
cursor: pointer;
color: #8aa2d3;
user-select: none;
font-size: 20px;
font-weight: 600;
}
.folder-line:hover {
color: #dd8da8;
}
/* 箭头 */
.tree-arrow {
display: inline-block;
width: 18px;
flex-shrink: 0;
color: #eaafd1;
}
/* 文件夹展开后的内容 */
.tree-files {
margin-left: 18px;
}
/* 文件 */
.file-line {
padding-left: 18px;
}
/* ├─ / └─ */
.tree-branch {
display: inline-block;
width: 28px;
flex-shrink: 0;
color: #b7c5dc;
font-family: monospace;
}
/* 日期 */
.tree-date {
display: inline-block;
width: 48px;
flex-shrink: 0;
margin-right: 8px;
color: #87aac9;
font-size: 1.01em;
}
/* 文章链接 */
.file-line a {
color: #555;
text-decoration: none;
}
.file-line a:hover {
color: #dd8da8;
}
.page-info {
font-family: ipxs, ipx, monospace !important;
font-size: 18px !important;
font-weight: 600 !important;
color: #87aac9 !important;
}
</style>
<script>
(function () {
function initArchiveWindow() {
const archiveTree = document.querySelector('#archives .archive-tree');
if (!archiveTree) return;
/*
* 防止 PJAX 重复初始化
*/
if (archiveTree.dataset.archiveInitialized === 'true') {
return;
}
archiveTree.dataset.archiveInitialized = 'true';
/*
* 初始化所有年份
*
* 2026 默认展开
* 其他年份默认折叠
*/
const folders = archiveTree.querySelectorAll('.tree-folder');
folders.forEach(function (folder, index) {
const button = folder.querySelector('.folder-line');
const files = folder.querySelector('.tree-files');
const arrow = folder.querySelector('.tree-arrow');
if (!button || !files || !arrow) return;
/*
* 绑定点击事件
*/
button.addEventListener('click', function () {
const isClosed =
window.getComputedStyle(files).display === 'none';
if (isClosed) {
// 展开
files.style.display = 'block';
arrow.textContent = '▾';
} else {
// 折叠
files.style.display = 'none';
arrow.textContent = '▸';
}
});
});
}
/*
* 普通页面首次加载
*/
document.addEventListener('DOMContentLoaded', function () {
initArchiveWindow();
});
/*
* Pjax 页面切换完成
*
* 你的 Pjax 如果是常见的 afterSwap / pjax:complete
* 这里都会尝试初始化
*/
document.addEventListener('pjax:complete', function () {
initArchiveWindow();
});
document.addEventListener('pjax:end', function () {
initArchiveWindow();
});
document.addEventListener('pjax:afterSwap', function () {
initArchiveWindow();
});
/*
* 暴露给 HTML onclick 使用
*
* 如果你已经把 onclick 改成
* toggleArchiveWindow(...)
* 这一部分就是保险机制。
*/
window.toggleArchiveWindow = function (archiveId, element) {
const archive = document.getElementById(archiveId);
if (!archive || !element) return;
const arrow = element.querySelector('.tree-arrow');
if (!arrow) return;
const isClosed =
window.getComputedStyle(archive).display === 'none';
if (isClosed) {
archive.style.display = 'block';
arrow.textContent = '▾';
} else {
archive.style.display = 'none';
arrow.textContent = '▸';
}
};
})();
</script>
<div class="empty-space"></div>
<!-- custom smart pagination -->
{{- partial "pagination.html" (dict "context" . "pagi" .Paginator) -}}
{{ end }}
效果如上图!顺便把tag窗口里的标签文字也换成了博客常用的像素字体,这部分是在\static\css\cafe.css中修改的。
/* 基本按钮样式 */
.tag-btn {
background-color: #f0f8ff;
color: #8aa2d3;
padding: 10px;
border: 1px solid #8aa2d3;
border-radius: 5px;
margin: 4px; /* 增加均匀的外间距 */
display: inline-block; /* 确保按钮大小受内容控制 */
text-align: center; /* 文本居中 */
white-space: nowrap; /* 防止按钮中的文字换行 */
font-family: ipxs,ipx;
}
也刚好以此纪念博客文章数量达到77篇。