首页动态顶部图

前言/Chatter

版本为:框架 Hexo 8.1.1|主题 Butterfly 5.5.4

正文

1.在主题根目录下的sourse/js/新建一个video-bg.js,内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
let scrollTick = false;

function initHomeVideo() {
// 📱 屏蔽移动端:如果屏幕宽度小于 768px,则不加载背景视频
if (window.innerWidth < 768) {
const existingVideo = document.querySelector(".header-video-bg");
if (existingVideo) {
existingVideo.remove(); // 如果有历史残留视频则移除,释放内存
}
window.removeEventListener('scroll', throttleScroll);
return; // 直接拦截,不再向下执行
}

const header = document.getElementById("page-header");
const siteInfo = document.getElementById("site-info");

if (!header || !siteInfo) {
window.removeEventListener('scroll', throttleScroll);
return;
}

let video = header.querySelector(".header-video-bg");
if (!video) {
// 1. 定义你的视频地址池(在这里添加或修改你的 webm 文件路径)
const videoList = [
"/media/webm1.webm",
"/media/webm2.webm",
"/media/webm3.webm",
"/media/webm4.webm",
"/media/webm6.webm",
"/media/webm7.webm"
];

// 2. 核心随机算法:从数组中随机抽取一个索引
const randomIndex = Math.floor(Math.random() * videoList.length);
const selectedVideo = videoList[randomIndex];

video = document.createElement("video");
video.className = "header-video-bg";
video.src = selectedVideo; // 3. 将随机选中的视频赋给 src
video.autoplay = true;
video.loop = true;
video.muted = true;
video.playsInline = true;
video.setAttribute("muted", "");
header.insertBefore(video, header.firstChild);
}

if (video.paused) {
video.play().catch(() => {});
}

window.removeEventListener('scroll', throttleScroll);
window.addEventListener('scroll', throttleScroll, { passive: true });

handleVideoScroll();
}

function throttleScroll() {
if (!scrollTick) {
window.requestAnimationFrame(() => {
handleVideoScroll();
scrollTick = false;
});
scrollTick = true;
}
}

function handleVideoScroll() {
const video = document.querySelector(".header-video-bg");
const header = document.getElementById("page-header");
if (!video || !header) return;

const headerHeight = header.offsetHeight;
const scrollTop = window.scrollY || document.documentElement.scrollTop;

// 💡 顶部图淡出淡入:修改为 1 / 2 (当滑过顶部总高度的 50% 时彻底透明)
const fadeEndPosition = headerHeight * (1 / 2);
const opacity = Math.max(0, 1 - scrollTop / fadeEndPosition);

video.style.opacity = opacity;

if (opacity <= 0) {
if (video.style.visibility !== "hidden") {
video.style.visibility = "hidden";
video.pause();
}
} else {
if (video.style.visibility !== "visible") {
video.style.visibility = "visible";
}
if (video.paused) {
video.play().catch(() => {});
}
}
}

document.addEventListener("DOMContentLoaded", initHomeVideo);
document.addEventListener("pjax:complete", initHomeVideo);

同样的,在sourse/css/中创建video-bg.css,内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/* ==========================================
Hexo-Butterfly 动态顶部图视频背景样式
========================================== */

/* 1. 确保父级容器具有定位上下文,并隐藏溢出 */
#page-header {
position: relative;
overflow: hidden;
}

/* 2. 视频背景核心样式 */
.header-video-bg {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
object-fit: cover; /* 核心:确保视频等比例缩放并铺满,不留白边 */
transform: translate(-50%, -50%); /* 配合 top/left 50% 实现完美居中裁剪 */
z-index: 0; /* 置于底层 */
pointer-events: none; /* 允许鼠标事件穿透视频,不影响点击顶部的菜单/按钮 */

/* 性能与动画优化 */
will-change: opacity, visibility;
transition: opacity 0.1s linear; /* 让 JS 控制的透明度变化更平滑 */
}

/* 3. 确保顶部图的原有内容(文字、菜单、波浪等)浮在视频上方 */
#page-header > :not(.header-video-bg) {
position: relative;
z-index: 1;
}

/* 4. 可选配置:如果视频太亮导致文字看不清,可以启用这个遮罩层 */
/*
#page-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.2); / * 20% 透明度的黑色遮罩,可根据需要调整 * /
z-index: 1;
}
#page-header > :not(.header-video-bg):not(::before) {
position: relative;
z-index: 2;
}
*/

