mirror of
https://github.com/8butubb/8butubb.github.io.git
synced 2026-10-13 08:50:04 +08:00
Compare commits
44
Commits
850c664ea2
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a8b2707cd | ||
|
|
d0c79a9f71 | ||
|
|
8b4c0c1fd5 | ||
|
|
8c8641d336 | ||
|
|
b190117582 | ||
|
|
ece663aad7 | ||
|
|
1d647d2c8b | ||
|
|
8f1e6db348 | ||
|
|
d2fb63e7d8 | ||
|
|
dd98cd87fa | ||
|
|
5f2b50e3a9 | ||
|
|
410a5f5217 | ||
|
|
02e2312fce | ||
|
|
2a7e4f0edc | ||
|
|
8b2707218b | ||
|
|
17701a362a | ||
|
|
de2561fc40 | ||
|
|
1fac0280d5 | ||
|
|
46ae27c127 | ||
|
|
57bbc5f201 | ||
|
|
6274bdd5a7 | ||
|
|
d48c972733 | ||
|
|
def46009ec | ||
|
|
a1b8cbd738 | ||
|
|
b512ad83a7 | ||
|
|
3e8cd729ba | ||
|
|
d99cf1bc78 | ||
|
|
3215b146af | ||
|
|
881387864d | ||
|
|
2dbf3bdec5 | ||
|
|
3ae95f7212 | ||
|
|
1b591dd010 | ||
|
|
bb74423a92 | ||
|
|
a2b524b258 | ||
|
|
1d111bd98b | ||
|
|
72a98f4f12 | ||
|
|
1e9bd08765 | ||
|
|
fa08f8a325 | ||
|
|
a138f5f0ad | ||
|
|
933499b024 | ||
|
|
a7dc2f1ad3 | ||
|
|
bfe250fce8 | ||
|
|
76a7cfd56a | ||
|
|
b5d1d71dbf |
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 });
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: 归档
|
||||||
|
layout: archives
|
||||||
|
url: /archives
|
||||||
|
---
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
title: "归档"
|
|
||||||
layout: "archives"
|
|
||||||
url: "/archives"
|
|
||||||
---
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: 跑步记录
|
||||||
|
date: 2026-03-12T08:00:00Z
|
||||||
|
---
|
||||||
|

