@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,379 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* 侧栏日历 + 活跃度热力图(P2 侧栏改造)。
|
|
4
|
+
* 月视图(周一起始,当月有文章日高亮)+ 近 12 个月热力图(每月 4 段,透明度 5 档):
|
|
5
|
+
* 顶部月份标签(§4.43),整列可点击跳转对应月份,单元悬停提示月份第几周 · 几篇。
|
|
6
|
+
* 年月标签可点击展开年月选择器(§4.47:年步进 + 12 月网格、未来月置灰),非当月时头部出现「回到本月」按钮(§4.54 移至日期 ▾ 之后)。
|
|
7
|
+
* 数据复用 /api/allPostMeta.json(加密文章已在端点侧排除)。
|
|
8
|
+
* 热力图下方文章列表(§4.40,§4.43 移至热力图下):默认当月全部,点选有文章日期过滤当日,再点取消;
|
|
9
|
+
* 按时间倒序,左侧标题右侧日期;条目悬停全文 title 可经 widgets 数组 calendar 项 specificConfig.postTooltip 开启(§4.41),默认关。
|
|
10
|
+
* <qy-calendar> 为 VT 安全 custom element:is:inline + customElements.get 防重复 define,
|
|
11
|
+
* 元数据缓存在 window.__qyAllPostMetaCache(与 random-posts 共用)。
|
|
12
|
+
*/
|
|
13
|
+
import { t } from "../i18n/index.js";
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
/** 站点语言(决定月份/星期文案的 locale) */
|
|
17
|
+
lang?: string;
|
|
18
|
+
/** 日历下方文章列表条目悬停显示全文 title(§4.41);默认关 */
|
|
19
|
+
postTooltip?: boolean;
|
|
20
|
+
/** 移动端(<768px)隐藏整个 widget(§4.49);默认否 */
|
|
21
|
+
hideMobile?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { lang = "zh-CN", postTooltip = false, hideMobile = false } = Astro.props;
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<section class:list={["side-card", "card", "reveal", "calendar-widget", { "side-hide-m": hideMobile }]}>
|
|
28
|
+
<!-- §4.47:「日历」标题移除,年月标签即卡片头部(加粗、可点选年月) -->
|
|
29
|
+
<qy-calendar
|
|
30
|
+
data-lang={lang}
|
|
31
|
+
data-api-url="/api/allPostMeta.json"
|
|
32
|
+
data-label-posts={t('dayPostsCount')}
|
|
33
|
+
data-label-heat-week={t('heatWeekTip')}
|
|
34
|
+
data-label-home={t('calendarHome')}
|
|
35
|
+
data-label-pick={t('calendarPick')}
|
|
36
|
+
data-post-tooltip={postTooltip ? '1' : '0'}
|
|
37
|
+
></qy-calendar>
|
|
38
|
+
</section>
|
|
39
|
+
|
|
40
|
+
<script is:inline>
|
|
41
|
+
if (!customElements.get('qy-calendar')) {
|
|
42
|
+
customElements.define(
|
|
43
|
+
'qy-calendar',
|
|
44
|
+
class extends HTMLElement {
|
|
45
|
+
connectedCallback() {
|
|
46
|
+
// 每次 connected 重建月视图:VT 往返后回到当月是期望行为
|
|
47
|
+
this.render();
|
|
48
|
+
}
|
|
49
|
+
getMeta() {
|
|
50
|
+
if (window.__qyAllPostMetaCache) {
|
|
51
|
+
return Promise.resolve(window.__qyAllPostMetaCache);
|
|
52
|
+
}
|
|
53
|
+
return fetch(this.dataset.apiUrl)
|
|
54
|
+
.then((r) => r.json())
|
|
55
|
+
.then((data) => {
|
|
56
|
+
window.__qyAllPostMetaCache = data;
|
|
57
|
+
return data;
|
|
58
|
+
})
|
|
59
|
+
.catch(() => null);
|
|
60
|
+
}
|
|
61
|
+
render() {
|
|
62
|
+
var self = this;
|
|
63
|
+
this.getMeta().then(function (meta) {
|
|
64
|
+
if (!meta) return;
|
|
65
|
+
// key: YYYY-MM-DD(本地时区) → 当日文章数
|
|
66
|
+
var byDay = {};
|
|
67
|
+
meta.forEach(function (p) {
|
|
68
|
+
var d = new Date(p.published);
|
|
69
|
+
var key =
|
|
70
|
+
d.getFullYear() +
|
|
71
|
+
'-' +
|
|
72
|
+
String(d.getMonth() + 1).padStart(2, '0') +
|
|
73
|
+
'-' +
|
|
74
|
+
String(d.getDate()).padStart(2, '0');
|
|
75
|
+
byDay[key] = (byDay[key] || 0) + 1;
|
|
76
|
+
});
|
|
77
|
+
var locale = self.dataset.lang || 'zh-CN';
|
|
78
|
+
var postsLabel = self.dataset.labelPosts || '{n}';
|
|
79
|
+
// 热力图单元悬停模板(§4.43):{m}=月份短文案、{w}=第几周、{n}=篇数
|
|
80
|
+
var weekTip = self.dataset.labelHeatWeek || '{m}第{w}周 · {n}';
|
|
81
|
+
// 「回到本月」按钮与年月标签按钮的 title/aria 文案(§4.47)
|
|
82
|
+
var homeLabel = self.dataset.labelHome || '';
|
|
83
|
+
var pickLabel = self.dataset.labelPick || '';
|
|
84
|
+
var today = new Date();
|
|
85
|
+
var thisMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
86
|
+
var cur = new Date(thisMonth);
|
|
87
|
+
// 年月选择器年份下界(§4.47):数据最早文章年份,无文章则为当年(须在 today 声明后)
|
|
88
|
+
var minYear = today.getFullYear();
|
|
89
|
+
meta.forEach(function (p) {
|
|
90
|
+
var y = new Date(p.published).getFullYear();
|
|
91
|
+
if (y < minYear) minYear = y;
|
|
92
|
+
});
|
|
93
|
+
var fmtMonth = new Intl.DateTimeFormat(locale, {
|
|
94
|
+
year: 'numeric',
|
|
95
|
+
month: 'long',
|
|
96
|
+
});
|
|
97
|
+
var fmtWeek = new Intl.DateTimeFormat(locale, { weekday: 'narrow' });
|
|
98
|
+
// 热力图悬停月份短文案(§4.43):zh="10月"、en="Oct"
|
|
99
|
+
var fmtMonthShort = new Intl.DateTimeFormat(locale, { month: 'short' });
|
|
100
|
+
// 周一起始的星期表头(2024-01-01 恰好是周一)
|
|
101
|
+
var weekHeads = [];
|
|
102
|
+
for (var i = 0; i < 7; i++) {
|
|
103
|
+
weekHeads.push(fmtWeek.format(new Date(2024, 0, 1 + i)));
|
|
104
|
+
}
|
|
105
|
+
var tooltip = function (key, n) {
|
|
106
|
+
return key + ' · ' + postsLabel.replace('{n}', n);
|
|
107
|
+
};
|
|
108
|
+
// 与 byDay 一致的 YYYY-MM-DD key(本地时区),列表过滤用
|
|
109
|
+
var dayKey = function (p) {
|
|
110
|
+
var d = new Date(p.published);
|
|
111
|
+
return (
|
|
112
|
+
d.getFullYear() +
|
|
113
|
+
'-' +
|
|
114
|
+
String(d.getMonth() + 1).padStart(2, '0') +
|
|
115
|
+
'-' +
|
|
116
|
+
String(d.getDate()).padStart(2, '0')
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
var esc = function (s) {
|
|
120
|
+
return String(s).replace(/[&<>"']/g, function (c) {
|
|
121
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
// 列表条目日期(§4.43):两位月/日,如 10/08
|
|
125
|
+
var fmtItemDate = new Intl.DateTimeFormat(locale, { month: '2-digit', day: '2-digit' });
|
|
126
|
+
// 当前选中的日期 key(null = 未选中,列表显示当月全部)
|
|
127
|
+
var selKey = null;
|
|
128
|
+
// 文章列表条目悬停显示全文 title(§4.41),默认关
|
|
129
|
+
var postTip = self.dataset.postTooltip === '1';
|
|
130
|
+
// 年月选择器状态(§4.47):展开态 + 选择器年份(打开时同步为当前浏览年份)
|
|
131
|
+
var pickOpen = false;
|
|
132
|
+
var pickYear = today.getFullYear();
|
|
133
|
+
|
|
134
|
+
// 年月选择器(§4.47):年步进行(minYear~今年,边界置灰)+ 12 月网格(未来月置灰、当前浏览月高亮)
|
|
135
|
+
var pickerHtml = function () {
|
|
136
|
+
if (!pickOpen) return '';
|
|
137
|
+
var cy = cur.getFullYear();
|
|
138
|
+
var cm = cur.getMonth();
|
|
139
|
+
var thisYear = today.getFullYear();
|
|
140
|
+
var html =
|
|
141
|
+
'<div class="cal-picker" role="group" aria-label="' + esc(pickLabel) + '">' +
|
|
142
|
+
'<div class="cal-picker-row">' +
|
|
143
|
+
'<button type="button" class="cal-py" data-dir="-1" aria-label="prev year"' +
|
|
144
|
+
(pickYear <= minYear ? ' disabled' : '') +
|
|
145
|
+
'>‹</button>' +
|
|
146
|
+
'<b class="cal-py-label">' + pickYear + '</b>' +
|
|
147
|
+
'<button type="button" class="cal-py" data-dir="1" aria-label="next year"' +
|
|
148
|
+
(pickYear >= thisYear ? ' disabled' : '') +
|
|
149
|
+
'>›</button>' +
|
|
150
|
+
'</div><div class="cal-picker-months">';
|
|
151
|
+
for (var m = 0; m < 12; m++) {
|
|
152
|
+
var d = new Date(pickYear, m, 1);
|
|
153
|
+
var future = d > thisMonth;
|
|
154
|
+
var on = pickYear === cy && m === cm;
|
|
155
|
+
html +=
|
|
156
|
+
'<button type="button" class="cal-pm' + (on ? ' on' : '') +
|
|
157
|
+
'" data-m="' + m + '"' + (future ? ' disabled' : '') + '>' +
|
|
158
|
+
fmtMonthShort.format(d) + '</button>';
|
|
159
|
+
}
|
|
160
|
+
return html + '</div></div>';
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
var monthHtml = function () {
|
|
164
|
+
var y = cur.getFullYear();
|
|
165
|
+
var m = cur.getMonth();
|
|
166
|
+
var lead = (new Date(y, m, 1).getDay() + 6) % 7;
|
|
167
|
+
var days = new Date(y, m + 1, 0).getDate();
|
|
168
|
+
// 是否当前月(§4.47):非当月才渲染「回到本月」按钮
|
|
169
|
+
var isThisMonth = y === thisMonth.getFullYear() && m === thisMonth.getMonth();
|
|
170
|
+
var html =
|
|
171
|
+
'<div class="cal-head">' +
|
|
172
|
+
'<button type="button" class="cal-nav" data-dir="-1" aria-label="prev">‹</button>' +
|
|
173
|
+
// §4.54 日期与「回到本月」成组:重置按钮紧跟日期 ▾ 之后
|
|
174
|
+
'<span class="cal-head-c">' +
|
|
175
|
+
// 年月标签改为可点按钮(§4.47):点击开合年月选择器
|
|
176
|
+
'<button type="button" class="cal-month' + (pickOpen ? ' open' : '') +
|
|
177
|
+
'" aria-expanded="' + pickOpen + '" title="' + esc(pickLabel) + '">' +
|
|
178
|
+
fmtMonth.format(cur) + '</button>' +
|
|
179
|
+
(isThisMonth
|
|
180
|
+
? ''
|
|
181
|
+
: '<button type="button" class="cal-home" title="' + esc(homeLabel) +
|
|
182
|
+
'" aria-label="' + esc(homeLabel) + '">↺</button>') +
|
|
183
|
+
'</span>' +
|
|
184
|
+
'<span class="cal-head-r">' +
|
|
185
|
+
'<button type="button" class="cal-nav" data-dir="1" aria-label="next">›</button>' +
|
|
186
|
+
'</span>' +
|
|
187
|
+
'</div>' +
|
|
188
|
+
pickerHtml() +
|
|
189
|
+
'<div class="cal-grid">';
|
|
190
|
+
for (var i = 0; i < 7; i++) {
|
|
191
|
+
html += '<span class="cal-week">' + weekHeads[i] + '</span>';
|
|
192
|
+
}
|
|
193
|
+
for (var i = 0; i < lead; i++) {
|
|
194
|
+
html += '<span class="cal-day dim"></span>';
|
|
195
|
+
}
|
|
196
|
+
for (var day = 1; day <= days; day++) {
|
|
197
|
+
var key =
|
|
198
|
+
y + '-' + String(m + 1).padStart(2, '0') + '-' + String(day).padStart(2, '0');
|
|
199
|
+
var n = byDay[key] || 0;
|
|
200
|
+
var cls = 'cal-day';
|
|
201
|
+
if (n > 0) cls += ' has-post';
|
|
202
|
+
if (selKey && key === selKey) cls += ' sel';
|
|
203
|
+
if (y === today.getFullYear() && m === today.getMonth() && day === today.getDate()) {
|
|
204
|
+
cls += ' today';
|
|
205
|
+
}
|
|
206
|
+
html +=
|
|
207
|
+
'<span class="' + cls + '"' +
|
|
208
|
+
(n > 0
|
|
209
|
+
? ' data-key="' + key + '" role="button" tabindex="0" title="' + tooltip(key, n) + '"'
|
|
210
|
+
: '') +
|
|
211
|
+
'>' + day + '</span>';
|
|
212
|
+
}
|
|
213
|
+
return html + '</div>';
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// 热力图下方文章列表(§4.40,§4.43 移至热力图下,§4.43 修正去统计标题):默认当月全部,
|
|
217
|
+
// 选中日期则只列当日;按时间倒序,左侧标题右侧日期,超宽标题省略号截断
|
|
218
|
+
var dayListHtml = function () {
|
|
219
|
+
var ym = cur.getFullYear() + '-' + String(cur.getMonth() + 1).padStart(2, '0');
|
|
220
|
+
var list = meta.filter(function (p) {
|
|
221
|
+
if (selKey) return dayKey(p) === selKey;
|
|
222
|
+
return dayKey(p).slice(0, 7) === ym;
|
|
223
|
+
});
|
|
224
|
+
// 时间倒序(filter 返回新数组,原地 sort 安全)
|
|
225
|
+
list.sort(function (a, b) {
|
|
226
|
+
return new Date(b.published) - new Date(a.published);
|
|
227
|
+
});
|
|
228
|
+
var html = '<div class="cal-list">';
|
|
229
|
+
if (list.length) {
|
|
230
|
+
html += '<div class="cal-list-items">';
|
|
231
|
+
list.forEach(function (p) {
|
|
232
|
+
var d = new Date(p.published);
|
|
233
|
+
html +=
|
|
234
|
+
'<a class="cal-post" href="/posts/' + p.id + '/"' +
|
|
235
|
+
(postTip ? ' title="' + esc(p.title) + '"' : '') +
|
|
236
|
+
'><span class="cal-post-title">' + esc(p.title) + '</span>' +
|
|
237
|
+
'<time class="cal-post-date" datetime="' + dayKey(p) + '">' +
|
|
238
|
+
fmtItemDate.format(d) + '</time></a>';
|
|
239
|
+
});
|
|
240
|
+
html += '</div>';
|
|
241
|
+
}
|
|
242
|
+
return html + '</div>';
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// 今年 1-12 月热力图(§4.44,原近 12 个月):还原 orchid-blog 原版形态——
|
|
246
|
+
// 月份标签独立一行(grid 12 列),下方 48 个正方形小格子平铺 12×4;
|
|
247
|
+
// 透明度 5 档 0 / .45 / .65 / .85 / 1,格子悬停提示月份第几周 · 几篇、点击跳转对应月份;
|
|
248
|
+
// 未来月份格子不渲染为按钮(月视图不允许翻到未来),仅作占位展示
|
|
249
|
+
var heatHtml = function () {
|
|
250
|
+
var y = today.getFullYear();
|
|
251
|
+
var labels = '';
|
|
252
|
+
var cells = '';
|
|
253
|
+
for (var m = 0; m < 12; m++) {
|
|
254
|
+
var d = new Date(y, m, 1);
|
|
255
|
+
var days = new Date(y, m + 1, 0).getDate();
|
|
256
|
+
var buckets = [0, 0, 0, 0];
|
|
257
|
+
for (var day = 1; day <= days; day++) {
|
|
258
|
+
var w = Math.min(Math.floor((day - 1) / 7), 3);
|
|
259
|
+
var key =
|
|
260
|
+
y + '-' + String(m + 1).padStart(2, '0') + '-' + String(day).padStart(2, '0');
|
|
261
|
+
buckets[w] += byDay[key] || 0;
|
|
262
|
+
}
|
|
263
|
+
var mShort = fmtMonthShort.format(d);
|
|
264
|
+
var future = d > thisMonth;
|
|
265
|
+
labels += '<b class="cal-heat-label">' + (m + 1) + '</b>';
|
|
266
|
+
for (var b = 0; b < 4; b++) {
|
|
267
|
+
var n = buckets[b];
|
|
268
|
+
var o = n === 0 ? 0 : n < 3 ? 0.45 : n < 6 ? 0.65 : n < 10 ? 0.85 : 1;
|
|
269
|
+
cells +=
|
|
270
|
+
'<i class="cal-heat-cell" data-y="' +
|
|
271
|
+
y +
|
|
272
|
+
'" data-m="' +
|
|
273
|
+
m +
|
|
274
|
+
'" style="--o:' +
|
|
275
|
+
o +
|
|
276
|
+
'"' +
|
|
277
|
+
(future ? '' : ' role="button" tabindex="0"') +
|
|
278
|
+
' title="' +
|
|
279
|
+
esc(weekTip.replace('{m}', mShort).replace('{w}', b + 1).replace('{n}', n)) +
|
|
280
|
+
'"></i>';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return (
|
|
284
|
+
'<div class="cal-heat-labels">' +
|
|
285
|
+
labels +
|
|
286
|
+
'</div><div class="cal-heat-grid">' +
|
|
287
|
+
cells +
|
|
288
|
+
'</div>'
|
|
289
|
+
);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
var build = function () {
|
|
293
|
+
// 渲染顺序(§4.43):月视图 → 热力图 → 文章列表
|
|
294
|
+
self.innerHTML =
|
|
295
|
+
monthHtml() + '<div class="cal-heat">' + heatHtml() + '</div>' + dayListHtml();
|
|
296
|
+
self.querySelectorAll('.cal-nav[data-dir]').forEach(function (btn) {
|
|
297
|
+
btn.addEventListener('click', function () {
|
|
298
|
+
var dir = parseInt(btn.dataset.dir, 10);
|
|
299
|
+
var next = new Date(cur.getFullYear(), cur.getMonth() + dir, 1);
|
|
300
|
+
// 不允许翻到未来月份
|
|
301
|
+
if (next > thisMonth) return;
|
|
302
|
+
cur = next;
|
|
303
|
+
selKey = null; // 翻月取消日期选中,列表回到当月全部
|
|
304
|
+
build();
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
// 年月标签开合选择器(§4.47):打开时同步选择器年份为当前浏览年份
|
|
308
|
+
var monthBtn = self.querySelector('.cal-month');
|
|
309
|
+
if (monthBtn) {
|
|
310
|
+
monthBtn.addEventListener('click', function () {
|
|
311
|
+
pickOpen = !pickOpen;
|
|
312
|
+
if (pickOpen) pickYear = cur.getFullYear();
|
|
313
|
+
build();
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
// 回到本月(§4.47):仅非当月渲染;同时收起选择器、取消日期选中
|
|
317
|
+
var homeBtn = self.querySelector('.cal-home');
|
|
318
|
+
if (homeBtn) {
|
|
319
|
+
homeBtn.addEventListener('click', function () {
|
|
320
|
+
cur = new Date(thisMonth.getFullYear(), thisMonth.getMonth(), 1);
|
|
321
|
+
selKey = null;
|
|
322
|
+
pickOpen = false;
|
|
323
|
+
build();
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
// 选择器年步进(§4.47):边界按钮已置灰,无需额外钳制
|
|
327
|
+
self.querySelectorAll('.cal-py').forEach(function (btn) {
|
|
328
|
+
btn.addEventListener('click', function () {
|
|
329
|
+
pickYear += parseInt(btn.dataset.dir, 10);
|
|
330
|
+
build();
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
// 选择器月份点击(§4.47):跳月并收起选择器
|
|
334
|
+
self.querySelectorAll('.cal-pm:not([disabled])').forEach(function (btn) {
|
|
335
|
+
btn.addEventListener('click', function () {
|
|
336
|
+
cur = new Date(pickYear, parseInt(btn.dataset.m, 10), 1);
|
|
337
|
+
selKey = null;
|
|
338
|
+
pickOpen = false;
|
|
339
|
+
build();
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
// 点热力图格子(§4.43 整列点击,§4.44 还原原版改格子级点击):跳转对应月份,列表跟随;
|
|
343
|
+
// 未来月份格子无 role 不绑(月视图不允许翻到未来);Enter/Space 同点按
|
|
344
|
+
self.querySelectorAll('.cal-heat-cell[role="button"]').forEach(function (el) {
|
|
345
|
+
var jump = function () {
|
|
346
|
+
cur = new Date(+el.dataset.y, +el.dataset.m, 1);
|
|
347
|
+
selKey = null;
|
|
348
|
+
build();
|
|
349
|
+
};
|
|
350
|
+
el.addEventListener('click', jump);
|
|
351
|
+
el.addEventListener('keydown', function (e) {
|
|
352
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
353
|
+
e.preventDefault();
|
|
354
|
+
jump();
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
// 点日期(§4.40):toggle 选中,列表在当日/当月间切换;Enter/Space 同点按
|
|
359
|
+
self.querySelectorAll('.cal-day.has-post').forEach(function (el) {
|
|
360
|
+
var toggle = function () {
|
|
361
|
+
selKey = selKey === el.dataset.key ? null : el.dataset.key;
|
|
362
|
+
build();
|
|
363
|
+
};
|
|
364
|
+
el.addEventListener('click', toggle);
|
|
365
|
+
el.addEventListener('keydown', function (e) {
|
|
366
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
367
|
+
e.preventDefault();
|
|
368
|
+
toggle();
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
};
|
|
373
|
+
build();
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
</script>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* 分类导航栏(§4.15,参考 orchid-blog CategoryBar;§4.21 改版):
|
|
4
|
+
* 仅首页渲染(index.astro post-list 顶部,与置顶/近期文章同列宽),
|
|
5
|
+
* 结构 = 首页 pill + 归档 pill + 分隔线 + 分类滚动区(pill 带计数,溢出横向滚动)
|
|
6
|
+
* + 分隔线 + 「更多」固定链接跳 /categories/ 分类总览页(始终显示)。
|
|
7
|
+
* 组件内自行聚合分类(仅取分类名与数量,不做 hydrate);
|
|
8
|
+
* 激活态在构建期由 Astro.url.pathname 判断(SSG 每页 pathname 可用),零客户端 JS。
|
|
9
|
+
* 溢出处理(§4.21,替代 §4.18 折叠下拉):溢出时整条横向滚动、右缘渐隐提示(app.ts
|
|
10
|
+
* 检测 scrollWidth 溢出后给 .category-bar 加 .is-scrollable),全量分类见「更多」页。
|
|
11
|
+
*/
|
|
12
|
+
import { getCollection } from "astro:content";
|
|
13
|
+
import config from "virtual:theme/config";
|
|
14
|
+
import { t } from "../i18n/index.js";
|
|
15
|
+
import { collectCategories } from "../utils.js";
|
|
16
|
+
|
|
17
|
+
const posts = await getCollection("posts", ({ data }) =>
|
|
18
|
+
import.meta.env.PROD ? !data.draft : true,
|
|
19
|
+
);
|
|
20
|
+
const cats = [...collectCategories(posts).entries()];
|
|
21
|
+
|
|
22
|
+
// 统一去尾斜杠后比较(dev 与 prerender 的 pathname 尾斜杠行为一致化)
|
|
23
|
+
const path = Astro.url.pathname.replace(/\/+$/, "");
|
|
24
|
+
const isHome = path === "";
|
|
25
|
+
const isArchive = path === "/archive";
|
|
26
|
+
const isCategory = (name: string) =>
|
|
27
|
+
path === `/categories/${encodeURIComponent(name)}`;
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
config.categoryBar.enabled && (
|
|
32
|
+
<nav class="category-bar card reveal" aria-label={t("categoryNav")}>
|
|
33
|
+
<a class:list={["category-pill", { "is-active": isHome }]} href="/">
|
|
34
|
+
{t("home")}
|
|
35
|
+
</a>
|
|
36
|
+
<a
|
|
37
|
+
class:list={["category-pill", { "is-active": isArchive }]}
|
|
38
|
+
href="/archive"
|
|
39
|
+
>
|
|
40
|
+
{t("archive")}
|
|
41
|
+
</a>
|
|
42
|
+
<span class="bar-divider" aria-hidden="true" />
|
|
43
|
+
<div class="bar-scroll">
|
|
44
|
+
{cats.map(([name, list]) => (
|
|
45
|
+
<a
|
|
46
|
+
class:list={["category-pill", { "is-active": isCategory(name) }]}
|
|
47
|
+
href={`/categories/${encodeURIComponent(name)}/`}
|
|
48
|
+
>
|
|
49
|
+
{name}
|
|
50
|
+
<span class="pill-count">{list.length}</span>
|
|
51
|
+
</a>
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
54
|
+
{/* 「更多」固定跳分类总览页(§4.21),始终显示 */}
|
|
55
|
+
<span class="bar-divider" aria-hidden="true" />
|
|
56
|
+
<a class="category-pill bar-more" href="/categories/">
|
|
57
|
+
{t("more")}
|
|
58
|
+
<svg
|
|
59
|
+
class="bar-more-arrow"
|
|
60
|
+
viewBox="0 0 24 24"
|
|
61
|
+
fill="none"
|
|
62
|
+
stroke="currentColor"
|
|
63
|
+
stroke-width="1.75"
|
|
64
|
+
stroke-linecap="round"
|
|
65
|
+
stroke-linejoin="round"
|
|
66
|
+
aria-hidden="true"
|
|
67
|
+
>
|
|
68
|
+
<path d="M5 12h14" />
|
|
69
|
+
<path d="m12 5 7 7-7 7" />
|
|
70
|
+
</svg>
|
|
71
|
+
</a>
|
|
72
|
+
</nav>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* 评论适配层:按 config.comment.provider 分发到具体实现(五套可切换)。
|
|
4
|
+
* 站点可经 override 覆盖本组件整体替换评论区外观。
|
|
5
|
+
*/
|
|
6
|
+
import config from "virtual:theme/config";
|
|
7
|
+
import Artalk from "./comments/Artalk.astro";
|
|
8
|
+
import Disqus from "./comments/Disqus.astro";
|
|
9
|
+
import Giscus from "./comments/Giscus.astro";
|
|
10
|
+
import Twikoo from "./comments/Twikoo.astro";
|
|
11
|
+
import Waline from "./comments/Waline.astro";
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
/** 各实现共享的页面路径(与 orchid-blog 口径一致:/posts/<id>、/guestbook,均无尾斜杠) */
|
|
15
|
+
path: string;
|
|
16
|
+
/** 页面级开关(文章传 frontmatter comment;缺省 true) */
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
/** Disqus 用页面标题(缺省回退 path) */
|
|
19
|
+
title?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { path, enabled = true, title } = Astro.props;
|
|
23
|
+
const c = config.comment;
|
|
24
|
+
// 配置边界校验(§4.62):provider 启用但对应后端地址/仓库信息为空(schema 默认空串)时
|
|
25
|
+
// 不挂载评论区——空 server 会让 SDK 向站点同源发请求并报错
|
|
26
|
+
const providerReady =
|
|
27
|
+
(c.provider === "artalk" && c.artalk.server.trim() !== "") ||
|
|
28
|
+
(c.provider === "twikoo" && c.twikoo.envId.trim() !== "") ||
|
|
29
|
+
(c.provider === "waline" && c.waline.serverURL.trim() !== "") ||
|
|
30
|
+
(c.provider === "giscus" &&
|
|
31
|
+
c.giscus.repo.trim() !== "" &&
|
|
32
|
+
c.giscus.repoId.trim() !== "") ||
|
|
33
|
+
(c.provider === "disqus" && c.disqus.shortname.trim() !== "");
|
|
34
|
+
const active = enabled && c.enabled && c.provider !== "none" && providerReady;
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
active && (
|
|
39
|
+
<section class="comments reveal" id="comments">
|
|
40
|
+
<h2 class="section-title">{c.title}</h2>
|
|
41
|
+
{c.provider === 'artalk' && <Artalk path={path} />}
|
|
42
|
+
{c.provider === 'twikoo' && <Twikoo path={path} />}
|
|
43
|
+
{c.provider === 'waline' && <Waline path={path} />}
|
|
44
|
+
{c.provider === 'giscus' && <Giscus />}
|
|
45
|
+
{c.provider === 'disqus' && <Disqus path={path} title={title} />}
|
|
46
|
+
</section>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { t } from "../i18n/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* 加密内容容器(M3b-b7):构建期把 slot 渲染为 HTML 后整体加密,
|
|
5
|
+
* 页面只输出密文(data-encrypted 属性)+ 锁 UI;解锁逻辑在 app.ts
|
|
6
|
+
* (Web Crypto 解密 → innerHTML 注入 → sessionStorage 缓存密码)。
|
|
7
|
+
* i18n 全部构建期渲染,无运行时动态文案需求。
|
|
8
|
+
*/
|
|
9
|
+
import { encryptContent } from "../utils/crypto.js";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
password: string;
|
|
13
|
+
slug: string;
|
|
14
|
+
hint?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { password, slug, hint = "" } = Astro.props;
|
|
18
|
+
const rawHtml = await Astro.slots.render("default");
|
|
19
|
+
const encryptedData = encryptContent(rawHtml, password, slug);
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<div id="encryptedContainer" class="enc-box" data-encrypted={encryptedData} data-slug={slug}>
|
|
23
|
+
<div class="enc-card">
|
|
24
|
+
<svg class="enc-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2 4 5.5v5.2c0 5 3.4 9.4 8 10.8 4.6-1.4 8-5.8 8-10.8V5.5L12 2z"/><rect x="9" y="10.5" width="6" height="5" rx="1"/><path d="M10.5 10.5V9a1.5 1.5 0 0 1 3 0v1.5"/></svg>
|
|
25
|
+
<h2 class="enc-title">{t('passwordProtected')}</h2>
|
|
26
|
+
<p class="enc-desc">{t('passwordProtectedDesc')}</p>
|
|
27
|
+
{hint && <p class="enc-hint">{t('passwordHint')}:{hint}</p>}
|
|
28
|
+
<form id="encForm" class="enc-form">
|
|
29
|
+
<input
|
|
30
|
+
id="encInput"
|
|
31
|
+
name="password"
|
|
32
|
+
type="password"
|
|
33
|
+
placeholder={t('passwordPlaceholder')}
|
|
34
|
+
autocomplete="off"
|
|
35
|
+
/>
|
|
36
|
+
<button type="submit">{t('passwordSubmit')}</button>
|
|
37
|
+
</form>
|
|
38
|
+
<p id="encError" class="enc-error" hidden>{t('passwordError')}</p>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
{/* 解密后 HTML 注入点:初始隐藏,保留正文占位语义 */}
|
|
42
|
+
<div id="decryptedContent" class="enc-content" hidden></div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
import config from "virtual:theme/config";
|
|
3
|
+
|
|
4
|
+
const { title, author, since, footer } = config;
|
|
5
|
+
const now = new Date().getFullYear();
|
|
6
|
+
const startYear = since ? parseInt(since.slice(0, 4), 10) : NaN;
|
|
7
|
+
const yearLabel =
|
|
8
|
+
!Number.isNaN(startYear) && startYear < now
|
|
9
|
+
? `${startYear}-${now}`
|
|
10
|
+
: String(now);
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<footer class="site-footer">
|
|
14
|
+
<div class="shell footer-inner">
|
|
15
|
+
{/* 页脚自定义 HTML(§4.16→§4.57):备案号等站点自有内容,置于版权行上方(对齐参考项目),作者自担内容安全 */}
|
|
16
|
+
{footer.customHtml && <div class="footer-custom" set:html={footer.customHtml} />}
|
|
17
|
+
{/* §4.54 对齐参考项目:一行版权 + RSS/Sitemap,一行 Powered by,不再重复主题/框架名 */}
|
|
18
|
+
<div class="footer-line">
|
|
19
|
+
© {yearLabel} {author || title}
|
|
20
|
+
<span class="footer-sep" aria-hidden="true">/</span>
|
|
21
|
+
<a href="/rss.xml">RSS</a>
|
|
22
|
+
<span class="footer-sep" aria-hidden="true">/</span>
|
|
23
|
+
<a href="/sitemap-index.xml">Sitemap</a>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="footer-sub">
|
|
26
|
+
Powered by <a href="https://astro.build" target="_blank" rel="noopener">Astro</a>
|
|
27
|
+
{" & "}
|
|
28
|
+
<a href="https://github.com/iwinder/qy-astro-theme-orchid" target="_blank" rel="noopener">Orchid</a>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</footer>
|