@windcoder/astro-theme-orchid 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -2
- package/package.json +70 -5
- package/src/components/AlbumCard.astro +73 -0
- package/src/components/Analytics.astro +22 -0
- package/src/components/BaseLayout.astro +188 -0
- package/src/components/Calendar.astro +379 -0
- package/src/components/CategoryBar.astro +74 -0
- package/src/components/Comment.astro +48 -0
- package/src/components/EncryptedContent.astro +42 -0
- package/src/components/Footer.astro +31 -0
- package/src/components/Header.astro +153 -0
- package/src/components/License.astro +29 -0
- package/src/components/Live2DWidget.astro +262 -0
- package/src/components/MusicPlayer.astro +940 -0
- package/src/components/OutdatedCard.astro +53 -0
- package/src/components/Pagination.astro +94 -0
- package/src/components/PhotoCard.astro +22 -0
- package/src/components/PostCard.astro +89 -0
- package/src/components/PostNav.astro +40 -0
- package/src/components/RecommendedPost.astro +166 -0
- package/src/components/SakuraEffect.astro +152 -0
- package/src/components/Sidebar.astro +334 -0
- package/src/components/Toc.astro +27 -0
- package/src/components/analytics/GoogleAnalytics.astro +22 -0
- package/src/components/analytics/La51Analytics.astro +52 -0
- package/src/components/analytics/MicrosoftClarity.astro +18 -0
- package/src/components/analytics/UmamiAnalytics.astro +86 -0
- package/src/components/comments/Artalk.astro +51 -0
- package/src/components/comments/Disqus.astro +43 -0
- package/src/components/comments/Giscus.astro +50 -0
- package/src/components/comments/Twikoo.astro +65 -0
- package/src/components/comments/Waline.astro +36 -0
- package/src/i18n/en.ts +164 -0
- package/src/i18n/index.ts +18 -0
- package/src/i18n/resolve.ts +49 -0
- package/src/i18n/zh-CN.ts +184 -0
- package/src/icons.ts +71 -0
- package/src/index.ts +411 -0
- package/src/plugins/plantuml-encoder.ts +95 -0
- package/src/plugins/rehype-diagram.ts +218 -0
- package/src/plugins/rehype-ec-vt-rerun.ts +80 -0
- package/src/plugins/rehype-email.ts +75 -0
- package/src/plugins/rehype-github-card.ts +114 -0
- package/src/plugins/rehype.ts +284 -0
- package/src/plugins/remark-diagram.ts +78 -0
- package/src/plugins/remark-directives.ts +100 -0
- package/src/plugins/remark-excerpt.ts +29 -0
- package/src/plugins/remark-image-grid.ts +97 -0
- package/src/plugins/remark-reading-time.ts +38 -0
- package/src/plugins/scripts/diagram-panzoom.js +388 -0
- package/src/plugins/scripts/plantuml-theme-switch.js +90 -0
- package/src/routes/404.astro +14 -0
- package/src/routes/_hydrate.ts +34 -0
- package/src/routes/about.astro +25 -0
- package/src/routes/api/allPostMeta.json.ts +26 -0
- package/src/routes/archive.astro +81 -0
- package/src/routes/categories/[category].astro +37 -0
- package/src/routes/categories/index.astro +29 -0
- package/src/routes/friends.astro +83 -0
- package/src/routes/gallery/[album].astro +97 -0
- package/src/routes/gallery/index.astro +100 -0
- package/src/routes/guestbook.astro +37 -0
- package/src/routes/index.astro +47 -0
- package/src/routes/og/[...slug].ts +281 -0
- package/src/routes/page/[page].astro +53 -0
- package/src/routes/posts/[...slug].astro +350 -0
- package/src/routes/robots.txt.ts +18 -0
- package/src/routes/rss.xml.ts +37 -0
- package/src/routes/series/[name].astro +66 -0
- package/src/routes/series/index.astro +64 -0
- package/src/routes/sponsor.astro +84 -0
- package/src/routes/tags/[tag].astro +40 -0
- package/src/routes/tags/index.astro +29 -0
- package/src/schema.ts +1218 -0
- package/src/scripts/app.ts +1041 -0
- package/src/scripts/share-poster.ts +468 -0
- package/src/styles/global.css +4864 -0
- package/src/utils/crypto.ts +46 -0
- package/src/utils/gallery.ts +76 -0
- package/src/utils/lqip.ts +18 -0
- package/src/utils/related.ts +101 -0
- package/src/utils.ts +233 -0
- package/src/vite.ts +105 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 图表 pan-zoom / 全屏交互脚本(共享,构建期由 rehype 插件以纯文本注入)
|
|
3
|
+
*
|
|
4
|
+
* 为 .diagram-container 提供:
|
|
5
|
+
* - 拖拽平移(触摸设备主区域禁用拖拽,避免干扰滚动)
|
|
6
|
+
* - 缩放控制栏(+、−、重置、全屏)
|
|
7
|
+
* - 双击放大/重置、全屏滚轮缩放、双指捏合
|
|
8
|
+
* - 全屏 overlay(独立缩放、Escape/背景点击关闭)
|
|
9
|
+
* - 响应 astro:page-load 页面切换
|
|
10
|
+
*
|
|
11
|
+
* identical inline 脚本 VT 换页不重跑,故以 window._diagramPanZoomInit
|
|
12
|
+
* 防重入 + astro:page-load 监听实现跨页初始化。
|
|
13
|
+
*/
|
|
14
|
+
(() => {
|
|
15
|
+
if (window._diagramPanZoomInit) return;
|
|
16
|
+
window._diagramPanZoomInit = true;
|
|
17
|
+
|
|
18
|
+
var MIN_SCALE = 0.5;
|
|
19
|
+
var MAX_SCALE = 5;
|
|
20
|
+
var SCALE_STEP = 1.2;
|
|
21
|
+
var overlays = new Set();
|
|
22
|
+
|
|
23
|
+
/** 深浅模式各有一套 SVG(Mermaid)时选当前可见目标 */
|
|
24
|
+
function selectTarget(container) {
|
|
25
|
+
var isDark = document.documentElement.classList.contains("dark");
|
|
26
|
+
var lightEl = container.querySelector(".mermaid-svg-light svg");
|
|
27
|
+
var darkEl = container.querySelector(".mermaid-svg-dark svg");
|
|
28
|
+
if (lightEl && darkEl) return isDark ? darkEl : lightEl;
|
|
29
|
+
return container.querySelector("svg, img, .diagram-panzoom-target");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function initInteraction(container) {
|
|
33
|
+
if (container.dataset.pzInit === "true") return;
|
|
34
|
+
container.dataset.pzInit = "true";
|
|
35
|
+
|
|
36
|
+
var targets = Array.from(
|
|
37
|
+
container.querySelectorAll(
|
|
38
|
+
".mermaid-svg-light svg, .mermaid-svg-dark svg",
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
if (targets.length === 0) {
|
|
42
|
+
var single = container.querySelector("svg, img, .diagram-panzoom-target");
|
|
43
|
+
if (single) targets = [single];
|
|
44
|
+
}
|
|
45
|
+
if (targets.length === 0) return;
|
|
46
|
+
|
|
47
|
+
var getActiveTarget = () => selectTarget(container) || targets[0];
|
|
48
|
+
|
|
49
|
+
var state = { scale: 1, tx: 0, ty: 0 };
|
|
50
|
+
var apply = () => {
|
|
51
|
+
targets.forEach((t) => {
|
|
52
|
+
t.style.transformOrigin = "center center";
|
|
53
|
+
t.style.transform =
|
|
54
|
+
"translate(" +
|
|
55
|
+
state.tx +
|
|
56
|
+
"px," +
|
|
57
|
+
state.ty +
|
|
58
|
+
"px) scale(" +
|
|
59
|
+
state.scale +
|
|
60
|
+
")";
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
var clamp = (s) => Math.min(MAX_SCALE, Math.max(MIN_SCALE, s));
|
|
64
|
+
var reset = () => {
|
|
65
|
+
state.scale = 1;
|
|
66
|
+
state.tx = 0;
|
|
67
|
+
state.ty = 0;
|
|
68
|
+
apply();
|
|
69
|
+
};
|
|
70
|
+
var zoomBy = (f, ox, oy) => {
|
|
71
|
+
var prev = state.scale;
|
|
72
|
+
var next = clamp(prev * f);
|
|
73
|
+
if (next === prev) return;
|
|
74
|
+
if (typeof ox === "number" && typeof oy === "number") {
|
|
75
|
+
var r = getActiveTarget().getBoundingClientRect();
|
|
76
|
+
var dx = ox - (r.left + r.width / 2);
|
|
77
|
+
var dy = oy - (r.top + r.height / 2);
|
|
78
|
+
var ratio = next / prev;
|
|
79
|
+
state.tx -= dx * (ratio - 1);
|
|
80
|
+
state.ty -= dy * (ratio - 1);
|
|
81
|
+
}
|
|
82
|
+
state.scale = next;
|
|
83
|
+
apply();
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// 控制栏:放大 / 缩小 / 重置 / 全屏
|
|
87
|
+
var controls = document.createElement("div");
|
|
88
|
+
controls.className = "diagram-controls";
|
|
89
|
+
[
|
|
90
|
+
[
|
|
91
|
+
"+",
|
|
92
|
+
"放大",
|
|
93
|
+
() => {
|
|
94
|
+
zoomBy(SCALE_STEP);
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
[
|
|
98
|
+
"−",
|
|
99
|
+
"缩小",
|
|
100
|
+
() => {
|
|
101
|
+
zoomBy(1 / SCALE_STEP);
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
["↺", "重置", reset],
|
|
105
|
+
[
|
|
106
|
+
"⛶",
|
|
107
|
+
"全屏",
|
|
108
|
+
() => {
|
|
109
|
+
openFullscreen(container);
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
].forEach((b) => {
|
|
113
|
+
var el = document.createElement("button");
|
|
114
|
+
el.type = "button";
|
|
115
|
+
el.className = "diagram-ctrl-btn";
|
|
116
|
+
el.textContent = b[0];
|
|
117
|
+
el.title = b[1];
|
|
118
|
+
el.addEventListener("click", (e) => {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
e.stopPropagation();
|
|
121
|
+
b[2]();
|
|
122
|
+
});
|
|
123
|
+
controls.appendChild(el);
|
|
124
|
+
});
|
|
125
|
+
container.appendChild(controls);
|
|
126
|
+
|
|
127
|
+
// 拖拽平移(鼠标;触摸交给原生滚动/捏合)
|
|
128
|
+
var dragging = false;
|
|
129
|
+
var sx = 0;
|
|
130
|
+
var sy = 0;
|
|
131
|
+
var stx = 0;
|
|
132
|
+
var sty = 0;
|
|
133
|
+
container.addEventListener("pointerdown", (e) => {
|
|
134
|
+
if (e.pointerType === "touch") return;
|
|
135
|
+
if (e.button !== 0) return;
|
|
136
|
+
if (e.target.closest(".diagram-controls")) return;
|
|
137
|
+
dragging = true;
|
|
138
|
+
sx = e.clientX;
|
|
139
|
+
sy = e.clientY;
|
|
140
|
+
stx = state.tx;
|
|
141
|
+
sty = state.ty;
|
|
142
|
+
if (container.setPointerCapture) container.setPointerCapture(e.pointerId);
|
|
143
|
+
container.style.cursor = "grabbing";
|
|
144
|
+
});
|
|
145
|
+
container.addEventListener("pointermove", (e) => {
|
|
146
|
+
if (!dragging) return;
|
|
147
|
+
state.tx = stx + (e.clientX - sx);
|
|
148
|
+
state.ty = sty + (e.clientY - sy);
|
|
149
|
+
apply();
|
|
150
|
+
});
|
|
151
|
+
var endDrag = (e) => {
|
|
152
|
+
if (!dragging) return;
|
|
153
|
+
dragging = false;
|
|
154
|
+
if (container.releasePointerCapture)
|
|
155
|
+
container.releasePointerCapture(e.pointerId);
|
|
156
|
+
container.style.cursor = "";
|
|
157
|
+
};
|
|
158
|
+
container.addEventListener("pointerup", endDrag);
|
|
159
|
+
container.addEventListener("pointercancel", endDrag);
|
|
160
|
+
|
|
161
|
+
// 双击:已放大则复位,否则以点击处为中心放大
|
|
162
|
+
container.addEventListener("dblclick", (e) => {
|
|
163
|
+
if (e.target.closest(".diagram-controls")) return;
|
|
164
|
+
if (state.scale !== 1) reset();
|
|
165
|
+
else zoomBy(SCALE_STEP * SCALE_STEP, e.clientX, e.clientY);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
apply();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function openFullscreen(container) {
|
|
172
|
+
var currentTarget = selectTarget(container);
|
|
173
|
+
if (!currentTarget) return;
|
|
174
|
+
|
|
175
|
+
var overlay = document.createElement("div");
|
|
176
|
+
overlay.className = "diagram-fullscreen-overlay";
|
|
177
|
+
var content = document.createElement("div");
|
|
178
|
+
content.className = "diagram-fs-content";
|
|
179
|
+
var clone = currentTarget.cloneNode(true);
|
|
180
|
+
clone.style.transform = "";
|
|
181
|
+
content.appendChild(clone);
|
|
182
|
+
|
|
183
|
+
var fsControls = document.createElement("div");
|
|
184
|
+
fsControls.className = "diagram-fs-controls";
|
|
185
|
+
var st = { scale: 1, tx: 0, ty: 0 };
|
|
186
|
+
var apply = () => {
|
|
187
|
+
clone.style.transformOrigin = "center center";
|
|
188
|
+
clone.style.transform =
|
|
189
|
+
"translate(" + st.tx + "px," + st.ty + "px) scale(" + st.scale + ")";
|
|
190
|
+
};
|
|
191
|
+
var zoom = (f, ox, oy) => {
|
|
192
|
+
var prev = st.scale;
|
|
193
|
+
var next = Math.min(MAX_SCALE, Math.max(MIN_SCALE, prev * f));
|
|
194
|
+
if (next === prev) return;
|
|
195
|
+
if (typeof ox === "number" && typeof oy === "number") {
|
|
196
|
+
var r = clone.getBoundingClientRect();
|
|
197
|
+
var dx = ox - (r.left + r.width / 2);
|
|
198
|
+
var dy = oy - (r.top + r.height / 2);
|
|
199
|
+
var ratio = next / prev;
|
|
200
|
+
st.tx -= dx * (ratio - 1);
|
|
201
|
+
st.ty -= dy * (ratio - 1);
|
|
202
|
+
}
|
|
203
|
+
st.scale = next;
|
|
204
|
+
apply();
|
|
205
|
+
};
|
|
206
|
+
var rst = () => {
|
|
207
|
+
st.scale = 1;
|
|
208
|
+
st.tx = 0;
|
|
209
|
+
st.ty = 0;
|
|
210
|
+
apply();
|
|
211
|
+
};
|
|
212
|
+
var close = () => {
|
|
213
|
+
document.removeEventListener("keydown", onKey);
|
|
214
|
+
overlay.remove();
|
|
215
|
+
overlays.delete(overlay);
|
|
216
|
+
};
|
|
217
|
+
var onKey = (e) => {
|
|
218
|
+
if (e.key === "Escape") close();
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
[
|
|
222
|
+
[
|
|
223
|
+
"+",
|
|
224
|
+
"放大",
|
|
225
|
+
() => {
|
|
226
|
+
zoom(SCALE_STEP);
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
[
|
|
230
|
+
"−",
|
|
231
|
+
"缩小",
|
|
232
|
+
() => {
|
|
233
|
+
zoom(1 / SCALE_STEP);
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
["↺", "重置", rst],
|
|
237
|
+
["✕", "关闭", close],
|
|
238
|
+
].forEach((b) => {
|
|
239
|
+
var el = document.createElement("button");
|
|
240
|
+
el.type = "button";
|
|
241
|
+
el.className = "diagram-ctrl-btn";
|
|
242
|
+
el.textContent = b[0];
|
|
243
|
+
el.title = b[1];
|
|
244
|
+
el.addEventListener("click", (e) => {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
e.stopPropagation();
|
|
247
|
+
b[2]();
|
|
248
|
+
});
|
|
249
|
+
fsControls.appendChild(el);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// 全屏滚轮缩放
|
|
253
|
+
content.addEventListener(
|
|
254
|
+
"wheel",
|
|
255
|
+
(e) => {
|
|
256
|
+
e.preventDefault();
|
|
257
|
+
zoom(e.deltaY < 0 ? SCALE_STEP : 1 / SCALE_STEP, e.clientX, e.clientY);
|
|
258
|
+
},
|
|
259
|
+
{ passive: false },
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// 全屏拖拽平移
|
|
263
|
+
var fdrag = false;
|
|
264
|
+
var fsx = 0;
|
|
265
|
+
var fsy = 0;
|
|
266
|
+
var fstx = 0;
|
|
267
|
+
var fsty = 0;
|
|
268
|
+
content.addEventListener("pointerdown", (e) => {
|
|
269
|
+
if (e.target.closest(".diagram-fs-controls")) return;
|
|
270
|
+
if (e.pointerType === "touch" && e.isPrimary === false) return;
|
|
271
|
+
fdrag = true;
|
|
272
|
+
fsx = e.clientX;
|
|
273
|
+
fsy = e.clientY;
|
|
274
|
+
fstx = st.tx;
|
|
275
|
+
fsty = st.ty;
|
|
276
|
+
if (content.setPointerCapture) content.setPointerCapture(e.pointerId);
|
|
277
|
+
});
|
|
278
|
+
content.addEventListener("pointermove", (e) => {
|
|
279
|
+
if (!fdrag) return;
|
|
280
|
+
st.tx = fstx + (e.clientX - fsx);
|
|
281
|
+
st.ty = fsty + (e.clientY - fsy);
|
|
282
|
+
apply();
|
|
283
|
+
});
|
|
284
|
+
var fEnd = (e) => {
|
|
285
|
+
if (!fdrag) return;
|
|
286
|
+
fdrag = false;
|
|
287
|
+
if (content.releasePointerCapture)
|
|
288
|
+
content.releasePointerCapture(e.pointerId);
|
|
289
|
+
};
|
|
290
|
+
content.addEventListener("pointerup", fEnd);
|
|
291
|
+
content.addEventListener("pointercancel", fEnd);
|
|
292
|
+
|
|
293
|
+
// 双指捏合缩放(以手势初始状态为基准,避免闪烁)
|
|
294
|
+
var pinchDist = 0;
|
|
295
|
+
var pinchScale = 1;
|
|
296
|
+
var pinchTx = 0;
|
|
297
|
+
var pinchTy = 0;
|
|
298
|
+
var pinchCx = 0;
|
|
299
|
+
var pinchCy = 0;
|
|
300
|
+
content.addEventListener(
|
|
301
|
+
"touchstart",
|
|
302
|
+
(e) => {
|
|
303
|
+
if (e.touches.length === 2) {
|
|
304
|
+
e.preventDefault();
|
|
305
|
+
var t0 = e.touches[0];
|
|
306
|
+
var t1 = e.touches[1];
|
|
307
|
+
pinchDist = Math.hypot(
|
|
308
|
+
t1.clientX - t0.clientX,
|
|
309
|
+
t1.clientY - t0.clientY,
|
|
310
|
+
);
|
|
311
|
+
pinchScale = st.scale;
|
|
312
|
+
pinchTx = st.tx;
|
|
313
|
+
pinchTy = st.ty;
|
|
314
|
+
pinchCx = (t0.clientX + t1.clientX) / 2;
|
|
315
|
+
pinchCy = (t0.clientY + t1.clientY) / 2;
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
{ passive: false },
|
|
319
|
+
);
|
|
320
|
+
content.addEventListener(
|
|
321
|
+
"touchmove",
|
|
322
|
+
(e) => {
|
|
323
|
+
if (e.touches.length === 2) {
|
|
324
|
+
e.preventDefault();
|
|
325
|
+
var t0 = e.touches[0];
|
|
326
|
+
var t1 = e.touches[1];
|
|
327
|
+
var newDist = Math.hypot(
|
|
328
|
+
t1.clientX - t0.clientX,
|
|
329
|
+
t1.clientY - t0.clientY,
|
|
330
|
+
);
|
|
331
|
+
var newScale = Math.min(
|
|
332
|
+
MAX_SCALE,
|
|
333
|
+
Math.max(MIN_SCALE, pinchScale * (newDist / pinchDist)),
|
|
334
|
+
);
|
|
335
|
+
var ratio = newScale / pinchScale;
|
|
336
|
+
st.scale = newScale;
|
|
337
|
+
st.tx = pinchCx - ratio * (pinchCx - pinchTx);
|
|
338
|
+
st.ty = pinchCy - ratio * (pinchCy - pinchTy);
|
|
339
|
+
apply();
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
{ passive: false },
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// 背景点击关闭
|
|
346
|
+
overlay.addEventListener("click", (e) => {
|
|
347
|
+
if (e.target === overlay) close();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
overlay.appendChild(content);
|
|
351
|
+
overlay.appendChild(fsControls);
|
|
352
|
+
document.body.appendChild(overlay);
|
|
353
|
+
overlays.add(overlay);
|
|
354
|
+
document.addEventListener("keydown", onKey);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function closeAll() {
|
|
358
|
+
overlays.forEach((o) => {
|
|
359
|
+
o.remove();
|
|
360
|
+
});
|
|
361
|
+
overlays.clear();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function initAll() {
|
|
365
|
+
document.querySelectorAll(".diagram-container").forEach((c) => {
|
|
366
|
+
initInteraction(c);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 暴露重建入口,供 PlantUML 图片加载完成后清理旧控制栏再初始化
|
|
371
|
+
window._diagramPanZoomReinit = (container) => {
|
|
372
|
+
var oldControls = container.querySelector(".diagram-controls");
|
|
373
|
+
if (oldControls) oldControls.remove();
|
|
374
|
+
container.dataset.pzInit = "false";
|
|
375
|
+
initInteraction(container);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
document.addEventListener("astro:before-preparation", closeAll);
|
|
379
|
+
document.addEventListener("astro:page-load", () => {
|
|
380
|
+
closeAll();
|
|
381
|
+
initAll();
|
|
382
|
+
});
|
|
383
|
+
if (document.readyState === "loading") {
|
|
384
|
+
document.addEventListener("DOMContentLoaded", initAll, { once: true });
|
|
385
|
+
} else {
|
|
386
|
+
initAll();
|
|
387
|
+
}
|
|
388
|
+
})();
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PlantUML 客户端脚本(构建期由 rehype 插件以纯文本注入 <script>)
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* - 跟随 html.dark 切换 img 的 light/dark 源(构建期已编码双主题 URL)
|
|
6
|
+
* - 图片加载失败降级为错误提示 + 重试按钮
|
|
7
|
+
* - 响应 astro:page-load(ClientRouter 换页)重新应用主题
|
|
8
|
+
*
|
|
9
|
+
* identical inline 脚本在 VT 换页时按内容去重不重跑,故以
|
|
10
|
+
* window.plantumlThemeInit 防重入 + 事件监听实现跨页生效。
|
|
11
|
+
*/
|
|
12
|
+
(() => {
|
|
13
|
+
if (window.plantumlThemeInit) return;
|
|
14
|
+
window.plantumlThemeInit = true;
|
|
15
|
+
|
|
16
|
+
function isDark() {
|
|
17
|
+
return document.documentElement.classList.contains("dark");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function applyTheme() {
|
|
21
|
+
var dark = isDark();
|
|
22
|
+
document
|
|
23
|
+
.querySelectorAll(".plantuml-wrapper img.plantuml-image")
|
|
24
|
+
.forEach((img) => {
|
|
25
|
+
var next = dark
|
|
26
|
+
? img.getAttribute("data-dark-src")
|
|
27
|
+
: img.getAttribute("data-light-src");
|
|
28
|
+
if (next && img.src !== next) img.src = next;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function showFallback(img, message) {
|
|
33
|
+
var wrapper = img.closest(".plantuml-wrapper");
|
|
34
|
+
var container = img.closest(".plantuml-diagram-container");
|
|
35
|
+
if (!wrapper || !container || wrapper.querySelector(".plantuml-error"))
|
|
36
|
+
return;
|
|
37
|
+
var box = document.createElement("div");
|
|
38
|
+
box.className = "plantuml-error";
|
|
39
|
+
var p = document.createElement("p");
|
|
40
|
+
p.textContent =
|
|
41
|
+
message || "PlantUML 图表加载失败,请检查网络或 PlantUML 服务可用性";
|
|
42
|
+
var retry = document.createElement("button");
|
|
43
|
+
retry.type = "button";
|
|
44
|
+
retry.textContent = "重试";
|
|
45
|
+
retry.addEventListener("click", () => {
|
|
46
|
+
box.remove();
|
|
47
|
+
img.style.display = "";
|
|
48
|
+
var light = img.getAttribute("data-light-src");
|
|
49
|
+
img.src =
|
|
50
|
+
light + (light.indexOf("?") > -1 ? "&" : "?") + "retry=" + Date.now();
|
|
51
|
+
});
|
|
52
|
+
box.appendChild(p);
|
|
53
|
+
box.appendChild(retry);
|
|
54
|
+
img.style.display = "none";
|
|
55
|
+
wrapper.appendChild(box);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function initAll() {
|
|
59
|
+
document
|
|
60
|
+
.querySelectorAll(".plantuml-wrapper img.plantuml-image")
|
|
61
|
+
.forEach((img) => {
|
|
62
|
+
if (img.dataset.plantumlInit === "true") return;
|
|
63
|
+
img.dataset.plantumlInit = "true";
|
|
64
|
+
img.addEventListener("error", () => {
|
|
65
|
+
showFallback(img);
|
|
66
|
+
});
|
|
67
|
+
// 加载完成后通知 pan-zoom 重建控制栏(若有共享脚本)
|
|
68
|
+
img.addEventListener("load", () => {
|
|
69
|
+
var container = img.closest(".diagram-container");
|
|
70
|
+
if (container && typeof window._diagramPanZoomReinit === "function") {
|
|
71
|
+
window._diagramPanZoomReinit(container);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
applyTheme();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 深浅切换(侧栏按钮直接改 html.dark,无自定义事件可听,用 Observer 兜底)
|
|
79
|
+
new MutationObserver(applyTheme).observe(document.documentElement, {
|
|
80
|
+
attributes: true,
|
|
81
|
+
attributeFilter: ["class"],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
document.addEventListener("astro:page-load", initAll);
|
|
85
|
+
if (document.readyState === "loading") {
|
|
86
|
+
document.addEventListener("DOMContentLoaded", initAll, { once: true });
|
|
87
|
+
} else {
|
|
88
|
+
initAll();
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import BaseLayout from "virtual:theme/components/BaseLayout.astro";
|
|
3
|
+
import { t } from "../i18n/index.js";
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<BaseLayout title={t('notFound')}>
|
|
7
|
+
<header class="archive-head reveal" style="text-align: center; max-width: none;">
|
|
8
|
+
<h1 class="page-title">404</h1>
|
|
9
|
+
<p class="page-sub">{t('notFoundDesc')}</p>
|
|
10
|
+
<p style="margin-top: 28px;">
|
|
11
|
+
<a class="btn" href="/">{t('backHome')}</a>
|
|
12
|
+
</p>
|
|
13
|
+
</header>
|
|
14
|
+
</BaseLayout>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { render } from "astro:content";
|
|
2
|
+
import type { PostLike } from "../utils.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 批量「补水」:remark/rehype 插件(阅读时长/自动摘要/首图)把结果写入
|
|
6
|
+
* file.data.astro.frontmatter,组件侧必须经 render(post).remarkPluginFrontmatter 读取。
|
|
7
|
+
* 本函数批量渲染并把这些字段 merge 回 post.data,供 PostCard 等组件直接消费。
|
|
8
|
+
*
|
|
9
|
+
* render() 是完整 Markdown 编译,代价高——只对当前页面用到的文章调用,
|
|
10
|
+
* 不要对全站列表无差别调用。
|
|
11
|
+
*/
|
|
12
|
+
export async function hydratePosts<T extends PostLike>(
|
|
13
|
+
posts: T[],
|
|
14
|
+
): Promise<T[]> {
|
|
15
|
+
return Promise.all(
|
|
16
|
+
posts.map(async (post) => {
|
|
17
|
+
const { remarkPluginFrontmatter } = await render(
|
|
18
|
+
post as unknown as Parameters<typeof render>[0],
|
|
19
|
+
);
|
|
20
|
+
const fm = remarkPluginFrontmatter as Record<string, unknown>;
|
|
21
|
+
return {
|
|
22
|
+
...post,
|
|
23
|
+
data: {
|
|
24
|
+
...post.data,
|
|
25
|
+
excerpt: (fm.excerpt as string | undefined) ?? post.data.excerpt,
|
|
26
|
+
firstImage:
|
|
27
|
+
(fm.firstImage as string | undefined) ?? post.data.firstImage,
|
|
28
|
+
words: (fm.words as number | undefined) ?? post.data.words,
|
|
29
|
+
minutes: (fm.minutes as number | undefined) ?? post.data.minutes,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getEntry, render } from "astro:content";
|
|
3
|
+
import BaseLayout from "virtual:theme/components/BaseLayout.astro";
|
|
4
|
+
import { t } from "../i18n/index.js";
|
|
5
|
+
|
|
6
|
+
// 正文来自站点 spec 集合(src/content/spec/about.md);
|
|
7
|
+
// 集合或条目缺失时降级为提示文案,不使整页 500
|
|
8
|
+
const entry = await getEntry("spec", "about").catch(() => null);
|
|
9
|
+
const { Content } = entry ? await render(entry) : { Content: null };
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<BaseLayout title={t('about')} page="about">
|
|
13
|
+
<header class="archive-head reveal">
|
|
14
|
+
<h1 class="page-title">{t('about')}</h1>
|
|
15
|
+
</header>
|
|
16
|
+
<article class="card page-card prose reveal" data-lightbox-gallery>
|
|
17
|
+
{
|
|
18
|
+
Content ? (
|
|
19
|
+
<Content />
|
|
20
|
+
) : (
|
|
21
|
+
<p class="tag-empty">{t('aboutEmpty')}</p>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
</article>
|
|
25
|
+
</BaseLayout>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { getCollection } from "astro:content";
|
|
2
|
+
import { getSortedPosts } from "../../utils.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 全站文章元数据(P1-12 随机文章栏数据源)。
|
|
6
|
+
* 脱敏:加密文章不输出(客户端无需过滤,亦不暴露哪些文章加密);
|
|
7
|
+
* draft 在 PROD 下不输出。
|
|
8
|
+
*/
|
|
9
|
+
export async function GET() {
|
|
10
|
+
const all = await getCollection("posts", ({ data }) =>
|
|
11
|
+
import.meta.env.PROD ? !data.draft : true,
|
|
12
|
+
);
|
|
13
|
+
const posts = getSortedPosts(all);
|
|
14
|
+
const body = posts
|
|
15
|
+
.filter((p) => !p.data.password)
|
|
16
|
+
.map((p) => ({
|
|
17
|
+
id: p.id,
|
|
18
|
+
title: p.data.title,
|
|
19
|
+
description: p.data.description,
|
|
20
|
+
published: p.data.published.getTime(),
|
|
21
|
+
category: p.data.category || "",
|
|
22
|
+
}));
|
|
23
|
+
return new Response(JSON.stringify(body), {
|
|
24
|
+
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection } from "astro:content";
|
|
3
|
+
import BaseLayout from "virtual:theme/components/BaseLayout.astro";
|
|
4
|
+
import config from "virtual:theme/config";
|
|
5
|
+
import { t } from "../i18n/index.js";
|
|
6
|
+
import { formatDate, type PostLike } from "../utils.js";
|
|
7
|
+
import { hydratePosts } from "./_hydrate.js";
|
|
8
|
+
|
|
9
|
+
// 归档 = 纯时间线(忽略置顶,按发布时间倒序),按年分组展示
|
|
10
|
+
const all = await getCollection("posts", ({ data }) =>
|
|
11
|
+
import.meta.env.PROD ? !data.draft : true,
|
|
12
|
+
);
|
|
13
|
+
const sorted = all.sort(
|
|
14
|
+
(a, b) => b.data.published.getTime() - a.data.published.getTime(),
|
|
15
|
+
);
|
|
16
|
+
const posts = await hydratePosts(sorted);
|
|
17
|
+
|
|
18
|
+
const groups = new Map<number, PostLike[]>();
|
|
19
|
+
for (const post of posts) {
|
|
20
|
+
const year = post.data.published.getFullYear();
|
|
21
|
+
const list = groups.get(year);
|
|
22
|
+
if (list) list.push(post);
|
|
23
|
+
else groups.set(year, [post]);
|
|
24
|
+
}
|
|
25
|
+
const years = [...groups.keys()];
|
|
26
|
+
const yearLabel =
|
|
27
|
+
years.length <= 1
|
|
28
|
+
? t("yearUnit", { n: years[0] ?? "" })
|
|
29
|
+
: `${Math.max(...years)} – ${Math.min(...years)}`;
|
|
30
|
+
|
|
31
|
+
// 归档按年折叠(§4.30 对齐参考站 foldArticle):开启时年份头变按钮可点,
|
|
32
|
+
// 最新一年默认展开,其余折叠
|
|
33
|
+
const fold = config.archive.fold;
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
<BaseLayout title={t('archive')} page="archive">
|
|
37
|
+
<header class="archive-head reveal">
|
|
38
|
+
<h1 class="page-title">{t('archive')}</h1>
|
|
39
|
+
<p class="page-sub">{t('totalPosts', { n: posts.length })} · {yearLabel}</p>
|
|
40
|
+
</header>
|
|
41
|
+
{
|
|
42
|
+
years.map((year, i) => {
|
|
43
|
+
const collapsed = fold && years.length > 1 && i > 0;
|
|
44
|
+
return (
|
|
45
|
+
<div class:list={["timeline", "reveal", { fold: collapsed }]}>
|
|
46
|
+
{fold ? (
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
class="archive-year"
|
|
50
|
+
aria-expanded={collapsed ? "false" : "true"}
|
|
51
|
+
>
|
|
52
|
+
<svg class="year-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
|
53
|
+
<span class="y">{year}</span>
|
|
54
|
+
<span class="c">{t('postsInYear', { n: groups.get(year)!.length })}</span>
|
|
55
|
+
</button>
|
|
56
|
+
) : (
|
|
57
|
+
<div class="archive-year">
|
|
58
|
+
<span class="y">{year}</span>
|
|
59
|
+
<span class="c">{t('postsInYear', { n: groups.get(year)!.length })}</span>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
<div class="timeline-items">
|
|
63
|
+
{groups.get(year)!.map((post) => (
|
|
64
|
+
<div class="timeline-item">
|
|
65
|
+
<a href={`/posts/${post.id}/`}>
|
|
66
|
+
<span class="t-date">{formatDate(post.data.published).slice(5)}</span>
|
|
67
|
+
<span class="t-title">{post.data.title}</span>
|
|
68
|
+
<span class="t-meta">
|
|
69
|
+
{post.data.category ?? ''}
|
|
70
|
+
{post.data.category && post.data.words ? ' · ' : ''}
|
|
71
|
+
{post.data.words ? t('wordsCount', { n: post.data.words }) : ''}
|
|
72
|
+
</span>
|
|
73
|
+
</a>
|
|
74
|
+
</div>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
</BaseLayout>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection } from "astro:content";
|
|
3
|
+
import BaseLayout from "virtual:theme/components/BaseLayout.astro";
|
|
4
|
+
import { t } from "../../i18n/index.js";
|
|
5
|
+
import { collectCategories, formatDate, getSortedPosts } from "../../utils.js";
|
|
6
|
+
|
|
7
|
+
// 先排序再聚合,保证每个分类内文章按新→旧排列
|
|
8
|
+
export async function getStaticPaths() {
|
|
9
|
+
const all = await getCollection("posts", ({ data }) =>
|
|
10
|
+
import.meta.env.PROD ? !data.draft : true,
|
|
11
|
+
);
|
|
12
|
+
const sorted = getSortedPosts(all);
|
|
13
|
+
return [...collectCategories(sorted).entries()].map(([category, posts]) => ({
|
|
14
|
+
params: { category },
|
|
15
|
+
props: { category, posts },
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { category, posts } = Astro.props;
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<BaseLayout title={t('categoryTitle', { category })} page="categories">
|
|
23
|
+
<header class="archive-head reveal">
|
|
24
|
+
<h1 class="page-title">{category}</h1>
|
|
25
|
+
<p class="page-sub">{t('totalPosts', { n: posts.length })}</p>
|
|
26
|
+
</header>
|
|
27
|
+
<div class="tag-list">
|
|
28
|
+
{
|
|
29
|
+
posts.map((post) => (
|
|
30
|
+
<a class="related-card card tag-item reveal" href={`/posts/${post.id}/`}>
|
|
31
|
+
<div class="rc-title">{post.data.title}</div>
|
|
32
|
+
<div class="rc-meta">{formatDate(post.data.published)}</div>
|
|
33
|
+
</a>
|
|
34
|
+
))
|
|
35
|
+
}
|
|
36
|
+
</div>
|
|
37
|
+
</BaseLayout>
|