需修改的地方皆有注释请自行修改
(其实这个JS和CSS不是配套的,原本的CSS写博客时忘记放上去了,后来弄丢了,上面的CSS是我把JS输给AI让它生成的对应的CSS)

2.在主题配置文件中的inject块分别添加

1
2
3
4
5
inject:
head:
- <link rel="stylesheet" href="/css/video-bg.css">
bottom:
- <script src="/js/video-bg.js"></script>

补充

貌似会有概率出现背景先加载出顶部图后加载出,导致背景会闪现一瞬间,相当影响美感,可以使用preload强制占屏3s来解决,若3s内顶部图未加载完成则延至8s,且此preload仅在首页顶部图全屏时显示,更改JS和CSS如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
let scrollTick = false;

function initHomeVideo(isPjax = false) {
// 📱 屏蔽移动端
if (window.innerWidth < 768) {
const existingVideo = document.querySelector(".header-video-bg");
if (existingVideo) existingVideo.remove();
window.removeEventListener('scroll', throttleScroll);
return;
}

const header = document.getElementById("page-header");
const siteInfo = document.getElementById("site-info");

// ==========================================
// 🧹 漏洞修复:离开首页时,显式暂停并清理视频
// ==========================================
if (!header || !siteInfo) {
window.removeEventListener('scroll', throttleScroll);

// 强制找出全站可能残留的首页视频,暂停并移除,彻底杜绝后台偷跑性能
const leftoverVideo = document.querySelector(".header-video-bg");
if (leftoverVideo) {
leftoverVideo.pause();
leftoverVideo.remove();
}

const existingLoader = document.querySelector(".ba-overclock-screen");
if (existingLoader) existingLoader.remove();

if (window.baProgressInterval) clearInterval(window.baProgressInterval);
if (window.baSafetyTimeout) clearTimeout(window.baSafetyTimeout);
return;
}

if (isPjax) {
window.scrollTo(0, 0);
}

// ==========================================
// 🎬 第一步:首页顶部视频初始化
// ==========================================
let video = header.querySelector(".header-video-bg");
if (!video) {
const videoList = [
"/media/webm1.webm",
"/media/webm2.webm",
"/media/webm3.webm",
"/media/webm4.webm",
"/media/webm6.webm",
"/media/webm7.webm"
];

const randomIndex = Math.floor(Math.random() * videoList.length);
const selectedVideo = videoList[randomIndex];

video = document.createElement("video");
video.className = "header-video-bg";
video.autoplay = true;
video.loop = true;
video.muted = true;
video.playsInline = true;
video.setAttribute("muted", "");
video.setAttribute("preload", "auto");

video.addEventListener('loadeddata', () => {
if (typeof window.setVideoReadySignal === 'function') window.setVideoReadySignal();
});

video.addEventListener('error', () => {
if (typeof window.setVideoReadySignal === 'function') window.setVideoReadySignal();
});

video.src = selectedVideo;
header.insertBefore(video, header.firstChild);
} else {
if (typeof window.setVideoReadySignal === 'function') window.setVideoReadySignal();
}

if (video.paused) {
video.play().catch(() => {});
}

// ==========================================
// 🔷 第二步:依照物理滚动高度决定是否显示 Preload
// ==========================================
const scrollTop = window.scrollY || document.documentElement.scrollTop;

if ((isPjax || scrollTop <= 10) && !document.querySelector(".ba-overclock-screen")) {

if (window.baProgressInterval) clearInterval(window.baProgressInterval);
if (window.baSafetyTimeout) clearTimeout(window.baSafetyTimeout);

const loader = document.createElement("div");
loader.className = "ba-overclock-screen";

loader.innerHTML = `
<div class="ba-laser-scanline"></div>
<div class="ba-grid-bg"></div>
<div class="ba-hud-side-panel ba-panel-left">
<div>SYSTEM_BOOT_INIT... OK</div>
<div>CONNECTING_TO_ARONA... v2.04</div>
<div>HEX_ADDR: 0x7FFF80A3D2F</div>
<div>MEM_ALLOC_STREAM: ACTIVE</div>
<div>CORE_TEMP: 38.5 C [NORMAL]</div>
<div>SECURE_PROTOCOL_SET: TRUE</div>
</div>
<div class="ba-hud-side-panel ba-panel-right">
<div>SANCTUM_TOWER_LINKING...</div>
<div>SIGNAL_STRENGTH: 98.4%</div>
<div>GATE_RECODE: OK [873]</div>
<div>BYPASS_FIREWALL: 100%</div>
<div>CHEST_DECRYPT_KEY: FOUND</div>
<div>STATUS: ALL_SYSTEMS_GO</div>
</div>
<div class="ba-hud-corners">
<div class="ba-corner tl"></div><div class="ba-corner tr"></div>
<div class="ba-corner bl"></div><div class="ba-corner br"></div>
</div>
<div class="ba-overclock-container">
<div class="ba-four-orbits">
<div class="ba-orbit ba-orbit-1"></div>
<div class="ba-orbit ba-orbit-2"></div>
<div class="ba-orbit ba-orbit-3"></div>
<div class="ba-orbit ba-orbit-4"></div>
<div class="ba-satellite-cube"></div>
<div class="ba-center-cross"></div>
</div>
<div class="ba-info-board">
<div class="ba-sys-header">// SCHALE OS SYSTEM INITIALIZE</div>
<div class="ba-main-title">NOW LOADING</div>
<div class="ba-counter-display">ACCESSING... <span id="baProgressNum">00</span>%</div>
</div>
<div class="ba-matrix-bar-box">
<div class="ba-matrix-bar-track">
<div class="ba-matrix-bar-fill"></div>
</div>
<div class="ba-matrix-sub-dots">
<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>
</div>
</div>
</div>
`;
document.body.appendChild(loader);

let isTimeReady = false;
let isVideoReady = false;
let currentPercent = 0;
const percentEl = document.getElementById("baProgressNum");

window.baProgressInterval = setInterval(() => {
if (currentPercent < 98) {
currentPercent += Math.floor(Math.random() * 4) + 1;
if (currentPercent > 98) currentPercent = 98;
if (percentEl) percentEl.innerText = currentPercent.toString().padStart(2, '0');
}
}, 40);

const tryUnlockAndHide = () => {
if (isTimeReady && isVideoReady) {
clearInterval(window.baProgressInterval);
if (percentEl) percentEl.innerText = "100";
setTimeout(() => {
hideHomePreloader();
}, 200);
}
};

// 🔒 严格保留你的 3 秒强制等待
setTimeout(() => {
isTimeReady = true;
tryUnlockAndHide();
}, 3000);

window.setVideoReadySignal = () => {
isVideoReady = true;
tryUnlockAndHide();
};

window.baSafetyTimeout = setTimeout(() => {
hideHomePreloader();
}, 8000);

window.hideHomePreloader = () => {
clearTimeout(window.baSafetyTimeout);
if (window.baProgressInterval) clearInterval(window.baProgressInterval);
const loaderEl = document.querySelector(".ba-overclock-screen");
if (loaderEl) {
loaderEl.classList.add("ba-fade-out");
setTimeout(() => loaderEl.remove(), 600);
}
};
} else {
const existingLoader = document.querySelector(".ba-overclock-screen");
if (existingLoader) existingLoader.remove();
}

window.removeEventListener('scroll', throttleScroll);
window.addEventListener('scroll', throttleScroll, { passive: true });

handleVideoScroll();
}