|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 画廊
|
||||||
|
description: 记录生活中的美好瞬间
|
||||||
|
---
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: Git 完整教程指南
|
title: Git 完整教程指南
|
||||||
tags: ["git", "版本控制", "教程"]
|
tags: [git, 版本控制, 教程]
|
||||||
date: 2025-10-08
|
date: 2025-10-11T08:00:00Z
|
||||||
summary: "从零开始学习 Git 版本控制系统,包含基础概念、常用命令、分支管理、协作流程等完整内容。"
|
summary: 从零开始学习 Git 版本控制系统,包含基础概念、常用命令、分支管理、协作流程等完整内容。
|
||||||
---
|
---
|
||||||
|
|
||||||
> 本教程适用于 Git 初学者到进阶用户,涵盖日常开发中 90% 的使用场景。
|
> 本教程适用于 Git 初学者到进阶用户,涵盖日常开发中 90% 的使用场景。
|
||||||
|
|
||||||
## 什么是 Git
|
## 什么是 Git
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: Linux 硬链接
|
title: Linux 硬链接
|
||||||
tags: ["linux"]
|
tags: [linux]
|
||||||
date: 2024-12-04
|
date: 2024-12-07T08:00:00Z
|
||||||
summary: "在 Linux 系统中,文件的存储与管理是通过链接来实现的。硬链接是其中一种重要的链接类型,本文将详细介绍硬链接的概念、创建方法及其应用场景。"
|
summary: 在 Linux 系统中,文件的存储与管理是通过链接来实现的。硬链接是其中一种重要的链接类型,本文将详细介绍硬链接的概念、创建方法及其应用场景。
|
||||||
---
|
---
|
||||||
在 Linux 系统中,文件的存储与管理是通过链接来实现的。硬链接是其中一种重要的链接类型,本文将详细介绍硬链接的概念、创建方法及其应用场景。
|
在 Linux 系统中,文件的存储与管理是通过链接来实现的。硬链接是其中一种重要的链接类型,本文将详细介绍硬链接的概念、创建方法及其应用场景。
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Linux文件系统概述
|
title: Linux文件系统概述
|
||||||
tags: ["linux"]
|
tags: [linux]
|
||||||
date: 2024-12-04
|
date: 2024-12-07T08:00:00Z
|
||||||
---
|
---
|
||||||
Linux的文件系统结构是其核心组成部分之一,它以一种层次化的方式组织数据,使得用户和程序能够高效地访问和管理文件。以下是一些关键概念和特点:
|
Linux的文件系统结构是其核心组成部分之一,它以一种层次化的方式组织数据,使得用户和程序能够高效地访问和管理文件。以下是一些关键概念和特点:
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: docker搭建思源笔记
|
title: docker搭建思源笔记
|
||||||
tags: ["linux","docker"]
|
tags: [linux, docker]
|
||||||
date: 2024-11-23
|
date: 2024-11-25T00:00:00Z
|
||||||
summary: "docker搭建思源笔记..."
|
summary: docker搭建思源笔记...
|
||||||
---
|
---
|
||||||
|
|
||||||
- `PUID`: 自定义用户 ID(可选,如果未提供,默认为 `1000`)
|
- `PUID`: 自定义用户 ID(可选,如果未提供,默认为 `1000`)
|
||||||
- `PGID`: 自定义组 ID(可选,如果未提供,默认为 `1000`)
|
- `PGID`: 自定义组 ID(可选,如果未提供,默认为 `1000`)
|
||||||
- `workspace_dir_host`:宿主机上的工作空间文件夹路径
|
- `workspace_dir_host`:宿主机上的工作空间文件夹路径
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: frp内网穿透使用指南
|
title: frp内网穿透使用指南
|
||||||
tags: ["linux"]
|
tags: [linux]
|
||||||
date: 2024-12-02
|
date: 2024-12-05T08:00:00Z
|
||||||
summary: "frp(Fast Reverse Proxy)是一个高性能的反向代理应用,主要用于内网穿透。..."
|
summary: frp(Fast Reverse Proxy)是一个高性能的反向代理应用,主要用于内网穿透。...
|
||||||
---
|
---
|
||||||
## 什么是 frp?
|
## 什么是 frp?
|
||||||
|
|
||||||
|
|||||||
+56
-216
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: gitea 自部署使用体验
|
title: gitea 自部署使用体验
|
||||||
tags: ["git","docker"]
|
tags: [git, docker]
|
||||||
date: 2025-11-27
|
date: 2025-11-30T08:00:00Z
|
||||||
summary: "本文基于 Docker Compose 一键启动 Gitea + Gitea Runner 的实战记录。"
|
summary: 本文基于 Docker Compose 一键启动 Gitea + Gitea Runner 的实战记录。
|
||||||
---
|
---
|
||||||
|
|
||||||
> 注意:本文基于 Docker Compose 一键启动 Gitea + Gitea Runner 的实战记录。
|
> 注意:本文基于 Docker Compose 一键启动 Gitea + Gitea Runner 的实战记录。
|
||||||
|
|
||||||
## 为什么使用 gitea
|
## 为什么使用 gitea
|
||||||
@@ -39,113 +38,72 @@ Gitea 采用微内核 + 插件扩展的轻量架构,核心组件如下:
|
|||||||
### 架构图(Mermaid)
|
### 架构图(Mermaid)
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TB
|
graph TD
|
||||||
subgraph "客户端层"
|
%% 客户端层
|
||||||
A[Web浏览器]
|
Client_Web[Web浏览器]
|
||||||
B[Git客户端]
|
Client_Git[Git客户端]
|
||||||
C[API客户端]
|
Client_API[API客户端]
|
||||||
|
|
||||||
|
%% 负载层
|
||||||
|
Proxy[Nginx / 反向代理]
|
||||||
|
|
||||||
|
%% 核心服务层
|
||||||
|
subgraph Gitea核心服务
|
||||||
|
Web[Web服务 / Gin]
|
||||||
|
API[RESTful API]
|
||||||
|
Git[Git协议处理器]
|
||||||
|
Auth[认证授权 / Session / OAuth]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph "负载均衡/反向代理"
|
%% 业务逻辑层
|
||||||
D[Nginx/Apache]
|
subgraph 业务逻辑层
|
||||||
|
Repo[仓库管理]
|
||||||
|
User[用户与组织]
|
||||||
|
ACL[权限控制 RBAC]
|
||||||
|
Hook[WebHook]
|
||||||
|
Mail[邮件服务]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph "Gitea核心服务"
|
%% 存储层
|
||||||
E[Web服务<br/>Gin框架]
|
subgraph 数据存储层
|
||||||
F[API服务<br/>RESTful]
|
DB[(数据库<br/>SQLite/MySQL)]
|
||||||
G[Git服务<br/>SSH/HTTP]
|
FS[(Git仓库文件)]
|
||||||
H[认证授权<br/>Session/OAuth]
|
LFS[(LFS对象)]
|
||||||
|
Cache[(Redis缓存)]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph "业务逻辑层"
|
%% 扩展
|
||||||
I[仓库管理]
|
Runner[[Gitea Runner<br/>CI/CD]]
|
||||||
J[用户管理]
|
Pack[[包管理<br/>npm/docker]]
|
||||||
K[组织管理]
|
|
||||||
L[权限控制<br/>RBAC]
|
|
||||||
M[WebHook]
|
|
||||||
N[邮件服务]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "数据存储层"
|
%% 关系连线
|
||||||
O[数据库<br/>SQLite/MySQL/PostgreSQL]
|
Client_Web --> Proxy
|
||||||
P[Git仓库<br/>文件系统]
|
Client_Git --> Proxy
|
||||||
Q[LFS对象存储]
|
Client_API --> Proxy
|
||||||
R[缓存<br/>Redis/Memcached]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "后台任务"
|
Proxy --> Web
|
||||||
S[定时任务<br/>Cron]
|
Proxy --> API
|
||||||
T[队列处理<br/>WebHook/邮件]
|
Proxy --> Git
|
||||||
U[索引构建]
|
|
||||||
V[镜像同步]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "可选扩展"
|
Web --> Auth
|
||||||
W[Gitea Runner<br/>CI/CD]
|
API --> Auth
|
||||||
X[包管理<br/>npm/docker/maven]
|
Git --> Auth
|
||||||
Y[代码搜索<br/>Bleve]
|
|
||||||
end
|
|
||||||
|
|
||||||
A --> D
|
Auth --> Repo
|
||||||
B --> D
|
Auth --> User
|
||||||
C --> D
|
Auth --> ACL
|
||||||
|
|
||||||
D --> E
|
Repo --> DB
|
||||||
D --> F
|
Repo --> FS
|
||||||
D --> G
|
Repo --> LFS
|
||||||
|
User --> DB
|
||||||
|
ACL --> DB
|
||||||
|
|
||||||
E --> H
|
Repo --> Hook
|
||||||
F --> H
|
Hook --> Mail
|
||||||
G --> H
|
|
||||||
|
|
||||||
H --> I
|
Runner -.-> API
|
||||||
H --> J
|
Pack -.-> FS
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 架构说明
|
### 架构说明
|
||||||
@@ -162,124 +120,6 @@ graph TB
|
|||||||
|
|
||||||
这种架构设计使得 Gitea 具有轻量级、高可用和易扩展的特点。
|
这种架构设计使得 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. 轻量级设计**
|
**1. 轻量级设计**
|
||||||
|
|||||||
@@ -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 确实是大善人。
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: markdown语法使用指南
|
title: markdown语法使用指南
|
||||||
tags: ["markdown"]
|
tags: [markdown]
|
||||||
date: 2025-03-06
|
date: 2025-03-09T08:00:00Z
|
||||||
summary: "用来学习markdown语法"
|
summary: 用来学习markdown语法
|
||||||
---
|
---
|
||||||
## 基础语法
|
## 基础语法
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: running_page部署
|
||||||
|
tags: [学习, 运动]
|
||||||
|
date: 2026-03-09T00:00:00Z
|
||||||
|
---
|
||||||
|
最近我的博客上线了一个新的页面,[running_page](https://run.butubb.cn)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
使用的是[https://github.com/yihong0618/running_page](https://github.com/yihong0618/running_page)这个仓库
|
||||||
|
|
||||||
|
大家有兴趣的可以看看
|
||||||
|
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: zerotier使用指南
|
title: zerotier使用指南
|
||||||
tags: ["linux"]
|
tags: [linux]
|
||||||
date: 2025-09-07
|
date: 2025-09-10T08:00:00Z
|
||||||
summary: "zerotier使用指南"
|
summary: zerotier使用指南
|
||||||
---
|
---
|
||||||
# 注册
|
# 注册
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: 一个NAS用户的UPS求生指南:从断电焦虑到真香体验
|
title: 一个NAS用户的UPS求生指南:从断电焦虑到真香体验
|
||||||
tags: ["nas"]
|
tags: [nas]
|
||||||
date: 2025-10-22
|
date: 2025-10-24T00:00:00Z
|
||||||
summary: '作为一个在群租房里"苟活"的绿联NAS用户,这两年我深刻体会到了什么叫"用电如履薄冰"...'
|
summary: "作为一个在群租房里\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"苟活\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"的绿联NAS用户,这两年我深刻体会到了什么叫\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"用电如履薄冰\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"..."
|
||||||
---
|
---
|
||||||
## 第一章:那些年,我们一起经历的断电惊魂
|
## 第一章:那些年,我们一起经历的断电惊魂
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
title: 减脂健身计划最新进展
|
||||||
|
tags: [健康]
|
||||||
|
date: 2026-02-27T08:00:00Z
|
||||||
|
---
|
||||||
|
## 近期进展汇报
|
||||||
|
今天是年后上班第一天,过年期间我的体重不重反轻,目前体重79.5kg。虽然减少的幅度下降,整个过年期间只减少了0.5kg,但是没有上涨我已经很满足了。
|
||||||
|
|
||||||
|
今年的长期目标是体重控制到70kg,短期目标是先控制到77.5kg,预计的话在4月份之前实现短期目标。
|
||||||
|
|
||||||
|
减肥的速度不能太快避免反弹,目前的方案还是维持原方案不变,减少进食的热量,增加运动的时间,近期可能会考虑增购蛋白粉的摄入。
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: 把Kindle变成信息屏!越狱+自动更换新闻壁纸全攻略 | 附开源代码
|
title: "把Kindle变成信息屏!越狱+自动更换新闻壁纸全攻略 | 附开源代码"
|
||||||
tags: ["学习"]
|
tags: [学习]
|
||||||
date: 2025-10-01
|
date: 2025-10-03T00:00:00Z
|
||||||
summary: "前些日子翻出尘封两年的KPW4,本想重拾阅读初心,却发现亚马逊中国早已物是人非。正当想着要不要给它闲鱼出手时转卖时,B站刷到Kindle全机型都支持越狱了。"
|
summary: 前些日子翻出尘封两年的KPW4,本想重拾阅读初心,却发现亚马逊中国早已物是人非。正当想着要不要给它闲鱼出手时转卖时,B站刷到Kindle全机型都支持越狱了。
|
||||||
---
|
---
|
||||||
|
|
||||||
前些日子翻出尘封两年的KPW4,本想重拾阅读初心,却发现亚马逊中国早已物是人非。
|
前些日子翻出尘封两年的KPW4,本想重拾阅读初心,却发现亚马逊中国早已物是人非。
|
||||||
正当想着要不要给它闲鱼出手时转卖时,B站刷到Kindle全机型都支持越狱了。
|
正当想着要不要给它闲鱼出手时转卖时,B站刷到Kindle全机型都支持越狱了。
|
||||||
|
|
||||||
|
|||||||
@@ -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` 获取相对视口的位置,这样就能保证无论格子在哪,提示框都能完完整整地展示出来。
|
||||||
|
|
||||||
|
## 最终效果
|
||||||
|
|
||||||
|
至此,大功告成。
|
||||||
|
|
||||||
|
打开归档页面,上面是绿油油的热力图,下面是传统的文章列表。切换暗黑模式也是秒切。最重要的是,全程没用任何外部库,轻量到了极致。
|
||||||
|
|
||||||
|
以后写博客的动力又多了一条:点亮今天的绿格子!
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
---
|
||||||
|
title: 给飞牛 NAS 加装大疆 4G 模块一代当备用网:拔掉网线也不掉线
|
||||||
|
tags: [nas, 4g, 大疆, 网络, 飞牛os]
|
||||||
|
date: 2026-10-05T00:00:00Z
|
||||||
|
summary: "单条网线的 NAS 一断线就彻底失联:中继连不上、机器人掉线、远程全废。这篇记录我给飞牛 NAS 插上一块淘宝二手 100 元淘来的大疆 4G 模块一代(QDC507),用开源项目 HiDeck 管它,并一路踩过 OVS 藏断线、僵尸默认路由、网口编号漂移、模块编号变化这些坑,最后实现「拔线自动切 4G、插回自动切回」的全过程。"
|
||||||
|
---
|
||||||
|
|
||||||
|
## 起因:单线 NAS 的焦虑
|
||||||
|
|
||||||
|
家里的 NAS 只有一条网线。平时岁月静好,但只要出现下面任意一种情况:
|
||||||
|
|
||||||
|
- 网线被谁碰掉(比如我插拔 USB 设备时顺手带下来)
|
||||||
|
- 光猫/路由器抽风重启
|
||||||
|
- 宽带施工挖断
|
||||||
|
|
||||||
|
NAS 上的东西就**全部对外失联**:远程访问进不来、自建的机器人掉线、同步全部卡住,而人在外面只能干瞪眼。
|
||||||
|
|
||||||
|
想上第二条宽带吧,又贵又没必要。所以很自然就想到了:**插一块 4G 模块当备用链路**,平时待命,主线断了自动顶上去。
|
||||||
|
|
||||||
|
## 硬件:大疆 4G 模块一代(二手 100 元)+ 一张流量卡
|
||||||
|
|
||||||
|
主角是一块**大疆 4G 模块一代**(型号 QDC507)—— 本来是大疆给无人机做 4G 图传增强用的,我在淘宝二手渠道 **100 元**拿下。现在回头看,这价钱也算「高价入手」了 😂(同款行情早就跌到几十块)。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*大疆 4G 模块一代(DJI Cellular 模块):U 盘大小的黑盒子,插上电脑就是一块普通 4G 网卡*
|
||||||
|
|
||||||
|
它本质上是**深度定制的 Quectel LTE 模块**,识别信息非常诚实:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lsusb | grep -i quectel # Bus 007 Device 003: ID 2c7c:0125 Quectel EC25 LTE modem
|
||||||
|
mmcli -L # /org/freedesktop/ModemManager1/Modem/0 QUECTEL Mobile Broadband Module
|
||||||
|
mmcli -m 0 | grep -i firmware # firmware revision: QDC507GLEFM21 ← QDC507 就是大疆一代的型号
|
||||||
|
ip -br link | grep wwan # wwan0 UP
|
||||||
|
ip route show default
|
||||||
|
# default via 192.168.20.1 dev enp4s0-ovs proto static metric 100 ← 有线(主)
|
||||||
|
# default via 10.200.35.20 dev wwan0 metric 5000 ← 4G(备)
|
||||||
|
```
|
||||||
|
|
||||||
|
USB 的 VID `2c7c` 是 Quectel 的,固件里又带着大疆的型号串 QDC507 —— **「大疆定制固件 + Quectel 硬件」**这个组合在折腾时很有用:网上大半资料是按 Quectel(EG25-G / EC25 那套)写的,照着查基本都能对上。
|
||||||
|
|
||||||
|
几个要点:
|
||||||
|
|
||||||
|
- 网卡驱动是 `qmi_wwan`,出来一个 `wwan0`;
|
||||||
|
- 拨号是 **ModemManager 的 initial EPS bearer** 自动做的,**NetworkManager 完全不接管它**(这点后面很关键);
|
||||||
|
- 运营商给的是内网地址(CGNAT)+ 一段 IPv6;
|
||||||
|
- 默认路由的 `metric` 决定优先级:**数字小的优先**。所以有线 100、4G 5000 —— 有线在的时候,4G 就是一条安静的备胎。
|
||||||
|
|
||||||
|
## 顺手安利:用 HiDeck 管理这块模块
|
||||||
|
|
||||||
|
能当网卡用之后,日常还得看它:信号多少、注册没注册、SIM 什么状态、以及最好用的——**收发短信**(查话费/流量余额就靠短信)。命令行 `mmcli` 都能看,但天天敲太累,于是我把这块交给了一个开源项目:**HiDeck**(`yibaiba/hideck`)—— 一个自托管的 Qualcomm 4G / LTE / 5G 模块 Web 控制台。
|
||||||
|
|
||||||
|
部署一句话(Docker,host 网络 + 需要 `/dev` 权限):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/yibaiba/hideck/main/deploy.sh | sh
|
||||||
|
# 浏览器打开 http://<NAS_IP>:7575 默认 admin / admin(首次登录强制改密码)
|
||||||
|
```
|
||||||
|
|
||||||
|
我常用的几项:
|
||||||
|
|
||||||
|
- **设备**:USB 模块自动发现(QMI / MBIM / AT),实时看信号、频段、SIM 身份 —— 换 USB 口、模块编号漂移这类问题,在这里一眼就能看出模块还在不在;
|
||||||
|
- **短信**:收件箱 + 发短信。查话费/流量就是给它发 `101` / `108` 到 `10001`,回复直接显示在页面上;
|
||||||
|
- **代理**:SOCKS5 / HTTP 绑定到这块模块(`SO_BINDTODEVICE`),需要"只有某个程序走 4G"时很省事;
|
||||||
|
- **电话 / VoWiFi**:还能当网络电话用(VoLTE / WiFi Calling),这部分我还没细折腾;
|
||||||
|
- **定时任务 + 通知**:支持 Bark、飞书、Telegram 等,做状态/流量提醒很方便。
|
||||||
|
|
||||||
|
我的分工是:**HiDeck 当模块的「仪表盘」**(信号、短信、临时代理),**链路切换交给系统那一套路由看门狗**(下面细说)—— 两者互不干扰。
|
||||||
|
|
||||||
|
## 飞牛的第一盆冷水:界面里根本没有 4G
|
||||||
|
|
||||||
|
打开飞牛「网络设置」,**找不到这块 4G 网卡**。
|
||||||
|
|
||||||
|
一开始以为是配置问题,后来翻了 `/usr/trim/bin/network_service` 二进制、它的日志、以及前端页面资源,发现里面连 `wwan`/`modem` 这类字样都没有 —— 结论很干脆:**飞牛的网络设置目前不支持 WWAN 类型的接口**。
|
||||||
|
|
||||||
|
所以 4G 这条链路只能靠命令行管(`mmcli` / `ip` / `nmcli`)。界面指望不上,但也不影响用。
|
||||||
|
|
||||||
|
## 大坑:拔掉网线,NAS 反而彻底失联
|
||||||
|
|
||||||
|
最反直觉的地方来了。
|
||||||
|
|
||||||
|
信心满满地拔掉网线,结果**整台 NAS 直接像死了一样**:远程中继连不上、飞书机器人静默、连域名都解析不出来。
|
||||||
|
|
||||||
|
但诡异的是:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -4 -s --interface wwan0 https://ifconfig.me/ip # ✅ 走 4G 单独测,通!
|
||||||
|
ip route show default # ✅ 4G 那条 metric 5000 明明在路由表里
|
||||||
|
```
|
||||||
|
|
||||||
|
**4G 是好的,路由也在,但流量就是不从它走。** 这就是典型的需要抓现场的问题 —— 等你回我消息的时候,故障现场早就过去了。
|
||||||
|
|
||||||
|
### 第一步:用「触发式采样器」抓现场
|
||||||
|
|
||||||
|
间歇性故障最忌讳猜。做法是写个采样器,**等状态跳变**(比如物理网口 `carrier` 从 1 变 0),跳变后立刻连采十几轮,把每一轮的路由表、DNS、每个接口单独发请求的出口 IP、中继域名可达性全记下来。
|
||||||
|
|
||||||
|
采样结果非常干净:
|
||||||
|
|
||||||
|
- 有线接口 `enp4s0` 的 `carrier=0`(物理上确实断了);
|
||||||
|
- 但桥接口 `enp4s0-ovs` 的 `carrier` **恒为 1**;
|
||||||
|
- 断网窗口里,ping/HTTPS/DNS 全是超时(`000`),而 `curl --interface wwan0` 单独走 4G 是通的。
|
||||||
|
|
||||||
|
### 第二步:根因 —— OVS 把断线藏了起来
|
||||||
|
|
||||||
|
我的网口套在 OVS 里。**OVS 会把物理断线藏起来**:
|
||||||
|
|
||||||
|
- 物理口 `enp4s0`:`carrier=0`(断了)
|
||||||
|
- OVS 内部口 `enp4s0-ovs`:`carrier` 永远是 1
|
||||||
|
|
||||||
|
于是 **NetworkManager 根本不知道线断了**,那条 `proto static metric 100` 的默认路由**不会撤销**。结果就是:所有流量继续往一个已经不可达的网关灌(黑洞),而 4G 的 `metric 5000` 老实排在后头,**永远轮不到**。
|
||||||
|
|
||||||
|
所以这不是 4G 的问题,是**僵尸默认路由**的问题。
|
||||||
|
|
||||||
|
## 解法:2 秒轮询的默认路由看门狗
|
||||||
|
|
||||||
|
思路很直接:**盯物理口的 carrier**,断了就把 4G 那条默认路由的 metric 从 5000 提到 50(压过有线的 100),线回来再撤掉。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PHYS=enp4s0 # 物理口(读 carrier 用)
|
||||||
|
LTE_DEV=wwan0 # 4G 网卡
|
||||||
|
LTE_PRIO_METRIC=50 # 临时优先权
|
||||||
|
|
||||||
|
carrier() { cat /sys/class/net/$PHYS/carrier 2>/dev/null || echo "?"; }
|
||||||
|
lte_gw() { ip route show default | awk -v d="$LTE_DEV" '$1=="default" && $5==d {print $3; exit}'; }
|
||||||
|
|
||||||
|
# 注意:判断 metric 必须用数字比较,别用字符串匹配(见下面的坑)
|
||||||
|
route_metric_is() { # $1=网关 $2=网卡 $3=metric
|
||||||
|
ip route show default | awk -v g="$1" -v d="$2" -v want="$3" '
|
||||||
|
$1=="default" && $3==g && $5==d { for (i=1;i<=NF;i++) if ($i=="metric" && $(i+1)+0==want+0) found=1 }
|
||||||
|
END { exit(found?0:1) }'
|
||||||
|
}
|
||||||
|
|
||||||
|
check_once() {
|
||||||
|
c=$(carrier); lg=$(lte_gw)
|
||||||
|
case "$c" in
|
||||||
|
0) # 有线断了 → 给 4G 提权
|
||||||
|
if [ -n "$lg" ] && ! route_metric_is "$lg" "$LTE_DEV" "$LTE_PRIO_METRIC"; then
|
||||||
|
ip route replace default via "$lg" dev "$LTE_DEV" metric "$LTE_PRIO_METRIC"
|
||||||
|
fi ;;
|
||||||
|
1) # 有线恢复 → 撤掉临时路由
|
||||||
|
if [ -n "$lg" ] && route_metric_is "$lg" "$LTE_DEV" "$LTE_PRIO_METRIC"; then
|
||||||
|
ip route del default via "$lg" dev "$LTE_DEV" metric "$LTE_PRIO_METRIC"
|
||||||
|
fi ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 这里有两个必须踩过才知道的坑
|
||||||
|
|
||||||
|
**坑 1:不要动 NetworkManager 管的那条路由。**
|
||||||
|
|
||||||
|
我最早的做法是去改**有线**那条默认路由的 metric(100 → 10000),结果 **30 秒内就被 NM 改回 100**,看门狗白干 ✗。
|
||||||
|
|
||||||
|
正确姿势是改**没人管的那条**:4G 的默认路由是 **ModemManager** 建的,NM 不碰它,所以把它从 `metric 5000` 提到 `metric 50`,稳稳压过有线的 100 ✓。
|
||||||
|
|
||||||
|
> 通用经验:**改配置前先问「这条配置属于哪个管理器?我改它会不会被覆盖?」** 会覆盖,就换一个等价但归你的杠杆。
|
||||||
|
|
||||||
|
**坑 2:判断 metric 别用 `grep` 做前缀匹配。**
|
||||||
|
|
||||||
|
我第二版用了 `grep "metric 50"` 来判断"临时优先路由还在不在",看着挺对,实际灾难:
|
||||||
|
|
||||||
|
```
|
||||||
|
default via 10.200.35.20 dev wwan0 metric 5000
|
||||||
|
^^^^ 里面就含 "metric 50"
|
||||||
|
```
|
||||||
|
|
||||||
|
于是有线恢复之后,脚本以为临时路由还在,**每 2 秒重复删一次、刷满日志** ✗。改用 `awk` 把 metric 取出来做**数字比较**之后,世界清净了。
|
||||||
|
|
||||||
|
## 部署:非 root 也能长期维护的「转发脚本 + 自更新」
|
||||||
|
|
||||||
|
这台 NAS 上我跑服务的账号没有 root,每次改脚本都要找人 `sudo` 重装,很烦。所以又套了一层:
|
||||||
|
|
||||||
|
1. `/usr/local/sbin/wan_carrier_fix.sh` 只放一个 **3 行的 stub**,干一件事:exec 到工作目录里的真身;
|
||||||
|
2. 真身脚本放在普通用户可写的工作目录里,主循环每 2 秒顺手比一下**自己的 mtime**,发现被改了就地 `exec "$SELF"` 重新加载。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# /usr/local/sbin/wan_carrier_fix.sh —— 只装一次,以后再也不碰
|
||||||
|
#!/bin/bash
|
||||||
|
exec /path/to/real/wan_carrier_fix.sh "$@"
|
||||||
|
```
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# /etc/systemd/system/wan-carrier-fix.service
|
||||||
|
[Unit]
|
||||||
|
Description=WAN carrier failover watchdog
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/local/sbin/wan_carrier_fix.sh
|
||||||
|
Restart=always
|
||||||
|
RestartSec=3
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
效果:**以后改脚本只要改工作目录里那份,2 秒后自动生效**,systemd 只负责兜底崩溃重启。省下的都是找 sudo 的时间。
|
||||||
|
|
||||||
|
(小细节:自更新重载必须用**绝对路径**,因为 systemd 给的工作目录可能是 `/`。)
|
||||||
|
|
||||||
|
## 切到 4G 之后要处理的两件事
|
||||||
|
|
||||||
|
**① 出口 IP 变了,中继要重新握手。**
|
||||||
|
4G 出口 IP 和宽带完全不是一个网段。家里的 NAS 中继(fnid)是长连接,切过去之后它那边不认旧会话,**必须重启一次中继服务**(`systemctl restart trim_connect`),重启完立刻可用 ✓。
|
||||||
|
|
||||||
|
**② 裸 4G 只有出、没有进。**
|
||||||
|
4G 是运营商 CGNAT,**入站不可能**。所以对外访问要么走中继、要么走 Tailscale —— 局域网直连在断线时必然不通,这不是故障。
|
||||||
|
|
||||||
|
顺带一个同源坑:**给隧道/反代配置回源地址时别写局域网 IP**。我原来写的是 `http://192.168.20.18:8080` 这种,一旦网卡上的地址没了,回源直接断(外部看到 530)。改成 `http://127.0.0.1:8080` 之后,**跟走哪条线路、哪个地址完全无关**了 ✓。
|
||||||
|
|
||||||
|
## 别让 4G 偷偷烧流量
|
||||||
|
|
||||||
|
装完之后我看了眼计数器,吓了一跳:**开机 36 分钟,4G 上行已经 3 GB** 😅
|
||||||
|
|
||||||
|
原因:机器上 **IPv6 的默认路由只有 4G 这一条**,所以所有 IPv6 出站流量都在走 4G。而跑着 qBittorrent 的话,做种上传会疯狂吃 IPv6。
|
||||||
|
|
||||||
|
两个处理:
|
||||||
|
|
||||||
|
- **治本(软件里就能做)**:qBittorrent → 设置 → 高级 → 「网络接口」选有线口、「可选 IP 地址」选有线地址 → 它只用有线收发,线断了它自己哑掉,**绝不会吃 4G** ✓
|
||||||
|
- **兜底**:加一个静默流量看门狗,**只在"有线本来就正常"时才可能报警**(当日 4G > 1 GB,或单轮增量 > 200 MB),断线期间 4G 是唯一出口,属正常使用,一个字都不说。
|
||||||
|
|
||||||
|
## 换个 USB 口的意外收获:别写死网卡名
|
||||||
|
|
||||||
|
后来我把 4G 模块换个 USB 口插,顺手发现两个"绝对不能写死"的东西:
|
||||||
|
|
||||||
|
1. **ModemManager 里的模块编号会变**:`Modem/0` 变成了 `Modem/2`;
|
||||||
|
2. **接口名也可能变**(这次还是 `wwan0`,但没理由指望它永远不变)。
|
||||||
|
|
||||||
|
所以所有脚本都改成了**自动识别**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 找 4G 网卡:优先看驱动是不是 qmi_wwan,退路才找 wwan*
|
||||||
|
for d in /sys/class/net/*; do
|
||||||
|
[ -e "$d/device/driver" ] || continue
|
||||||
|
[ "$(basename "$(readlink -f "$d/device/driver")")" = "qmi_wwan" ] && basename "$d"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
另外还遇到一次 ModemManager 状态错乱:`mmcli` 报 `state: failed / failed reason: sim-missing`,可数据其实是通的(模块自己续着会话)。`systemctl restart ModemManager` 让它重新识别 SIM 即可 —— **但要注意,这时 MM 已经不托管任何数据会话了,一旦掉线它不会自动重连**,所以别放着不管。
|
||||||
|
|
||||||
|
## 效果
|
||||||
|
|
||||||
|
现在这套跑下来,日常是这样的:
|
||||||
|
|
||||||
|
| 场景 | 表现 |
|
||||||
|
| --- | --- |
|
||||||
|
| 拔掉网线 | 1~2 秒内 4G 提权顶上,NAS 服务、远程访问、机器人都不断 |
|
||||||
|
| 插回网线 | 自动撤掉临时的 4G 路由,出口回到有线,4G 回到待命 |
|
||||||
|
| 只有 4G 时 | 远程访问走中继(需重启一次中继服务),局域网直连不通属正常 |
|
||||||
|
| 有线正常时 | 4G 只当备胎,流量几乎为零(qB 已绑定网卡) |
|
||||||
|
|
||||||
|
成本就是一块二手大疆 4G 模块(100 元)+ 一张流量卡,比第二条宽带便宜太多了。
|
||||||
|
|
||||||
|
## 小结
|
||||||
|
|
||||||
|
- **单线 NAS 的备用链路,值得折腾**;大疆一代 4G 模块(本质是 Quectel LTE 模块)在 Linux 上几乎是插上就能用,日常管理交给 HiDeck 很省心;
|
||||||
|
- **断线切换的坑常常不在备用链路本身,而在主链路的"僵尸状态"**(本例:OVS 藏了物理断线 → 默认路由不撤 → 备用链路永远轮不到);
|
||||||
|
- **别跟上层管理器抢它管的配置**,换个"没人管的等价杠杆"(改 4G 那条 metric,而不是有线那条);
|
||||||
|
- **判断数值别用字符串匹配**,`metric 50` 会被 `metric 5000` 命中;
|
||||||
|
- **脚本别写死网卡名/模块编号**,用 `qmi_wwan` 驱动去认;
|
||||||
|
- 计量的链路上,**把大流量程序绑到主网卡**,再配一个"只在异常时才说话"的看门狗。
|
||||||
|
|
||||||
|
折腾的过程很烦,但结果是"真香":现在拔网线,我甚至感觉不到。
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
title: 过年期间的运动记录
|
||||||
|
tags: [健康, 运动]
|
||||||
|
date: 2026-02-28T08:00:00Z
|
||||||
|
---
|
||||||
|
整个过年期间我的运动计划没有荒废,自认为执行的还行。
|
||||||
|
|
||||||
|
基本每天都有在运动,吃上面也很克制,不会暴饮暴食。
|
||||||
|
|
||||||
|
整个二月份截止目前运动量,跑步75km,力量训练1小时,力量训练还是偏少,下个月计划办个健身卡加强一下力量训练。
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: 重新用回macos
|
title: 重新用回macos
|
||||||
tags: ["日常","mac"]
|
tags: [日常, mac]
|
||||||
date: 2025-12-01
|
date: 2025-12-04T08:00:00Z
|
||||||
summary: ""
|
summary: ""
|
||||||
---
|
---
|
||||||
|
|
||||||
最近几天,我重新用回了去年使用的macbookpro,屏幕有一条黑线,使用的是外接显示器
|
最近几天,我重新用回了去年使用的macbookpro,屏幕有一条黑线,使用的是外接显示器
|
||||||
深感还是mac系统简洁好用,没有windows那么多的莫名其妙的bug
|
深感还是mac系统简洁好用,没有windows那么多的莫名其妙的bug
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: 长期健康生活计划
|
title: 长期健康生活计划
|
||||||
tags: ["健康"]
|
tags: [健康]
|
||||||
date: 2026-01-17
|
date: 2026-01-20T08:00:00Z
|
||||||
summary: "人的一生最重要的就是健康"
|
summary: 人的一生最重要的就是健康
|
||||||
---
|
---
|
||||||
- 最近开始尝试更规律的生活,调整饮食与运动习惯。一方面是真的想对身体好一点,另一方面,也希望生活有个稳定的节奏,让自己心态更积极。
|
- 最近开始尝试更规律的生活,调整饮食与运动习惯。一方面是真的想对身体好一点,另一方面,也希望生活有个稳定的节奏,让自己心态更积极。
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: 面向死亡备份
|
||||||
|
tags: ["健康","备份","死亡","感想"]
|
||||||
|
date: 2026-07-10
|
||||||
|
---
|
||||||
|
面向死亡备份,这是个有意思的话题。
|
||||||
|
多年以后我不再了,我已不在人间,我的这些数据要怎么处理呢?
|
||||||
+4
-6
@@ -1,8 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "🔍 搜索" # in any language you want
|
title: 🔍 搜索
|
||||||
layout: "search" # is necessary
|
layout: search
|
||||||
# url: "/archive"
|
summary: search
|
||||||
# description: "Description for Search"
|
placeholder: 搜索
|
||||||
summary: "search"
|
|
||||||
placeholder: "搜索框内的默认显示"
|
|
||||||
---
|
---
|
||||||
@@ -1,18 +1,25 @@
|
|||||||
baseURL = 'https://blog.butubb.cn/'
|
baseURL = 'https://blog.butubb.cn/'
|
||||||
languageCode = 'zh-cn'
|
languageCode = 'zh-cn'
|
||||||
title = '日志'
|
title = "life's logs"
|
||||||
theme = 'PaperMod'
|
theme = 'PaperMod'
|
||||||
defaultContentLanguage = 'zh-cn'
|
defaultContentLanguage = 'zh-cn'
|
||||||
|
|
||||||
[outputs]
|
[outputs]
|
||||||
home = ["HTML", "RSS", "JSON"]
|
home = ["HTML", "RSS", "JSON"]
|
||||||
|
|
||||||
|
[markup]
|
||||||
|
[markup.goldmark]
|
||||||
|
[markup.goldmark.renderer]
|
||||||
|
unsafe = true
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
[params.homeInfoParams]
|
||||||
|
Title = "<img src=\"https://cdn.jsdelivr.net/gh/8butubb/image/img/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_2026-03-07_095359_687.jpg\" alt=\"butubb\" style=\"width:120px;height:120px;border-radius:50%;display:block;margin:0 auto 1rem auto;\">\n你好!我是 butubb,安徽人,现工作居住在苏州。"
|
||||||
|
Content = "喜欢互联网冲浪🏄♂️,折腾机器💻,看电影🎥,编程🧑💻,听音乐🎶等"
|
||||||
|
|
||||||
ShowToc = true
|
ShowToc = true
|
||||||
defaultTheme = "auto"
|
defaultTheme = "auto"
|
||||||
# 内联表必须写在一行内
|
mainSections = ["posts"]
|
||||||
homeInfoParams = { Title = "你好!我是butubb,安徽人,现工作居住在苏州。", Content = "喜欢互联网冲浪🏄♂️,折腾机器💻,看电影🎥,编程🧑💻,听音乐🎶等" }
|
|
||||||
|
|
||||||
|
|
||||||
[taxonomies]
|
[taxonomies]
|
||||||
tag = "tags"
|
tag = "tags"
|
||||||
@@ -38,3 +45,13 @@ defaultContentLanguage = 'zh-cn'
|
|||||||
name = "搜索"
|
name = "搜索"
|
||||||
url = "/search"
|
url = "/search"
|
||||||
weight = 4
|
weight = 4
|
||||||
|
|
||||||
|
[[menu.main]]
|
||||||
|
name = "画廊"
|
||||||
|
url = "/gallery"
|
||||||
|
weight = 5
|
||||||
|
|
||||||
|
[[menu.main]]
|
||||||
|
name = "Run"
|
||||||
|
url = "https://run.butubb.cn"
|
||||||
|
weight = 6
|
||||||
@@ -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"> {{ 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"> {{ 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 */}}
|
||||||
@@ -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 -}}
|
||||||
@@ -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 }}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
{{- if not (.Param "hideFooter") }}
|
||||||
|
<footer class="footer">
|
||||||
|
{{- if not site.Params.footer.hideCopyright }}
|
||||||
|
{{- if site.Copyright }}
|
||||||
|
<span>{{ site.Copyright | markdownify }}</span>
|
||||||
|
{{- else }}
|
||||||
|
<span>© {{ now.Year }} <a href="{{ "" | absLangURL }}">{{ site.Title }}</a></span>
|
||||||
|
{{- end }}
|
||||||
|
{{- print " · "}}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- with site.Params.footer.text }}
|
||||||
|
{{ . | markdownify }}
|
||||||
|
{{- print " · "}}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
<span>
|
||||||
|
Powered by
|
||||||
|
<a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo</a> &
|
||||||
|
<a href="https://github.com/adityatelange/hugo-PaperMod/" rel="noopener" target="_blank">PaperMod</a>
|
||||||
|
<br>
|
||||||
|
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 }}
|
||||||
|
|
||||||
|
{{- if (not site.Params.disableScrollToTop) }}
|
||||||
|
<a href="#top" aria-label="go to top" title="Go to Top (Alt + G)" class="top-link" id="top-link" accesskey="g">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 6" fill="currentColor">
|
||||||
|
<path d="M12 6H0l6-6z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- 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) {
|
||||||
|
// Set the scroll position
|
||||||
|
const scrollPosition = localStorage.getItem("menu-scroll-position");
|
||||||
|
if (scrollPosition) {
|
||||||
|
menu.scrollLeft = parseInt(scrollPosition, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.onscroll = function () {
|
||||||
|
localStorage.setItem("menu-scroll-position", menu.scrollLeft);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||||
|
anchor.addEventListener("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var id = this.getAttribute("href").substr(1);
|
||||||
|
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||||
|
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView({
|
||||||
|
behavior: "smooth"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView();
|
||||||
|
}
|
||||||
|
if (id === "top") {
|
||||||
|
history.replaceState(null, null, " ");
|
||||||
|
} else {
|
||||||
|
history.pushState(null, null, `#${id}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{{- if (not site.Params.disableScrollToTop) }}
|
||||||
|
<script>
|
||||||
|
var mybutton = document.getElementById("top-link");
|
||||||
|
window.onscroll = function () {
|
||||||
|
if (document.body.scrollTop > 800 || document.documentElement.scrollTop > 800) {
|
||||||
|
mybutton.style.visibility = "visible";
|
||||||
|
mybutton.style.opacity = "1";
|
||||||
|
} else {
|
||||||
|
mybutton.style.visibility = "hidden";
|
||||||
|
mybutton.style.opacity = "0";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- if (not site.Params.disableThemeToggle) }}
|
||||||
|
<script>
|
||||||
|
document.getElementById("theme-toggle").addEventListener("click", () => {
|
||||||
|
const html = document.querySelector("html");
|
||||||
|
if (html.dataset.theme === "dark") {
|
||||||
|
html.dataset.theme = 'light';
|
||||||
|
localStorage.setItem("pref-theme", 'light');
|
||||||
|
} else {
|
||||||
|
html.dataset.theme = 'dark';
|
||||||
|
localStorage.setItem("pref-theme", 'dark');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- if (and (eq .Kind "page") (ne .Layout "archives") (ne .Layout "search") (.Param "ShowCodeCopyButtons")) }}
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll('pre > code').forEach((codeblock) => {
|
||||||
|
const container = codeblock.parentNode.parentNode;
|
||||||
|
|
||||||
|
const copybutton = document.createElement('button');
|
||||||
|
copybutton.classList.add('copy-code');
|
||||||
|
copybutton.innerHTML = '{{- i18n "code_copy" | default "copy" }}';
|
||||||
|
|
||||||
|
function copyingDone() {
|
||||||
|
copybutton.innerHTML = '{{- i18n "code_copied" | default "copied!" }}';
|
||||||
|
setTimeout(() => {
|
||||||
|
copybutton.innerHTML = '{{- i18n "code_copy" | default "copy" }}';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
copybutton.addEventListener('click', (cb) => {
|
||||||
|
if ('clipboard' in navigator) {
|
||||||
|
navigator.clipboard.writeText(codeblock.textContent);
|
||||||
|
copyingDone();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(codeblock);
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
copyingDone();
|
||||||
|
} catch (e) { };
|
||||||
|
selection.removeRange(range);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (container.classList.contains("highlight")) {
|
||||||
|
container.appendChild(copybutton);
|
||||||
|
} else if (container.parentNode.firstChild == container) {
|
||||||
|
// td containing LineNos
|
||||||
|
} else if (codeblock.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName == "TABLE") {
|
||||||
|
// table containing LineNos and code
|
||||||
|
codeblock.parentNode.parentNode.parentNode.parentNode.parentNode.appendChild(copybutton);
|
||||||
|
} else {
|
||||||
|
// code blocks not having highlight as parent class
|
||||||
|
codeblock.parentNode.appendChild(copybutton);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
|
||||||
+1
-1
Submodule themes/PaperMod updated: 7d061d56d4...154d006e01
Reference in New Issue
Block a user