Compare commits

...

27 Commits

Author SHA1 Message Date
butubb b190117582 chore: 删除无用的测试markdown文件 2026-07-11 08:04:04 +08:00
butubb ece663aad7 chore: 删除github同步元数据文件
移除了本地的github sync元数据配置文件,清理同步缓存记录
2026-07-11 08:00:54 +08:00
butubb 1d647d2c8b Merge branch 'main' of github.com:8butubb/8butubb.github.io 2026-07-11 07:57:36 +08:00
butubb 8f1e6db348 chore(themes): 更新PaperMod子模块到最新版本
将PaperMod主题子模块从旧提交c4ca7ca更新到154d006
2026-07-11 07:48:49 +08:00
butubb d2fb63e7d8 chore: add new post and add icp backup info
add the 面向死亡备份 markdown post, and add wanbei icp backup link in footer
2026-07-10 12:44:00 +08:00
butubb dd98cd87fa Sync 2026-07-06 13:53:57 +08:00
butubb 5f2b50e3a9 Sync 2026-07-06 13:47:47 +08:00
butubb 410a5f5217 Sync 2026-07-06 13:46:32 +08:00
butubb 02e2312fce sync: 推送 19 个文件变更 2026-07-06 13:30:17 +08:00
butubb 2a7e4f0edc sync: 推送 19 个文件变更 2026-07-06 13:29:33 +08:00
butubb 8b2707218b sync: 推送 23 个文件变更 2026-07-06 13:28:58 +08:00
butubb 17701a362a sync: 推送 24 个文件变更 2026-07-06 13:28:05 +08:00
butubb de2561fc40 sync: 推送 1 个文件变更 2026-07-06 12:56:27 +08:00
butubb 1fac0280d5 sync: 推送 1 个文件变更 2026-07-06 12:55:39 +08:00
butubb 46ae27c127 sync: 推送 1 个文件变更 2026-07-06 12:55:03 +08:00
butubb 57bbc5f201 sync: 推送 1 个文件变更 2026-07-06 12:54:27 +08:00
butubb 6274bdd5a7 sync: 推送 1 个文件变更 2026-07-06 12:45:39 +08:00
butubb d48c972733 1 2026-06-11 15:42:16 +08:00
butubb def46009ec add photo 2026-05-07 15:30:47 +08:00
butubb a1b8cbd738 新增图库 2026-05-07 15:26:46 +08:00
butubb b512ad83a7 fix 2026-03-28 21:38:31 +08:00
butubb 3e8cd729ba 1 2026-03-28 16:50:54 +08:00
butubb d99cf1bc78 修改归档页 2026-03-28 16:28:22 +08:00
butubb 3215b146af 1 2026-03-19 16:46:25 +08:00
butubb 881387864d update 2026-03-19 15:52:15 +08:00
butubb 2dbf3bdec5 u 2026-03-19 15:36:53 +08:00
butubb 3ae95f7212 update 2026-03-19 15:32:59 +08:00
31 changed files with 1245 additions and 275 deletions
+303
View File
@@ -0,0 +1,303 @@
/* ========================================
Gallery — PaperMod Design Language
======================================== */
/* ---------- Page Layout ---------- */
/* Widen the main container for gallery pages */
.main:has(.gallery-page-content) {
max-width: calc(var(--nav-width) + var(--gap) * 2);
}
.gallery-page-content {
max-width: 100%;
}
/* ---------- Album Section ---------- */
.gallery-album {
background: var(--entry);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--gap);
margin-bottom: var(--gap);
transition: transform 0.1s;
}
.gallery-album:active {
transform: scale(0.99);
}
.gallery-album-header {
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 4px;
}
.gallery-album-title {
font-size: 24px;
font-weight: 600;
color: var(--primary);
line-height: 1.3;
}
.gallery-album-count {
font-size: 13px;
color: var(--secondary);
flex-shrink: 0;
}
.gallery-album-desc {
color: var(--secondary);
font-size: 14px;
line-height: 1.6;
margin-bottom: var(--content-gap);
}
/* ---------- Image Grid ---------- */
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: calc(var(--content-gap) * 0.5);
}
.gallery-card {
position: relative;
border-radius: var(--radius);
overflow: hidden;
cursor: pointer;
background: var(--code-bg);
border: 1px solid var(--border);
transition: transform 0.1s;
}
[data-theme="dark"] .gallery-card {
background: var(--tertiary);
}
.gallery-card:active {
transform: scale(0.96);
}
.gallery-card-img {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.gallery-card-img img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.gallery-card:hover .gallery-card-img img {
transform: scale(1.05);
}
.gallery-card::after {
content: '';
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0);
transition: background 0.2s ease;
pointer-events: none;
border-radius: inherit;
}
.gallery-card:hover::after {
background: rgba(0, 0, 0, 0.08);
}
[data-theme="dark"] .gallery-card:hover::after {
background: rgba(0, 0, 0, 0.2);
}
/* ---------- Empty State ---------- */
.gallery-empty {
text-align: center;
padding: calc(var(--gap) * 3) 0;
color: var(--secondary);
}
.gallery-empty svg {
margin: 0 auto var(--gap);
opacity: 0.4;
}
.gallery-empty p {
font-size: 16px;
}
/* ---------- Lightbox ---------- */
.lightbox {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.25s ease, visibility 0.25s ease;
}
.lightbox.active {
opacity: 1;
visibility: visible;
}
.lightbox-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.85);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
}
[data-theme="dark"] .lightbox-backdrop {
background: rgba(0, 0, 0, 0.92);
}
.lightbox-body {
position: relative;
max-width: 92vw;
max-height: 88vh;
z-index: 1;
}
.lightbox-body img {
max-width: 92vw;
max-height: 88vh;
object-fit: contain;
border-radius: var(--radius);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
/* Lightbox Buttons */
.lightbox-btn {
position: absolute;
z-index: 2;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: white;
transition: background 0.2s, transform 0.1s;
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
padding: 0;
}
.lightbox-btn:hover {
background: rgba(255, 255, 255, 0.22);
}
.lightbox-btn:active {
transform: scale(0.92);
}
.lightbox-btn svg {
width: 20px;
height: 20px;
}
.lightbox-close-btn {
top: 20px;
right: 20px;
}
.lightbox-prev-btn {
left: 20px;
top: 50%;
transform: translateY(-50%);
}
.lightbox-prev-btn:active {
transform: translateY(-50%) scale(0.92);
}
.lightbox-next-btn {
right: 20px;
top: 50%;
transform: translateY(-50%);
}
.lightbox-next-btn:active {
transform: translateY(-50%) scale(0.92);
}
.lightbox-counter {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 2;
color: rgba(255, 255, 255, 0.7);
font-size: 13px;
background: rgba(0, 0, 0, 0.4);
padding: 4px 12px;
border-radius: 20px;
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
}
/* ---------- Responsive ---------- */
@media (max-width: 768px) {
:root {
--gap: 14px;
}
.gallery-grid {
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.gallery-album {
padding: 14px;
}
.gallery-album-title {
font-size: 20px;
}
.lightbox-btn {
width: 38px;
height: 38px;
}
.lightbox-btn svg {
width: 16px;
height: 16px;
}
.lightbox-prev-btn {
left: 10px;
}
.lightbox-next-btn {
right: 10px;
}
.lightbox-close-btn {
top: 10px;
right: 10px;
}
}
@media (max-width: 480px) {
.gallery-grid {
grid-template-columns: repeat(2, 1fr);
}
.gallery-album-title {
font-size: 18px;
}
}
+102
View File
@@ -0,0 +1,102 @@
document.addEventListener('DOMContentLoaded', function () {
const lightbox = document.getElementById('lightbox');
const lightboxImg = document.getElementById('lightbox-img');
const lightboxClose = document.getElementById('lightbox-close');
const lightboxPrev = document.getElementById('lightbox-prev');
const lightboxNext = document.getElementById('lightbox-next');
const lightboxCounter = document.getElementById('lightbox-counter');
const lightboxBackdrop = document.getElementById('lightbox-backdrop');
const cards = Array.from(document.querySelectorAll('.gallery-card'));
let currentIndex = -1;
function openLightbox(index) {
if (index < 0 || index >= cards.length) return;
currentIndex = index;
const src = cards[index].dataset.src;
if (!src) return;
lightboxImg.src = src;
lightbox.classList.add('active');
document.body.style.overflow = 'hidden';
updateCounter();
}
function closeLightbox() {
lightbox.classList.remove('active');
document.body.style.overflow = '';
currentIndex = -1;
}
function showPrev() {
if (currentIndex > 0) {
openLightbox(currentIndex - 1);
}
}
function showNext() {
if (currentIndex < cards.length - 1) {
openLightbox(currentIndex + 1);
}
}
function updateCounter() {
if (lightboxCounter) {
lightboxCounter.textContent = (currentIndex + 1) + ' / ' + cards.length;
}
}
// Click on gallery cards
cards.forEach(function (card, index) {
card.addEventListener('click', function () {
openLightbox(index);
});
});
// Lightbox controls
lightboxClose.addEventListener('click', closeLightbox);
lightboxBackdrop.addEventListener('click', closeLightbox);
lightboxPrev.addEventListener('click', function (e) {
e.stopPropagation();
showPrev();
});
lightboxNext.addEventListener('click', function (e) {
e.stopPropagation();
showNext();
});
// Keyboard navigation
document.addEventListener('keydown', function (e) {
if (!lightbox.classList.contains('active')) return;
switch (e.key) {
case 'Escape':
closeLightbox();
break;
case 'ArrowLeft':
showPrev();
break;
case 'ArrowRight':
showNext();
break;
}
});
// Touch swipe support
var touchStartX = 0;
var touchEndX = 0;
lightbox.addEventListener('touchstart', function (e) {
touchStartX = e.changedTouches[0].screenX;
}, { passive: true });
lightbox.addEventListener('touchend', function (e) {
touchEndX = e.changedTouches[0].screenX;
var diff = touchStartX - touchEndX;
if (Math.abs(diff) > 50) {
if (diff > 0) {
showNext();
} else {
showPrev();
}
}
}, { passive: true });
});
+3 -3
View File
@@ -1,5 +1,5 @@
---
title: "归档"
layout: "archives"
url: "/archives"
title: 归档
layout: archives
url: /archives
---
+5
View File
@@ -0,0 +1,5 @@
---
title: 跑步记录
date: 2026-03-12T08:00:00Z
---
![123](https://cdn.jsdelivr.net/gh/8butubb/image/img/20260309120246636.png)
+4
View File
@@ -0,0 +1,4 @@
---
title: 画廊
description: 记录生活中的美好瞬间
---
+3 -4
View File
@@ -1,10 +1,9 @@
---
title: Git 完整教程指南
tags: ["git", "版本控制", "教程"]
date: 2025-10-08
summary: "从零开始学习 Git 版本控制系统,包含基础概念、常用命令、分支管理、协作流程等完整内容。"
tags: [git, 版本控制, 教程]
date: 2025-10-11T08:00:00Z
summary: 从零开始学习 Git 版本控制系统,包含基础概念、常用命令、分支管理、协作流程等完整内容。
---
> 本教程适用于 Git 初学者到进阶用户,涵盖日常开发中 90% 的使用场景。
## 什么是 Git
+3 -3
View File
@@ -1,8 +1,8 @@
---
title: Linux 硬链接
tags: ["linux"]
date: 2024-12-04
summary: "在 Linux 系统中,文件的存储与管理是通过链接来实现的。硬链接是其中一种重要的链接类型,本文将详细介绍硬链接的概念、创建方法及其应用场景。"
tags: [linux]
date: 2024-12-07T08:00:00Z
summary: 在 Linux 系统中,文件的存储与管理是通过链接来实现的。硬链接是其中一种重要的链接类型,本文将详细介绍硬链接的概念、创建方法及其应用场景。
---
在 Linux 系统中,文件的存储与管理是通过链接来实现的。硬链接是其中一种重要的链接类型,本文将详细介绍硬链接的概念、创建方法及其应用场景。
+2 -2
View File
@@ -1,7 +1,7 @@
---
title: Linux文件系统概述
tags: ["linux"]
date: 2024-12-04
tags: [linux]
date: 2024-12-07T08:00:00Z
---
Linux的文件系统结构是其核心组成部分之一,它以一种层次化的方式组织数据,使得用户和程序能够高效地访问和管理文件。以下是一些关键概念和特点:
+3 -4
View File
@@ -1,10 +1,9 @@
---
title: docker搭建思源笔记
tags: ["linux","docker"]
date: 2024-11-23
summary: "docker搭建思源笔记..."
tags: [linux, docker]
date: 2024-11-25T00:00:00Z
summary: docker搭建思源笔记...
---
- `PUID`: 自定义用户 ID(可选,如果未提供,默认为 `1000`
- `PGID`: 自定义组 ID(可选,如果未提供,默认为 `1000`
- `workspace_dir_host`:宿主机上的工作空间文件夹路径
+3 -3
View File
@@ -1,8 +1,8 @@
---
title: frp内网穿透使用指南
tags: ["linux"]
date: 2024-12-02
summary: "frpFast Reverse Proxy)是一个高性能的反向代理应用,主要用于内网穿透。..."
tags: [linux]
date: 2024-12-05T08:00:00Z
summary: frpFast Reverse Proxy)是一个高性能的反向代理应用,主要用于内网穿透。...
---
## 什么是 frp
+58 -218
View File
@@ -1,10 +1,9 @@
---
title: gitea 自部署使用体验
tags: ["git","docker"]
date: 2025-11-27
summary: "本文基于 Docker Compose 一键启动 Gitea + Gitea Runner 的实战记录。"
tags: [git, docker]
date: 2025-11-30T08:00:00Z
summary: 本文基于 Docker Compose 一键启动 Gitea + Gitea Runner 的实战记录。
---
> 注意:本文基于 Docker Compose 一键启动 Gitea + Gitea Runner 的实战记录。
## 为什么使用 gitea
@@ -39,113 +38,72 @@ Gitea 采用微内核 + 插件扩展的轻量架构,核心组件如下:
### 架构图(Mermaid
```mermaid
graph TB
subgraph "客户端层"
A[Web浏览器]
B[Git客户端]
C[API客户端]
graph TD
%% 客户端层
Client_Web[Web浏览器]
Client_Git[Git客户端]
Client_API[API客户端]
%% 负载层
Proxy[Nginx / 反向代理]
%% 核心服务层
subgraph Gitea核心服务
Web[Web服务 / Gin]
API[RESTful API]
Git[Git协议处理器]
Auth[认证授权 / Session / OAuth]
end
subgraph "负载均衡/反向代理"
D[Nginx/Apache]
%% 业务逻辑层
subgraph 业务逻辑层
Repo[仓库管理]
User[用户与组织]
ACL[权限控制 RBAC]
Hook[WebHook]
Mail[邮件服务]
end
subgraph "Gitea核心服务"
E[Web服务<br/>Gin框架]
F[API服务<br/>RESTful]
G[Git服务<br/>SSH/HTTP]
H[认证授权<br/>Session/OAuth]
%% 存储层
subgraph 数据存储层
DB[(数据库<br/>SQLite/MySQL)]
FS[(Git仓库文件)]
LFS[(LFS对象)]
Cache[(Redis缓存)]
end
subgraph "业务逻辑层"
I[仓库管理]
J[用户管理]
K[组织管理]
L[权限控制<br/>RBAC]
M[WebHook]
N[邮件服务]
end
%% 扩展
Runner[[Gitea Runner<br/>CI/CD]]
Pack[[包管理<br/>npm/docker]]
subgraph "数据存储层"
O[数据库<br/>SQLite/MySQL/PostgreSQL]
P[Git仓库<br/>文件系统]
Q[LFS对象存储]
R[缓存<br/>Redis/Memcached]
end
%% 关系连线
Client_Web --> Proxy
Client_Git --> Proxy
Client_API --> Proxy
subgraph "后台任务"
S[定时任务<br/>Cron]
T[队列处理<br/>WebHook/邮件]
U[索引构建]
V[镜像同步]
end
Proxy --> Web
Proxy --> API
Proxy --> Git
subgraph "可选扩展"
W[Gitea Runner<br/>CI/CD]
X[包管理<br/>npm/docker/maven]
Y[代码搜索<br/>Bleve]
end
Web --> Auth
API --> Auth
Git --> Auth
A --> D
B --> D
C --> D
D --> E
D --> F
D --> G
E --> H
F --> H
G --> H
H --> I
H --> J
H --> K
H --> L
I --> O
I --> P
J --> O
K --> O
L --> O
I --> Q
M --> T
N --> T
S --> V
U --> R
T --> N
W --> P
X --> Q
Y --> R
Auth --> Repo
Auth --> User
Auth --> ACL
style A fill:#e1f5fe
style B fill:#e1f5fe
style C fill:#e1f5fe
style D fill:#fff3e0
style E fill:#f3e5f5
style F fill:#f3e5f5
style G fill:#f3e5f5
style H fill:#f3e5f5
style I fill:#e8f5e9
style J fill:#e8f5e9
style K fill:#e8f5e9
style L fill:#e8f5e9
style M fill:#e8f5e9
style N fill:#e8f5e9
style O fill:#ffebee
style P fill:#ffebee
style Q fill:#ffebee
style R fill:#ffebee
style S fill:#fce4ec
style T fill:#fce4ec
style U fill:#fce4ec
style V fill:#fce4ec
style W fill:#e0f2f1
style X fill:#e0f2f1
style Y fill:#e0f2f1
Repo --> DB
Repo --> FS
Repo --> LFS
User --> DB
ACL --> DB
Repo --> Hook
Hook --> Mail
Runner -.-> API
Pack -.-> FS
```
### 架构说明
@@ -162,124 +120,6 @@ graph TB
这种架构设计使得 Gitea 具有轻量级、高可用和易扩展的特点。
```mermaid
graph TB
subgraph "客户端层"
A[Web浏览器]
B[Git客户端]
C[API客户端]
end
subgraph "负载均衡层"
D[Nginx/反向代理]
end
subgraph "Gitea核心服务"
E[Web服务<br/>Gin框架]
F[REST API]
G[Git协议处理器]
H[认证授权<br/>RBAC]
I[中间件<br/>CSRF/Session]
end
subgraph "业务逻辑层"
J[仓库管理]
K[用户/组织管理]
L[问题跟踪]
M[PR/MR流程]
N[WebHook系统]
O[通知系统]
end
subgraph "数据存储层"
P[Git仓库<br/>文件系统]
Q[SQLite/MySQL<br/>PostgreSQL]
R[LFS对象存储]
S[会话存储]
T[缓存Redis]
end
subgraph "后台服务"
U[定时任务<br/>镜像同步]
V[队列服务<br/>异步任务]
W[邮件服务]
X[索引构建]
end
subgraph "扩展组件"
Y[Gitea Runner<br/>CI/CD]
Z[包管理<br/>npm/maven]
AA[容器注册表]
end
A --> D
B --> D
C --> D
D --> E
D --> F
D --> G
E --> H
F --> H
G --> H
H --> I
I --> J
I --> K
I --> L
I --> M
I --> N
I --> O
J --> P
K --> Q
L --> Q
M --> P
N --> V
O --> W
V --> U
V --> X
V --> W
J --> Y
J --> Z
J --> AA
P --> R
Q --> S
Q --> T
style A fill:#e1f5fe
style B fill:#e1f5fe
style C fill:#e1f5fe
style D fill:#fff3e0
style E fill:#f3e5f5
style F fill:#f3e5f5
style G fill:#f3e5f5
style H fill:#f3e5f5
style I fill:#f3e5f5
style J fill:#e8f5e8
style K fill:#e8f5e8
style L fill:#e8f5e8
style M fill:#e8f5e8
style N fill:#e8f5e8
style O fill:#e8f5e8
style P fill:#fce4ec
style Q fill:#fce4ec
style R fill:#fce4ec
style S fill:#fce4ec
style T fill:#fce4ec
style U fill:#fff9c4
style V fill:#fff9c4
style W fill:#fff9c4
style X fill:#fff9c4
style Y fill:#e0f2f1
style Z fill:#e0f2f1
style AA fill:#e0f2f1
```
### 架构特点说明
**1. 轻量级设计**
+24
View File
@@ -0,0 +1,24 @@
---
title: GitHub 白嫖指南
tags: [网站, GitHub, 博客]
date: 2026-03-21T00:00:00Z
---
## GitHub 白嫖指南
GitHub 俗称"男人的交友市场",但它的实用价值远不止如此。今天就来分享一下我在 GitHub 上白嫖的那些东西。
### 博客搭建
本博客使用 Hugo 静态生成器,通过 GitHub Pages 部署,域名托管在 Cloudflare 上并解析到该仓库。这套方案完全免费,只需承担域名的成本。
### 图床方案
使用 GitHub 作为图床存储,借助 PicGo 工具上传,配合公益 GitHub CDN 加速,实现高效的图片管理。
### RunningPage 部署
还用 GitHub Pages 部署了一个 RunningPage 应用,解析到 run.butubb.cn 域名,展示运动数据。
### 后续优化
GitHub 在国内访问不稳定,后续考虑接入 CDN 服务以提升用户体验。仅需一个域名成本,就能实现上述所有功能,GitHub 确实是大善人。
+3 -3
View File
@@ -1,8 +1,8 @@
---
title: markdown语法使用指南
tags: ["markdown"]
date: 2025-03-06
summary: "用来学习markdown语法"
tags: [markdown]
date: 2025-03-09T08:00:00Z
summary: 用来学习markdown语法
---
## 基础语法
+2 -3
View File
@@ -1,9 +1,8 @@
---
title: running_page部署
tags: ["学习","运动"]
date: 2026-03-07
tags: [学习, 运动]
date: 2026-03-09T00:00:00Z
---
最近我的博客上线了一个新的页面,[running_page](https://run.butubb.cn)
![11](https://cdn.jsdelivr.net/gh/8butubb/image/img/20260309120246636.png)
+3 -3
View File
@@ -1,8 +1,8 @@
---
title: zerotier使用指南
tags: ["linux"]
date: 2025-09-07
summary: "zerotier使用指南"
tags: [linux]
date: 2025-09-10T08:00:00Z
summary: zerotier使用指南
---
# 注册
@@ -1,8 +1,8 @@
---
title: 一个NAS用户的UPS求生指南:从断电焦虑到真香体验
tags: ["nas"]
date: 2025-10-22
summary: '作为一个在群租房里"苟活"的绿联NAS用户,这两年我深刻体会到了什么叫"用电如履薄冰"...'
tags: [nas]
date: 2025-10-24T00:00:00Z
summary: "作为一个在群租房里\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"苟活\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"的绿联NAS用户,这两年我深刻体会到了什么叫\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"用电如履薄冰\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"..."
---
## 第一章:那些年,我们一起经历的断电惊魂
@@ -1,9 +1,8 @@
---
title: 减脂健身计划最新进展
tags: ["健康"]
date: 2026-02-24
tags: [健康]
date: 2026-02-27T08:00:00Z
---
## 近期进展汇报
今天是年后上班第一天,过年期间我的体重不重反轻,目前体重79.5kg。虽然减少的幅度下降,整个过年期间只减少了0.5kg,但是没有上涨我已经很满足了。
@@ -1,10 +1,9 @@
---
title: 把Kindle变成信息屏!越狱+自动更换新闻壁纸全攻略 | 附开源代码
tags: ["学习"]
date: 2025-10-01
summary: "前些日子翻出尘封两年的KPW4,本想重拾阅读初心,却发现亚马逊中国早已物是人非。正当想着要不要给它闲鱼出手时转卖时,B站刷到Kindle全机型都支持越狱了。"
title: "把Kindle变成信息屏!越狱+自动更换新闻壁纸全攻略 | 附开源代码"
tags: [学习]
date: 2025-10-03T00:00:00Z
summary: 前些日子翻出尘封两年的KPW4,本想重拾阅读初心,却发现亚马逊中国早已物是人非。正当想着要不要给它闲鱼出手时转卖时,B站刷到Kindle全机型都支持越狱了。
---
前些日子翻出尘封两年的KPW4,本想重拾阅读初心,却发现亚马逊中国早已物是人非。
正当想着要不要给它闲鱼出手时转卖时,B站刷到Kindle全机型都支持越狱了。
+23
View File
@@ -0,0 +1,23 @@
---
title: 新手运动建议
tags: [运动, 健康]
date: 2026-03-22T08:00:00Z
---
作为一个坚持运动了3个多月的新手,我总结了一些经验和建议,希望能帮助刚开始运动的初学者少走弯路。
## 开始永远不晚
无论何时开始运动都不会太迟,重要的是迈出第一步。不要因为年龄、体能基础或过往经历而犹豫,每个人都是从零开始的。关键是找到适合自己的方式,循序渐进地开展运动计划。
## 适度即可,无需过度
运动不需要追求极限,找到自己舒适的节奏最重要。如果不是竞技运动,一定要注意适量,避免过度运动造成身体伤害。过度训练容易导致疲劳、受伤甚至对运动产生厌倦,反而得不偿失。
## 坚持胜于强度
运动的关键不在于单次时长或强度,而在于持之以恒。衡量成果的标准是全年运动的频率和总次数,而非某一次的运动量。每周三次温和的锻炼,持之以恒一年,效果远好于偶尔一次高强度训练。
## 倾听身体信号
学会区分正常的肌肉酸痛和潜在的伤痛。如果感到关节或肌肉异常疼痛,应当休息或咨询专业人士,避免小伤变成大问题。
@@ -0,0 +1,135 @@
---
title: 给 Hugo 博客归档页加上 Github 风格热力图
date: 2026-03-31T16:26:31Z
draft: false
tags: [博客, Hugo, 前端, 魔改]
categories: [博客]
---
最近看着博客的归档页面,总觉得光秃秃的列表差点意思。平时经常看 Github 主页那块绿色的代码提交热力图(Heatmap),看着自己的“绿格子”一天天变多,那种正反馈确实挺让人上瘾的。
于是就萌生了一个想法:**能不能在我的博客归档页也加一个类似 Github 风格的文章更新热力图?**
折腾了一番后,完美搞定。这篇文章记录一下具体的实现过程。
## 需求分析
在动手之前,我给自己定了几个硬性要求:
1. **零依赖,不拖慢速度**:网上很多教程教的是用 ECharts 之类的图表库。虽然效果好,但为了一个小图引入这么大的库,对静态博客的加载速度很不友好。我要用纯原生 JS 和 CSS 来画。
2. **主题适配**:我的博客用的是 PaperMod 主题,支持暗黑模式切换。这个热力图必须能无缝跟着主题切换颜色。
3. **响应式布局,拒绝丑陋的滚动条**:手机端和电脑端屏幕宽度不一样。我不希望在手机上出现一个很长很长的横向滚动条。它必须能根据当前窗口的宽度,自动计算并显示能够放得下的“周数”。
4. **边缘防遮挡的悬浮提示**:鼠标放上去要能显示当天的文章信息,而且如果是最右侧(今天)或者最顶部的数据,提示框不能被屏幕边缘截断。
## 动手实现
Hugo 的修改逻辑很简单,不要去动 `themes/` 目录下的源码,而是利用它的覆盖机制。
`themes/PaperMod/layouts/_default/archives.html` 复制到项目根目录的 `layouts/_default/archives.html`。接下来所有的修改都在这个本地文件里进行。
### 1. 数据获取
第一步是把博客里所有文章的日期拿出来。这一步利用 Hugo 强大的模板语法,在 HTML 里直接把数据渲染成 JS 的数组:
```html
<script>
const postsData = [
{{- range where site.RegularPages "Type" "in" site.Params.mainSections }}
{ date: "{{ .Date.Format "2006-01-02" }}", title: "{{ .Title | htmlEscape }}" },
{{- end }}
];
// 按日期归类统计
const postMap = new Map();
postsData.forEach(p => {
if (!postMap.has(p.date)) postMap.set(p.date, []);
postMap.get(p.date).push(p);
});
</script>
```
这段代码会在编译时把所有文章遍历一遍,最终生成一个干净的 JSON 数据交给浏览器的 JS 处理。
### 2. 页面结构与 CSS Grid 布局
放弃了图表库,我选择用 CSS Grid 来画格子。HTML 结构非常简单:左侧是“一三五”的星期提示,右侧是滚动容器,里面装月份标签和格子容器,底部放一个“少 -> 多”的图例。
样式的核心在于颜色的定义。为了适配 PaperMod 的暗黑模式,我直接绑定了主题的 `html.dark` 选择器,定义了两套 CSS 变量:
```css
:root {
--heatmap-level-0: #ebedf0;
--heatmap-level-1: #9be9a8;
--heatmap-level-2: #40c463;
--heatmap-level-3: #30a14e;
--heatmap-level-4: #216e39;
/* ...其他颜色变量 */
}
html.dark, html[data-theme="dark"], body.dark {
--heatmap-level-0: #161b22;
--heatmap-level-1: #0e4429;
--heatmap-level-2: #006d32;
--heatmap-level-3: #26a641;
--heatmap-level-4: #39d353;
}
```
网格的画法直接利用 `display: grid; grid-template-rows: repeat(7, 12px); grid-auto-flow: column;`,让格子自动按列排布(也就是一列代表一周)。
### 3. 自适应宽度的 JS 逻辑
这是我觉得最巧妙的一步。与其用 `overflow-x: auto` 弄出一个滑动条,不如直接算好能放下几列。
```javascript
// 获取容器实际宽度
const containerWidth = scrollContainer.clientWidth;
const cellWidth = 15; // 12px格子 + 3px间距
// 计算最多能放下多少周,上限是53周(一整年)
const maxWeeks = Math.floor(containerWidth / cellWidth);
const weeksToShow = Math.min(53, maxWeeks);
const daysToShow = weeksToShow * 7;
```
有了 `daysToShow`,就可以倒推出起始日期。接着用一个 for 循环,每天生成一个 `div` 塞进 grid 容器里。根据当天发文的数量,给 `div` 设置不同的 `data-level` 属性,配合 CSS 变色。
同时,我还监听了 `window.resize` 事件,只要你拖动浏览器窗口大小,它就会自动重新计算并重绘,体验极度丝滑。
### 4. Tooltip 边缘防遮挡
最后一个细节是鼠标悬浮的提示框。最开始写的时候发现,如果你今天发了文章(位置在屏幕最右侧),弹出的提示框会被浏览器边缘吃掉一半。
所以加上了一点碰撞检测的数学计算:
```javascript
// ... 省略部分代码
const rect = cell.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
let top = rect.top - tooltipRect.height - 8;
let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
// 顶部防遮挡:如果上面没空间了,就翻转到格子下面
if (top < 10) {
top = rect.bottom + 8;
}
// 左右防遮挡
if (left < 10) {
left = 10;
} else if (left + tooltipRect.width > window.innerWidth - 10) {
left = window.innerWidth - tooltipRect.width - 10;
}
tooltip.style.position = 'fixed';
tooltip.style.left = `${left}px`;
tooltip.style.top = `${top}px`;
```
`fixed` 定位配合 `getBoundingClientRect` 获取相对视口的位置,这样就能保证无论格子在哪,提示框都能完完整整地展示出来。
## 最终效果
至此,大功告成。
打开归档页面,上面是绿油油的热力图,下面是传统的文章列表。切换暗黑模式也是秒切。最重要的是,全程没用任何外部库,轻量到了极致。
以后写博客的动力又多了一条:点亮今天的绿格子!
+2 -3
View File
@@ -1,9 +1,8 @@
---
title: 过年期间的运动记录
tags: ["健康","运动"]
date: 2026-02-25
tags: [健康, 运动]
date: 2026-02-28T08:00:00Z
---
整个过年期间我的运动计划没有荒废,自认为执行的还行。
基本每天都有在运动,吃上面也很克制,不会暴饮暴食。
+2 -3
View File
@@ -1,9 +1,8 @@
---
title: 重新用回macos
tags: ["日常","mac"]
date: 2025-12-01
tags: [日常, mac]
date: 2025-12-04T08:00:00Z
summary: ""
---
最近几天,我重新用回了去年使用的macbookpro,屏幕有一条黑线,使用的是外接显示器
深感还是mac系统简洁好用,没有windows那么多的莫名其妙的bug
@@ -1,8 +1,8 @@
---
title: 长期健康生活计划
tags: ["健康"]
date: 2026-01-17
summary: "人的一生最重要的就是健康"
tags: [健康]
date: 2026-01-20T08:00:00Z
summary: 人的一生最重要的就是健康
---
- 最近开始尝试更规律的生活,调整饮食与运动习惯。一方面是真的想对身体好一点,另一方面,也希望生活有个稳定的节奏,让自己心态更积极。
+7
View File
@@ -0,0 +1,7 @@
---
title: 面向死亡备份
tags: ["健康","备份","死亡","感想"]
date: 2026-07-10
---
面向死亡备份,这是个有意思的话题。
多年以后我不再了,我已不在人间,我的这些数据要怎么处理呢?
+5 -5
View File
@@ -1,6 +1,6 @@
---
title: "🔍 搜索"
layout: "search"
summary: "search"
placeholder: "搜索"
---
title: 🔍 搜索
layout: search
summary: search
placeholder: 搜索
---
+7 -3
View File
@@ -7,11 +7,10 @@ defaultContentLanguage = 'zh-cn'
[outputs]
home = ["HTML", "RSS", "JSON"]
# ===== 新增区域:告诉 Hugo 允许渲染安全的 HTML =====
[markup]
[markup.goldmark]
[markup.goldmark.renderer]
unsafe = true # 允许在 Markdown 中渲染原始 HTML
unsafe = true
[params]
[params.homeInfoParams]
@@ -47,7 +46,12 @@ defaultContentLanguage = 'zh-cn'
url = "/search"
weight = 4
[[menu.main]]
name = "画廊"
url = "/gallery"
weight = 5
[[menu.main]]
name = "Run"
url = "https://run.butubb.cn"
weight = 5
weight = 6
+419
View File
@@ -0,0 +1,419 @@
{{- define "main" }}
<header class="page-header">
<h1>
{{ .Title }}
{{- if (.Param "ShowRssButtonInSectionTermList") }}
{{- $rss := (.OutputFormats.Get "rss") }}
{{- if (eq .Kind `page`) }}
{{- $rss = (.Parent.OutputFormats.Get "rss") }}
{{- end }}
{{- with $rss }}
<a href="{{ .RelPermalink }}" title="RSS" aria-label="RSS">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" height="23">
<path d="M4 11a9 9 0 0 1 9 9" />
<path d="M4 4a16 16 0 0 1 16 16" />
<circle cx="5" cy="19" r="1" />
</svg>
</a>
{{- end }}
{{- end }}
</h1>
{{- if .Description }}
<div class="post-description">
{{ .Description }}
</div>
{{- end }}
</header>
<!-- Heatmap Start -->
<div class="heatmap-wrapper">
<div class="heatmap-container" id="heatmap-container">
<div class="heatmap-weekdays">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div class="heatmap-scroll">
<div class="heatmap-months" id="heatmap-months"></div>
<div class="heatmap-grid" id="heatmap-grid"></div>
</div>
</div>
<div class="heatmap-legend">
<span></span>
<div class="heatmap-cell" data-level="0"></div>
<div class="heatmap-cell" data-level="1"></div>
<div class="heatmap-cell" data-level="2"></div>
<div class="heatmap-cell" data-level="3"></div>
<div class="heatmap-cell" data-level="4"></div>
<span></span>
</div>
<div class="heatmap-tooltip" id="heatmap-tooltip"></div>
</div>
<style>
:root {
--heatmap-level-0: #ebedf0;
--heatmap-level-1: #9be9a8;
--heatmap-level-2: #40c463;
--heatmap-level-3: #30a14e;
--heatmap-level-4: #216e39;
--heatmap-tooltip-bg: #fff;
--heatmap-tooltip-border: #d0d7de;
}
html.dark, html[data-theme="dark"], body.dark {
--heatmap-level-0: #161b22;
--heatmap-level-1: #0e4429;
--heatmap-level-2: #006d32;
--heatmap-level-3: #26a641;
--heatmap-level-4: #39d353;
--heatmap-tooltip-bg: #1c2128;
--heatmap-tooltip-border: #444c56;
}
.heatmap-wrapper {
margin: 1rem 0 2rem 0;
position: relative;
width: 100%;
overflow: hidden;
}
.heatmap-container {
display: flex;
align-items: flex-end;
padding: 1rem;
background: var(--entry);
border-radius: var(--radius);
justify-content: center;
}
.heatmap-weekdays {
display: flex;
flex-direction: column;
gap: 3px;
font-size: 10px;
color: var(--secondary);
padding-right: 8px;
padding-bottom: 2px;
flex-shrink: 0;
}
.heatmap-weekdays span {
height: 12px;
line-height: 12px;
}
.heatmap-scroll {
position: relative;
flex-grow: 1;
overflow: hidden;
display: flex;
justify-content: flex-end;
}
.heatmap-scroll::-webkit-scrollbar {
height: 6px;
}
.heatmap-scroll::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
.heatmap-months {
height: 20px;
position: absolute;
top: 0;
left: 0;
right: 0;
font-size: 10px;
color: var(--secondary);
pointer-events: none;
}
.heatmap-months span {
position: absolute;
bottom: 4px;
}
.heatmap-grid {
display: grid;
grid-template-rows: repeat(7, 12px);
grid-auto-flow: column;
gap: 3px;
margin-top: 20px;
}
.heatmap-cell {
width: 12px;
height: 12px;
border-radius: 2px;
background-color: var(--heatmap-level-0);
cursor: pointer;
transition: opacity 0.2s;
}
.heatmap-cell:hover {
opacity: 0.7;
outline: 1px solid var(--border);
}
.heatmap-cell[data-level="1"] { background-color: var(--heatmap-level-1); }
.heatmap-cell[data-level="2"] { background-color: var(--heatmap-level-2); }
.heatmap-cell[data-level="3"] { background-color: var(--heatmap-level-3); }
.heatmap-cell[data-level="4"] { background-color: var(--heatmap-level-4); }
.heatmap-legend {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
margin-top: 10px;
font-size: 12px;
color: var(--secondary);
width: 100%;
padding-right: 0.5rem;
}
.heatmap-legend span {
margin: 0 4px;
}
.heatmap-tooltip {
display: none;
position: absolute;
background: var(--heatmap-tooltip-bg);
color: var(--primary);
border: 1px solid var(--heatmap-tooltip-border);
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
pointer-events: none;
z-index: 1000;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
white-space: nowrap;
line-height: 1.5;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function() {
const postsData = [
{{- range where site.RegularPages "Type" "in" site.Params.mainSections }}
{ date: `{{ .Date.Format "2006-01-02" }}`, title: `{{ .Title | plainify }}` },
{{- end }}
];
const postMap = new Map();
postsData.forEach(p => {
if (!postMap.has(p.date)) postMap.set(p.date, []);
postMap.get(p.date).push(p);
});
const grid = document.getElementById('heatmap-grid');
const monthsContainer = document.getElementById('heatmap-months');
const tooltip = document.getElementById('heatmap-tooltip');
const scrollContainer = document.querySelector('.heatmap-scroll');
const containerWrapper = document.querySelector('.heatmap-container');
function renderHeatmap() {
if (!grid || !monthsContainer) return;
grid.innerHTML = '';
monthsContainer.innerHTML = '';
// Check if elements are available
if (!scrollContainer || !containerWrapper) return;
// Calculate how many weeks we can fit based on container width
const cellWidth = 15; // 12px cell + 3px gap
let containerWidth = containerWrapper.clientWidth;
if (containerWidth <= 0) {
containerWidth = Math.min(window.innerWidth, 800);
}
containerWidth -= 40; // 40px for padding and weekdays
// Max weeks is 53, but limit to what fits in container
const maxWeeks = Math.floor(containerWidth / cellWidth);
const weeksToShow = Math.min(53, Math.max(10, maxWeeks)); // show at least 10 weeks
const daysToShow = weeksToShow * 7;
const today = new Date();
// 强制使用本地时区并去除时间影响
today.setHours(23, 59, 59, 999);
const startDate = new Date(today.getTime());
startDate.setDate(startDate.getDate() - daysToShow + 1);
// Adjust start date to be a Sunday
const startDay = startDate.getDay();
if (startDay !== 0) {
startDate.setDate(startDate.getDate() - startDay);
}
let currentMonth = -1;
let weekIndex = 0;
// Calculate total days from adjusted start date to today
const totalDays = Math.round((today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) + 1;
for (let i = 0; i < totalDays; i++) {
const d = new Date(startDate.getTime());
d.setDate(startDate.getDate() + i);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const localDateStr = `${y}-${m}-${day}`;
const count = postMap.has(localDateStr) ? postMap.get(localDateStr).length : 0;
const cell = document.createElement('div');
cell.className = 'heatmap-cell';
let level = 0;
if (count === 1) level = 1;
else if (count === 2) level = 2;
else if (count === 3) level = 3;
else if (count >= 4) level = 4;
cell.setAttribute('data-level', level);
// Ensure the first day starts on the correct row
if (i === 0) {
cell.style.gridRow = startDate.getDay() + 1;
}
if (d.getMonth() !== currentMonth) {
currentMonth = d.getMonth();
// Only show month label if it's the first week of the month or we are at the start
if (weekIndex > 0 || d.getDate() < 15) {
const monthLabel = document.createElement('span');
monthLabel.textContent = (d.getMonth() + 1) + '月';
// Calculate right offset
const rightOffset = ((totalDays - i) / 7) * cellWidth - cellWidth;
if (rightOffset >= 0) {
monthLabel.style.right = `${rightOffset}px`;
monthsContainer.appendChild(monthLabel);
}
}
}
if (d.getDay() === 6) weekIndex++;
cell.addEventListener('mouseenter', (e) => {
const titles = postMap.has(localDateStr)
? postMap.get(localDateStr).map(p => p.title).join('<br>')
: '无文章';
const countText = count > 0 ? `${count} 篇文章` : '0 篇文章';
tooltip.innerHTML = `<strong>${localDateStr}</strong><br>${countText}<br><span style="font-size: 0.8em; color: var(--secondary)">${titles}</span>`;
tooltip.style.display = 'block';
const rect = cell.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
// Calculate position relative to viewport
let top = rect.top - tooltipRect.height - 8;
let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
// Adjust if it goes off screen top
if (top < 10) {
top = rect.bottom + 8;
}
// Adjust if it goes off screen left/right
if (left < 10) {
left = 10;
} else if (left + tooltipRect.width > window.innerWidth - 10) {
left = window.innerWidth - tooltipRect.width - 10;
}
tooltip.style.position = 'fixed';
tooltip.style.left = `${left}px`;
tooltip.style.top = `${top}px`;
tooltip.style.transform = 'none';
tooltip.style.marginTop = '0';
});
cell.addEventListener('mouseleave', () => {
tooltip.style.display = 'none';
});
grid.appendChild(cell);
}
}
// Initial render
renderHeatmap();
// Re-render on window resize to adjust visible weeks
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
renderHeatmap();
// scroll to right on resize
if (scrollContainer) {
scrollContainer.scrollLeft = scrollContainer.scrollWidth;
}
}, 100);
});
// render initially
renderHeatmap();
// scroll to right initially
setTimeout(() => {
if (scrollContainer) {
scrollContainer.scrollLeft = scrollContainer.scrollWidth;
}
}, 100);
});
</script>
<!-- Heatmap End -->
{{- $pages := where site.RegularPages "Type" "in" site.Params.mainSections }}
{{- if site.Params.ShowAllPagesInArchive }}
{{- $pages = site.RegularPages }}
{{- end }}
{{- range $pages.GroupByPublishDate "2006" }}
{{- if ne .Key "0001" }}
<div class="archive-year">
{{- $year := replace .Key "0001" "" }}
<h2 class="archive-year-header" id="{{ $year }}">
<a class="archive-header-link" href="#{{ $year }}">
{{- $year -}}
</a>
<sup class="archive-count">&nbsp;{{ len .Pages }}</sup>
</h2>
{{- range .Pages.GroupByDate "January" }}
<div class="archive-month">
<h3 class="archive-month-header" id="{{ $year }}-{{ .Key }}">
<a class="archive-header-link" href="#{{ $year }}-{{ .Key }}">
{{- .Key -}}
</a>
<sup class="archive-count">&nbsp;{{ len .Pages }}</sup>
</h3>
<div class="archive-posts">
{{- range .Pages }}
{{- if eq .Kind "page" }}
<div class="archive-entry">
<h3 class="archive-entry-title entry-hint-parent">
{{- .Title | markdownify }}
{{- if .Draft }}
<span class="entry-hint" title="Draft">
<svg xmlns="http://www.w3.org/2000/svg" height="15" viewBox="0 -960 960 960" fill="currentColor">
<path
d="M160-410v-60h300v60H160Zm0-165v-60h470v60H160Zm0-165v-60h470v60H160Zm360 580v-123l221-220q9-9 20-13t22-4q12 0 23 4.5t20 13.5l37 37q9 9 13 20t4 22q0 11-4.5 22.5T862.09-380L643-160H520Zm300-263-37-37 37 37ZM580-220h38l121-122-18-19-19-18-122 121v38Zm141-141-19-18 37 37-18-19Z" />
</svg>
</span>
{{- end }}
</h3>
<div class="archive-meta">
{{- partial "post_meta.html" . -}}
</div>
<a class="entry-link" aria-label="post link to {{ .Title | plainify }}" href="{{ .Permalink }}"></a>
</div>
{{- end }}
{{- end }}
</div>
</div>
{{- end }}
</div>
{{- end }}
{{- end }}
{{- end }}{{/* end main */}}
+75
View File
@@ -0,0 +1,75 @@
{{- define "main" -}}
{{- if .Title }}
<header class="page-header">
<h1>{{ .Title }}</h1>
{{- with .Description }}
<div class="post-description">{{ . | markdownify }}</div>
{{- end }}
</header>
{{- end }}
<div class="gallery-page-content">
{{- $albums := where .Site.RegularPages "Type" "gallery" }}
{{- range $album := $albums }}
{{- $images := findRE `!\[.*?\]\((.*?)\)` $album.RawContent }}
{{- if $images }}
<section class="gallery-album">
<div class="gallery-album-header">
<h2 class="gallery-album-title">{{ $album.Title }}</h2>
<span class="gallery-album-count">{{ len $images }} 张</span>
</div>
{{- with $album.Params.description }}
<p class="gallery-album-desc">{{ . }}</p>
{{- end }}
<div class="gallery-grid">
{{- range $image := $images }}
{{- $url := replaceRE `!\[.*?\]\((.*?)\)` "$1" $image }}
<div class="gallery-card" data-src="{{ $url }}">
<div class="gallery-card-img">
<img src="{{ $url }}" alt="{{ $album.Title }}" loading="lazy">
</div>
</div>
{{- end }}
</div>
</section>
{{- end }}
{{- end }}
{{- if not $albums }}
<div class="gallery-empty">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="48" height="48">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<path d="M21 15l-5-5L5 21"/>
</svg>
<p>暂无图片</p>
</div>
{{- end }}
</div>
<div class="lightbox" id="lightbox">
<div class="lightbox-backdrop" id="lightbox-backdrop"></div>
<div class="lightbox-body">
<img id="lightbox-img" src="" alt="">
</div>
<button class="lightbox-btn lightbox-close-btn" id="lightbox-close" title="关闭">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
<button class="lightbox-btn lightbox-prev-btn" id="lightbox-prev" title="上一张">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15,18 9,12 15,6"/>
</svg>
</button>
<button class="lightbox-btn lightbox-next-btn" id="lightbox-next" title="下一张">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9,18 15,12 9,6"/>
</svg>
</button>
<div class="lightbox-counter" id="lightbox-counter"></div>
</div>
{{- end -}}
+9
View File
@@ -0,0 +1,9 @@
{{- if eq .Type "gallery" }}
{{- $galleryJS := resources.Get "js/gallery.js" | minify }}
{{- if not site.Params.assets.disableFingerprinting }}
{{- $galleryJS = $galleryJS | fingerprint }}
<script defer crossorigin="anonymous" src="{{ $galleryJS.RelPermalink }}" integrity="{{ $galleryJS.Data.Integrity }}"></script>
{{- else }}
<script defer crossorigin="anonymous" src="{{ $galleryJS.RelPermalink }}"></script>
{{- end }}
{{- end }}
+27
View File
@@ -22,6 +22,8 @@
Deploy on github pages
<br>
<a href="https://icp.gov.moe/?keyword=20250363" target="_blank" rel="noopener noreferrer">萌ICP备20250363号</a>
<br>
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">皖ICP备2026019386号</a>
</span>
</footer>
{{- end }}
@@ -36,6 +38,29 @@
{{- partial "extend_footer.html" . }}
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs';
document.addEventListener("DOMContentLoaded", function() {
mermaid.initialize({ startOnLoad: true, theme: 'default' });
document.querySelectorAll('.language-mermaid').forEach(function(block) {
const container = block.closest('.highlight') || block.parentNode;
const div = document.createElement('div');
div.className = 'mermaid';
// Some Markdown parsers will escape < > " & etc.
// So we unescape them before sending to Mermaid
const txt = document.createElement("textarea");
txt.innerHTML = block.innerHTML;
div.textContent = txt.value;
if (container) {
container.parentNode.replaceChild(div, container);
}
});
});
</script>
<script>
let menu = document.getElementById('menu');
if (menu) {
@@ -152,3 +177,5 @@
});
</script>
{{- end }}