function throttleScroll() {
if (!scrollTick) {
window.requestAnimationFrame(() => {
handleVideoScroll();
scrollTick = false;
});
scrollTick = true;
}
}

function handleVideoScroll() {
const video = document.querySelector(".header-video-bg");
const header = document.getElementById("page-header");
if (!video || !header) return;

const headerHeight = header.offsetHeight;
const scrollTop = window.scrollY || document.documentElement.scrollTop;

const fadeEndPosition = headerHeight * (1 / 2);
const opacity = Math.max(0, 1 - scrollTop / fadeEndPosition);

video.style.opacity = opacity;

if (opacity <= 0) {
if (video.style.visibility !== "hidden") {
video.style.visibility = "hidden";
video.pause();
}
} else {
if (video.style.visibility !== "visible") {
video.style.visibility = "visible";
}
if (video.paused) {
video.play().catch(() => {});
}
}
}

document.addEventListener("DOMContentLoaded", () => initHomeVideo(false));
document.addEventListener("pjax:complete", () => initHomeVideo(true));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
#page-header {
position: relative;
overflow: hidden;
z-index: 1;
}

.header-video-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: -1;
pointer-events: none;
will-change: opacity;
}

@media screen and (max-width: 768px) {
.header-video-bg {
display: none;
}
}

/* ==========================================
🔷 蔚蓝档案·极致花哨全动态超频预加载遮罩
========================================== */
.ba-overclock-screen {
position: fixed;
top: 0; left: 0;
width: 100vw; height: 100vh;
background: radial-gradient(circle at center, #f0f9ff 0%, #cae7ff 60%, #a3d6ff 100%);
z-index: 99999;
display: flex;
justify-content: center;
align-items: center;
opacity: 1;
font-family: 'Courier New', Consolas, monospace;
transition: opacity 0.6s cubic-bezier(0.25, 1, 0.5, 1);
pointer-events: all;
overflow: hidden;
/* ⚡ 优化 1:强制启用 GPU 硬件加速 */
backface-visibility: hidden;
perspective: 1000px;
}

/* 📡 全屏激光横向扫描线 */
.ba-laser-scanline {
position: absolute;
top: -5%; left: 0;
width: 100%; height: 4px;
background: linear-gradient(90deg, transparent, #00bfff, transparent);
box-shadow: 0 0 12px #00bfff, 0 0 4px #ffffff;
opacity: 0.5;
z-index: 4;
animation: baLaserScan 3s linear infinite;
will-change: top;
}

/* 🕸️ 背景微透战术网格 */
.ba-grid-bg {
position: absolute;
width: 100%; height: 100%;
background-image:
linear-gradient(rgba(0, 153, 255, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 153, 255, 0.05) 1px, transparent 1px);
background-size: 40px 40px;
background-position: center;
z-index: 1;
}

/* 🤖 左右两侧数字化战术跑码流面板 */
.ba-hud-side-panel {
position: absolute;
top: 15vh; /* 🔒 严格保留你的原版高度 */
width: 220px;
font-size: 10px;
color: #3a8cd1;
line-height: 2.2;
opacity: 0.5;
z-index: 2;
pointer-events: none;

@media screen and (max-width: 1024px) {
display: none;
}
}

/* 🎯 仅新增此项:强制内部每一行文本绝不换行,彻底锁死 text-align 的边缘对齐线 */
.ba-hud-side-panel div {
white-space: nowrap;
}

.ba-panel-left { left: 4vw; text-align: left; border-left: 2px solid rgba(0, 153, 255, 0.3); padding-left: 10px; }
.ba-panel-right { right: 4vw; text-align: right; border-right: 2px solid rgba(0, 153, 255, 0.3); padding-right: 10px; }

/* 📐 HUD 定位角框 */
.ba-hud-corners {
position: absolute;
width: 96vw; height: 92vh; /* 🔒 严格保留你的原版尺寸与无 top/left 定位 */
z-index: 2;
pointer-events: none;
}
.ba-corner { position: absolute; width: 30px; height: 30px; border: 4px solid #0099ff; opacity: 0.7; }
.tl { top: 0; left: 0; border-right: none; border-bottom: none; }
.tr { top: 0; right: 0; border-left: none; border-bottom: none; }
.bl { bottom: 0; left: 0; border-right: none; border-top: none; }
.br { bottom: 0; right: 0; border-left: none; border-top: none; }

/* 隐藏渐隐 */
.ba-overclock-screen.ba-fade-out { opacity: 0; pointer-events: none; }

/* 核心内容容器 */
.ba-overclock-container {
position: relative;
z-index: 3;
display: flex;
flex-direction: column;
align-items: center;
width: 400px;
}

/* 🔷 四维功能复合光环 */
.ba-four-orbits {
position: relative;
width: 140px; height: 140px;
margin-bottom: 30px;
}
.ba-orbit {
position: absolute; top: 0; left: 0; right: 0; bottom: 0; margin: auto; border-radius: 50%;
will-change: transform;
}
.ba-orbit-1 { width: 130px; height: 130px; border: 2px dashed #00bfff; animation: baClockwise 8s linear infinite; }
.ba-orbit-2 { width: 105px; height: 105px; border: 2px dotted #0050af; animation: baCounter 4s linear infinite; }
.ba-orbit-3 { width: 80px; height: 80px; border: 4px solid transparent; border-top-color: #1fc8db; border-bottom-color: #1fc8db; animation: baClockwise 1.8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
.ba-orbit-4 { width: 55px; height: 55px; border: 1px dashed #0050af; }

/* 🛰️ 公转悬浮粒子方块 */
.ba-satellite-cube {
position: absolute;
top: 5px; left: 5px; width: 10px; height: 10px;
background-color: #0099ff;
box-shadow: 0 0 8px #0099ff;
transform-origin: 65px 65px;
animation: baClockwise 3s linear infinite;
will-change: transform;
}
/* 中心十字准心 */
.ba-center-cross { position: absolute; top: 50%; left: 50%; width: 20px; height: 20px; transform: translate(-50%, -50%); }
.ba-center-cross::before, .ba-center-cross::after { content: ''; position: absolute; background-color: #00bfff; }
.ba-center-cross::before { top: 9px; left: 0; width: 20px; height: 2px; }
.ba-center-cross::after { top: 0; left: 9px; width: 2px; height: 20px; }

/* 🔷 系统文本信息板 */
.ba-info-board { text-align: center; margin-bottom: 25px; width: 100%; user-select: none; }
.ba-sys-header { font-size: 11px; font-weight: bold; color: #4e7ba6; letter-spacing: 2px; }
.ba-main-title { font-size: 24px; font-weight: 900; color: #004ba0; letter-spacing: 4px; margin: 4px 0; font-family: 'Arial Black', sans-serif; animation: baFlicker 2s infinite; }
.ba-counter-display { font-size: 13px; font-weight: bold; color: #0099ff; background: rgba(255, 255, 255, 0.6); padding: 4px 15px; border-radius: 20px; display: inline-block; box-shadow: 0 2px 6px rgba(0,153,255,0.1); }

/* 🔷 高级分段式能量晶体条 */
.ba-matrix-bar-box { width: 75%; display: flex; flex-direction: column; align-items: center; }
.ba-matrix-bar-track {
width: 100%; height: 10px;
background-color: rgba(255, 255, 255, 0.6);
border: 1px solid rgba(0, 80, 175, 0.2);
border-radius: 2px; overflow: hidden; position: relative;
}
.ba-matrix-bar-fill {
width: 100%; height: 100%;
background: repeating-linear-gradient(
-45deg,
#0099ff, #0099ff 10px,
#5cd2ff 10px, #5cd2ff 20px
);
position: absolute;
animation: baBarLoad 3s cubic-bezier(0.1, 0.8, 0.3, 1) forwards, baBarFlow 1s linear infinite;
will-change: left, background-position;
}
.ba-matrix-sub-dots { width: 96%; display: flex; justify-content: space-between; margin-top: 5px; }
.ba-matrix-sub-dots span { width: 4px; height: 4px; background-color: #0050af; opacity: 0.3; border-radius: 50%; }

/* ==========================================
🎬 极致动效核心关键帧
========================================== */
@keyframes baLaserScan {
0% { top: -5%; }
100% { top: 105%; }
}

@keyframes baClockwise {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

@keyframes baCounter {
0% { transform: rotate(0deg); }
100% { transform: rotate(-360deg); }
}

@keyframes baBarLoad {
0% { left: -100%; }
100% { left: 0%; }
}

@keyframes baBarFlow {
0% { background-position: 0 0; }
100% { background-position: 28px 0; }
}

@keyframes baFlicker {
0%, 100% { opacity: 1; }
49% { opacity: 1; }
50% { opacity: 0.93; }
55% { opacity: 0.95; }
56% { opacity: 1; }
}

实在有些冗长哈哈XD

Twikoo1.7.11配置R2图床

前提准备

前往Cloudflare进入左侧R2存储桶,首次进入需要绑定借记卡,没有的话可以上网寻找生成网站,貌似审核还挺严格的,我之前看到别人的博客里有介绍的方法与网站才通过,不过寻找当然是找不回那个博客和网站了XD

正文

Twikoo前端配置*

1.创建一个新的R2存储桶例如twikoo-img,其他默认。创建好后进入设置,设置自定义域,用自己的二级域名就行,如img.speacer.one,然后等待创建成功

2.回到概述页,点击右下角API令牌管理,创建一个新的User API,名称任意,权限选择对象读与写,然后指定刚才创建的R2存储桶,最后点击创建令牌并完成身份验证。请务必将生成的密钥与终结点记录下来!!

3.登录TWikoo的管理面板,选择插件,IMAGE_CDN选择S3/R2/MiniO,S3_REGION留空,S3_BUCKET填写R2存储桶名称(例如twikoo-img),S3_ACCESS_KEY_ID与S3_Secret_Access_Key就填对应的用户API密钥,S3_ENDPOINT填终结点的域名,S3_CDN_URL就填R2存储桶绑定的域名(如https://img.speacer.one),其余留空即可

TWikoo图片重命名及压缩

图片重命名

由于上传图片包含某些特殊字符会导致R2错误,所以需要对上传的图片进行重命名才能确保每张照片均能上传成功。直接在在主题配置文件中的inject块添加

1
2
3
inject:
bottom:
- <script>document.addEventListener('change',function(e){if(e.target&&"file"===e.target.type){const t=e.target.files;if(t&&t.length>0){const n=new DataTransfer;let r=!1;for(let e=0;e<t.length;e++){const a=t[e];if(a.type.startsWith("image/")){r=!0;let e=a.name.substring(a.name.lastIndexOf(".")).toLowerCase();(!e||e.length>5)&&(e=".png");const o=new File([a],"img_"+Date.now()+"_"+Math.floor(1e3*Math.random())+e,{type:a.type});n.items.add(o)}else n.items.add(a)}r&&(e.target.files=n.files)}}},!0);</script>

应该是采用时间戳+随机数的格式重命名

图片压缩

(可选,如果你需要图片压缩)在sourse/js/中新建一个twikoo-compress.js,内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
(function () {
// === 压缩参数配置 ===
const MAX_WIDTH = 1600; // 图片最大宽度(超过会自动等比例缩小)
const MAX_HEIGHT = 1600; // 图片最大高度
const QUALITY = 0.8; // 压缩质量 (0.1 到 1.0 之间,0.75 是肉眼几乎无损的最佳平衡点)

// === 核心压缩逻辑 ===
function compressImage(file) {
return new Promise((resolve) => {
// 如果不是图片,或者是 GIF 动图,则不压缩(保留动图效果)
if (!file.type.startsWith('image/') || file.type === 'image/gif') {
return resolve(file);
}

const reader = new FileReader();
reader.onload = function (e) {
const img = new Image();
img.onload = function () {
let width = img.width;
let height = img.height;

// 计算等比例缩放尺寸
if (width > height) {
if (width > MAX_WIDTH) {
height *= MAX_WIDTH / width;
width = MAX_WIDTH;
}
} else {
if (height > MAX_HEIGHT) {
width *= MAX_HEIGHT / height;
height = MAX_HEIGHT;
}
}

// 创建 Canvas 进行绘制和压制
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);

// 导出为 jpeg 格式及对应质量
canvas.toBlob((blob) => {
if (!blob) return resolve(file);

// 重新封装为 File 对象,保持原文件名(后缀改jpeg)
const newFileName = file.name.replace(/\.[^/.]+$/, "") + ".jpg";
const compressedFile = new File([blob], newFileName, {
type: 'image/jpeg',
lastModified: Date.now()
});

// 如果压缩后反而变大了(极少见),就用原图
resolve(compressedFile.size < file.size ? compressedFile : file);
}, 'image/jpeg', QUALITY);
};
img.onerror = () => resolve(file);
img.src = e.target.result;
};
reader.onerror = () => resolve(file);
reader.readAsDataURL(file);
});
}

// === 劫持 Fetch 请求(Twikoo 核心上传通道) ===
const originalFetch = window.fetch;
window.fetch = async function (...args) {
let [resource, config] = args;

// 检测是否为 Twikoo 的图片上传行为(包含 FormData 且含有 file 字段)
if (config && config.body instanceof FormData && config.body.has('file')) {
const file = config.body.get('file');
if (file instanceof File) {
try {
const compressedFile = await compressImage(file);
config.body.set('file', compressedFile); // 用压缩后的文件替换原文件
} catch (err) {
console.error('Twikoo 图片压缩失败,转为原图上传:', err);
}
}
}
return originalFetch.apply(this, args);
};
})();

然后在在主题配置文件中的inject块添加

1
2
3
inject:
bottom:
- <script src="/js/twikoo-compress.js"></script>